J'avais déjà parlé, dans de précédents articles, de la génération de xml et
de json avec Jersey. Et si maintenant, on s'amusait à générer du
protobuf ?
On parle de protobuf pour Protocol Buffers, une techno Google pour encoder des
structures de données. Ce format de données compact est utilisé en interne chez
Google des échanges de données. Etant basé sur la déclaration d'une structure
de données dans un idl, protobuf possède plusieurs implémentation et est ainsi
utilisable dans plusieurs langage. En java, la génération du code cible se fait
avec ant. Mais bien sur reste utilisable avec maven par le plugin ant.
Nous allons reprendre notre Hello qui avait d'exemple. Voici sa structure
protobuf :
package nfrancois.poc;
option java_package = "nfrancois.poc.protobuf.model";
option java_outer_classname = "HelloProto";
message Hello {
required string name = 1;
required string message = 2;
}
La structure se comprend assez facilement. Attention par contre, au trompeur
package de 1ère ligne, qui n'est pas lié à la notion de package que nous avons
en java. Il sert comme espace de nommage et éviter des collisions de nom si
plusieurs objets protobuf portent le même nom. Puisque depuis cette idl, je
pourrai aussi bien générer en C++ ou un autre langage, le vrai nom de package
java est indiqué par l'option "java_package", de la même façon pour le nom de
classe qui va tout encapsuler qui sera "java_outer_classname"
Pour plus d'information sur protobuf, je vous invite à consulter sa
page google code.
Le générateur protobuf générera un fichier HelloProto.java, qui permettra de
manipuler les Hello : création via pattern builder, encodage/désencodage,
... Le "vrai" Hello sera encapuslé au sein de ce dernier. Comme je disais, je
génère le java par le ant plugin de maven :
<plugin>
<groupid>org.apache.maven.plugins</groupId>
<artifactid>maven-antrun-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<target>
<exec executable='protoc'>
<arg value='--java_out=src/main/java' />
<arg value='src/main/resources/Hello.proto' />
</exec>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
et bien sûr des dépendances protobuf
<dependency>
<groupid>com.google.protobuf</groupId>
<artifactid>protobuf-java</artifactId>
<version>2.4.0a</version>
</dependency>
Le contrat de test sera assez proche de se que nous avions dans les tests
précédents :
@Test
public void shoulReplyHello(){
// Given
String message = "Hello";
String name ="Nicolas";
Hello hello = HelloProto.Hello.newBuilder().setName(name).setMessage(message).build();
when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(hello);
// When
ClientResponse response = resource().path("hello").path(name).get(ClientResponse.class);
// Then
verify(helloServiceMock).saysHelloToSomeone(name);
assertThat(response.getClientResponseStatus()).isEqualTo(Status.OK);
assertThat(response.getType().toString()).isEqualTo("application/x-protobuf");
Hello entity = response.getEntity(Hello.class);
assertThat(entity).isNotNull().isEqualTo(hello);
}
La resource REST, elle aussi va peut évoluer :
@Path("hello")
@Singleton
@Produces("application/x-protobuf")
public class HelloResource {
@Inject
private HelloService helloService;
@GET
@Path("/{name}")
public Hello reply(@PathParam("name") String name){
return helloService.saysHelloToSomeone(name);
}
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
}
La difficulté à laquelle il faut se confronter, c'est que Jersey ne permet pas
de gérer de base le protobuf… Pas grave, on va s'occuper de faire le lien entre
l'encodage/désencodage de protobuf et Jersey.
Commençons par le reader qui s'occupe de désencoder le protobuff. Pour celà,
nous devons implémenter l'interface MessageBodyReader où nous aurons du code
technique protobuf.
@Provider
@Consumes("application/x-protobuf")
@Singleton
public class ProtobufMessageBodyReader implements MessageBodyReader<Message> {
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType) {
return Message.class.isAssignableFrom(type);
}
public Message readFrom(Class<Message> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
try {
Method newBuilder = type.getMethod("newBuilder");
GeneratedMessage.Builder builder = (GeneratedMessage.Builder) newBuilder.invoke(type);
return builder.mergeFrom(entityStream).build();
} catch (Exception e) {
throw new WebApplicationException(e);
}
}
}
C'est par le content type "application/x-protobuf" que JAX-RS fera matcher le
type le reader/writer à l'entrée/sortie de la resource. Pour l'encodage, c'est
l'interface MessageBodyWriter qu'il faut implémenter.
@Provider
@Produces("application/x-protobuf")
@Singleton
public class ProtobufMessageBodyWriter implements MessageBodyWriter<Message> {
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType) {
return Message.class.isAssignableFrom(type);
}
private Map<Object, byte[]> buffer = new WeakHashMap<Object, byte[]>();
public long getSize(Message m, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
m.writeTo(baos);
} catch (IOException e) {
return -1;
}
byte[] bytes = baos.toByteArray();
buffer.put(m, bytes);
return bytes.length;
}
public void writeTo(Message m, Class type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
entityStream.write(buffer.remove(m));
}
}
La configuration de test, quant à elle sera un peu plus complexe, car il faut
que la partie cliente puisse désencoder toute seule le
protobuf :
protected AppDescriptor configure() {
ClientConfig clientConfig = new DefaultClientConfig();
clientConfig.getClasses().add(ProtobufMessageBodyReader.class);
clientConfig.getClasses().add(ProtobufMessageBodyWriter.class);
injector = Guice.createInjector(new ServletModule() {
@Override
protected void configureServlets() {
bind(ProtobufMessageBodyReader.class);
bind(ProtobufMessageBodyWriter.class);
bind(HelloResource.class);
serve("/*").with(GuiceContainer.class);
}
});
return new WebAppDescriptor.Builder()
.contextListenerClass(GuiceTestConfig.class)
.filterClass(GuiceFilter.class)
.clientConfig(clientConfig)
.servletPath("/")
.build();
}
Le code complet est ici.
