Chaque jour, de nombreux développeurs utilisent le framework Spring pour l’injection de dépendances et la gestion des transactions. Majeures, ces 2 fonctionnalités ne nécessitent pas un gros effort d’apprentissage. Pour autant, leurs mises en œuvre par le framework est complexe. Par curiosité intellectuelle, mais également afin d’éviter certains pièges et de profiter pleinement des capacités de Spring, il est intéressant de comprendre les mécanismes internes du framework qu’on utilise au quotidien : cycle de vie d’un bean, proxy, intercepteur, post-processeur, fabrique de beans, hiérarchie de contextes, portée …
Les slides de cette présentation ont pour objectif de vous les faire les introduire.
2. Objectifs
Exploiter certaines fonctionnalités méconnues de Spring
Découvrir le fonctionnement interne du conteneur
Eviter certains pièges
Apprendre à partir d’exemples concrets issus d’applications métiers
3. Sommaire
Post-processeurs et fabriques de beans Spring
Intercepteur transactionnel et pièges de l’annotation @Transactional
Lever des ambiguïtés lors de l’injection de beans
Injection de beans de portées différentes
Hiérarchie de contextes Spring
Créer sa propre annotation
Architecture pluggable
Accès au contexte Spring
4. Caractéristiques
o Appelé pendant le processus de création de tout bean Spring
o Implémente l’interface BeanPostProcessor
o 2 méthodes appelées avant et après la méthode d’initialisation du bean
o Peuvent être ordonnés
Etapes de mise à disposition d’un bean
1. Création du bean par constructeur ou appel de méthode statique
2. Valorisation des propriétés du bean
3. Résolution des références vers d’autres beans (wiring)
4. Appel de la méthode postProcessBeforeInitialization() de chaque post-processor
5. Appel des méthodes d’initialisation du bean (@PostConstruct, afterPropertiesSet)
6. Appel de la méthode postProcessAfterInitialization() de chaque post-processor
7. Le bean est prêt à être utilisé par le conteneur
Les post-processeurs de bean (1/5)
5. Les post-processeurs de bean (2/5)
Chargement en
mémoire de la
définition des
beans
XML
Conf
Spring
Java
Classes
annotées
Instanciation du
bean ou appel de
la fabrique beans
Etapes de mise à disposition des beans de portées singleton d’une application
Pour chaque
bean trouvé Appel des setters
Injection de beans
*
postProcess
BeforeInitialization
Pour chaque
BeanPost
Processor
*
Initalisation
Bean prêt
à l’emploi postProcess
AfterInitialization
afterPropertiesSet
@PostConstruct
*
Possibilités multiples :
1. modifier un bean
2. renvoyer un proxy
3. ajouer un intercepteur
6. Usage des post-processeurs par Spring et des frameworks tiers
Les post-processeurs de bean (3/5)
Post-Processeur Description
AutowiredAnnotationBeanPostProcessor Active l’auto-wiring via les annotations @Autowired ou
@Inject
CommonAnnotationBeanPostProcessor Active la détection des annotations de la JSR-250
AsyncAnnotationBeanPostProcessor Active la détection de l’annotation @Async
ScheduledAnnotationBeanPostProcessor Active la détection de l’annotation @Scheduled
JmsListenerAnnotationBeanPostProcessor Active la détection de l’annotation @JmsListener
ApplicationContextAwareProcessor Permet de passer le contexte applicatif Spring aux beans
implémentant l’interface ApplicationContextAware
ScriptFactoryPostProcessor Permet d’accéder au résultat de scripts (Groovy, JRuby )
BusExtensionPostProcessor Active la détection automatique d’extension de bus CXF
JaxWsWebServicePublisherBeanPostProcessor Support CXF des annotations JAX-WS
CamelBeanPostProcessor Intégration de beans Spring dans Apache Camel
7. Exemple d’utilisation
Disposer d’une annotation @Alias se substituant à la syntaxe XML
Demande d’évolution SPR-6736
Définition d’un alias en XML :
Equivalent en annotation :
Les post-processeurs de bean (4/5)
<bean id="movieController" class="MovieController"/>
<alias name="movieController" alias="filmController"/>
@Controller("movieController")
@Alias("filmController")
public class MovieController { }
8. Mise en œuvre d’un AliasPostProcessor
Les post-processeurs de bean (5/5)
@Target(TYPE)
@Retention(RUNTIME)
@Documented
public @interface Alias {
String value();
}
public class AliasPostProcessor
implements BeanPostProcessor, BeanFactoryPostProcessor {
private ConfigurableListableBeanFactory configurableBeanFactory;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
Annotation annotation = targetClass.getAnnotation(Alias.class);
if (annotation != null) {
configurableBeanFactory.registerAlias(beanName, ((Alias) annotation).value());
}
return bean;
}
9. Lors du mécanisme d’injection, met un bean à disposition du conteneur
Instancie ou réutilisation des beans (partage)
Massivement utilisé par le conteneur Spring
Exemple : HibernateSessionFactoryBean
Implémente l’interface FactoryBean
Ce n’est pas la fabrique qui est injectée mais le bean créé par la
fabrique (méthode getObject())
Fabrique de beans Spring (1/3)
10. Exemple d’une fabrique fusionnant des listes de beans
Input : plusieurs listes de beans Spring
Output : une seule liste de beans Spring
Utilisation avec la syntaxe XML :
Fabrique de beans Spring (2/3)
<bean id="annotatedClasses" class="ListMerger">
<property name="sourceLists">
<util:list>
<ref bean="customerAnnotatedClasses" />
<ref bean="aggreementAnnotatedClasses" />
</util:list>
</property>
</bean>
11. Implémentation de la fabrique fusionnant des listes de beans
Fabrique de beans Spring (3/3)
public class ListMerger<V> implements FactoryBean<List<V>> {
private List<V> result = new ArrayList<V>();
@Override
public List<V> getObject() { return result; }
@Override
public boolean isSingleton() { return true; }
@Override
public Class<?> getObjectType() { return List.class; }
public void setSourceLists(List<List<V>> sourceLists) {
for (List<V> l : sourceLists) { this.result.addAll(l); }
}
}
12. Points de vigilance avec @Transactional (1/2)
L’annotation @Transactional permet de délimiter des transactions
Point 1 : Commit ou Rollback ?
@Transactional
public void addMovie(Movie movie) throws BusinessException {
movieDao.save(movie);
throw new BusinessException();
}
@Transactional
public void addMovie(Movie movie) throws BusinessException {
movieDao.save(movie);
throw new TechnicalException();
}
Par défaut, lorsqu’une checked exception est levée, Spring valide la transaction
Remédiation : utilisation du rollbackFor @Transactional(rollbackFor = Exception.class)
ou création d’une annotation dédiée @MyTransactional
Commit
Rollback
13. Points de vigilance avec @Transactional (2/2)
Point 2: le film est-il sauvegardé dans une transaction ?
@Service
public class MovieService {
@Autowired IMovieDao movieDao;
public void addThenIndexMovie(Movie movie) {
addMovie(movie);
indexMovie(movie);
}
@Transactional
public void addMovie(Movie movie) { … }
private void indexMovie(Movie movie) { … }
}
@RequestMapping(value = "/movie/new")
public String create(@Valid Movie movie) {
movieService.addThenIndexMovie(movie);
return "redirect:/movie/" + movie.getId();
}
@RequestMapping(value = "/movie/new")
public String create(@Valid Movie movie) {
movieService.addMovie(movie);
return "redirect:/movie/" + movie.getId();
}
Scénario 1 : appel direct à addMovie
Scénario 2 : appel indirect à addMovie
Transactionnel
Non transactionnel
14. Lors de l’injection de beans, Spring peut modifier la chaîne d’appel
Design Pattern Proxy ou Intercepteur
Exemples d’usage :
Greffer des aspects
Insérer un comportement transactionnel
2 types de proxy en fonction de l’appelé :
Interface par l’utilisation de proxy dynamique java.lang.reflect.Proxy
Classe par instrumentation de code
Intercepteur transactionnel (1/2)
15. Intercepteur transactionnel (2/2)
MovieController JdkDynamicAopProxy TransactionInterceptor MovieService
addMovie
invoke
addMovie
Ouverture d’une
transaction car
addMovie annoté
addThenIndexMovie
addThenIndexMovie
addMovie
Pas de transaction car on ne
repasse pas par le proxy
16. Levée d’ambiguité avec @Primary (1/2)
2 implémentations d’une même interface
@Service
public class NetflixService implements IMovieService { }
@Service
public class ImdbService implements IMovieService { }
Spring est incapable de déterminer quel bean injecter
@Autowired
private IMovieService movieService;
NoUniqueBeanDefinitionException : expected single matching bean but found 2:
imdbService,netflixService
17. Levée d’ambiguité avec @Primary (2/2)
Solution 1 : utilisation d’un qualifier
@Autowired
@Qualifier("netflixService")
private IMovieService movieService;
Solution 2 : définition d’un bean principal (bean par défaut)
@Service
@Primary
public class NetflixService implements IMovieService { }
Permet de ne pas alourdir l’injection de dépendance lorsque dans la plupart des cas c’est le bean
netflixService qui doit être injecté.
Autres exemples : DataSourceTransactionManager vs JmsTransactionManager
NetflixService vs MockMovieService
18. Comment injecter un bean de portée session dans un singleton ?
Rappels
Un bean de portée Singleton
doit d’être thread-safe
est créé au démarrage du conteneur
Un bean de portée session est créé pour chaque session web utilisateur
Cas d’usage
Dans les contrôleurs, ne plus manipuler directement la session HTTP
Toutes les données à mettre en session sont modélisées dans un ou plusieurs beans
Avoir accès dans un service métier aux informations de l’utilisateur connecté
Besoin : contrôle des habilitations, historisation, logs
Un bean de portée requête peut remplacer l’utilisation du ThreadLocal
Injection de beans de portée différente (1/3)
19. Solution
Proxifier le bean de portée Session
Le proxy est chargé d’aiguiller les appels vers le bean session approprié
Illustration
Injection de beans de portée différente (2/3)
Service
singleton
Conteneur Spring
Proxy
Informations
Utilisateur
Contrôleur
singleton
James
session
John
session
getName() getName()
20. Exemples de mise en oeuvre
Injection de beans de portée différente (3/3)
@Configuration
public class UserConfig {
@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserDetails userDetails() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (UserDetails) authentication.getPrincipal();
}
}
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MovieModelView {
private Movie selectedMovie;
private String email;
…
}
21. Les scopes request et session n’ont de sens que dans un conteneur web
Erreur lorsqu’ils sont utilisés dans un simple ApplicationContext
java.lang.IllegalStateException: No Scope registered for scope 'session’
Tests de beans de portée web
@WebAppConfiguration
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class TestWebScope {
Solution 1 : l’annotation
@WebAppConfiguration
Solution 2 : déclarer un bean
CustomScopeConfigurer et utiliser le scope
SimpleThreadScope
@Configuration
static class Config {
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer csc = new CustomScopeConfigurer();
Map<String, Object> map = new HashMap<String, Object>();
map.put("session", new SimpleThreadScope());
map.put("request", new SimpleThreadScope());
csc.setScopes(map);
return csc;
} }
22. Une application Spring est composée d’un ou plusieurs contextes applicatifs
Les contextes applicatifs Spring peuvent être hiérarchiques
C’est typiquement le cas dans les applications web basées sur Spring MVC
Hiérarchie de contextes (1/4)
Root
WebApplicationContext
DAO
Services métiers
Child
WebApplicationContext
Contrôleurs IHM
Child
WebApplicationContext
Contrôleurs REST
23. Les beans déclarés dans le contexte parent sont visibles des contextes
enfants
Respect du découpage en couche
Alternative aux multi-wars
Chargement du contexte parent depuis un listener JEE déclaré dans le
web.xml :
Hiérarchie de contextes (2/4)
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/business-config.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
24. Chargement des contextes enfants via les DispatcherServlet déclarés
dans le web.xml
Hiérarchie de contextes (3/4)
<servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>org.springframework.web
.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:mvc-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>rest</servlet-name>
<servlet-class>org.springframework.web
.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:rest-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>rest</servlet-name>
<url-pattern>/rest</url-pattern>
</servlet-mapping>
25. L’annotation @ContextHierarchy rend possible l’écriture de tests
d’intégration avec hiérarchie de contextes
Hiérarchie de contextes (4/4)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(name = "parent", locations = "classpath:business-config.xml"),
@ContextConfiguration(name = "child", locations = "classpath:spring-mvc-config.xml") })
public class SpringConfigTest {
@Autowired
private WebApplicationContext childContext;
@Test
public void parentChildContext) {
ApplicationContext parentContext = childContext.getParent();
assertTrue(parentContext.containsBean("myService"));
assertTrue(childContext.containsBean("myService"));
assertFalse(parentContext.containsBean("myController"));
assertTrue(childContext.containsBean("myController"));
}
}
26. Possibilité d’étendre le jeu d’annotations @Component, @Service,
@Repository et @Controller
Exemple d’annotation utilisée sur les classes de mapping objet-objet :
Créer sa propre annotation @Component
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Mapper {
String value() default "";
}
@Pointcut("within(@com.mycompagny.spring.Mapper *)")
public void mapper() { /**/ }
Exemple d’utilisation en AOP dans les points de coupe :
27. Spring permet d’injecter une collection de beans du même type
Architecture pluggable
@Service
public class PluginManager {
@Autowired
private List<IPlugin> pluginList;
@Autowired
private Map<String, IPlugin> pluginMap;
}
Mécanisme d’extension très simple
Exemple : ajout de nouveaux formats de fichiers dans un composant
d’upload de fichiers
28. Depuis une Servlet
Accès au contexte Spring
public class MovieServlet extends HttpServlet {
private IMovieService movieService;
@Override
public void init() throws ServletException {
ApplicationContext applicationContext
= WebApplicationContextUtils.getWebApplicationContext(getServletContext());
movieService = applicationContext.getBean(IMovieService.class);
}
@Component
public class MonBean {
@Autowired
private ApplicationContext applicationContext;
Depuis un bean Spring
public class MonBean implements
ApplicationContextAware {
@Override
public void setApplicationContext(
ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
29. Conclusion
Les mécanismes utilisés en interne par Spring permettent
d’étendre les fonctionnalités du framework
Connaître le fonctionnement des proxy et des
intercepteurs permet d’éviter des bugs
Dans une application web, la portée des beans doit être
choisie judicieusement
Notas del editor
Démystifier le côté magique de certains comportements (ex: @Async)
Les directives XML telles que <context;annotation-config> activent une ou plusieurs BPP
Ordre déterminé par l’interfacr Ordered
Post-processeurs couramment utilisés dans Spring :
CommonAnnotationBeanPostProcessor : active la détection des annotations de la JSR-250 utilisées notamment pour les méthodes d’initialisation, de destruction et l’autowiring de ressources JEE.
RequiredAnnotationBeanPostProcessor : active la détection de l’annotation @Required qui permet de préciser les propriétés obligatoires dans un Bean.
AsyncAnnotationBeanPostProcessor : active la détection de l’annotation @Async qui permet d’exécuter des méthodes en asynchrone
Les directives XML telles que <context;annotation-config> activent une ou plusieurs BPP
Ordre déterminé par l’interfacr Ordered
Post-processeurs couramment utilisés dans Spring :
CommonAnnotationBeanPostProcessor : active la détection des annotations de la JSR-250 utilisées notamment pour les méthodes d’initialisation, de destruction et l’autowiring de ressources JEE.
RequiredAnnotationBeanPostProcessor : active la détection de l’annotation @Required qui permet de préciser les propriétés obligatoires dans un Bean.
AsyncAnnotationBeanPostProcessor : active la détection de l’annotation @Async qui permet d’exécuter des méthodes en asynchrone
Autre exemple d’utilisation bien plus complexe : mécanisme de notification
BeanFactoryPostProcessor permet d’avoir accès au ConfigurableListableBeanFactory
Utilisation : création de beans complexes
Autres exemples : TransactionProxyFactoryBean, EhCacheFactoryBean, ThreadPoolExecutorFactoryBean, VelocityEngineFactoryBean
Combinaison fréquente : annotation + fabrique de beans