MFormations
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é

ConceptUsage
@ComponentAnnotation de classe
@AutowiredInjection de dépendance
@ConfigurationClasse de configuration
@BeanDéfinition de bean
@ScopeCycle de vie
@AspectDécoupage transversal
@EventListenerÉcoute d'événements
@ValueInjection de valeur
ResourceLoaderChargement de ressources
@ProfileConfiguration par environnement