Modern Java Engineering
Chapitre 6
Chapitre 06 : Spring Core
Chapitre 06 : Spring Core
Cours : Spring Core
1. IoC Container
1.1 Principe
L'Inversion de Contrôle (IoC) signifie que le contrôle du cycle de vie et des dépendances des objets est transféré de l'application au conteneur Spring.
Sans Spring : Avec Spring :
new Service() @Autowired
new Repository() Service → Repository (injecté)
new Config() Config → (propriétés externes)
1.2 ApplicationContext
// Création du contexte
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// Récupération d'un bean
MyService service = context.getBean(MyService.class);
MyService service = context.getBean("myService", MyService.class);
// Vérification
if (context.containsBean("myService")) {
System.out.println("Bean présent");
}
// Liste des beans
String[] names = context.getBeanDefinitionNames();
1.3 Types de Contextes
// Annotation (moderne)
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
// XML (legacy)
ClassPathXmlApplicationContext ctx =
new ClassPathXmlApplicationContext("applicationContext.xml");
// Spring Boot
// ConfigurableApplicationContext créé automatiquement
2. Dependency Injection
2.1 Modes d'Injection
// ✅ 1. Constructor Injection (RECOMMANDÉ)
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// Spring injecte automatiquement les dépendances
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
// ✅ 2. Setter Injection (pour dépendances optionnelles)
@Service
public class NotificationService {
private EmailService emailService;
@Autowired(required = false)
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
}
// ❌ 3. Field Injection (déconseillé - difficile à tester)
@Service
public class PaymentService {
@Autowired
private PaymentGateway paymentGateway; // ❌ Pas final, pas testable facilement
}
2.2 @Autowired
@Component
public class OrderService {
// Constructor injection (Spring 4.3+ : @Autowired optionnel si 1 constructeur)
private final OrderRepository repository;
private final InventoryClient inventoryClient;
public OrderService(OrderRepository repository,
@Autowired(required = false) InventoryClient inventoryClient) {
this.repository = repository;
this.inventoryClient = inventoryClient;
}
// Method injection
@Autowired
public void configure(MetricsRegistry metrics) {
this.metrics = metrics;
}
// Qualifier (plusieurs beans du même type)
@Autowired
@Qualifier("primaryDb")
private DataSource primaryDataSource;
@Autowired
@Qualifier("secondaryDb")
private DataSource secondaryDataSource;
}
2.3 @Primary et @Qualifier
@Configuration
public class DatabaseConfig {
@Bean
@Primary // Prioritaire
public DataSource primaryDataSource() {
return new HikariDataSource(/* config prod */);
}
@Bean
@Qualifier("secondary")
public DataSource secondaryDataSource() {
return new HikariDataSource(/* config dev */);
}
}
// Utilisation
@Service
public class DataService {
private final DataSource primary;
private final DataSource secondary;
public DataService(@Qualifier("secondary") DataSource secondary,
DataSource primary) { // @Primary par défaut
this.primary = primary;
this.secondary = secondary;
}
}
2.4 Injection de Collections
@Component
public class ReportService {
// Injecte tous les beans implémentant ReportGenerator
private final List<ReportGenerator> generators;
// Injecte une Map avec les noms des beans
private final Map<String, ReportGenerator> generatorMap;
public ReportService(List<ReportGenerator> generators,
Map<String, ReportGenerator> generatorMap) {
this.generators = generators;
this.generatorMap = generatorMap;
}
}
// @Order pour ordonner
@Component @Order(1)
class PdfReportGenerator implements ReportGenerator { }
@Component @Order(2)
class ExcelReportGenerator implements ReportGenerator { }
3. Bean Lifecycle
3.1 Cycle de Vie
Spring Container
↓
1. Instanciation (constructeur)
2. Injection des dépendances (@Autowired)
3. PostConstruct (@PostConstruct)
4. InitializingBean.afterPropertiesSet()
5. Custom init-method (@Bean(initMethod="init"))
↓ Bean prêt à l'emploi
6. @PreDestroy
7. DisposableBean.destroy()
8. Custom destroy-method
@Component
public class MyBean {
public MyBean() {
System.out.println("1. Constructeur");
}
@Autowired
public void setDependency(Dependency dep) {
System.out.println("2. Injection");
}
@PostConstruct
public void init() {
System.out.println("3. PostConstruct");
}
@PreDestroy
public void cleanup() {
System.out.println("6. PreDestroy");
}
}
3.2 Bean Scopes
@Component
@Scope("singleton") // Défaut : un bean par conteneur Spring
public class SingletonBean { }
@Component
@Scope("prototype") // Nouvelle instance à chaque injection
@Lazy // La création est retardée jusqu'à la première utilisation
public class PrototypeBean { }
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
// Un bean par requête HTTP (WebApplicationContext)
public class RequestBean { }
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
// Un bean par session HTTP
public class SessionBean { }
// Scope personnalisé
@Component
@Scope("thread")
public class ThreadBean { }
3.3 Bean Post Processors
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof MyService) {
System.out.println("Avant init: " + beanName);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof MyService) {
System.out.println("Après init: " + beanName);
}
return bean;
}
}
// BeanFactoryPostProcessor (agit sur les définitions de beans)
@Component
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) {
BeanDefinition bd = factory.getBeanDefinition("myBean");
bd.setScope("prototype");
}
}
3.4 @Lazy
@Configuration
public class AppConfig {
@Bean
@Lazy // Créé seulement quand injecté
public ExpensiveBean expensiveBean() {
return new ExpensiveBean();
}
}
@Component
@Lazy
public class LazyService { }
3.5 @DependsOn
@Configuration
public class AppConfig {
@Bean
@DependsOn({"dataSource", "cacheManager"})
public DatabaseInitializer initializer() {
return new DatabaseInitializer();
}
}
4. @Configuration et @ComponentScan
4.1 Java Configuration
@Configuration
@ComponentScan(basePackages = "com.example")
@PropertySource("classpath:application.properties")
public class AppConfig {
@Value("${db.url}")
private String dbUrl;
@Value("${db.pool.size:10}")
private int poolSize;
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(dbUrl);
config.setMaximumPoolSize(poolSize);
return new HikariDataSource(config);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// Conditionnel
@Bean
@ConditionalOnProperty(name = "feature.cache.enabled", havingValue = "true")
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
}
4.2 @ComponentScan
@Configuration
@ComponentScan(
basePackages = {"com.example.service", "com.example.repository"},
basePackageClasses = Application.class,
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = ".*Test.*"
),
includeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Service.class
)
)
public class AppConfig { }
4.3 @Profile
@Configuration
@Profile("dev")
public class DevConfig {
@Bean
public DataSource dataSource() {
return new H2DataSource(); // H2 en dev
}
}
@Configuration
@Profile("prod")
public class ProdConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(/* PostgreSQL */); // PostgreSQL en prod
}
}
5. AOP (Aspect Oriented Programming)
5.1 Concepts
Aspect = Pointcut + Advice
Pointcut : où ?
Advice : quand ? (Before, After, Around)
JoinPoint : point d'exécution dans le code
Weaving : liaison aspect + code
5.2 Dépendance
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
5.3 Définir un Aspect
@Aspect
@Component
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
// Pointcut : toutes les méthodes des services
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void beforeService(JoinPoint jp) {
log.info("Appel: {}.{}()",
jp.getTarget().getClass().getSimpleName(),
jp.getSignature().getName());
}
@AfterReturning(value = "serviceMethods()", returning = "result")
public void afterReturning(JoinPoint jp, Object result) {
log.info("Retour: {} = {}", jp.getSignature().getName(), result);
}
@AfterThrowing(value = "serviceMethods()", throwing = "error")
public void afterThrowing(JoinPoint jp, Throwable error) {
log.error("Erreur: {} - {}", jp.getSignature().getName(), error.getMessage());
}
@Around("serviceMethods()")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed(); // Exécute la méthode
} finally {
long duration = (System.nanoTime() - start) / 1_000_000;
if (duration > 100) {
log.warn("Performance: {}.{}() a pris {}ms",
pjp.getTarget().getClass().getSimpleName(),
pjp.getSignature().getName(),
duration);
}
}
}
}
5.4 Pointcut Expressions
@Pointcut("execution(public * com.example.service.*.*(..))")
// Toutes les méthodes publiques de tous les services
@Pointcut("within(com.example.service..*)")
// Toutes les méthodes dans le package service et sous-packages
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
// Toutes les méthodes annotées @Transactional
@Pointcut("bean(*Service)")
// Tous les beans dont le nom se termine par Service
@Pointcut("this(com.example.service.UserService)")
// Toutes les méthodes de l'interface UserService (proxy JDK)
@Pointcut("target(com.example.service.UserService)")
// Toutes les méthodes de l'implémentation (proxy CGLIB)
5.5 Custom Annotation pour AOP
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}
@Aspect
@Component
public class ExecutionTimeAspect {
@Around("@annotation(LogExecutionTime)")
public Object logTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
Object result = pjp.proceed();
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.printf("%s executé en %dms%n",
pjp.getSignature().getName(), duration);
return result;
}
}
// Utilisation
@Service
public class ReportService {
@LogExecutionTime
public void generateReport() {
// ...
}
}
6. Spring Expression Language (SpEL)
6.1 Syntaxe
@Component
public class SpELDemo {
// Valeurs littérales
@Value("#{2 + 3}")
private int sum; // 5
@Value("#{'Hello ' + 'World'}")
private String greeting; // "Hello World"
// Booléen
@Value("#{2 > 1}")
private boolean trueValue;
// Appel de méthode
@Value("#{T(java.util.UUID).randomUUID().toString()}")
private String uuid;
// Bean reference
@Value("#{userRepository.findByEmail('admin@test.com')}")
private User admin;
// Condition
@Value("#{systemProperties['user.region'] ?: 'FR'}")
private String region;
// Collection
@Value("#{'${app.servers}'.split(',')}")
private List<String> servers;
// Map
@Value("#{${app.config}}")
private Map<String, String> config;
// Opérateur elvis
@Value("#{systemProperties['user.name'] ?: 'anonymous'}")
private String userName;
}
// Dans @Cacheable
@Cacheable(value = "users", key = "#id + '-' + #region")
@Cacheable(value = "users", key = "#user.email")
@Cacheable(value = "users", condition = "#id > 100")
6.2 Utilisation Programmative
ExpressionParser parser = new SpelExpressionParser();
// Expression simple
Expression exp = parser.parseExpression("'Hello World'.length()");
int length = exp.getValue(Integer.class); // 11
// Avec contexte
User user = new User("Alice");
EvaluationContext context = new StandardEvaluationContext(user);
String name = parser.parseExpression("name").getValue(context, String.class);
// Opérations sur collections
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
context = new StandardEvaluationContext(numbers);
exp = parser.parseExpression("#root.?[#this > 3]");
List<Integer> filtered = exp.getValue(context, List.class); // [4, 5]
7. ResourceLoader
@Component
public class ResourceService {
private final ResourceLoader resourceLoader;
public ResourceService(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public void loadResources() throws IOException {
// Fichier système
Resource file = resourceLoader.getResource("file:/path/to/file.txt");
// Classpath
Resource classpath = resourceLoader.getResource("classpath:data.json");
// URL
Resource url = resourceLoader.getResource("https://api.example.com/data");
// Lecture
if (classpath.exists()) {
String content = Files.readString(Path.of(classpath.getURI()));
System.out.println(content);
}
// Métadonnées
System.out.println("Filename: " + classpath.getFilename());
System.out.println("Exists: " + classpath.exists());
System.out.println("Readable: " + classpath.isReadable());
}
}
// Injection directe
@Service
public class ConfigService {
@Value("classpath:config.properties")
private Resource configFile;
@Value("file:${app.config.path}")
private Resource externalConfig;
@Value("https://api.example.com/version")
private Resource versionUrl;
}
8. Events
8.1 Créer un Événement
// Événement personnalisé
public class OrderCreatedEvent extends ApplicationEvent {
private final Long orderId;
private final String customerEmail;
private final double total;
public OrderCreatedEvent(Object source, Long orderId,
String customerEmail, double total) {
super(source);
this.orderId = orderId;
this.customerEmail = customerEmail;
this.total = total;
}
// Getters
}
// Publication
@Component
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public Order createOrder(OrderDTO dto) {
Order order = saveOrder(dto);
// Publie l'événement
publisher.publishEvent(new OrderCreatedEvent(this,
order.getId(), order.getCustomerEmail(), order.getTotal()));
return order;
}
}
8.2 Écouter les Événements
@Component
public class OrderEventListener {
private static final Logger log = LoggerFactory.getLogger(OrderEventListener.class);
// @EventListener (Spring 4.2+)
@EventListener
@Async
public void handleOrderCreated(OrderCreatedEvent event) {
log.info("Commande {} créée pour {}", event.getOrderId(), event.getCustomerEmail());
// Envoyer email, mettre à jour analytics, etc.
}
// Condition
@EventListener(condition = "#event.total > 1000")
public void handleLargeOrder(OrderCreatedEvent event) {
log.warn("⚠️ Grande commande: {} - {}€", event.getOrderId(), event.getTotal());
// Notifier manager
}
// Ordre
@EventListener
@Order(1)
public void firstHandler(OrderCreatedEvent event) { }
@EventListener
@Order(2)
public void secondHandler(OrderCreatedEvent event) { }
}
// Transactional Event Listener
@Component
public class InventoryEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
// Exécuté seulement si la transaction a été validée
updateInventory(event.getOrderId());
}
}
8.3 Événements Génériques
// Événement générique
public class EntityCreatedEvent<T> extends ApplicationEvent {
private final T entity;
public EntityCreatedEvent(Object source, T entity) {
super(source);
this.entity = entity;
}
public T getEntity() { return entity; }
}
// Listener générique
@Component
public class GenericEventListener {
@EventListener
public void handleUserCreated(EntityCreatedEvent<User> event) {
User user = event.getEntity();
System.out.println("Utilisateur créé: " + user.getName());
}
@EventListener
public void handleOrderCreated(EntityCreatedEvent<Order> event) {
Order order = event.getEntity();
System.out.println("Commande créée: " + order.getId());
}
}
9. Résumé
| Concept | Usage |
|---|---|
| @Component | Annotation de classe |
| @Autowired | Injection de dépendance |
| @Configuration | Classe de configuration |
| @Bean | Définition de bean |
| @Scope | Cycle de vie |
| @Aspect | Découpage transversal |
| @EventListener | Écoute d'événements |
| @Value | Injection de valeur |
| ResourceLoader | Chargement de ressources |
| @Profile | Configuration par environnement |