insideIT.fr : le blog des architectes IT de SFEIR

Aller au contenu | Aller au menu | Aller à la recherche

jeudi 23 juin 2011

Du Protobuf dans mon Jersey

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.

mardi 26 avril 2011

Manipulation de la Remote Api GAE avec Guice/Objectify

Avec la version 1.4.3 de app engine, arrive une api qui jusque là était disponible seulement en python : la remote api. Celle çi permet de se connecter de façon sécurisée au datastore afin d’y insérer des données par exemple.
La documentation, bien que courte, donne de bonnes explications sur la façon de mettre en place. Mais, c’est le cas simple... Si au sein de mon application app engine, j’ai mis en place Guice et Objectify, je me retrouve confronté à 2 problèmes :
1/ Pour fonctionner ma servlet doit être déclarée dans la configuration Guice.
2/ Je manipule des objets métier géré par Objectify et non pas des Entity.

Imaginons je possède l'objet "métier" suivant :

public class Hello {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String message;
private String name;
public Hello(){
}
public Hello(String message, String name) {
super();
this.message = message;
this.name = name;
}
// Getter & Setter/hascode/equals methods ...
}

Je vais devoir modifier mon GuiceServletConfig pour la mapper avec la servlet. Aussi une servlet déclarée doit Guice doit être un singleton.

public class GuiceServletConfig extends GuiceServletContextListener {
@Override
protected Injector getInjector() {
return Guice.createInjector(new ServletModule() {
@Override
protected void configureServlets() {
bind(RemoteApiServlet.class).in(Singleton.class);
serve("/remote_api").with(RemoteApiServlet.class);
// D'autres bindings ou serve ...
}
});
}
}

Le 1er problème est ainsi résolu.
Il ne me reste plus qu'à transformer mes Hello en Entity. Une petite ballade dans le code source permet de trouver la solution.


public static Entity helloToEntity(Hello hello) {
Objectify ofy = ObjectifyService.begin();
EntityMetadata<Hello> metadata = factory.getMetadataForEntity(hello);
return metadata.toEntity(hello, ofy);
}


Il ne reste plus qu'à écrire un petit batch d'alimentation :


public static void main(String[] args) throws IOException {
RemoteApiOptions options = new RemoteApiOptions()
.server("maSuperApplication.appspot.com", 443)
.credentials("monEmailAMoi@gmail.com", "monMotDePasse");
RemoteApiInstaller installer = new RemoteApiInstaller();
installer.install(options);
List<Entity> hellos = Lists.newArrayList(helloToEntity(new Hello("Hello", "Nicolas")),
helloToEntity(new Hello("Bonjour", "Vincent")),
helloToEntity(new Hello("Salut", "Guillaume")),
helloToEntity(new Hello("Enchanté", "M. Gendal")));
try {
DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
for (Entity hello : hellos) {
Key key = ds.put(hello);
System.out.println("Hello key=" + key);
}
} finally {
installer.uninstall();
}
}


En console, nous obtiendrons :
Hello key=Hello(3001)
Hello key=Hello(4001)
Hello key=Hello(1002)
Hello key=Hello(5001)

Et dans l'interface web du datastore, nous voyons bien nos 4 Hellos :

samedi 16 avril 2011

Du Jersey, du Guice et de l'App Engine 3/3

Et pour finir cette série d'article, nous allons nous intéresser à la génération de JSon avec Jersey et bien sûr à sa testabilité.

Le xml, c'est bien gentil, mais dans des échanges REST ça peut être un peu lourd, surtout si le consommateur est un appareil mobile. La génération JSon avec Jersey peut s'appuyer sur JAX-B. Et oui, c'est justement pour cela que l'on s'en est occupé dans le précédant article. Le mapping, lui, ne change pas.

La resource ne nécessite qu'un petit changement :

 @GET
 @Path("{name}")
 @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) 
 public Hello reply(@PathParam("name") String name){
  return helloService.saysHelloToSomeone(name);
 }

Comme on est sympa, on permet de renvoyer soit du Json, soit du Xml, c'est le consommateur qui décide. Par défaut, c'est le 1er format qui est choisit, soit le JSon.

La génération JSon va nécessiter un peu de configuration, principalement à cause du type de JSon généré. C'est le ContextResolver qui va s'occuper de ça :

@Provider
@Singleton
public class JAXBContextResolver implements ContextResolver<JAXBContext> {

 /** Package that contains object that can be mapped */
 private static final String JAXB_OBJECT_PACKAGE = Hello.class.getPackage().getName();

 private final JAXBContext context;

 public JAXBContextResolver() throws Exception {
  this.context = new JSONJAXBContext(JSONConfiguration.natural().build(), 
                                     JAXB_OBJECT_PACKAGE);
 }

 @Override
 public JAXBContext getContext(Class<?> objectType) {
  if(objectType.getPackage().getName().equals(JAXB_OBJECT_PACKAGE)){
   return context;
         }
  return null;
 }
}

Ici le type de JSon souhaité est le natural. Cet objet doit être passés dans le même package que les resources, il profitera ainsi lui aussi de la découverte automatique au démarrage de Guice. Ce resolver n'est pas obligatoire, sans lui, le JSon généré est par défaut en mode mapped.

La configuration des tests, va devoir évoluer un peu pour prendre en compte notre génération en ode natural. La méthode configure() devient :

@Override
 protected AppDescriptor configure() {
  ClientConfig clientConfig = new DefaultClientConfig();
  clientConfig.getClasses().add(JAXBContextResolver.class);
  injector = Guice.createInjector(new ServletModule() {
   @Override
   protected void configureServlets() {
    bind(getTestingResourceClass());
    bind(JAXBContextResolver.class);
    serve("/*").with(GuiceContainer.class);
   }
  }); 
  return new WebAppDescriptor.Builder()
           .contextListenerClass(GuiceTestConfig.class)
           .filterClass(GuiceFilter.class)
           .clientConfig(clientConfig)
           .servletPath("/")
           .build();
 }

Ainsi du coté serveur comme du coté client, les échanges seront dans le même format. Nos tests deviendront :

@Test
 public void shoulReplyHelloInXml(){
  doShoulReplyHello(MediaType.APPLICATION_XML_TYPE);
 }
 
 @Test
 public void shoulReplyHelloInJson(){
  doShoulReplyHello(MediaType.APPLICATION_JSON_TYPE);
 } 
 
 private void doShoulReplyHello(MediaType type){
  String message = "Hello";
  String name ="Nicolas";
  Hello hello = new Hello(message, name);
  when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(hello);
  
  ClientResponse response = resource().path("hello").path(name)
                                      .accept(type).get(ClientResponse.class);
  
  verify(helloServiceMock).saysHelloToSomeone(name);
  assertThat(response.getClientResponseStatus()).isEqualTo(Status.OK);
  assertThat(response.getType()).isEqualTo(type);
  Hello entity = response.getEntity(Hello.class);
  assertThat(entity).isNotNull().isEqualTo(hello);  
  
 } 

Une des différenciation entre les types de JSon générés se fait sur la façon dont sont écrites les listes. En mode natural, nous avons par exemple : objet1, objet2, ... avec des objet {"attributA":"valeurA", ....}

Imaginons que nous avons une autre ressource qui par grande politesse retourne 2 Hellos :

@Path("doublehello")
@Singleton
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class DoubleHelloResource {
 
 @Inject
 private HelloService helloService;
 
 @GET
 @Path("/{name}")
 public List<Hello> reply(@PathParam("name") String name){
  List<Hello> hellos = new ArrayList<Hello>();
  hellos.add(helloService.saysHelloToSomeone(name));
  hellos.add(helloService.saysHelloToSomeone(name));
  return hellos;
 }
 
}

Pour vérifier sa bonne génération, nous aurions le test suivant :

@Test
 public void shoudHaveTwoHello(){
  String message = "Hello";
  String name ="Nicolas";
  when(helloServiceMock.saysHelloToSomeone("Nicolas"))
                        .thenReturn(new Hello(message, name)); 
  ClientResponse response = resource().path("doublehello").path(name)
                                      .get(ClientResponse.class);
  assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
  assertThat(response.getType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE);
  List<hello> hellos = response.getEntity(new GenericType<List<Hello>>(){});
  assertThat(hellos).isNotNull().hasSize(2);
 }
 
 @Test
 public void shoudBeInNaturalJson(){
  String message = "Hello";
  String name ="Nicolas";
  when(helloServiceMock.saysHelloToSomeone("Nicolas"))
                                .thenReturn(new Hello(message, name)); 
  ClientResponse response = resource().path("doublehello").path(name)
                                      .get(ClientResponse.class);
  assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
  assertThat(response.getType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE);
  String hellos = response.getEntity(String.class);
  assertThat(hellos).isEqualTo(naturalHelloJSon(message, name));
 }
 
 public String naturalHelloJSon(String message, String name){
  StringBuilder sb = new StringBuilder();
  sb.append("[{\"message\":\"").append(message)
    .append("\",\"name\":\"").append(name).append("\"},");
  sb.append("{\"message\":\"").append(message)
    .append("\",\"name\":\"").append(name).append("\"}]");
  return sb.toString();
 }

Même s'il est un format intéressant, le JSon souffre d'un problème lié au javascript : celui du cross-domain qui fait que l'on ne peut pas interroger un autre domain que celui de la page web. <a href="http://en.wikipedia.org/wiki/JSONP">JSonP</a> permet d'évincer cette contrainte.

Jersey permet aussi de générer ce type de réponse mais un peu moins facilement. Nous allons créer une nouvelle méthode pour ce type de réponse :

 @GET
 @Path("{name}.jsonp")
 @Produces("application/x-javascript")
 public JSONWithPadding replyWithJsonP(@PathParam("name") String name,
           @QueryParam("callback") @DefaultValue(CALLBACK_DEFAULT_NAME) String callback){
  Hello hello = helloService.saysHelloToSomeone(name);
  return new JSONWithPadding(hello, callback);
 } 

Son test reste dans l'optique des précédents :

@Test
 public void shoudBeJsonpWithCallbackNameParam(){
  String message = "Hello";
  String name ="Nicolas";
  when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(new Hello(message, name));
  String callbackName = "monCallback";
  
  ClientResponse response = resource().path("hello").path(name+".jsonp")
                                      .queryParam("callback", callbackName)
                                        .get(ClientResponse.class);
  assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
  assertThat(response.getType().toString()).isEqualTo("application/x-javascript");
  assertThat(response.getEntity(String.class)).isNotNull().startsWith(callbackName);
 }

Je n'ai pas malheureusement pas trouvé comment unmarshmaller ce message.

Et voilà, ce tour d'horizon est fini, amusez vous bien avec ces quelques technos. Comme à chaque fois, le code source est disponible

mercredi 4 juin 2008

Google App Engine ouvert à tous

Il y a quelques temps, nous vous parlions ici même de Google App Engine, un projet de Google permettant  aux développeurs d’héberger leurs applications WEB sur ses infrastructures.

Les applications ainsi déployées bénéficient de la même qualité de service et de la montée en charge que l’on connaît pour des applications telles que Gmail, Google Finance, …
Le programme était officiellement ouvert à près de 10 000 développeurs depuis le début du mois d’avril.

Chez SFEIR, nous avions organisé une petite rencontre pour présenter cette technologie aux personnes le désirant. A cette occasion, Didier Girard avait porté son jeu KeyboardWarrior (application GWT) pour fonctionner sur Google App Engine. Curieux d’étudier la capacité de montée en charge de son application, il a organisé il y a une semaine environ un "load test". Les résultats ont été publiés ici.

Les retours des utilisateurs en termes de disponibilité de l’application sont diverses, aucun problème pour certains, un peu de lenteur pour d’autres. Quoi qu’il en soit l’application a tout de même supporté un pic de près de 23 requêtes par seconde pendant une minute, ce qui est plutôt pas mal pour un hébergement gratuit.

Mercredi dernier, Google a annoncé sur un de ses blogs l’ouverture de Google App Engine à tous, de ce fait vous pourrez bientôt tester et vous faire votre propre idée. Toutefois, avant de vous lancer dans la réalisation de votre première application, vous devriez peut être jeter un œil à quelques articles. En voici quelques uns qui expliquent comment faire en sorte que votre application soit scalable et comment il faut penser les applications pour Google App Engine. En effet certains concepts de cette technologie nécessitent que l’on pense autrement. C’est notamment le cas pour l’API datastore (persistance des données), pour laquelle il faut oublier les concepts des bases de données relationnelles.

Google a également annoncé quelle allait être la politique de prix pratiquée pour l’hébergement d’applications sur Google App Engine. Tout d’abord, l’offre d’entrée reste gratuite avec 500Mb de stockage et assez de bande passante et de CPU pour supporter l’équivalent de 5 millions de pages vues pas mois.

Les tarifs se présentent ensuite ainsi :

-10 à 12 cents par heure de CPU

-15 à 18 cents par GB de données par mois

-11 à 13 cents par GB de bande passante sortante

-9 à 11 cents par GB de bande passante entrante

Cette annonce est accompagnée par la mise à disposition de deux nouvelles APIs, une pour la manipulation d’images et une autre pour gérer un cache afin de diminuer les requêtes du datastore.

Je vous invite à suivre les retours des personnes qui assistent aux Google I/O sur Google App Engine, nous en saurons sûrement plus.