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

mardi 22 mars 2011

Retour d'expérience d'un projet GWT : 7 Mise en place de Gin sur le projet (7/8)

Le contexte:

Le projet a pour but d'améliorer l'ergonomie et l'usabilité du back office d’un produit en cours de développement réalisé en collaboration avec SFEIR

Dans le cadre de ce projet, la version de GWT utilisée passe de la 1.7.1 à la 2.0.3 afin de bénéficier des nouvelles fonctionnalités offertes par cette version. De nouveaux pattern de développement ont également été mis en place au cours de ce projet.

Voici une série d'article sur les nouveautés de GWT 2, les choix d'architectures, et bibliothèques utilisés qui font part de notre retour d'expérience sur le sujet. Ces articles ont été écris par David Aboulkheir, Patrice de Saint Steban et Cyril Lakech

  1. Nouveautés de GWT 2.0
  2. UiBinder, enfin une forte collaboration entre le designer et le développeur
  3. Intégration facile de maquette Html en GWT 2
  4. Architecture Modèle-Vue-Presenteur
  5. Implémentation Modèle-Vue-Présenteur
  6. Ecrire des tests unitaires avec Mockito
  7. Mise en place de Gin sur le projet
  8. Internationalisation

Définitions de GIN/GUICE

Définition de Gin# : “GIN (GWT INjection) brings automatic dependency injection (DI) to Google Web Toolkit client-side code. GIN is built on top of Guice and uses (a subset of) Guice's binding language. (See GuiceCompatibility for details.) By using GWT's compile-time Generator support, GIN has little-to-no runtime overhead compared to manual DI.”

Définition de Guice# : “Guice is a dependency injection (DI) container”

Pour résumer, Gin est une librairie permettant de faire de l’injection de dépendance par paramètrage sur des projets GWT. Le but de l’injection de dépendance est de se passer du code permettant de relier les différents modules de l’application entre eux en le centralisant, ce qui améliore la lisibilité du code. Pour cela, il suffit de créer une factory ainsi que la configuration du binding des différents modules.

Configuration de Gin

Ajout de Gin en dépendance du projet (Maven / Classpath)

Il faut que les librairies Gin et Guice soient dans les dépendances Maven / Classpath du projet. Pour cela, il faut ajouter dans le pom.xml les dépendances suivantes :

     <dependency>
          <groupId>com.googlecode.gwt.inject</groupId>
          <artifactId>gin</artifactId>
          <version>1.0</version>
          <scope>provided</scope>
      </dependency>
      <dependency>
          <groupId>com.google.inject</groupId>
          <artifactId>guice</artifactId>
          <version>2.0</version>
          <scope>provided</scope>
      </dependency>

Le scope provided est utilisé pour que ces librairies ne soient pas embarquées dans le war généré par le projet qui les utilise.

Ajout de Gin en dépendance du projet (GWT)

Il faut que les librairies Gin et Guice soient dans les dépendances GWT du projet. Pour cela, il faut ajouter dans le Module.gwt.xml la dépendance suivante :

<module>
...
<inherits name="com.google.gwt.inject.Inject"/>
...
</module>

Création d’un injecteur de dépendances Gin

Pour pouvoir récupérer les modules dont les dépendances sont gérées par Gin, il suffit de créer une interface annotée dans laquelle on défini pour chacun des modules de l’application une méthode permettant de le récupérer.

@GinModules(ModuleInjectorModule.class)
public interface ModuleGInjector extends Ginjector {
  AppPresenter getAppPresenter();
  FooterPresenter getFooterPresenter();
  DisplayFactory getDisplayFactory();
  GlobalLayout getGlobalLayout();
}

Par exemple, dans notre cas, nous avons besoin de Gin pour gérer les dépendances du Presenter AppPresenter, nous avons donc défini une méthode getAppPresenter qui retourne un AppPresenter. En appelant cette méthode, Gin va s’occuper d’instancier automatiquement le Presenter en lui injectant les dépendances telles qu’elles sont configurées dans le module de configuration du binding Gin.

L’interface est annotée avec l’annotation @GinModules qui prend en paramètre la class du module de configuration du binding Gin.

Création du module configuration du binding Gin

Gin a besoin de connaitre la configuration du binding des modules de l’application. Pour cela, il faut créer une classe dans laquelle les liaisons entre les modules sont définies.

public class ModuleInjectorModule extends AbstractGinModule {
  @Override
  protected void configure() {
      //Say to GIN to bind the Display to the FooterView and
      //use a Singleton scope for the application
      //This singleton view will be injected to the MainLayout
      bind(FooterPresenter.Display.class).to(FooterView.class).in(Singleton.class);
      bind(GlobalLayout.class).in(Singleton.class);
      bind(AppPresenter.Display.class).to(GlobalLayout.class).in(Singleton.class);
  }

  /**
   * This Provider is used by GIN to inject all HandlerManager field
   * Use the legacy GlobalEventBus.get()
   */
  @Provides
  HandlerManager getGlobalEventBus() {
      return GlobalEventBus.get();
  }
}

Par exemple, dans notre cas, le FooterPresenter définie sa vue via l’inner classe Display qui est implémentée par la FooterView et cette classe n’a qu’une seule instance dans l’application. Ce qui se traduit par la ligne suivante :

bind(FooterPresenter.Display.class).to(FooterView.class)
                                  .in(Singleton.class);

Il est également possible de fournir a Gin des classes dont il ne gère pas le cycle de vie. Par exemple, dans notre cas, comme nous avons progressivement migré vers Gin, et pendant cette phase de migration, certaines classes devaient être a la fois utilisée par Gin et également être utilisée par le système d’injection de dépendance manuel utilisé précédemment.

C’est le cas pour notre Bus d'évènement par exemple, c’est pourquoi nous avons ajouté cette méthode :

  @Provides
  HandlerManager getGlobalEventBus() {
      return GlobalEventBus.get();
  }

Instancier le Gin Injector dans l’application et l’utiliser

Pour utiliser l’injecteur de dépendance Gin, il suffit d’ajouter dans le Module GWT de l’application un attribut static puis de l’utiliser comme suit:

public class Module implements EntryPoint {
   private final ModuleGInjector injector =  GWT.create(ModuleGInjector.class); 
   public void onModuleLoad() {
      ...
      MyPresenter myPresenter = injector.getMyPresenter();
      ...
   }
}

Il est nécessaire de faire ce GWT.create une fois dans l'application pour que GIN puisse être appeler au moins une fois.

Aller plus loin avec Gin

Gin permet de binder les classes GWT utilisant le deferred binding comme les interfaces des constantes et messages pour l'internationalisation, ce qui facilite leur utilisation car c’est Gin qui s’occupera de les instanciers pour nous.

Gin permet également de binder les services RPC GWT pour faciliter leur utilisation.

Gin permet d’utiliser des fonctionnalités de Guice dans GWT mais toutes ne sont pas accessibles à cause des limitations de GWT.

Voir le tableau récapitulatif des fonctionnailités disponibles

Voir la documentation de Guice pour plus d’informations

jeudi 19 février 2009

Intégration de Guice avec les RemoteServiceServlet de GWT

La dernière version snapshot de Guice intégre des fonctionnalités intéressantes pour les Servlets, notamment de nouveaux Builders qui viennent du projet warp-servlet et qui permettent de configurer des mappings de Servlet ou de Filter directement dans un module. Vous pouvez obtenir les sources de l’exemple décrit dans cet article ici.

Au lieu de définir nos RemoteServiceServlet dans le web.xml (ce qui est désormais le cas avec GWT 1.6), nous allons déclarer un GuiceServletContextListener et un GuiceFilter :


 1 <?xmlversion="1.0"encoding="UTF-8"?>
2 <web-app>
3
4
<!-- Default page to serve -->
5 <welcome-file-list>
6 <welcome-filecda.html</welcome-file>
7 </welcome-file-list>
8
9
<listener>
10 <listener-classcom.sfeir.server.CdaGuiceServletConfig
11 </listener-class>
12 </listener>
13
14
<filter>
15 <filter-nameguiceFilter</filter-name>
16 <filter-classcom.google.inject.servlet.GuiceFilter
17 </filter-class>
18 </filter>
19
20
<filter-mapping>
21 <filter-nameguiceFilter</filter-name>
22 <url-pattern/*</url-pattern>
23 </filter-mapping>
24
25
</web-app

GuiceServletContextListener crée une instance d’Injector avec la méthode getInjector(), nous pouvons alors définir notre module en redéfinissant cette méthode :


 1 public class CdaGuiceServletConfig extends GuiceServletContextListener
 2 {
 3     @Override
 4     protected Injector getInjector()
 5     {
 6         return Guice.createInjector(new ServletModule()
 7         {
 8             @Override
 9             protectedvoid configureServlets()
10             {
11                 serve("/cda/echo").with(EchoServiceImpl.class);
12                 serve("/cda/spreadsheet").with(SpreadSheetServiceImpl.class);
13 ...

Nous utilisons pour cela une classe anonyme qui hérite de ServletModule : les méthodes «serve» et «filter» de cette dernière nous permettent de faire l’équivalent de huit lignes de XML pour déclarer une Servlet ou un Filter en une seule ligne de Java.

«serve» fonctionne directement avec les RemoteServiceServlet de GWT (il faut néanmoins ajouter y l’annotation Singleton), par contre les scopes Session et Request de la classe ServletScopes ne peuvent pas être utilisés : nous allons devoir hériter de la classe RemoteServiceServlet pour y ajouter notre propre Scope Session et par la même occasion ajouter un champ «injector» qui contiendra l’objet Injector créé par notre classe CdaGuiceServletConfig.

Dans notre ServletModule, nous rajoutons un appel à «bind» qui associe l’interface SpreadsheetManager à une implémentation :


 1                 bind(SpreadsheetManager.class).
2 to(SpreadsheetManagerImpl.class);

Ainsi l’appel de getInstance de l’object Injector avec SpreadsheetManager.class en paramètre renverra une instance de SpreadsheetManagerImpl.


 1 @Singleton
2 public class SpreadSheetServiceImpl extends GuiceRPCServlet implements SpreadSheetService
3 {
4
5
@Override
6 public List<String> fetchSpreadsheetList()
7 {
8 SpreadsheetManager manager = injector.getInstance(SpreadsheetManager.class);
9 ...

Par défaut, l’Injector de Guice renvoie toujours une nouvelle instance. Nous pouvons également utiliser l’annotation Singleton ou dans le cas présent notre propre annotation GwtSessionScoped afin qu’une même instance soit réutilisée, par exemple le temps d’une session.


 1                 bindScope(GwtSessionScoped.class,
2 GuiceRPCServlet.SESSION);
3 bind(GoogleService.class).toProvider(
4 SpreadsheetServiceProvider.class).
5 in(GwtSessionScoped.class);

Ainsi les champs injectés de type GoogleService seront créés une première fois puis récupérés dans la session en cours. Ce mécanisme fonctionne quand l’Injector de Guice est appelé, il est donc exclu de l’utiliser sur des champs d’une RemoteServiceServlet.


 1 public class SpreadsheetManagerImpl implements SpreadsheetManager
2 {
3 @Inject
4 private GoogleService service;
5
6
@Inject
7 @Named("user")
8 private String username;
9 @Inject
10 @Named("pass")
11 private String password;
12
13
@Override
14 public void authenticate()
15 {
16 try
17 {
18 service.setUserCredentials(username,password);
19 } ...

L’annotation Named est utile pour injecter objets de même type sans avoir à créer de nouvelle annotation. La méthode statique «named» crée cette annotation pour l’utiliser dans les instructions de binding.


 1                 bindConstant().annotatedWith(named("user")).
2 to("cappelle.florent@gmail.com");
3 bindConstant().annotatedWith(named("pass")).
4 to("*************");

Finalement, Guice permet aussi d’ajouter des fonctions transverses à nos services en «bindant» un intercepteur sur un ensemble de méthodes :


 1                 bindInterceptor(subclassesOf(SpreadsheetManager.class),
2 returns(subclassesOf(List.class)),
3 new LoggingInterceptor());

Ici, LoggingInterceptor est appliqué aux classes qui héritent de SpreadsheetManager et plus particulièrement aux méthodes de ces classes dont le retour est de type List. Les classes d’interception doivent implémenter MethodInterceptor de l’API aopalliance.

Vous aurez sans doute deviné que LoggingInterceptor fait un log des appels de méthode, la sécurité et les transactions étant les autres exemples typiques d’utilisation des MethodInterceptor.

En conclusion, Google Guice constitue une alternative intéressante à Spring pour l’injection de dépendance. Dans certains cas, on pourra également utiliser les méthodes de la classe SpringIntegration afin de réutiliser une configuration XML existante.

Liens :