Accueil > DynaResume, Java > Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step1]

Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step1]


Dans le billet précédant [step0], j’ai présenté ce que je souhaitais effectuer dans les billets intitulés Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM. Pour rappel, mon idée est d’expliquer pas à pas comment créer une application cliente eclipse RCP qui communiquera avec des services hébérgés sur un serveur OSGI. L’application RCP affichera une liste d’utilisateurs User récupérée via un service UserService qui sera hébérgé sur le serveur OSGI. Dans ce billet nous n’allons pas encore faire d’OSGI, mais nous allons créer un client (Java main) qui va communiquer avec un service UserService qui retournera une liste de User. Ce service qui est une interface sera récupéré via une factory de services ServicesFactory. Nous découperons chacune de ces couches (Client/Service/Domain (POJO User)) dans un projet Java distinct.

Voici un schéma de ce que nous allons effectuer dans ce billet :

Ce schéma met en évidence trois projets Java :

Les dépendances entre les projets sont gérées classiquement via le Java Build Path du projet. Nous verrons en fin de ce billet les 2 problèmes courants que nous rencontrons avec ce type de dépendance classique :

Vous pouvez télécharger les projets org.dynaresume_step1.zip et org.dynaresume_step1-commons-lang.zip (zip qui contient les projets Java qui utilisent 2 versions de la librairie Apache commans-lang*.jar et qui montre en évidence le problème de ClassLoader) présentés dans ce billet.

I .Prérequis

Pour la rédaction de mes billets, j’ai téléchargé la dernière version d’Eclipse qui actuellement est Eclipse Galileo (Eclipse 3.5). Je conseille plus exactement de télécharger la distribution Eclipse for RCP/Plug-in Developers (183 MB ). Cette distribution est faite pour développer des applications RCP, les sources de l’API RCP y sont disponibles. Cependant je pense que dans un billet je vais devoir utiliser une application WEB classique. Un serveur (Tomcat) sera nécéssaire et j’utiliserais sûrement la distribution Eclipse IDE for Java EE Developers (189 MB ). Dans la suite des billets nous aurrons aussi besoin d’une Base de Données (mais j’y reviendrais).

II .org.dynaresume.domain

Le projet org.dynaresume.domain est le domaine applicatif du projet, il contiendra tous les POJO de notre projet.

II-A .Création Java Project

Ici nous allons créer le projet Java org.dynaresume.domain pour cela sélectionnez le menu File/New/Other puis Java Project :

Cliquez sur Next et renseignez le champs Project name avec org.dynaresume.domain :

Cliquez sur Finish le projet Java est créé.

II-B .Classe User

Créez la classe org.dynaresume.domain.User comme suit :

package org.dynaresume.domain;

public class User {

	private String login;
	private String password;

	public User(String login, String password) {
		setLogin(login);
		setPassword(password);
	}

	public String getLogin() {
		return login;
	}

	public void setLogin(String login) {
		this.login = login;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}
}

III .org.dynaresume.services

Nous allons créer le projet Java org.dynaresume.services qui sera notre couche service qui contiendra le service UserService qui retournera la liste des User. Pour cela, créez le projet Java org.dynaresume.services. Ce projet doit faire référence au projet Java org.dynaresume.domain car il doit retourner une liste de POJO User.

III-A .Java Build Path/Add Project

Ici nous allons ajouter au projet org.dynaresume.services une dépendance sur le projet org.dynaresume.domain. Pour cela sélectionnez le projet org.dynaresume.services, puis cliquez sur le bouton droit. Sélectionnez l’item Properties puis le noeud Java Build Path et sélectionnez l’onglet Projects :

Cliquez sur le bouton Add. Ceci ouvre uen boîte de dialogue qui propose tous les projets du workspace dont on peut dépendre :

Sélectionnez le projet org.dynaresume.domain, puis cliquez sur OK. La dépendance est maintenant créée :

III-B .Interface UserService

Créez l’interface org.dynaresume.services.UserService comme suit :

package org.dynaresume.services;

import java.util.Collection;

import org.dynaresume.domain.User;

public interface UserService {

	Collection<User> findAllUsers();
}

III-C .Implémentation UserServiceImpl

Créez la classe org.dynaresume.services.impl.UserServiceImpl comme suit :

package org.dynaresume.services.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.dynaresume.domain.User;
import org.dynaresume.services.UserService;

public class UserServiceImpl implements UserService {

	private Collection<User> users = null;

	public Collection<User> findAllUsers() {
		if (users == null) {
			users = createUsers();
		}
		return users;
	}

	private Collection<User> createUsers() {
		List<User> users = new ArrayList<User>();
		users.add(new User("angelo", ""));
		users.add(new User("djo", ""));
		users.add(new User("keulkeul", ""));
		users.add(new User("pascal", ""));
		return users;
	}
}

On peut remarquer que les implémentations des services se retrouvent dans le packages *.impl. Ceci est une bonne règle de nommage qui permet de distinguer facilement les interfaces aux implémentations. Nous verrons dans le prochain billet que cette règle de nommage nous servira aussi pour rendre inaccéssible cette classe aux autres Bundles (projet Java) dans un environnement OSGI.

III-D .Factory de Services

Créez la factory de services org.dynaresume.services.ServicesFactory comme suit :

package org.dynaresume.services;

import org.dynaresume.services.impl.UserServiceImpl;

public class ServicesFactory {

	private static ServicesFactory INSTANCE = new ServicesFactory();
	private UserService userService = null;

	private ServicesFactory() {

	}

	public static ServicesFactory getInstance() {
		return INSTANCE;
	}

	public UserService getUserService() {
		if (userService == null) {
			userService = createUserService();
		}
		return userService;
	}

	private UserService createUserService() {
		UserService userService = new UserServiceImpl();
		return userService;
	}
}

La factory de services est un singleton qui a une méthode ServicesFactory#getUserService() qui retourne une implémentation de l’interface UserService.

IV .org.dynaresume.simplemainclient

Créez le projet Java org.dynaresume.simplemainclient. Ajoutez les dépendances aux 2 projets org.dynaresume.domain et org.dynaresume.services .

Créez la classe org.dynaresume.simplemainclient.SimpleMainClient comme suit :

package org.dynaresume.simplemainclient;

import java.util.Collection;

import org.dynaresume.domain.User;
import org.dynaresume.services.ServicesFactory;
import org.dynaresume.services.UserService;

public class SimpleMainClient {

	public static void main(String[] args) {
		UserService userService = ServicesFactory.getInstance().getUserService();
		Collection<User> users = userService.findAllUsers();

		for (User user : users) {
			System.out.println("User [login=" + user.getLogin() + ", password=" + user.getPassword() + "]");
		}
	}
}

Ce Main Java utilise la méthode UserService#findAllUsers() pour afficher la liste de User. A cette étape on peut lancer le client qui appelle le service qui retourne la liste d’utilisateurs.

IV-A .Run As – Java Application

Pour lancer le main de la classe SimpleMainClient, sélectionnez la classe, puis ouvrez le menu contextuel (en cliquant sur le bouton droit) et cliquez sur Run As / Java Application :

La console eclipse doit afficher la liste des utilisateurs :

User [login=angelo, password=]
User [login=djo, password=]
User [login=keulkeul, password=]
User [login=pascal, password=]

V .Problème Java Build Path

Les dépendances entre les projets est effectuées via le Java Build Path classique qui lorsque l’on lance le Main de SimpleMainClient construit le classpath avec ces projets. Nous allons voir les 2 problèmes avec ce type de dépendance.

V-A .UserServiceImpl Non protégé

Dans la classe org.dynaresume.simplemainclient.SimpleMainClient, si vous tentez d’utiliser la classe UserServiceImpl, vous pourrez constater que l’autocomplétion propose cette classe :

Cette classe ne devrait jamais être utilisée par une classe autre que celle de la factory de Service. A ce stade la classe SimpleMainClient peut utiliser UserServiceImpl. Ceci est dangereux surtout si la version suivante du projet org.dynaresume.services effectue un refactoring en renommant UserServiceImpl ou en changeant ces méthodes.

En pur Java il est impossible de régler ce problème si ce n’est de déclarer la classe UserServiceImpl en private et de la mettre dans le même package que la classe ServicesFactory. Mais cette solution ne permet plus de mettre les classes dans des packages souhaités ce qui nous ramenerait à avoir un seul niveau de package pour toutes les interfaces et implémentations.

V-B .ClassLoader unique

Un ClassLoader permet comme son nom l’indique de charger des classes . Dans notre cas les 3 projets partagent le même ClassLoader. Pour s’en rendre compte nous allons afficher le ClassLoader dans la console Eclipse. Modifiez la classe SimpleMainClient comme suit :

public static void main(String[] args) {
  System.out.println("SimpleMainClient ClassLoader : " + SimpleMainClient.class.getClassLoader());
...
}

et modifiez la classe ServicesFactory comme suit :

private ServicesFactory() {
  System.out.println("ServicesFactory ClassLoader : " + ServicesFactory.class.getClassLoader());
}

Relancez et vous verrez dans la console :

SimpleMainClient ClassLoader : sun.misc.Launcher$AppClassLoader@133056f
ServicesFactory ClassLoader : sun.misc.Launcher$AppClassLoader@133056f

qui montre que les 2 classes partagent le même ClassLoader.

Ceci est problématique car chaque projet ne peut pas avoir sa propre version de jar d’une librairie donnée. Nous allons illustrer le problème en utilisant 2 versions différentes de la librairie Apache commons-lang*.jar dans 2 projets :

Ce schéma montre que :

  • le projet Java Client org.dynaresume.simplemainclient pointera sur la librairie commons-lang-1.0.jar de version 1.0. Cette librairie est stockée dans le repertoire lib (qu’il faut créer) du projet.
  • le projet Java Services org.dynaresume.services pointera sur la librairie commons-lang-2.4.jar de version 2.4. Cette librairie est stockée dans le repertoire lib (qu’il faut créer) du projet.

Vous pouvez télécharger le zip org.dynaresume_step1-commons-lang.zip qui contient les projets avec les librairies commans-lang*.jar.

La différence entre ces 2 versions de commans-lang*.jar est que la classe utilitaire org.apache.commons.lang.StringUtils possède une méthode StringUtils#isBlank(String String) dans la version 2.4 et pas dans la version 1.0. Nous souhaitons utiliser StringUtils#isBlank(String String) dans la couche Services.

V-B-1 .Java Build Path/Add Jar (commons-lang-2.4.jar)

Ici nous allons ajouter au projet org.dynaresume.services la librairie jar commons-lang-2.4.jar. Pour cela sélectionnez le projet org.dynaresume.services puis cliquez sur le bouton droit. Sélectionnez l’item Properties puis le noeud Java Build Path puis sélectionnez l’onglet Libraries :

Cliquez sur le bouton Add JARs…

Puis cliquez sur OK, la librairie est ajoutée :

V-B-2 .Utilisation commons dans la couche Services

Modifiez le code de ServicesFactory pour utiliser la méthode utilitaire StringUtils#isBlank(String str) de commons-lang-2.4.jar :

...
private ServicesFactory() {
  StringUtils.isBlank("");
}
...

V-B-3 .Java Build Path/Add Jar (commons-lang-1.0.jar)

Ajoutez la librairie commons-lang-1.0.jar dans le projet org.dynaresume.simplemainclient

Puis changez l’ordre de chargement des librairies pour que la librairie commons-lang-1.0.jar soit chargé AVANT le projet org.dynaresume.services (Services), autrement dit avant le chargement de la libraire commons-lang-2.4.jar :

Relancez le Main SimpleMainClient et vous aurrez l’erreur suivante :

Exception in thread "main" java.lang.NoSuchMethodError: org.apache.commons.lang.StringUtils.isBlank(Ljava/lang/String;)Z
at org.dynaresume.services.ServicesFactory.<init>(ServicesFactory.java:23)
at org.dynaresume.services.ServicesFactory.<clinit>(ServicesFactory.java:19)
at org.dynaresume.simplemainclient.SimpleMainClient.main(SimpleMainClient.java:23)

La méthode StringUtils.isBlank(String str) n’est pas trouvée car la librairie commons-lang-1.0.jar est chargée AVANT la librairie commons-lang-2.4.jar. Il est donc impossible de faire cohabiter 2 versions différentes avec des dépendances classiques. Ce problème se trouve aussi dans les applications WEB lorsque l’application WEB a une version d’un JAR et que le serveur (Tomcat…) a une autre version du même JAR.

VI .Conclusion

Dans ce billet nous avons mis en place un client java qui appelle un service. Nous avons vu les 2 problemes posés par des dependances classiques que nous règlerons dans le prochain billet à l’aide de OSGI.

Vous pouvez lire le billet suivant [step2].

Catégories :DynaResume, Java Étiquettes : ,