L'exploration du code source de Spring Framework et de Spring Boot est une étape fondamentale pour comprendre en profondeur leurs mécanismes internes. Ce guide détaille les prérequis et les étapes pour compiler les sources de Spring 6.x et Spring Boot 3.x, ainsi que des exemples illustratifs des fonctionnalités clés.
Compilation du code source de Spring 6.x
Pour compiler Spring 6.x, assurez-vous de disposer des outils suivants :
- JDK 17 ou supérieur
- Gradle 8.4 ou supérieur
- Un IDE comme IntelliJ IDEA 2023.2 ou plus récent
Le fichier gradle.properties à la racine du projet Spring doit contenir des configurations similaires à celles-ci pour optimiser la compilation :
version=6.0.2-SNAPSHOT
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
org.gradle.parallel=true
kotlinVersion=1.7.21
kotlin.stdlib.default.dependency=false
Notez que les scripts init.d de Gradle, s'ils sont configurés dans votre dépôt, seront lus avant ces propriétés.
Exemples de modules Spring Core
Compréhension du conteneur Spring
L'exemple suivant explore le cycle de vie des beans Spring, l'injection de dépendances, les interfaces de rappel et les processeurs de beans.
package com.example.spring.internals.core;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Configuration
public class DemonstrationPrincipale {
static class ServicePrincipal implements BeanNameAware, InitializingBean, DisposableBean {
@Autowired
private ServiceAuxiliaire serviceAuxiliaire;
private String identifiantInstance;
@Value("valeurParDefaut")
private String parametreConfig;
public void executer() {
System.out.println("ServicePrincipal#executer: Paramètre configuré = " + parametreConfig);
if (serviceAuxiliaire != null) {
serviceAuxiliaire.traiter();
}
}
public void methodeInitPersonnalisee() {
System.out.println("ServicePrincipal#methodeInitPersonnalisee: Initialisation personnalisée.");
}
public void methodeDestructionPersonnalisee() {
System.out.println("ServicePrincipal#methodeDestructionPersonnalisee: Nettoyage personnalisé.");
}
@Override
public void setBeanName(String nomBean) {
this.identifiantInstance = nomBean;
System.out.println("ServicePrincipal#setBeanName: Nom du bean = " + nomBean);
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("ServicePrincipal#afterPropertiesSet: Propriétés injectées.");
}
@Override
public void destroy() throws Exception {
System.out.println("ServicePrincipal#destroy: Nettoyage du bean.");
}
}
@Component
static class ServiceAuxiliaire implements InitializingBean {
@Autowired
private ServicePrincipal servicePrincipal; // Attention au cycle circulaire pour la démo
public void traiter() {
System.out.println("ServiceAuxiliaire#traiter: Service auxiliaire en action.");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("ServiceAuxiliaire#afterPropertiesSet: Initialisation du service auxiliaire.");
}
}
@Component
static class ComposantExterne {
@Autowired
@Lazy
private ServiceAuxiliaire serviceAuxiliaire;
@Value("123")
private String configurationValeur;
public void demarrer() {
System.out.println("ComposantExterne#demarrer: Config value = " + configurationValeur);
serviceAuxiliaire.traiter();
}
}
@Configuration
static class ConfigurationBeans {
@Bean(initMethod = "methodeInitPersonnalisee", destroyMethod = "methodeDestructionPersonnalisee")
public ServicePrincipal obtenirServicePrincipal() {
return new ServicePrincipal();
}
}
@Component
static class TraitementGlobalBean implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("TraitementGlobalBean#postProcessBeforeInitialization: " + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("TraitementGlobalBean#postProcessAfterInitialization: " + beanName);
return bean;
}
}
@Component
static class TraitementInstanciationBean implements InstantiationAwareBeanPostProcessor {
@Override
public Object postProcessBeforeInstantiation(Class> beanClass, String beanName) throws BeansException {
if (beanClass.equals(ServicePrincipal.class)) {
System.out.println("TraitementInstanciationBean#postProcessBeforeInstantiation: " + beanName + " (ServicePrincipal)");
// Pour cette démo, nous ne retournons rien pour laisser Spring créer l'instance.
}
return null;
}
@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
System.out.println("TraitementInstanciationBean#postProcessAfterInstantiation: " + beanName);
return true;
}
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
System.out.println("TraitementInstanciationBean#postProcessProperties: " + beanName);
return pvs;
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext contexteApplication = new AnnotationConfigApplicationContext(DemonstrationPrincipale.class, ConfigurationBeans.class);
ServicePrincipal service = contexteApplication.getBean(ServicePrincipal.class);
service.executer();
ComposantExterne externe = contexteApplication.getBean(ComposantExterne.class);
externe.demarrer();
contexteApplication.close();
}
}
Aspect-Oriented Programming (AOP)
Pour activer l'AOP basé sur AspectJ, la dépendance org.aspectj:aspectjweaver doit être ajoutée au fichier build.gradle. L'annotation @EnableAspectJAutoProxy sur une classe de configuration active la prise en charge des aspects.
package com.example.spring.internals.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Configuration
@EnableAspectJAutoProxy
public class DemoAOP {
@Component
static class CalculateurMathematique {
public int diviser(int numerateur, int denominateur) {
System.out.println("CalculateurMathematique#diviser est exécuté.");
return numerateur / denominateur;
}
}
@Component
@Aspect
static class AspectDeLog {
// Déclare un point de coupe pour toutes les méthodes 'diviser' de CalculateurMathematique
@Pointcut("execution(public int com.example.spring.internals.aop.DemoAOP.CalculateurMathematique.diviser(..))")
public void pointDeCoupeOperation() {}
@Around("pointDeCoupeOperation()")
public Object journalisationAutour(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("--- Début Around ---");
Class> classeCible = pjp.getTarget().getClass();
String nomMethode = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
System.out.println("Classe cible: " + classeCible.getName());
System.out.println("Méthode appelée: " + nomMethode);
System.out.println("Arguments: " + Arrays.toString(args));
try {
Object resultat = pjp.proceed(); // Exécute la méthode cible
System.out.println("Résultat de l'opération: " + resultat);
return resultat;
} catch (Throwable e) {
System.err.println("Exception dans l'opération: " + e.getMessage());
throw e; // Relaisser l'exception après la journalisation
} finally {
System.out.println("--- Fin Around ---");
}
}
@Before("pointDeCoupeOperation()")
public void journalisationAvant(JoinPoint jp) {
System.out.println("-> Avant l'exécution de " + jp.getSignature().getName() + " avec arguments: " + Arrays.asList(jp.getArgs()));
}
@After("pointDeCoupeOperation()")
public void journalisationApres(JoinPoint jp) {
System.out.println("<- Après l'exécution de " + jp.getSignature().getName());
}
@AfterReturning(value = "pointDeCoupeOperation()", returning = "valeurRetour")
public void journalisationRetour(JoinPoint jp, Object valeurRetour) {
System.out.println("<= Retour de " + jp.getSignature().getName() + ", valeur: " + valeurRetour);
}
@AfterThrowing(value = "pointDeCoupeOperation()", throwing = "exceptionGeneree")
public void journalisationException(JoinPoint jp, Exception exceptionGeneree) {
System.err.println("!!! Exception capturée après " + jp.getSignature().getName() + ": " + exceptionGeneree.getMessage());
}
}
public static void main(String[] args) {
ApplicationContext contexte = new AnnotationConfigApplicationContext(DemoAOP.class);
CalculateurMathematique calculateur = contexte.getBean(CalculateurMathematique.class);
System.out.println("\n--- Test d'opération valide ---");
try {
int resultat = calculateur.diviser(10, 2);
System.out.println("Résultat final: " + resultat);
} catch (Exception e) {
System.err.println("Une erreur inattendue est survenue: " + e.getMessage());
}
System.out.println("\n--- Test d'opération avec erreur ---");
try {
calculateur.diviser(10, 0); // Division par zéro
} catch (Exception e) {
System.err.println("Gestion de l'exception de division par zéro dans le main: " + e.getMessage());
}
((AnnotationConfigApplicationContext) contexte).close();
}
}
Accès aux données avec JdbcTemplate et transactions
Pour l'accès aux données et la gestion transactionnelle, les dépendances suivantes sont nécessaires :
dependencies {
api(project(":spring-context"))
api(project(":spring-jdbc"))
api(project(":spring-tx"))
optional("org.aspectj:aspectjweaver") // Si AOP est utilisé
implementation('mysql:mysql-connector-java:8.0.32')
implementation('com.alibaba:druid:1.2.16') // Ou une autre pool de connexions
}
L'exemple ci-dessous configure une source de données Druid, JdbcTemplate et un gestionnaire de transactions pour démontrer des opérations CRUD simples et le rollbakc transactionnel.
package com.example.spring.internals.jdbc;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
@Configuration
@EnableTransactionManagement
public class ConfigurationJdbcTransaction {
@Bean
public DataSource sourceDeDonnees() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUsername("root"); // Remplacez par votre utilisateur
druidDataSource.setPassword("your_password"); // Remplacez par votre mot de passe
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC");
return druidDataSource;
}
@Bean
public JdbcTemplate creerJdbcTemplate(@Autowired DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public DataSourceTransactionManager gestionnaireTransactions(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Data
static class Utilisateur {
private Integer id;
private String nomUtilisateur;
}
@Repository
static class DaoUtilisateurs {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<Utilisateur> recupererTousLesUtilisateurs() {
String sql = "SELECT id, nom_utilisateur FROM utilisateur";
List<Utilisateur> utilisateurs = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Utilisateur.class));
System.out.println("Utilisateurs trouvés: " + utilisateurs);
return utilisateurs;
}
@Transactional
public void insererUtilisateurEtErreur(String nomUtilisateur) {
System.out.println("Tentative d'ajout d'utilisateur avec transaction: " + nomUtilisateur);
String sql = "INSERT INTO utilisateur(nom_utilisateur) VALUES(?)";
jdbcTemplate.update(sql, nomUtilisateur);
// Simule une erreur pour tester le rollback transactionnel
throw new RuntimeException("Erreur simulée pour tester le rollback transactionnel.");
}
}
public static void main(String[] args) {
ApplicationContext contexte = new AnnotationConfigApplicationContext(ConfigurationJdbcTransaction.class);
DaoUtilisateurs daoUtilisateurs = contexte.getBean(DaoUtilisateurs.class);
// Assurez-vous d'avoir une table 'utilisateur' dans votre base de données:
// CREATE TABLE utilisateur (id INT AUTO_INCREMENT PRIMARY KEY, nom_utilisateur VARCHAR(255));
System.out.println("--- Avant l'opération transactionnelle ---");
daoUtilisateurs.recupererTousLesUtilisateurs();
System.out.println("\n--- Exécution de l'opération transactionnelle avec erreur ---");
try {
daoUtilisateurs.insererUtilisateurEtErreur(UUID.randomUUID().toString().substring(0, 8));
} catch (RuntimeException e) {
System.err.println("Exception capturée: " + e.getMessage());
}
System.out.println("\n--- Après l'opération transactionnelle (vérification du rollback) ---");
daoUtilisateurs.recupererTousLesUtilisateurs();
((AnnotationConfigApplicationContext) contexte).close();
}
}
Test de Spring MVC
Pour mettre en place un serveur web embarqué avec Spring MVC, ajoutez les dépendances suivantes :
dependencies {
api(project(":spring-context"))
api(project(":spring-webmvc"))
api(project(":spring-web"))
implementation("org.apache.tomcat.embed:tomcat-embed-core:9.0.78") // Ou version plus récente
implementation("javax.servlet:javax.servlet-api:4.0.1") // Pour Servlet API
// ... autres dépendances (JDBC, AOP si nécessaire)
}
Cet exemple configure un contrôleur REST simple et démarre un serveur Tomcat embarqué pour le tester.
package com.example.spring.internals.web;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;
import java.io.File;
import java.util.Date;
@Configuration
public class ConfigurationWebMvc {
@Bean
public MultipartResolver resolverMultipart() {
return new StandardServletMultipartResolver();
}
@RestController
static class ControleurApiSimple {
@GetMapping("/salutation")
public String saluer() {
return "Bonjour de Spring MVC! Il est " + new Date().toString();
}
@GetMapping("/erreur")
public String provoquerErreur() {
throw new RuntimeException("Erreur déclenchée intentionnellement.");
}
}
/**
* Démarre un serveur Tomcat embarqué avec le contexte Spring MVC.
*/
private static void demarrerServeurTomcat(WebApplicationContext appContext) {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080); // Utiliser le port 8080
tomcat.getConnector().setURIEncoding("UTF-8");
// Création du répertoire temporaire pour l'application web
File baseDir = new File(System.getProperty("java.io.tmpdir"), "tomcat-webapp");
baseDir.mkdirs(); // Assurez-vous que le répertoire existe
// Configuration du contexte web pour l'application
String cheminContexte = ""; // Chemin racine de l'application
Context contexteWeb = tomcat.addContext(cheminContexte, baseDir.getAbsolutePath());
contexteWeb.addLifecycleListener(new Tomcat.FixContextListener());
// Enregistrement du DispatcherServlet de Spring
Tomcat.addServlet(contexteWeb, "gestionnaireRequêtes", new DispatcherServlet(appContext));
contexteWeb.addServletMappingDecoded("/*", "gestionnaireRequêtes");
try {
tomcat.start();
System.out.println("Serveur Tomcat démarré sur http://localhost:8080/salutation");
tomcat.getServer().await(); // Maintient le thread principal en vie
} catch (LifecycleException e) {
System.err.println("Erreur au démarrage de Tomcat: " + e.getMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
AnnotationConfigWebApplicationContext contexteWeb = new AnnotationConfigWebApplicationContext();
contexteWeb.register(ConfigurationWebMvc.class);
contexteWeb.refresh();
demarrerServeurTomcat(contexteWeb);
}
}
Compilation du code source de Spring Boot 3.x
Pour la compilation de Spring Boot 3.x, téléchargez d'abord le code source (par exemple, Spring Boot 3.2.0). Pour améliorer les performances ou la fiabilité des téléchargements de dépendances, il est recommandé de configurer un miroir Maven dans les fichiers build.gradle et settings.gradle du projet :
repositories {
maven { url "https://maven.aliyun.com/repository/public" } // Exemple de miroir Aliyun
mavenCentral()
}
Ajout de tests pour Spring Boot
Pour tester des fonctionnalités de Spring Boot, vous pouvez créer un nouveau module de test. Ajoutez les dépendances nécessaires dans son fichier build.gradle :
plugins {
id "java"
id "org.springframework.boot.conventions" // Plugin Spring Boot pour les conventions
}
description = "Module d'étude pour Spring Boot"
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-webflux")) // Si WebFlux est nécessaire
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
}
Test de l'auto-configuration
L'auto-configuration est une caractéristique clé de Spring Boot. L'exemple suivant illustre comment créer des services conditionnels basés sur des propriétés ou la présence de classes.
package com.example.springboot.internals.autoconfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
// --- Définitions de services pour l'auto-configuration ---
interface ServiceSpecialise {
String obtenirMessage();
}
class ServicePrincipal implements ServiceSpecialise {
@Override
public String obtenirMessage() {
return " - Message du Service Principal.";
}
}
class ServiceSecondaire implements ServiceSpecialise {
@Override
public String obtenirMessage() {
return " - Message du Service Secondaire.";
}
}
// --- Classes factices pour les conditions OnClass ---
class MaClasseA {}
class MaClasseB {}
// class MaClasseC {} // Peut être utilisée pour tester l'absence
// --- Configurations automatiques ---
@AutoConfiguration
@ConditionalOnClass(name = {"com.example.springboot.internals.autoconfig.MaClasseA", "com.example.springboot.internals.autoconfig.MaClasseB"})
// @ConditionalOnMissingClass({"com.example.springboot.internals.autoconfig.MaClasseC"}) // Exemple d'utilisation
public class ConfigurationAutoConditionnelle {
@Bean
public MaClasseA maClasseA() {
return new MaClasseA();
}
}
// Fichier META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports doit contenir:
// com.example.springboot.internals.autoconfig.ConfigurationAutoUtilisateur
@AutoConfiguration
public class ConfigurationAutoUtilisateur {
@Bean
@ConditionalOnProperty(prefix = "app.service", name = "type", havingValue = "secondaire", matchIfMissing = false)
public ServiceSpecialise serviceSecondaire() {
return new ServiceSecondaire();
}
@Bean
@ConditionalOnProperty(prefix = "app.service", name = "type", havingValue = "principal", matchIfMissing = true)
public ServiceSpecialise servicePrincipal() {
return new ServicePrincipal();
}
}
// --- Application Spring Boot pour tester l'auto-configuration ---
@SpringBootApplication
@Controller
public class ApplicationWebPrincipale {
@Autowired(required = false) // 'required=false' si aucun service ne correspond aux conditions
private ServiceSpecialise serviceInjecte;
@GetMapping("/statut")
@ResponseBody
public String obtenirStatut() {
if (this.serviceInjecte != null) {
return "Application démarrée le " + new Date().toString() + this.serviceInjecte.obtenirMessage();
}
return "Application démarrée le " + new Date().toString() + " - Aucun service spécialisé injecté.";
}
public static void main(String[] args) {
SpringApplication.run(ApplicationWebPrincipale.class, args);
}
}
Pour tester les conditions @ConditionalOnProperty, vous pouvez ajouter des propriétés dans un fichier application.properties ou application.yml:
# Test pour ServicePrincipal (matchIfMissing=true par défaut si la propriété n'est pas définie ou vaut 'principal')
# app.service.type=principal
# Test pour ServiceSecondaire
app.service.type=secondaire