@Conditional de Spring 4 au coeur de l'auto-configuration de Spring Boot

Spring Boot est un des derniers projets du portefeuille Spring. Il fait partie des socles d’exécution standard Spring proposés et mis en avant par Pivotal (avec Spring XD et Grails)

En cours de ce post, nous verrons comment il tire partie d’une nouvelle fonctionnalité de Spring 4 mettant en oeuvre l’annotation de @Conditional.

L’auto-configuration dans Spring Boot

springboot-bisSpring Boot fournit plusieurs services facilitant la mise en oeuvre et l’exécution d’applications Spring.

Parmi ces principales caractéristiques, on peut mettre en avant :

  • une orientation full-java de la configuration Spring (JavaConfig)
    complétée par des capacités d’auto-configuration poussées (que nous allons aborder plus en détail ci-dessous)
  • une mise en avant de l’utilisation d’un conteneur de servlet embarqué (tomcat ou jetty)
    avec en particulier un packaging de war “exécutable”
  • une mise à disposition de projets “starter” contenant déjà toutes les dépendances nécessaires pour les principales typologies de projet :
    ajouter un ou plusieurs de ces projets dans les dépendances de votre application et vous avez déjà tout ce qu’il vous faut pour démarrer immédiatement sur le cœur de votre logique métier : cf spring-boot-starters
  • mais aussi : un cli mettant à contribution Groovy, des emplacements par défaut pour la configuration, des extensions pour faciliter l’exploitation en production, etc…

Les possibilités d’auto-configuration sont particulièrement puissantes :
Le module autoconfigure est capable d’ajouter “auto-magiquement” les beans d’infrastructure nécessaires à votre application : une datasource, un transaction manager, une entity manager factory JPA, tous le trains de beans nécessaires à Spring MVC, un tomcat ou jetty embarqué, etc… (la liste est longue)
Le tout configuré avec des valeurs par défauts adéquates. Si ces défauts ne vous conviennent pas : une bonne partie des valeurs sont paramétrables via des propriétés et sinon, il vous suffit de définir vous même le bean en question : Spring Boot le détectera et gardera votre définition.

Spring Boot utilise et combine plusieurs stratégies pour décider s’il doit ou non définir un bean :

  • la plupart des beans sont créés si le ou les jar(s) adéquats sont dans le classpath
    mais leur absence peut aussi être utilisée
  • l’existence ou la valeur de certaines propriétés (système, spring, …) peut influencer sur la création de certains beans
  • l’existence d’un autre bean peut être utilisée
  • et bien sûr que le bean en question ne soit pas déjà créé dans le context Spring

En java, cette fonctionnalité est activée via le simple ajout d’une annotation @EnableAutoConfiguration sur une classe @Configuration.
(Il n’est d’ailleurs pas nécessaire de lancer votre application via Spring Boot pour en bénéficier, il s’agit d’une annotation @EnableXXX classique comme @EnableTransactionManagement par exemple)

Ce projet est très motivant au premier abord, mais passé l’euphorie initiale du hello world de quelques lignes qui lance tomcat, spring et spring mvc, le tout configuré aux petits oignons … la dure réalité s’impose à nous :
la magie c’est bien tant que ça marche et qu’il n’est pas nécessaire de faire des customisations, mais dans le cas contraire (c’est à dire tout le temps sur de vrais projets en fait…), la magie doit laisser la place à la compréhension.

La documentation (déjà assez bien fournie mais encore incomplète) et les forums ne fournissent pas toujours la réponse simplement.
Je préfère avant tout comprendre le fonctionnement sous-jacent pour garder un regard critique sur les solutions proposées sur le net ou pouvoir trouver moi-même la réponse si celle ci n’est pas si facile à trouver dans les ressources disponibles.

C’est donc ici qu’intervient Spring 4 et son nouveau mécanisme : @Conditional

L’annotation @Conditionnal de Spring 4

spring-newSpring 3.1 avait introduit la notion de Profile : cette fonction permet de conditionner la création de certains beans suivant le ou les profils ayant été activés (sur la ligne de commande, programmatiquement sur l’objet Environment de Spring, dans des fichiers de conf tel que web.xml, etc…)
cf doc de référence ou ce blog.

Dans Spring 4, la notion de Profile existe toujours mais sa mise en œuvre en JavaConfig (via l’annotation @Profile) repose maintenant sur un mécanisme plus générique basé sur l’utilisation de la nouvelle annotation @Conditional.

Tout comme @Profile, @Conditional peut s’appliquer sur une classe @Configuration ou une méthode @Bean pour conditionner la prise en compte des définitions de beans. Mais elle sera probablement plutôt utilisée en tant que méta-annotation. (Ce qui est en particulier le cas dans Spring Boot ou sur @Profile comme nous le verrons ci-dessous).

A la différence de @Profile, @Conditional permet de fournir à Spring une implémentation quelconque pour déterminer si le ou les beans sur lesquels elle s’applique doivent être ajoutés ou non au context spring. N’importe quelle stratégie peut donc maintenant être utilisée pour effectuer ce choix.

Pour ce faire, la stratégie à utiliser doit être fournie à @Conditional via une classe implémentant la nouvelle interface Condition :

Voici un exemple d’utilisation directement sur une classe @Configuration :

@Configuration @Conditional(MySpecificCondition.class) public class SpecificConfiguration { @Bean public MySpecificService mySpecificService() { return new MySpecificService() } }

Cette configuration ne sera prise en compte (et donc l’ensemble de ces @Bean) que dans le cas où la condition spécifiée dans l’annotation @Conditional renvoie true. L’implémentation de cette condition peut prendre cette forme :

public class MySpecificCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return new File("/apps/bin/a_wonderful_external_tool").exists(); } }

Via le paramètre ConditionContext, l’implémentation de la condition a accès à l’Environment, le classloader, le context spring en cours de construction ; et via le paramètre AnnotatedTypeMetadata il peut facilement récupérer les annotations associées à l’élément annoté avec @Conditional en cours de traitement (ce qui permet en particulier de récupérer des attributs positionnés sur des annotations méta-annotés avec @Conditional comme c’est le cas pour @Profile dont l’implémentation est montrée ci-dessous)

L’utilisation sur des classes annotées @Configuration est particulièrement puissante car elle permet de conditionner la prise en compte en cascade des autres @Configuration que la configuration courante inclue ou importe (via ces inner class ou des @Import)
( Voici un exemple issu de Spring Boot : JpaRepositoriesAutoConfiguration )

Notons que @Conditional est spécifique aux configurations @Configuration et au component scan, elle n’a pas d’équivalent en configuration XML (contrairement à la notion de Profile qui y est supportée)

La nouvelle implémentation de @Profile

La doc de référence de Spring 4 illustre @Conditional en montrant la nouvelle implémentation de @Profile :

L’annotation @Profile est maintenant méta-annotée avec une annotation @Conditional :

@Conditional(ProfileCondition.class) public @interface Profile { /* ... */ }

Et voici l’implémentation de Condition associée :

class ProfileCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { if (context.getEnvironment() != null) { MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { if (context.getEnvironment().acceptsProfiles(((String[]) value))) { return true; } } return false; } } return true; } }

Plus aucun code spécifique à @Profile n’est maintenant présent dans les classes chargées de prendre en compte les classes @Configuration (cf par exemple AnnotatedBeanDefinitionReader.registerBean ). Seul ProfileCondition évalue le Profile dans ce type de configuration maintenant.

La surcouche de Spring Boot sur @Conditional

Comme nous l’avons vu, @Conditional fournit donc les bases génériques à la mise en œuvre de stratégies complexes de définition conditionnelle de beans Spring.
Toutefois, hormis son utilisation pour @Profile, Spring core ne fournit aucune autre implémentation de Condition.

Le mécanisme d’auto-configuration de Spring Boot utilisant intensivement @Conditional pour implémenter les mécanismes d’auto-configuration, il définit pour cela plusieurs annotations méta-annotées @Conditional dans le package org.springframework.boot.autoconfigure.condition :

@ConditionalOnBean @ConditionalOnClass @ConditionalOnExpression @ConditionalOnMissingBean @ConditionalOnMissingClass @ConditionalOnNotWebApplication @ConditionalOnResource @ConditionalOnWebApplication

Ces annotations sont omniprésentes au sein des classes @Configuration définies par le module spring-boot-autoconfigure (mais aussi par le module spring-boot-actuator)

Voici un exemple simple de l’utilisation qui en est fait :

@Configuration @ConditionalOnClass({ JdbcTemplate.class, PlatformTransactionManager.class }) public class DataSourceTransactionManagerAutoConfiguration implements Ordered { @Override public int getOrder() { return Integer.MAX_VALUE; } @Autowired(required = false) private DataSource dataSource; @Bean @ConditionalOnMissingBean(name = "transactionManager") @ConditionalOnBean(DataSource.class) public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(this.dataSource); } }

Cette @Configuration n’est prise en compte (dans sa globalité) que si Spring JDBC et Spring Transaction sont dans le classpath ( du fait de l’annotation @ConditionalOnClass placée au niveau de la classe)
Si c’est le cas, la création du bean transactionManager est alors considérée. Il sera défini si :

  • il n’y a pas déjà un bean nommé “transactionManager” actuellement défini dans l’applicationContext (du fait de l’annotation @ConditionalOnMissingBean)
  • il existe une datasource précédemment définie dans l’applicationContext (du fait de l’annotation @ConditionalOnBean)
    (notons que cette datasource a pu elle aussi être définie par une configuration automatique … d’où l’importance de l’ordre de prise en compte de ces configurations)

La page http://projects.spring.io/spring-boot/docs/spring-boot-autoconfigure/README.html fournit quelques infos supplémentaires sur ce mécanisme.

On pourrait se demander pourquoi les annotations @ConditionalOn* n’ont pas été définies directement dans Spring 4 Core.
La réponse se trouve ici : SPR-10964 ; l’utilisation de certaines d’entre elles (@ConditionalOnMissingBean et @ConditionalOnBean) nécessite la maîtrise parfaite de l’ordre de définition des beans pour fonctionner correctement ; les auteurs de Spring Boot ont donc préféré ne pas les rendre standard pour le moment.

Obtenir de la visibilité sur les configurations générés par Spring Boot

Spring Core ne fournit pas grand chose (rien en fait …) permettant de savoir si une @Configuration a ou n’a pas été prise en compte par un ensemble de condition
(La classe ConditionEvaluator en particulier ne contient aucun log)

Les auteurs de Spring Boot ont donc créé une classe mère pour toutes leurs implémentations de Condition dont le principal but est de faciliter le diagnostique via l’émission de traces et la capture des résultats d’évaluation de chaque condition : org.springframework.boot.autoconfigure.condition.SpringBootCondition

L’activation du logger “org.springframework.boot.autoconfigure.condition” en mode trace permet d’obtenir ce type d’information au fur et à mesure de l’évaluation des Condition :

... Condition OnBeanCondition on org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration#propertySourcesPlaceholderConfigurer matched due to @ConditionalOnMissingBean (types: org.springframework.context.support.PropertySourcesPlaceholderConfigurer; SearchStrategy: current) found no beans Condition OnBeanCondition on org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration#serverProperties matched due to @ConditionalOnMissingBean (types: org.springframework.boot.context.embedded.properties.ServerProperties; SearchStrategy: all) found no beans ...

Mais il est plus approprié d’attendre la fin de la création du context spring pour obtenir ce rapport en activant le logger “org.springframework.boot.autoconfigure” en mode debug :

2014-01-02 03:04:05.006 DEBUG 6980 --- [ main] nitializer$AutoConfigurationReportLogger : ========================= AUTO-CONFIGURATION REPORT ========================= Positive matches: ----------------- MessageSourceAutoConfiguration - @ConditionalOnMissingBean (types: org.springframework.context.MessageSource; SearchStrategy: all) found no beans (OnBeanCondition) PropertyPlaceholderAutoConfiguration#propertySourcesPlaceholderConfigurer - @ConditionalOnMissingBean (types: org.springframework.context.support.PropertySourcesPlaceholderConfigurer; SearchStrategy: current) found no beans (OnBeanCondition) ServerPropertiesAutoConfiguration#serverProperties - @ConditionalOnMissingBean (types: org.springframework.boot.context.embedded.properties.ServerProperties; SearchStrategy: all) found no beans (OnBeanCondition) Negative matches: ----------------- RabbitAutoConfiguration - required @ConditionalOnClass classes not found: org.springframework.amqp.rabbit.core.RabbitTemplate,com.rabbitmq.client.Channel (OnClassCondition) AopAutoConfiguration - required @ConditionalOnClass classes not found: org.aspectj.lang.annotation.Aspect,org.aspectj.lang.reflect.Advice (OnClassCondition) BatchAutoConfiguration - required @ConditionalOnClass classes not found: org.springframework.batch.core.launch.JobLauncher (OnClassCondition) JpaRepositoriesAutoConfiguration - required @ConditionalOnClass classes not found: org.springframework.data.jpa.repository.JpaRepository (OnClassCondition) MongoRepositoriesAutoConfiguration - required @ConditionalOnClass classes not found: com.mongodb.Mongo,org.springframework.data.mongodb.repository.MongoRepository (OnClassCondition) DataSourceAutoConfiguration - required @ConditionalOnClass classes not found: org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType (OnClassCondition) DataSourceTransactionManagerAutoConfiguration - required @ConditionalOnClass classes not found: org.springframework.jdbc.core.JdbcTemplate,org.springframework.transaction.PlatformTransactionManager (OnClassCondition) JmsTemplateAutoConfiguration - required @ConditionalOnClass classes not found: org.springframework.jms.core.JmsTemplate,javax.jms.ConnectionFactory (OnClassCondition) DeviceResolverAutoConfiguration - required @ConditionalOnClass classes not found: org.springframework.mobile.device.DeviceResolverHandlerInterceptor,org.springframework.mobile.device.DeviceHandlerMethodArgumentResolver (OnClassCondition) HibernateJpaAutoConfiguration - required @ConditionalOnClass classes not found: org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean,org.springframework.transaction.annotation.EnableTransactionManagement,javax.persistence.EntityManager,org.hibernate.ejb.HibernateEntityManager (OnClassCondition) ReactorAutoConfiguration - required @ConditionalOnClass classes not found: reactor.spring.context.config.EnableReactor (OnClassCondition) ThymeleafAutoConfiguration - required @ConditionalOnClass classes not found: org.thymeleaf.spring3.SpringTemplateEngine (OnClassCondition) DispatcherServletAutoConfiguration - web application classes not found (OnWebApplicationCondition) EmbeddedServletContainerAutoConfiguration - web application classes not found (OnWebApplicationCondition) MultipartAutoConfiguration - required @ConditionalOnClass classes not found: javax.servlet.Servlet,org.springframework.web.multipart.support.StandardServletMultipartResolver (OnClassCondition) WebMvcAutoConfiguration - web application classes not found (OnWebApplicationCondition) WebSocketAutoConfiguration - required @ConditionalOnClass classes not found: org.springframework.web.socket.WebSocketHandler (OnClassCondition)

Il s’agit ici de traces issues d’un projet ne dépendant que de spring-boot-starter; c’est à dire avec pratiquement rien d’autre que spring-boot. La plupart des @Configuration ne s’activent donc pas ici puisqu’il manque les classes nécessaires dans le classpath.
On peut remarquer par exemple que la condition sur le @Bean propertySourcesPlaceholderConfigurer de PropertyPlaceholderAutoConfiguration a été activée suite à l’absence d’un bean du même type.

Ces traces sont intéressantes mais restent limitées au résultat de l’évaluation des conditions. Il faudra activer d’autres traces ou regarder le source des classes *AutoConfiguration pour avoir une vision précise des beans créés et de leur configuration (ainsi que des éventuelles propriétés qui permettent d’intervenir sur cette configuration)

Notons que si une classe *AutoConfiguration s’active et que pour une raison quelconque cela ne vous convient pas, il suffit de l’exclure lors de l’activation de la fonctionnalité d’AutoConfiguration :

@EnableAutoConfiguration(exclude={EmbeddedDatabaseConfiguration.class})

Conclusion

Au cours de ce post, je vous ai présenté rapidement Spring Boot. J’ai insisté sur le module d’auto-configuration afin de pouvoir vous présenter en détail l’usage qu’il fait de la nouvelle annotation @Conditional introduit dans Spring 4.

Je trouve personnellement Spring Boot assez intéressant.
Par contre, ne nous y trompons pas, sur de vrais projets, il ne facilitera la mise en oeuvre d’une application Spring qu’aux personnes maîtrisant déjà Spring (ainsi que les besoins de chacun des modules mises en oeuvre : Spring MVC en tête).
En particulier, il me semble utile de comprendre les mécanismes décrits dans ce post pour pouvoir être autonome sur sa mise en oeuvre et sa customisation.

Spring Boot nous donne un exemple concret (et assez poussé) de la mise en oeuvre de @Conditional.
Il est possible de ré-utiliser les annotations @ConditionalOn* qu’il fournit dans vos propres projets même sans utiliser le reste de Spring Boot.
Attention toutefois, l’utilisation correcte de @ConditionalOnMissingBean et @ConditionalOnBean hors de Spring Boot n’est pas triviale. Elles doivent probablement être réservés aux mécanismes d’auto-configuration de SpringBoot (que vous pouvez étendre sans problème vous même) afin de ne pas vous exposer à des problématiques d’ordre de chargement des définitions de beans.

En dehors de Spring Boot, si la notion de @Profile ne vous suffit pas, vous pouvez bien sûr écrire vos propres Condition.
Un des exemples régulièrement utilisé sur le net pour illustrer son utilité, est la mise en oeuvre de Condition détectant le type d’OS de la machine hôte : certains beans pourront ainsi être définis que si l’hôte est sous Windows ou que si l’hôte est sous Linux/Unix ; et ceci automatiquement sans qu’il soit nécessaire de spécifier explicitement un “profile” sur la ligne de commande ou dans les fichiers de propriétés.

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.