Spring JavaConfig Tips : initialisation et destruction des beans

springicon

Spring propose depuis longtemps plusieurs mécanismes permettant d’enrichir le comportement du conteneur lors de la création ou la destruction des beans :

  • des interfaces, configurations et annotations permettant de notifier le bean lors de sa construction/destruction
  • des interfaces Aware permettant d’injecter automatiquement certaines dépendances particulières
  • une interface (FactoryBean) permettant d’avoir entièrement la main sur la création et la destruction d’un objet (nous en reparlerons plus en détail la prochaine fois).

Un certain nombre d’exemples de config XML fonctionnent grâce à ces mécanismes. De nombreux frameworks tiers, dans le cadre de leur intégration avec Spring, les utilisent aussi.

Lorsqu’on utilise JavaConfig, il faut garder en tête ces mécanismes pour adapter correctement les anciennes configurations XML encore beaucoup présentes dans les différentes documentations. En effet, si JavaConfig continue de les gérer, ils ont un impact sur la façon de définir ces configurations…

Callback sur le cycle de vie des beans

A l’origine Spring proposait deux méthodes pour notifier un bean lors de sa construction et lors de se destruction :

  • implémenter l’interface InitializingBean (méthode afterPropertiesSet() ) ou l’interface DisposableBean (méthode destroy()) dans le bean (ce qui entraine une dépendance sur Spring)
  • ou indiquer à Spring une init-method et une destroy-method dans la configuration XML du bean (la documentation Spring elle-même encourageant cette méthode afin de ne pas coupler les beans applicatifs à Spring)

Dans les deux cas, Spring appelle la méthode d’initialisation après l’instanciation du bean et l’injection de ces dépendances ; et appelle la méthode de destruction avant de détruire définitivement le bean.

Spring a ensuite ajouté le support des annotations Java @PostConstruct et @PreDestroy qui joue le même rôle que les interfaces Spring mais de façon standard.

JavaConfig supporte les interfaces InitializingBean et DisposableBean ainsi que les deux annotations standard mentionnées : si l’instance du bean renvoyée par une méthode annotée @Bean utilise ces mécanismes, ils seront correctement mis en oeuvre (attention toutefois, seul le bean renvoyé par la méthode @Bean est géré de cette façon ! Voir ci-dessous : la disparition des “inner bean”). Notons qu’il s’agit bien de la classe de l’objet renvoyé et non le type de retour de la méthode @Bean qui est pris en compte par Spring. Dans l’exemple suivant, la méthode myBean renvoie une instance de l’interface MyBeanInterface (qui n’étend ni InitializingBean ni DisposableBean) mais Spring attend d’obtenir la vraie classe de l’objet (MyBeanImplementation) pour prendre en compte les interfaces (ou les annotations)

@Configuration
        public class MyConfiguration {

               @Bean
               public MyBeanInterface myBean() {
                      return new MyBeanImplementation();
               }
        }

        public interface MyBeanInterface {

        }

        public class MyBeanImplementation implements MyBeanInterface, InitializingBean, DisposableBean {

               @Override
               public void afterPropertiesSet() throws Exception {
                     System.out.println(this.getClass().getSimpleName() + ".afterPropertiesSet called" );
               }

               @Override
               public void destroy() throws Exception {
                     System.out.println(this.getClass().getSimpleName() + ".destroy called" );
               }
        }

Toutefois, pour les classes provenant de frameworks tiers, qui n’utilisent pas les annotations standards (et encore moins les interfaces Spring), indiquer à Spring quelles sont les méthodes à appeler à la création et à la destruction reste nécessaire.

Lorsque JavaConfig a été introduit, il a donc été prévu un moyen de définir ces callbacks sur les méta-données des méthodes @Bean de manière équivalente aux attributs initMethod et destroyMethod présents dans la configuration XML :

@Configuration
        public class MyConfiguration {

               @Bean(initMethod = "init", destroyMethod = "destroy" )
               public MyLegacyBean myBean() {
                      return new MyLegacyBean();
               }
        }

        public class MyLegacyBean {

               public void init() throws Exception {
                     System.out.println(this.getClass().getSimpleName() + ".init called");
               }

               public void destroy() throws Exception {
                     System.out.println(this.getClass().getSimpleName() + ".destroy called" );
               }
        }

Le cadeau (empoisonné) de @Bean : la destroy method inferrée

JavaConfig et @Bean viennent avec un cadeau bonus :

  • si le bean renvoyé définit une méthode “public void close()” ou “public void shutdown()”, celle-ci est automatiquement prise en compte par Spring comme méthode de destruction du bean (ce point est documenté dans la javadoc de l’attribut destroyMethod de @Bean).

C’est sympa de la part de Spring, mais à mon avis ici, ils ont dépassé les limites entre les comportements par défaut bien choisis et l’introduction d’une part de “magie” difficile à maîtriser pour l’utilisateur.

Cela m’a récemment valu un bug où je ne voulais justement pas que la méthode close() de mon bean soit appelé : j’avais donc en toute connaissance de cause omis de configurer la destroyMethod sur l’annotation @Bean. Dans mon cas, il fallait malheureusement explicitement renseigner cet attribut avec une valeur vide pour empêcher cet appel…

Les interfaces Aware

Certains beans ont besoin d’accéder à certains objets particuliers de l’environnement d’exécution (Spring, conteneur web …) pour fonctionner. Il s’agit la plupart du temps de beans spéciaux, souvent fournis par le framework spring lui-même, utilisés pour interagir intimement avec le container Spring.

Afin de rester modulaire et extensible, Spring propose un certains nombres d’interface Aware, permettant de flagguer ces beans. Lorsqu’un bean implémente l’une de ces interfaces, Spring le détecte et injecte automatiquement les objets nécessaires dans les beans en question.

Un exemple est ServletContextAware issu du module spring-web :

package org.springframework.web.context;

import javax.servlet.ServletContext;
import org.springframework.beans.factory.Aware;

public interface ServletContextAware extends Aware {

       void setServletContext(ServletContext servletContext);

}

Au sein d’un contexte Spring de type Web, tout bean implémentant cette interface recevra une référence au ServletContext de la webapp lors de sa création.

Là encore, JavaConfig ne remet pas en cause le support de toutes ces interfaces, mais comme nous le verrons au paragraphe suivant, seuls les objets renvoyés par les méthodes @Bean bénéficient de ce support.

La disparition des “inner bean”

Les deux mécanismes décrits précédemment impliquent les mêmes contraintes sur les configurations JavaConfig : seuls les vrais beans (les objets managés par Spring) peuvent en bénéficier. En JavaConfig, cela signifie qu’il faut que ces objets soient renvoyés directement par une méthode @Bean. En particulier, une transposition “naïve” de certaines configuration XML en JavaConfig pourra ne pas fonctionner comme prévu à cause de ces mécanismes. Par exemple, si on reprend cette configuration XML issue de la javadoc de BeanNameAutoProxyCreator dont j’ai déjà parlé dans mon dernier blog sur JavaConfig :

 <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
   <property name="customTargetSourceCreators">
     <list>
       <bean class="org.springframework.aop.framework.autoproxy.target.LazyInitTargetSourceCreator"/>
     </list>
   </property>
 </bean>

On pourrait penser qu’elle peut être transposée en JavaConfig de cette façon :

@Bean
    public BeanNameAutoProxyCreator lazyInitAutoProxyCreator() {
      BeanNameAutoProxyCreator autoProxyCreator = new BeanNameAutoProxyCreator();
      TargetSourceCreator[] targetSourceCreators = {new LazyInitTargetSourceCreator()};
      autoProxyCreator.setCustomTargetSourceCreators(targetSourceCreators);
      return autoProxyCreator; 
    }

Mais ce n’est pas du tout le cas ici, car l’objet LazyInitTargetSourceCreator n’est alors pas correctement initialisé… Et le pire ici, c’est que cela ne génère pas d’erreur, le LazyInitTargetSourceCreator ne fait juste pas son travail, et les beans Lazy ne sont pas proxyfiés…

Le problème est dû au fait que LazyInitTargetSourceCreator implémente l’interface BeanFactoryAware (on notera qu’elle implémente aussi DisposableBean) ce qui lui permet normalement de recevoir une référence au BeanFactory nécessaire à son bon fonctionnement. Dans la configuration XML, il est configuré via un “inner-bean” (un bean anonyme) qui reste malgré tout visible par Spring et bénéficie donc de l’injection du BeanFactory via BeanFactoryAware. En JavaConfig, le LazyInitTargetSourceCreator doit avoir sa propre méthode @Bean s’il veut recevoir le BeanFactory (et accessoirement s’il veut être détruit correctement à la fin du contexte via l’implémentation de DisposableBean) :

@Bean
    public BeanNameAutoProxyCreator lazyInitAutoProxyCreator() {
            BeanNameAutoProxyCreator autoProxyCreator = new BeanNameAutoProxyCreator();
            TargetSourceCreator[] targetSourceCreators = {lazyInitTargetSourceCreator()};
            autoProxyCreator.setCustomTargetSourceCreators(targetSourceCreators);
            return autoProxyCreator; 
    }

    @Bean
    public LazyInitTargetSourceCreator lazyInitTargetSourceCreator() { 
            return new LazyInitTargetSourceCreator();
    }

La notion de inner-bean (ou de bean anonyme) que l’on trouve dans la configuration XML n’existe donc plus en JavaConfig. Il est nécessaire de déclarer (et donc de nommer) explicitement chacun des beans manipulés pour bénéficier de tous les mécanismes internes de Spring.

Author image
Fort de plus de 20 ans d'expérience autour de l'écosystème JavaEE, Fabien accompagne ses clients dans la conception et le cadrage de leurs projets.