22 février 2012

JAX-RS et CDI sont dans un bateau...


Ceux qui ont eu la chance (?) de bosser avec moi savent que, en termes d'architecture des applications Java, je défends le concept de modèle métier riche, par opposition au Anemic Model Domain.

L'idée part d'un constat simple : on a souvent un beau modèle objet du domaine métier, bien adapté pour accueillir les données de l'application, et on déporte toute la logique de leur gestion dans des classes techniques spécialisées, qu'on appelle des "services" parce que c'est un mot qui peut dire tout et n'importe quoi alors pourquoi se priver ?

On se retrouve donc avec un modèle métier qui ne fait rien, noyé dans des couches de services métier, de services web, de services techniques (celui-là je l'adore) et de DAO (qu'on a pas appelé service pour une fois).

Avec JPA et ses annotations, on peut rapidement réduire la couche DAO à peu de choses, voir l'éliminer purement et simplement comme je l'ai fait avec Fonzie (un jour, promis, je reprendrai le développement ce projet ... quand j'aurais le temps). JPA étant déjà une abstraction de la persistance, je ne vois aucun intérêt à rajouter une couche DAO pour faire une requête JPA. En quoi se faire injecter un DAO est plus "propre" que se faire injecter un PersistenceContext et utiliser des NamedQueries ?

Pour aller plus loin, c'est à dire faire que nos objets métier portent eux-même la logique métier, on est rapidement confronté à un problème : nos objets ont besoin de faire appel à d'autres objets "collaborateurs" pour faire autre chose que de la conversion franc-euro. Spring propose depuis 2006 une solution via l'annotation @Configurable et l'instrumentation par AspectJ. Pour l'avoir utilisé sur un gros projet qui se reconnaitra, ça marche bien mais c'est un peu encombrant pour les développeurs (oui, AJDT, je pense à toi). Depuis, Java EE 6 est sorti et propose CDI.

Pour des besoins internes, j'ai développé une application d'intégration de services REST déployée sur RUN@Cloud. N'en déplaise à mes collègues, pas de Scala, d'Erlang ou de Ruby ici, mais du Java EE 6 brut de spécification, histoire de vérifier si Antonio raconte des bobards pendant ses conférences.

J'ai un objet métier Ticket, que je veux exposer avec JAX-RS :

@Path("/ticket/{id}")
public class Ticket {

    private int id;

    public Ticket(@PathParam("id") int it) {
        this.id = id;
        // Load data from id;
    }
}

Cela fonctionne très bien tant qu'il s'agit de faire un System.out, maintenant j'aimerais faire des choses plus concrètes, nécessitant l'utilisation d'un autre composant.

Après quelques errements, et avec l'aide de Paul, j'ai compris comment activer CDI pour obtenir l'injection de mes dépendances. Comme j'ai peur de ne pas m'en souvenir pour la prochaine fois, et que ça peut servir à d'autres, voici la recette : d'abord placer le fameux fichier bean.xml vide sous WEB-INF, puis modifier mon code pour CDI-fier ma classe JAX-RS :

@Path("/ticket/{id}")
@RequestScoped
public class Ticket {

    @PathParam("id")
    private int id;

    @Inject
    private AnotherComponent foobar;

    @PostConstruct
    public void load() throws IOException {
        // Load data from id;
    }

    @POST
    @Produces("text/plain")
    public String notify() throws Exception {
        foobar.sendNotification(id, getEmail());
        return "ok";
    } 
}



petite explication :
Dans un premier temps j'avais utilisé un constructeur prenant en paramètre l'ID du chemin REST et chargeant les données. Couplé avec CDI, je dois annoter ce constructeur @Inject pour qu'il soit utilisé à la place du constructeur par défaut, mais CDI dans ce cas cherche à m'injecter l'ID. Je ne sais pas si c'est un bug de RestEasy-cdi dans JBoss, toujours est-il que j'ai du suivre le plan B.


Je récupère donc l'ID par injection d'attribut depuis la requête REST. L'initialisation du bean est confiée à un @PostConstruct, ce qui en termes de gestion du cycle de vie est nettement plus explicite que de tout mettre dans le constructeur.


Et voila, mon Ticket est désormais un objet de mon domaine métier, exposé comme service REST, qui sur un POST gère un traitement métier via des composants avec qui il collabore et qu'il obtient par injection de dépendance. Pas besoin de TicketService, TicketResource ou autre classe purement artificielle juste introduite pour respecter un joli modèle en couches.

5 commentaires:

waddle a dit…

Trop bien, comme ça si je veux changer de framework MVC, faire du SOAP je dois changer mes beans métier. Et puis si je fais un batch, aucun problème, j'embarque toutes les libs web pour avoir le droit de manipuler des beans persistés.
T'as raison, le découplage c'est has been comme notion.

Seb a dit…

Dans certains cas le modèle métier n'est utilisé que par l'application web frontale, donc dans ces cas là why not, ça évite l' over-engineering !
Pour les autres cas le découplage peut se discuter, tout comme le fait de faire des batchs avec le même modèle métier que la couche web (surtout avec un orm).
C'est un éternel débat, j'ai déjà vu le cas sur un projet avec Pojo Vs EMF côté serveur et résultat des courses le serveur n'a pas bougé mais on a pu placer une brique supplémentaire de conversion, pénible à maintenir car fortement liée au modèle (pas d'introspection pour cause de performances).

nicolas deloof a dit…

waddle: je peux aussi utiliser mon "Ticket" dans un contexte batch sans couche web, il n'y aura juste personne pour interpréter les annotations JAS-RS.

En 15 ans, je n'ai eu qu'une fois à partager le modèle métier avec un batch. Je ne dis pas que le découplage c'est naze, mais qu'appliqué partout et tout le temps juste parce que c'est bien ça crée des applis compliquées pour rien, avec des durées de dev qui sont propres au mode Java. Ruby on Rails ne s'encombre pas de ces contraintes et se porte très bien, merci pour lui.

Quand tu ajoute une couche SOAP, si tu ne change pas ton modèle, tu te coltine des centaines de lignes de recopie de tes beans vers to code généré wsl2java. Pas sur que ce soit plus productif

Colin Hebert a dit…

Je ne suis pas trop fan de l'idée de coupler les objets métier avec quoi que ce soit.
Même avoir des annotations JAX-B dedans est quelque peu ennuyeux.

C'est pour ça que j'aime particulièrement le système de "Mix-ins" par Jackson pour découpler la description du Marshalling/unMarshalling (http://www.cowtowncoder.com/blog/archives/2009/08/entry_305.html) de l'objet lui même.

Comme ça, si je veux proposer une API, je peux directement partager des objets métiers sans apporter des dépendances à la noix avec.


Sur un autre domaine, faire tourner le service REST avec l'injection n'est pas si "simple" que ça vu que JAX-RS 1.1 n'a pas été prévu pour marcher avec JSR-330. J'ai du faire quelque chose de ce genre, il y a quelques jours, les solutions pour palier à ce problème sont très dépendantes des implementations de DI et JAX-RS.

Mais bon au final une petite configuration dans maven avec un profil permet de garder le projet libre d'implémentations particulières.

Typiquement pour Jersey + Spring, il faut un package de contrib (com.sun.jersey.contribs:jersey-spring) + une servlet spéciale dans le web.xml.


Enfin ça serait fixé par JAX-RS 2 (http://jcp.org/en/jsr/detail?id=339)

Loïc Descotte a dit…

Etant anti-anemic moi aussi je ne peux que aimer :)
Pour JEE6 on ne peut malheureusement pas injecter facilement un entité-manager dans une entité métier, donc les couches services restent dures à éviter (sauf si on utilise fonzy bien sur)
Pour ça j'aime bien l'approche Play qui permet d'accéder de n'importe où à la "couche" persistence de manière statique... tout en gardant l'aspect transactionnel - ce qui n'est pas aisé à faire avec JEE6 ...