Blog Zenika

#CodeTheWorld

JavaWeb

L’auto-configuration Spring Boot expliquée : La Magie Derrière @SpringBootApplication


spring boot

Tu ajoutes @SpringBootApplication, tu lances ton projet… et tout fonctionne.

Connexion à la base de données ✔
Serveur web ✔
Injection de dépendances ✔

Mais c’est quoi cette magie 😲?

Dans un premier temps, nous allons voir comment se faisait la configuration avant Spring Boot et dans un second temps nous allons décortiquer l’auto-configuration de Spring Boot. C’est OK? Let’s go !


1. C’était comment la vie avec Spring Framework sans Spring Boot ?

Lorsqu’on développe une application web on a différents besoins. Souvent ils concernent : communiquer avec une base de données et gérer des requêtes serveur. Pour cela, on va configurer ces moyens de communication soit via des fichiers xml soit via des beans (objets java gérés par spring).

par exemple pour configurer un accès à la base de données via des beans on pourrait avoir (cas simple) :

a) le DataSource

C’est un objet qui contient l’URL, les identifiants de connexion à la base de données , et le pool de connexions.

@Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/testdb");
ds.setUsername("root");
ds.setPassword("password");
return ds;
}

b) le EntityManagerFactory

Cet objet sert à instancier un EntityManager qui fait la liaison entre les objets Java (entités) et les tables de la base de données. Il prend DataSource en paramètre.

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean emf =
new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource());
emf.setPackagesToScan("com.example.entity");
HibernateJpaVendorAdapter vendorAdapter =
new HibernateJpaVendorAdapter();
emf.setJpaVendorAdapter(vendorAdapter);
return emf;
}

c) un Transaction Manager

Il sert à effectuer les opérations sur la base de données dans le cadre de transactions. Il prend EntityManagerFactory en paramètre.

@Bean
public PlatformTransactionManager transactionManager(
EntityManagerFactory emf) {
JpaTransactionManager transactionManager =
new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}

La configuration complète pour communiquer avec la base de données pouvait donc ressembler à ça :

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.example.repository")
public class PersistenceConfig {

@Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/testdb");
ds.setUsername("root");
ds.setPassword("password");
return ds;
}

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean emf =
new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource());
emf.setPackagesToScan("com.example.entity");
HibernateJpaVendorAdapter vendorAdapter =
new HibernateJpaVendorAdapter();
emf.setJpaVendorAdapter(vendorAdapter);
return emf;
}

@Bean
public PlatformTransactionManager transactionManager(
EntityManagerFactory emf) {
JpaTransactionManager transactionManager =
new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
}

Il fallait aussi configurer soi-même la servlet principale qui gère les requêtes : La DispatcherServlet. Cette servlet est chargée de router les requêtes du serveur vers le contrôleur adéquat.

Un exemple de configuration de la DispatcherServlet.

public class WebAppInitializer implements WebApplicationInitializer {

@Override
public void onStartup(ServletContext container) throws ServletException {

AnnotationConfigWebApplicationContext context =
new AnnotationConfigWebApplicationContext();
context.register(WebConfig.class, PersistenceConfig.class);

ServletRegistration.Dynamic dispatcher =
container.addServlet("dispatcher", new DispatcherServlet(context));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}

On devait donc écrire tout ce code même dans les cas les plus simples. C’était lourd, fastidieux et complexe ❌.

2. Spring Boot fait le travail tout seul

Il est important de comprendre que l’auto-configuration de spring boot ne vient pas annuler ou remplacer la configuration vue précédemment. Elle reste nécessaire. Cependant, l’auto-configuration vient déclarer ces beans automatiquement en suivant certaines règles.

Voyons ça étape par étape 🔎

1️⃣ Etape I : @SpringBootApplication : le point d’entrée

C’est l’annotation qui définit un projet spring boot. Elle est présente dans la classe principale là ou se trouve la méthode main. Par exemple :

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

Cette annotation est un alias pour 3 autres annotations. C’est à dire qu’en écrivant @SpringBootApplication en réalité on déclare les 3 annotations suivantes :

🔸@SpringBootConfiguration : elle indique que la classe est une classe de configuration qui peut contenir des beans.

🔸@ComponentScan : elle détermine les packages qui vont être scannés au démarrage de spring. Le but étant d’enregistrer les BeanDefinition des classes annotées @component, @service, @repository, @configuration dans la BeanFactory. Un BeanDefinition est un “plan” qui sert à construire un Bean. Un peu comme une classe avec un objet.

🔸@EnableAutoConfiguration : C’est ELLE qui nous intéresse. Au démarrage cette annotation réalise l’import de la classe AutoConfigurationImportSelector.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})// <=== 👈 Importation ICI 👈
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

2️⃣ Etape II :AutoConfigurationImportSelector va lire un fichier qui contient la liste des classes d’auto-configuration

A une certaine étape dans le processus de démarrage, AutoConfigurationImportSelector va lire un fichier qui contient la liste des classes d’auto-configuration.

Hein ? Qui? quoi? quel fichier listant quelles classes d’auto-configuration?

quels fichiers?

Je t’explique 👇

chaque dépendance dans ton pom.xml (ou gradle.build) est ajoutée au classpath de l’application. Spring boot vient avec une dépendance un peu spéciale qui s’appelle spring-boot-autoconfigure.

spring-boot-autoconfigure.jar

Cette dépendance contient un fichier AutoConfiguration.imports dans son dossier META-INF. Le chemin complet est le suivant : META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

AutoConfiguration.imports

Ce fichier AutoConfiguration.imports contient une liste de classes d’autoconfiguration. Ces classes sont déjà déclarées et configurées avec des propriétés par défaut.

org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
....
....

AutoConfigurationImportSelector va donc lire ce fichier et enregistrer les classes d’autoconfiguration. Il ne crée ni les BeanDefinition de ces classes, ni les bean eux mêmes. Il ne fait QUE enregistrer ces classes comme “des classes potentielles à utiliser pour autoconfigurer l’application”.

📄AutoConfigurationImportSelector retourne une liste de classes
🔄ConfigurationClassParser les lit en profondeurs pour détecter les annotations à l’intérieur de ces classes.
🔄ConfigurationClassBeanDefinitionReader crée les BeanDefinition

🔄 ApplicationContext active ou non les classes de configuration

Les BeanDefinition des classes sont créées et enregistrées dans BeanFactory. Mais ce n’est pas pour autant qu’elles seront activées.

En effet, chaque classe d’auto-configuration contient des conditions d’activation. Si les conditions sont remplies, le BeanDefinition de la classe est utilisé pour effectivement créer le bean. Sinon, le bean n’est pas crée et l’autoconfiguration portée par cette classe n’est pas prise en compte dans l’application.

4️⃣ Un exemple concret : WebMvcAutoConfiguration

Une des classes qu’on retrouve dans AutoConfigure.imports est WebMvcAutoConfiguration. Elle configure automatiquement Spring MVC.

En lisant le fichier AutoConfigurationImportSelector va enregistrer cette classe (et toutes les autres) comme une autoconfiguration possible.

Une fois que toutes les classes sont enregistrées par AutoConfigurationImportSelector et que ApplicationContext a crée les BeanDefinition qui correspondent, vient alors le moment d’activer (créer le bean) ou de ne pas activer (ne pas créer le bean) de chaque BeanDefinition.

L’activation dépend des conditions décrites dans la classe elle-même. Ci-après le code de classe WebMvcAutoConfiguration.

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
public class WebMvcAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
return new RequestMappingHandlerAdapter();
}
}

Le bean est crée si les conditions suivantes sont remplies:

  • ✅ @ConditionalOnClass 👉 les classes Servlet, DispatcherServlet et WebMvcConfigurer sont présentes dans le classpath
  • ✅ @ConditionalOnWebApplication👉 l’application est de type SERVLET (pour info le type d’application est déduite automatiquement au démarrage)
  • ✅ @ConditionalOnMissingBean 👉 il n’existe pas déjà un bean RequestMappingHandlerAdapter.

Si les conditions sont réunies un bean RequestMappingHandlerAdapter est automatiquement crée et utilisé par spring boot sans que tu n’aies à faire quoi que ce soit. Magique 🌟

diagramme

3. En résumé

1️⃣ @SpringBootApplication active @EnableAutoConfiguration
2️⃣ @EnableAutoConfiguration importe AutoConfigurationImportSelector 
3️⃣ AutoConfigurationImportSelector lit AutoConfiguration.imports
4️⃣ Les classes d’auto-configuration sont chargées et les conditions (@ConditionalOnClass, @ConditionalOnMissingBean) décident quels beans créer

👉 Résultat : Spring Boot configure ton application automatiquement.

Si cet article t’a plu, n’hésite pas à me suivre pour plus de contenu sur Java et Spring Boot 👋.

Auteur/Autrice

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.

En savoir plus sur Blog Zenika

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Poursuivre la lecture