Spring : constructor injection et setter injection

Au menu aujourd’hui, les constructor injections et les setter injections dans Spring. Si vous êtes arrivés sur cette page, vous devez sûrement déjà savoir que les deux permettent d’injecter un bean dans un autre via Spring. Voyons déjà ce à quoi ressemble les deux méthodes.

Description

Prenons un exemple simple. Nous avons une classe UserService faisant elle-même un appel à un service de rôles appelé RoleService. Notre classe java est construite comme ceci :

public class UserService
{
	private RoleService roleService;
}

On veut qu’une instance de cette classe soit créée par Spring au démarrage de l’application, nous déclarons donc dans notre fichier de configuration Spring (alinto.services.xml):

<bean id="userService" class="com.alinto.services.UserService">
	<constructor-arg ref="beanRoleService" />
</bean>

<bean id="beanRoleService" class="com.alinto.services.RoleService" />

Ici, on a donc déclaré deux nouveaux beans, un étant de type UserService et l’autre de type RoleService. On a ensuite injecté le bean roleService dans userService. Ensuite, on modifie dans notre classe Java :

public class UserService
{
    private RoleService roleService;
    public UserService( RoleService roleService ) 
    { 
        this.roleService = roleService; 
    }
}

Comme vous l’aurez sûrement deviné, nous venons de faire une constructor injection. Facile, n’est-ce pas ? Allez maintenant on va faire la même chose mais avec une setter injection. Nous changeons donc notre alinto.services.xml :

<bean id="userService" class="com.alinto.services.UserService">
    <!-- Ici on change la façon d'injecter le bean --> 
    <property name="roleService" ref="beanRoleService" />
</bean>

<bean id="beanRoleService" class="com.alinto.services.RoleService" />

et on change notre classe Java

public class UserService
{
    private RoleService roleService;
    // On supprime le constructeur et on rajoute un setter pour le paramètre roleService
    public void setRoleService( RoleService roleService)
    {
        this.roleService = roleService;
    }
}

Et voila, le tour est joué, vous pourrez utiliser sans aucun souci votre paramètre roleService dans vos méthodes de la classe UserService. Maintenant, nous allons nous intéresser aux légères différences entre les deux méthodes.

Les différences

  • Tout d’abord, une différence historique. En effet, les constructor injections viennent de PicoContainer et Guice alors que les setter injections viennent de Spring
  • L’ordre des opérations, c’est à dire a quel moment sont réellement injectés les beans à notre classe Java. En effet, dans le cas d’une constructor injection, le bean est injecté directement dans le constructeur. Lors d’une setter injection, cela se fait après le constructeur. 
  • Le fait de ne pas avoir de constructeur par défaut. En effet, pour une constructor injection, vous avez donc besoin d’un constructeur avec un paramètre du type du bean que vous voulez injecter.

Vous me direz que ce sont des différences assez minimes. Cependant, celles ci nous ont posé des problèmes lors de notre développement.

Les AOP

Comment, les AOP ? Quel est le rapport avec notre sujet initial ? Puis d’ailleurs qu’est-ce que c’est ? Avouez que vous avez pensez ça en lisant le titre !

Bon, comme le sujet d’aujourd’hui ne concerne pas les AOP, si vous ne connaissez pas, je vous conseille d’aller voir la partie dédiée sur springsource ( anglophobes passez votre chemin ! ) . Dans notre cas, on souhaitait mettre autour d’une classe un AOP afin d’ouvrir une transaction. Sur ce sujet, je vous conseille notre article sur les transactions. Pour vous montrer notre xml ressemblait donc à peu près a cela :

<bean id="userService" class="com.alinto.services.UserService">
    <constructor-arg="roleService" ref="beanRoleService"/>
</bean>

<bean id="beanRoleService" class="com.alinto.services.RoleService"/>

Et nous avons voulu rajouter l’AOP suivante :

<aop:config>
    <aop:pointcut id="userServiceMethods" expression="execution(* com.alinto.services.UserService.*(..))" />
    <aop:advisor advice-ref="userTxAdvice" pointcut-ref="userServiceMethods" />
</aop:config>

Voila l’erreur à laquelle nous avons eu le droit.

org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class com.alinto.services.UserService]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:527)

En effet, les aop via CgLib nécessitent d’avoir un constructeur par défaut, c’est à dire sans argument. Nous nous sommes donc dit qu’on allait passer d’une constructor injection à une setter injection. Pour cela, nous avons tout simplement changé notre constructor-arg en property et rajouté le setter dans la classe. Mais le constructeur appelait certaines méthodes de RoleService lors de son exécution. Il nous fallait trouver un moyen de les appeler après le constructeur et après que la setter injection soit faite.

Nous avons trouvé trois moyens de contourner ce problème qui font tous la même chose, c’est à dire appeler une méthode après que les injections aient été faites :

  • L’interface InitializingBean : elle permet d’implémenter la méthode afterPropertiesSet(). On changerait donc notre classe UserService par :
public class UserService implements InitializingBean
{
    private RoleService roleService;

    public void setRoleService( RoleService roleService)
    {
        this.roleService = roleService;
    }

    @Override
    public void afterPropertiesSet() throws Exception
    {
        this.roleService.updateRole(RoleEnum.USER);
    }

}
  •  L’annotation @PostConstruct, qui n’est pas dans Spring mais est située dans la librairie J2ee. On change dans notre classe :
public class UserService
{
    private RoleService roleService;

    public void setRoleService( RoleService roleService)
    {
        this.roleService = roleService;
    }

    @PostContruct
    public void afterConstructor()
    {
        this.roleService.updateRole(RoleEnum.USER);
    }
}

Ensuite, on modifie notre alinto.services.xml :

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	<!-- N oubliez pas de rajouter le namespace -->
        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-2.5.xsd">

	<context:annotation-config />

        <bean id="userService">
            <!-- Ici on change la façon d'injecter le bean -->
            <property name="roleService" ref="beanRoleService" "/>
        </bean>

        <bean id="beanRoleService" class="com.alinto.services.RoleService" />
  • Enfin, la troisième solution, et aussi celle que nous avons choisie car elle permet d’avoir une bonne visibilité sur le lien entre la configuration Spring et le code et aussi parce qu’elle est indépendante de Spring, c’est à dire que si nous cessions d’utiliser Spring, nous pourrions réutiliser ce que nous avons fait. Cette méthode consiste à définir une init-method pour notre bean userService. Voici donc notre classe UserService finale.
public class UserService
{
    private RoleService roleService;

    public void setRoleService( RoleService roleService)
    {
        this.roleService = roleService;
    }

    // Notre méthode d'initialisation
    public void init()
    {
        this.roleService.updateRole(RoleEnum.USER);
    }

}

Et enfin le corps de notre alinto.services.xml

<!-- On définit quelle méthode sera appelée grâce à l'argument init-method -->
<bean id="userService" class="com.alinto.services.UserService" init-method="init">
    <property name="roleService" ref="beanRoleService" />
</bean>

<bean id="beanRoleService" class="com.alinto.services.RoleService"/>

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *