jeudi 15 juillet 2010

Jouons avec CDI et Wicket

Après une série d'articles consacrés à Play framework, je vous propose aujourd'hui d'étudier l'intégration de CDI (context and dependency injection) avec Wicket. Voyons comment ces 2 frameworks peuvent être mariés et quelle souplesse CDI va nous apporter lors du développement d'une application web avec Wicket.


CDI est une des spécifications de Java EE6. Cette spec apporte des solutions pour l'injection de dépendance et la gestion des scopes (requêtes, session, application et le petit nouveau, le scope conversation).
Nous allons utiliser Weld, l'implémentation de JBoss dérivée de Seam, qui est également l'implémentation de référence de CDI.

Voici les dépendances maven à utiliser :


            javax.enterprise
            cdi-api
            provided
            1.0
        

        
            javax.annotation
            jsr250-api
            provided
            1.0
        

        
            org.jboss.weld
            weld-wicket
            1.0.1-Final
        
      
        
            org.apache.wicket
            wicket
            1.4.9
        


Si vous utilisez JBoss ou Glassfish, pas besoin de configuration supplémentaire, Weld sera pris en compte automatiquement par le serveur.

Sinon, si vous utilisez par exemple Tomcat ou Jetty, ajoutez ce listener à votre fichier web.xml :


      org.jboss.weld.environment.servlet.Listener


Avec ces serveurs, ajoutez aussi ceci à vos dépendances maven :


            org.jboss.weld.servlet
            weld-servlet
            1.0.1-Final
        

et supprimez la mention "provided" pour les librairies de Weld et de la JSR 250.


Voyons maintenant un peu de code Java. Comme vous le savez peut être, Wicket est un framework stateful. Il garde dans la session http un certain nombre d'informations, comme la valeurs des attributs définis dans une page web. C'est le framework qui gère la durée de rétention de ces informations. Il le fait de manière plutôt intelligente, par exemple pour assurer le bon fonctionnement du bouton retour du navigateur. Cependant on aimerait parfois pouvoir gérer plus finement ce mécanisme. Nous allons pouvoir le faire grâce à CDI et aux conversations.

Définition d'un bean CDI :

@ConversationScoped
public class ConversationBean implements Serializable
{
   private int value;
   
   @Inject Conversation conversation;
   

   @PostConstruct
   public void init(){
      this.value = -1;
   }

    void x2() {
        value = value * 2;
    }


    void begin() {
        conversation.begin();
        this.value = 1;
    }

    void end() {
        conversation.end();
    }
    

    public int getValue() {
        return value;
    }
}

Ce bean a une portée "ConversationScoped". Cela signifie que son état sera conservé côté serveur durant la durée de la conversation. La méthode begin() permet de démarrer la conversation , end() permet de la fermer. Il contient uniquement un attribut value, initialisé à -1 à la création de l'objet (grâce à l'annotation @PostConstruct), et initialisé à 1 au démarrage d'une conversation. Lé méthode x2() multiplie par 2 l'entier value.

Créons maintenant une page côté Wicket pour accéder à notre bean CDI :

Partie Java:
@Inject ConversationBean cbean;

   public TestPage(){
       final Label number = new Label("number", new Model());
       number.setOutputMarkupId(true);
       add(number);
       AjaxLink begin = new AjaxLink("begin") {

            @Override
            public void onClick(AjaxRequestTarget target) {
              cbean.begin();
              number.setDefaultModelObject(cbean.getValue());
              target.addComponent(number);
            }
        };
        add(begin);
         AjaxLink x2 = new AjaxLink("x2") {

            @Override
            public void onClick(AjaxRequestTarget target) {
               cbean.x2();
               number.setDefaultModelObject(cbean.getValue());
               target.addComponent(number);
            }
        };
        add(x2);
         AjaxLink end = new AjaxLink("end") {

            @Override
            public void onClick(AjaxRequestTarget target) {
              cbean.end();
              setResponsePage(TestPage.class);
            }
        };
        add(end);
}
On injecte notre ConversationBean avec l'annotation @Inject. Notez que l'on n'a plus besoin de Spring pour injecter nos dépendance lorsque l'on utilise Weld avec Wicket.


Partie html :
<html>
   <head></head>
   <body>
      <h1>Test</h1>
         <span wicket:id="number"/><br/>
         <a wicket:id="begin">Start</a>-<a wicket:id="x2">x2</a>-<a wicket:id="end">End</a>
   </body>
</html>


On obtient la page suivante :

Au démarrage on retrouve la valeur d'initialisation de l'attribut value : -1.
En cliquant sur x2 plusieurs fois de suite, on voit qu'on ne peut pas faire plusieurs multiplications de suite. On reste bloquer à la valeur -2. C'est normal, le bean CDI ne conserve pas d'état côté serveur tant que la conversation n'est pas démarrée. On recommence donc à chaque fois la même opération avec un attribut value initialisé à -1.
Par contre si on clique sur Start, la conversation démarre, value est positionné à 1 et chaque clic sur x2 va doubler la valeur affichée : 1, 2, 4, 8, 16 etc.
En cliquant sur End, on peut mettre fin à cette conversation, libérer le bean CDI en mémoire sur le serveur et recharger la page.

Grâce à CDI, on a pu prendre la main sur la rétention côté serveur de l'état du label number de notre page Wicket.
Dans un  prochain exemple nous verrons comment mettre ça en pratique dans un cas d'utilisation réel, avec un wizard, des objets persistants, des transactions...

5 commentaires:

SinuS a dit…

Salut ...
Tu pourrais fournir les sources de cet exemple s'il te plait ? :)

je connait un petit bout sur wicket que j'ai abandonné ... surement à cause d'une mauvaise compréhension de la DI et d'un bug qui se produisait avec le bouton "Back" du navigateur en utilisant l'ancienne lib wicket-javaee pour faire appel à des ejb session et jpa ...

Bref, le problème c'est que là en déployant une archive war générée avec maven (m2eclipse) sous glassfish3 correspondant à ce que tu as mis comme source, ça me donne un :
--------------------------------
java.lang.NullPointerException
at tn.stce.app.TestPage$1.onClick(TestPage.java:21)
--------------------------------
c a d "cbean.begin();"

et donc j'aurai bien aimé savoir où se situe mon erreur.

Bref, sinon ça me donne un espoir (grâce à la découverte de CDI et de Weld que je viens juste de rencontrer sur ton blog) de pouvoir utilisé wicket en prod à la place d'etre obligé de toujours développer en php pour mes client :s

Loïc Descotte a dit…

Salut! Visiblement ton problème vient plutôt de ton EJB lui même, ça va etre difficile de trouver comme ça d'où ça vient.
Par contre je ne pense pas avoir encore les sources de cet article, je regarde ce soir mais je ne te garantie rien. Mais en fait il n'y a rien de plus que ce qui est écrit ici!

Loïc Descotte a dit…

Et au fait si tu veux faire des appli web en java je te conseille vraiment de regarder play framework, super puissant et qui permet de conserver la simplicité de PHP

Pour commencer il y a ce tuto
Et après tu peux regarder les article sur le blog ici

SinuS a dit…

Merci pour le lien qui me parait très utile :)
je pense que je vais me mettre bientot à playframework enfin ... si j'arrive à le faire fonctionner correctement avec eclipse et sous glassfish :)

Sinon, j'ai trouvé la source de mon probleme si ça t’intéresse : apparement @ConversationScoped ne fonctionne pas sous glassfish du moins c'est ce que j'ai cru comprendre sur http://seamframework.org/Community/ConversationScopeNotWorking.

J'ai découvert la source du pb en essayant numberguess example for Apache Wicket, et vu qu'il fonctionnait sous mon glassfish 3.0.1, j'ai essayer de remplacer @ConversationScoped de ton exemple par @SessionScoped et là miracle :D

enfin ... j'espère que ça pourra éviter que certains ne terminent de s'arracher les cheveux ... moi il m'en reste plus bcp :s

Loïc Descotte a dit…

Salut Sinus,
Pour Play tu n'as pas besoin de t'embêter avec un serveur tout est automatique et inclus.

Merci d'avoir remonté ton bug, apparemment il a été corrigé dans les versions récentes de Weld, c'est étrange...

Enregistrer un commentaire