Dans mon dernier article, je n’ai fait que mentionner l’interface FactoryBean. Nous allons ici voir en détail l’impact de cette interface sur les configurations Spring JavaConfig.
Dans Spring, l’interface FactoryBean permet de définir des objets ayant un rôle spécial : ils ne sont pas directement des Beans Spring : ce sont des objets qui servent à créer une instance de bean Spring : des factory au sens premier du terme. Lorsque la définition d’un bean pointe vers une classe implémentant FactoryBean, Spring appelle sa méthode getObject() et c’est l’objet retourné par cet appel qui sera le bean réellement associé à la définition en question.
Ces beans posent quelques problèmes avec JavaConfig : le premier étant qu’il ne s’adapte pas de manière naturel à la syntaxe de JavaConfig :
- en JavaConfig, les méthodes @Bean sont plutôt supposées renvoyer l’instance du bean final lui-même.
- or ici, comme nous allons le voir, c’est plutôt l’instance de la FactoryBean qu’il faudrait renvoyer à Spring pour qu’il puisse mettre en œuvre complètement ces mécanismes d’initialisation…
Un premier exemple d’utilisation de FactoryBean
Un exemple très classique de FactoryBean dans Spring est LocalContainerEntityManagerFactoryBean. Il permet de créer un bean de type EntityManagerFactory nécessaire à l’utilisation de JPA.
Spring Data JPA donne un exemple de configuration JavaConfig permettant de créer un EntityManagerFactory et un TransactionManager associé :
@Configuration
@EnableTransactionManagement
public class ApplicationConfig {
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.HSQL).build();
}
@Bean
public EntityManagerFactory entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.acme.domain");
factory.setDataSource(dataSource());
factory.afterPropertiesSet();
return factory.getObject();
}
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory());
return txManager;
}
}
Il y a plusieurs choses à noter ici :
- la méthode entityManagerFactory() renvoie directement un objet du type cible : EntityManagerFactory
- la FactoryBean n’est pas exposée au contexte Spring, elle n’est utilisée qu’à l’intérieur de l’implémentation de la méthode
- la méthode getObject() est appelée explicitement
- l’implémentation est aussi obligée d’appeler elle-même la méthode afterPropertiesSet() pour initialiser la FactoryBean avant de pouvoir appeler getObject()
- La méthode transactionManager() peut appeler la méthode entityManagerFactory() pour récupérer sa dépendance vers l’EntityManagerFactory, ce qui est une pratique classique en JavaConfig.
Ici le support des objets FactoryBean présent dans Spring n’est pas du tout utilisé, c’est l’implémentation de la méthode @Bean qui émule directement les différents appels nécessaires sur la FactoryBean pour obtenir l’instance de l’objet cible.
C’est la façon la plus simple d’utiliser une FactoryBean en JavaConfig malheureusement elle ne fonctionne pas tout le temps.
Et je la considère d’ailleurs fausse dans le cas de LocalContainerEntityManagerFactoryBean même s’il est probable qu’elle ne posera problème que dans certains cas d’utilisation aux limites. Nous y reviendrons plus bas.
Une alternative un peu plus complexe
L’article http://www.baeldung.com/2011/12/13/the-persistence-layer-with-spring-3-1-and-jpa/ propose quand à lui une version différente qui a ma préférence.
En réutilisant les mêmes principes qui y sont utilisés, l’équivalent de la configuration ci-dessus s’écrie alors plutôt de cette façon :
@Configuration
@EnableTransactionManagement
public class JpaConfiguration {
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder .setType(EmbeddedDatabaseType.HSQL).build();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter( vendorAdapter);
factory.setPackagesToScan("com.acme.domain");
factory.setDataSource(dataSource());
return factory ;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(emf);
return txManager ;
}
}
Mettons en valeur les différences :
- la méthode entityManagerFactory() renvoie maintenant une instance de la FactoryBean : LocalContainerEntityManagerFactoryBean et non de EntityManagerFactory
- A cause de cela la méthode transactionManager() ne peut maintenant plus appeler directement la méthode entityManagerFactory() pour obtenir sa dépendance de type de EntityManagerFactory. A la place, elle demande à Spring d’injecter le bean de type EntityManagerFactory en tant que paramètre de la méthode transactionManager (Étonnamment, la documentation décrivant qu’une telle injection est supportée ne semble pas exister mais cela correspond à ce qu’il est possible de faire en CDI (Context And Dependency Injection) ou en Guice)
- il n’est plus nécessaire d’appeler afterPropertiesSet() et getObject(). Le support de InitializingBean et de FactoryBean par JavaConfig suffit : les deux méthodes (et bien d’autres d’ailleurs) seront appelés automatiquement par le framework.
A noter : on pourrait se dire qu’il suffisait d’appeler entityManagerFactory().getObject() dans transactionManager() pour obtenir l’instance de EntityManagerFactory à utiliser. Dans ce cas précis, cela fonctionnerait. Mais c’est une mauvaise idée dans le cas général car Spring ne peut alors pas instrumenter l’appel à getObject() : si le bean doit être proxyifié, en particulier s’il est scopé, cet appel bypasse Spring et on n’obtient alors pas la bonne référence au bean. De la même façon, si la méthode getObject() de la FactoryBean renvoie une nouvelle instance à chaque fois, Spring ne sera pas capable de forcer le renvoie du même singleton à chaque appel.
En quoi cette deuxième version est meilleure
La deuxième version est à mon sens bien meilleure car elle permet d’activer entièrement les mécanismes implicites à l’œuvre dans Spring.
En particulier, le support des interfaces spéciales de Spring : InitializingBean, DisposableBean, *Aware dont j’ai parlé dans mon précédent blog.
Et cela est particulièrement important dans le cas des FactoryBean qui les utilisent très souvent.
LocalContainerEntityManagerFactoryBean par exemple implémente un grand nombre de ces interfaces :
- ResourceLoaderAware
- LoadTimeWeaverAware
- BeanClassLoaderAware
- BeanFactoryAware
- BeanNameAware
- InitializingBean
- DisposableBean
- et bien sûr FactoryBean
Dans la première version de la configuration proposée, l’implémentation de la méthode entityManagerFactory() n’émule directement que les appels associés à InitializingBean et FactoryBean. Toutes les interfaces Aware sont ignorés et la LocalContainerEntityManagerFactoryBean ainsi créé n’est probablement pas capable d’activer certains mécanismes les nécessitant (qui ne sont manifestement pas utiles dans le cas général puisque cela fonctionne tout de même ici)
L’interface DisposableBean aussi est ignorée mais dans ce cas précis ce n’est pas très grave car la méthode destroy() de LocalContainerEntityManagerFactoryBean se contente d’appeler la méthode close() de l’EntityManagerFactory créé ; et grâce au fait que par défaut @Bean enregistre tout seul la méthode close() comme destroyMethod (cf là aussi mon précédent blog), elle sera correctement appelée dans ce cas.
Toutefois, là encore, dans le cas général (si la logique de destruction codée dans la FactoryBean est plus complexe et/ou si aucune méthode close ou shutdown n’existe sur le bean retourné), tout ou partie des traitements de destruction seront ignorés.
Conclusion
Les classes FactoryBean ont été introduite bien avant JavaConfig dans Spring et comme nous l’avons vu leur utilisation n’est pas naturelle dans JavaConfig.
On trouve régulièrement sur internet des exemples d’utilisation de FactoryBean préconisant un appel manuel à afterPropertiesSet() et getObject().
Je vous ai décrit ici pourquoi cette préconisation n’est pas toujours la bonne et je vous invite à systématiquement utiliser la deuxième méthode décrite ici : faire renvoyer la FactoryBean par vos méthodes @Bean et laisser Spring appliquer l’ensemble de ces mécanismes internes lui-même.