JEE 6 : JEE enfin productif, léger et testable ? Partie 1
Java Enterprise Edition 6 est sortie depuis quelques temps déjà. Cette plateforme a été évoquée dans de nombreuses conférences à Devoxx 2010 et un certain nombre de livres traitent déjà du sujet. Les retours sont à peu près tous les mêmes : Java EE devient enfin plus léger et productif ! Les serveurs d'applications, en particuliers Glassfish et JBoss, implémentent maintenant nativement les spécifications de JEE6 et la blogosphère a déjà bien décortiqué chacune d'entres elles. De plus une partie des nouveautés provient de frameworks JEE déjà éprouvées tels que JBoss Seam. Bref démarrer un projet avec JEE 6 semble aujourd'hui tout à fait viable voir attrayant.
Néanmoins même si on peut admirer les nouvelles possibilités de la plateforme, un certain nombre de questions peuvent se poser aux personnes responsables de projets basés sur Java. On peut se demander ou est l'intérêt pour un projet de migrer ou de démarrer sur cette nouvelle version, sous entendu :
- Qu'est ce que ça va m'apporter par rapport à un JEE5 déjà bien éprouvé et testé sur des projets d'envergures ?
- Quel sera le coût de migration pour mon projet existant, ou quel sera le coût d'apprentissage de cette plateforme par rapport à d'autres ?
J'ai donc voulu répondre par moi même à ces questions et me suis lancé dans le développement d'un PoC pour tester les fonctionnalités principales de ce nouvel environnement. Comme presque tous les articles et livres traitant de JEE 6 se basent sur Glassfish 3 et que j'ai vu plus de JBoss chez les clients d'OCTO, j'ai choisi ce dernier pour mes tests. Tous les exemples tournent sans donc sans problèmes sur JBoss AS 6.
Une des grosses difficultés rencontrées avec JEE 5 était la testabilité et plus concrètement les tests d'intégration qui nécessitaient toujours plus ou moins de déployer un conteneur lourd ralentissant fortement l'exécution des tests. L'article s'agrémente donc d'un certain nombre d'exemples de tests JUnit (présentant surtout la façon de procéder) pour vous faciliter l'écriture des vôtres.
Le contexte
Notre application aura pour rôle de gérer des formations. Nous aurons donc deux types de données : Formation et Participant. Bien que l'application soit amenée à évoluer l'article se limitera à ce contexte simplifié pour faciliter la lecture de l'article.
Modèle de données
JEE6 apporte une nouvelle version de JPA, JPA2 qui apporte son lot de nouveautés. L'API JPA a peu évolué. En effet les améliorations se sont essentiellement concentrées sur deux axes : la testabilité et l'API Criteria.
Commençons donc par définir nos deux entités :
@Entity
@NamedQuery(name = "findAllParticipants", query = "SELECT f FROM Participant f")
public class Participant {
@Id @GeneratedValue
private Long id;
@Column(length = 3)
private String trigram;
private Boolean animator;
public Boolean getAnimator() {
return animator;
}
public void setAnimator(Boolean animator) {
this.animator = animator;
}
@ManyToMany
@JoinTable(name = "participants_formations", joinColumns = @JoinColumn(name = "participant_fk"), inverseJoinColumns = @JoinColumn(name = "formation_fk"))
private List<Formation> formationsDone;
// Getters and setters
}
@Entity
@NamedQuery(name = "findAllFormations", query = "SELECT f FROM Formation f")
public class Formation {
@Id @GeneratedValue
private Long id;
@ManyToMany(mappedBy = "formationsDone", fetch = FetchType.EAGER, cascade = CascadeType.MERGE)
private List<Participant> participants;
@OneToOne
private Participant animator;
@Column(nullable = false)
private String title;
private Date date;
// getters and setters
}
Jusque là rien de bien nouveau par rapport à ce que l'on connaissait, on n'est donc pas perdu. Commençons donc par écrire quelques tests unitaires. Pour cela nous aurons besoin d'une base de données. Dans mon cas j'ai choisi derby mais le choix est libre (HSQL, H2...)
Il nous faut pour cela une unité de persistance à laquelle on ajoute nos classes :
<persistence -unit name="formation" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>com.octo.octoformation.entities.Participant</class>
<class>com.octo.octoformation.entities.Formation</class>
<properties>
<property name="hibernate.archive.autodetection" value="class" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
<property name="hibernate.connection.url"
value="jdbc:derby://localhost:1527/octoformation;create=true" />
<property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.EmbeddedDriver" />
<property name="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect" />
</properties>
</persistence>
Ainsi nous allons pouvoir créer un EntityManager et faire un test d'intégration sur la persistence de nos deux entités :
public class TestFormationTest {
private static EntityManagerFactory emf;
private static EntityManager em;
private static EntityTransaction tx;
@BeforeClass
public static void initEntityManager() {
emf = Persistence.createEntityManagerFactory("formation2");
em = emf.createEntityManager();
}
@AfterClass
public static void closeEntityManager() throws SQLException {
em.close();
emf.close();
}
@Before
public void initTransaction() {
tx = em.getTransaction();
}
@Test
public void testCreateParticipant() {
Participant participant = new Participant();
participant.setTrigram("MRO");
tx.begin();
em.persist(participant);
tx.commit();
assertNotNull(participant.getId());
}
Le test est partiel ici car on ne teste que la persistence de Participant mais vous avez compris l'idée : on peut dans un test JUnit facilement récupérer un entityManager et l'utiliser pour persister des entités JPA2. L'idée ici est que si la persistance a fonctionné, l'entité se verra attribuer un Id auto-généré : on teste donc que celui-ci n'est pas nul.
Accès aux données, EJB 3.1
Maintenant que nous avons vérifié le fonctionnement de ce modèle de données, nous allons donc développer une couche d'accès aux données transactionelle, et donc utiliser le nouveau modèle d'EJB : EJB 3.1 et plus particulièrement une sous partie de ce modèle : EJB Lite. Lite pourquoi ? Car la nouvelle spécification EJB permet maintenant de n'utiliser qu'un scope réduit des EJB (Session Stateless et Stateful, Singleton, Intercepteurs et Transactions). Et si vous ne dépassez pas le scope de cette sous-spécification, vous pourrez alors construire votre projet sous forme de war et non plus uniquement sous forme d'ear. Ceci s'avère une vraie avancée et laisse imaginer la possibilité de faire tourner des EJB simples dans un tomcat un jour. Le modèle Singleton évoqué est un nouveau type d'EJB que je ne détaillerais pas ici mais il s'agit d'un EJB respectant le pattern Singleton (instance unique pour la jvm).
Autre nouveauté d'EJB 3.1 : il n'est plus nécessaire de déclarer d'interfaces. En effet, le modèle Java assez communément utilisé (une interface même si on a qu'une implémentation) est de plus en plus controversé et s'avérait totalement inutile dans la majorité des cas d'utilisation des EJB : l'utilisation d'EJB Remote est très rare et il est tout aussi rare de trouver plusieurs implémentations du même EJB. Bref la possibilité est toujours présente : heureusement car cela s'avère tout de même parfois indispensable mais plus obligatoire. De plus les performances des EJB ont été améliorées. Et rassurez vous le modèle d'EJB 2 qui a terrorisé tant de développeurs était déjà mort avec EJB 3 et est définitivement enterré avec EJB 3.1.
Voilà donc le code de notre EJB :
@Stateless
public class FormationManager {
@PersistenceContext(unitName = "formation")
private EntityManager em;
public Formation findFormationById(Long id) {
return em.find(Formation.class, id);
}
public Formation createFormation(Formation form) {
em.persist(form);
return form;
}
public Formation updateFormation(Formation form) {
em.merge(form);
return form;
}
public List<formation> getAllFormations() {
return em.createNamedQuery("findAllFormations").getResultList();
}
}
On voit donc que l'écriture d'un EJB a encore été simplifiée : l'injection de l'entityManager est très simple et on écrit très peu de code.
Néanmoins, la spécification imposant une datasource JTA il nous faudra changer notre unité de persistence lors du déploiement. Je vous conseille donc d'avoir plusieurs persistence.xml en fonctions de l'environnement: par exemple test, développement et production.
<persistence-unit name="formation" transaction-type="JTA">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>com.octo.octoformation.entities.Participant</class>
<class>com.octo.octoformation.entities.Formation</class>
<properties>
<property name="hibernate.archive.autodetection" value="class" />
<!-- properties -->
</properties>
</persistence-unit>
<persistence-unit name="formation" transaction-type="JTA">
<jta -data-source>java:/DerbyDS</jta>
<class>com.octo.octoformation.entities.Participant</class>
<class>com.octo.octoformation.entities.Formation</class>
<properties>
<property name="hibernate.archive.autodetection" value="class" />
<property name="hibernate.format_sql" value="true" />
Personnellement je n'ai jamais été un grand fan de SQL, c'est pourquoi les ORM et entre autre Hibernate ont été une libération pour moi. Néanmoins le HQL et ses dérivés se rapprochent toujours fortement du SQL. Jusque là contrairement à .NET ou Rails, Java se contentait de nous permettre de faire du pseudo-objet dans une chaîne de caractères pour construire une requête. Voyons donc maintenant ce que nous permet l'API de Critiera dont je vous parlais au début de l'article. Admettons que l'on veuille récupérer tous les participants qui ont été animateurs (getAllAnimators) ou bien que l'on souhaite retrouver un participant via son identifiant (findByTrigram), ici son trigramme : trois lettres le désignant de manière unique.
Rien ne vaut un bon exemple pour s'expliquer :
@Stateless
public class ParticipantManager {
Logger logger = LoggerFactory.getLogger("com.octo.octoformation.services.ejb.ParticipantManager");
@PersistenceContext(unitName = "formation")
private EntityManager em;
public Participant createParticipant(Participant participant) {
em.persist(participant);
return participant;
}
public void deleteParticipant(Participant participant) {
em.remove(participant);
}
public Participant updateParticipant(Participant participant) {
em.merge(participant);
return participant;
}
public Participant findByTrigram(String trigram) {
Participant p;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Participant> cq = cb.createQuery(Participant.class);
Root<Participant> participant = cq.from(Participant.class);
cq.select(participant).where(
cb.equal(participant.<String> get("trigram"), trigram));
p = em.createQuery(cq).getSingleResult();
logger.info("Trying to get participant by trigram");
return p;
}
public List<Participant> getAllAnimators() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Participant> cq = cb.createQuery(Participant.class);
Root<Participant> participant = cq.from(Participant.class);
cq.select(participant).where(
cb.isTrue(participant.<Boolean> get("animator")));
List<Participant> participantList = em.createQuery(cq).getResultList();
return participantList;
}
}
On voit donc qu'on peut écrire des requêtes en mode objet complet : on ne fait qu'appeler des méthodes de l'objet CriteriaQuery en définissant nos critères au fur et à mesure. A part pour les requêtes extrêmement complexes il ne sera donc plus nécessaire d'écrire de requêtes SQL ou HQL sous forme de String qu'il faudra exécuter pour les tester. Ce type d'écriture permet presque de garantir que quelque chose qui compile fonctionnera. Le résultat ne sera pas forcément celui escompté (on peut toujours se tromper dans sa requête) mais peu de chances de crash de requêtes si le compilateur laisse passer la requête. La majorité des clauses SQL classiques sont disponibles :
- join(...)
- orderBy(...)
- distinct(true)
- aving(...)
- groupBy(...)
- ...
Testabilité des EJB
Un des gros points noirs des EJB jusqu'à aujourd'hui était leur testabilité. Il fallait soit démarrer un serveur d'application et lancer les tests d'intégration ensuite, soit utiliser un conteneur d'EJB embarqué (type jboss embedded) très long / complexe à configurer. JEE 6 simplifie désormais cela grâce essentiellement à une classe : EJBContainer. Le seul pré-requis est d'ajouter les bonnes dépendances à votre projet, dans le cas présent j'ai utilisé glassfish embedded et l'implémentation EJB liée. Aucune configuration n'a été nécessaire.
Il suffit donc d'ajouter les éléments suivants dans votre pom (si vous utilisez maven) :
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.ejb</artifactId>
<version>3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.extras</groupId>
<artifactId>glassfish-embedded-all</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
L'exemple suivant montre l'utilisation d'EJBContainer (très simple) et l'appel des méthodes précédemment écrites dans l'EJB FormationManager. Par rapport à ce que l'on pouvait voir dans JEE 5 la simplification est énorme.
public class TestFormationManagerTest {
private static EJBContainer ec;
private static Context ctx;
@BeforeClass
public static void initContainer() throws Exception {
Map<String, Object> props = new HashMap<String, Object>();
props.put(EJBContainer.MODULES, new File [] {new File ("target/classes")});
props.put(EJBContainer.APP_NAME, "octoformation");
props.put(EJBContainer.PROVIDER, "org.glassfish.ejb.embedded.EJBContainerProviderImpl");
ec = EJBContainer.createEJBContainer(props);
ctx = ec.getContext();
}
@AfterClass
public static void closeContainer() throws Exception {
ec.close();
}
@Test
public void createFormation() throws Exception {
Formation formation = new Formation();
formation.setTitle("JEE 6");
formation.setDescription("Learning jee6");
FormationManager formationManager = (FormationManager) ctx.lookup("java:global/octoformation/FormationManager");
formation = formationManager.createFormation(formation);
assertNotNull(formation.getId());
}
@Test
public void testCriteriaOnParticipant() throws Exception {
Participant mro = new Participant();
mro.setTrigram("MRO");
Participant jja = new Participant();
jja.setTrigram("JJA");
jja.setAnimator(Boolean.TRUE);
Formation formation = new Formation();
formation.setTitle("JEE 6");
formation.setAnimator(jja);
formation.setDescription("Learning jee6");
formation.addParticipant(jja);
formation.addParticipant(mro);
FormationManager formationManager = (FormationManager) ctx.lookup("java:global/octoformation/FormationManager");
ParticipantManager participantManager = (ParticipantManager) ctx.lookup("java:global/octoformation/ParticipantManager");
participantManager.createParticipant(mro);
participantManager.createParticipant(jja);
formationManager.createFormation(formation);
assertNotNull(jja.getId());
assertNotNull(mro.getId());
assertNotNull(formation.getId());
List<Participant> animators = participantManager.getAllAnimators();
assertNotNull(animators);
assertTrue(animators.size() == 1);
assertTrue(animators.get(0).getTrigram().equals("JJA"));
}
L'initialisation du conteneur est importante, c'est elle qui va vous permettre d'injecter l'EJB via JNDI dans votre test. Deux choses importantes à noter :
- il faut peu de code technique pour tester notre EJB
- le test est plutôt rapide à s'exécuter (ici moins de 10s avec deux tests en plus de ceux présent dans l'exemple de code ci dessus)
Pour conclure
Pour conclure cette première partie on peut donc citer les éléments suivants :
Ecrire des services transactionnels (EJB) en JEE 6 est plus productif qu'en JEE5 :
- L'interface n'est plus obligatoire : on écrit moins de code
- Il est beaucoup plus facile de faire des tests d'intégration il y aura donc moins de risques de regressions
- L'accès aux données via JPA est aussi nettement plus aisé : L'écriture de requête se fait en pure objet et non en pseudo SQL. Un langage orienté objet est beaucoup plus naturel pour un développeur d'aujourd'hui, il passera donc certainement moins de temps à rédiger ses requêtes qu'en SQL.
Le coût de migration est relativement faible : en fait il n'y a rien de particulièrement différent entre JEE5 et JEE6, il y a surtout moins de code à écrire et moins de configuration à mettre en place.
Dans un second article nous verrons comment encore gagner en productivité et testabilité via l'exposition de nos données par des WebServices REST et une interface JSF2.