juin
2008
Après avoir gouté aux joies de la DI (Dependancy Injection) de Spring via annotations, je n’en peux plus me passer.
Seulement, je n’ai fait celà que dans un contexte Web, et plus précisément avec JSF.
Dans un tel environnement, on est en mode managé, c’est à dire qu’on crée rien à la main, et le tout est configurable, ce qui permet à Spring de s’intégrer parfaitement dans l’equation, en offrant d’une manière totalement transparente la DI.
Par contre, dans un environnement standard (SE), les choses ne sont pas aussi simples …
En effet, dans un tel environnement, c’est à la charge du développeur de créer ses classes. Or, si on veut faire bénéficier ces classes de DI, il faut que Spring le fasse.
Plus précisément, le point qui pose problème ici est le point final qui consomme les dépendances.
C’est à dire, que si on a les 3 classes suivantes:
- model.dao.TestDao: représente un DAO fictif
- model.service.TestService: représente un service fictif, et ayant une dépendance vers TestDao
- Et enfin, une classe client.TestConsumer qui a une dépendance vers TestService
Et voici leur code:
model.dao.TestDao:
package model.dao;
@Repository
public class TestDao {
//Une méthode fictive qui ne fait que retourner 10 ... impressionnant !
public int getValue(){
return 10;
}
}
model.service.TestService:
package model.service;
@Service
public class TestService {
@Resource
private TestDao testDao;
//Une méthode fictive qui ne fait que retourner le double
// de ce que retourne le DAO ... impressionnant !
public int getValue() {
return 2*testDao.getValue();
}
}
client.TestConsumer:
package client;
@Component
public class TestConsumer {
@Resource
private TestService testService;
//Une méthode fictive qui ne fait que retourner ce que retourne le service
public int getValue() {
return testService.getValue();
}
}
Rien de spécial à noter, seulement ques ces classes sont annotés avec @Repository, @Service et @Component pour les déclarer comme mangés par Spring, et que les dépendances sont déclarés via l’annotation @Resource (la classe TestService a besoin d’un TestDao, et Testconsumer a besoin d’un TestService).
Et voici le fichier de configuration applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="model,client" />
</beans>
On déclare juste à Spring que les classes des packages model et client (ainsi que leurs fils) doivent être scannés pour vois si elles sont annotés avec un des stéréotypes (@Repository, @Service, @Controller, @Componenet), et si elle le sont, elle seront ajoutés en tant que Spring Beans.
Pour que la DI marche correctement, il faut obligatoirement que Spring ait la main sur la classe finale que l’utilisateur va manipuler, c’est à dire TestConsumer.
La solution directe à ce problème serait de créer à la main une instance d’ApplicationContext, et de récupérer depuis elle une instance de TestConsumer, ce qui donne:
public class Test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
"applicationContext.xml", Test.class);
TestConsumer testConsumer = (TestConsumer) applicationContext
.getBean("testConsumer");
System.out.println(testConsumer.getValue());
}
}
Ce qui afficherait 20 à la console. Génial !
Seulement, c’est pénible à mourir à écrire … imaginer une application Swing avec des centaines de classes (et de dépendances) avec du code pareil … Pas beau, pas beau du tout.
C’est pour celà que j’ai consacré dun peu de temps à jouer avec l’API et la documentation de Spring pour voir comment faire pour rendre la chose moins pénible.
Et je viens de pondre un truc qui pourrait en effet régler ça ! Merci l’API super pensé et ouvert de Spring.
Voici le code de la bête:
package spring.di.in.se.can.be.easy;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Injector {
private static ApplicationContext applicationContext;
public static void init(String... configLocations) {
applicationContext = new ClassPathXmlApplicationContext(configLocations);
}
public static void init(Class clazz, String... configLocations) {
applicationContext = new ClassPathXmlApplicationContext(
configLocations, clazz);
}
public static void configure(Object object) {
applicationContext.getAutowireCapableBeanFactory().autowireBean(object);
}
}
Super simple comme vous le notez: juste deux initialiseurs (pas constructeurs, vu qu’on est dans le domaine statique là) et une méthode configure qui prend un objet comme paramètre et qui est composé d’une seule ligne de code.
Maintenat, ce qui compte est de montrer comment l’utiliser.
Pour revenir à l’exemple précédent, on va pouvoir se débarasser du code superflu de l’initialisation de l’applicationContext ainsi que du cast.
Voici ce que ça donne:
public class Test {
public static void main(String[] args) {
Injector.init(Test.class, "applicationContext.xml");
TestConsumer cl = new TestConsumer();
System.out.println(cl.getValue());
}
}
Vous remarquerez que l’on peut maitnenat créer à la main (avec new) les classe consommatrices de dépendances (TestConsumer).
Il faut juste initialiser Injector au tout début de la méthode main avec le chemin de fichier xml de configuration de Spring.
Cependant, il faut aussi modifier la classe qui consomme les dépendance (Testconsumer), comme ceci:
public class TestConsumer {
@Resource
private TestService testService;
public TestConsumer() {
Injector.configure(this);
}
public int getValue() {
return testService.getValue();
}
}
Ce qui a changé est:
- Je n’ai plus à la déclarer en tant que composant Spring (plus d’annotation @Component au niveau de la classe) mais je garde les déclarations de dépendances via @Resource.
- Dans le constructeur, j’invoque la méthode configure de la classe Injector en lui passant this (instance courante) comme paramètre.
Et c’est tout ! Le tour est joué.
Ca induit un lien de ses classes avec la classe Injector, et ça impose de ne pas oublier d’appeler Injector.init(this) dans chaque constructeur, mais c’est beaucoup plus léger et agréable que de passer par l’ApplicationContext de Spring.
En espérant que vous trouverez ceci utile
(Suite à la remarque d’evenisse, ceci ne fonctionne qu’avec Spring 2.5.2).
Oui exactement ce que propose Alexandre
@Hikage: Une chose comme le propose Alexandre ?
@Tarul: Là je ne suis pas sûr de comprendre … L’injector dépend d’une applicationContext pour fonctionner … l’Injector permet juste d’éviter d’instancier à la main un ApplicationContext pour récupérer les beans … Veux tu donner plus de détails s’il te plaît ?
@Alexandre: Merci infiniment pour ton post: je me suis permis d’éditer/fusionner tes 3 commentaires
Sinon, oui, tout à fait d’accord que passer par le weaving (loadtime, compiletime ou runtime) est en effet une option alléchante et transparente.
Seulement, imagines que tu packages ton application dans un jar exécutable… là, tu ne peux plus passer d’agents à la JVM, à moins de créer un .sh ou .bat
Bonjour,
Aujourd’hui, il est déjà possible de faire plus simple que ce que tu proposes mais c’est assez méconnu par les utilisateurs de Spring (car existe depuis la 2.5).
Annote ton TestConsumer
par @Configurable.
Rajoute un @Autowired devant les properties que tu veux injecter (@Ressource peut peut-être aussi fonctionner mais je préfère le @Autowired car il checke les type des classes et/ou les noms des beans).
Ajoute dans applciationContext.xml
<aop:aspectj-autoproxy /> <br />
<context:load-time-weaver/> <br />
Rajoute dans les paramètres de ta JVM pour ajouter l’agent aspectj -javaagent:C:HOMEWAREmaven-2_localorgspringframeworkspring-agent2.5.2spring-agent-2.5.2.jar
Tu peux désormais après avoir créer ton context spring, faire
TestConsumer cl = new TestConsumer();
A chaque que tu le crées avec un new, AspectJ d’injectera tes dépendances au runtime.
C’est très pratique comme tu dis quand tu fais une GUI ou un server standalone (sans serveur d’application).
J’avais posté chez Spring de faire cette nouvelle fonctionnalité mais en fait elle existait déjà en 2.5 (http://jira.springframework.org/browse/SPR-4602?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel)
Il est aussi possible d’utiliser aspectj au buildtime (pour ne pas à avoir à rajouter les l’agent et un peu mieux pour les perf) mais le problème c’est qu’aujoud’hui il n’existe pas de version stable pour la 1.6.
J’espère avoir été clair, n’hésitez pas à aller voir l’issue jira qui contient un exemple qui fonctionne.
Alexandre
Merci pour ce billet.
Mais ton injector est-il nécessaire si on utilise un application-context complet?
C’est pas mal comme pratique en effet !
Personnellement, dès que j’ai le temps, je teste le système basé sur AspectJ et le load time weaver, et qui fait la même chose complètement de manière complètement transparente. ( Outre les performances bien entendu )