Les dessous du framework Spring
3 décembre 2015
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
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
Caractéristiqueso Appelé pendant le processus de création de tout bean Springo Implémente l’interface BeanPostProcessor
o 2 méthodes appelées avant et après la méthode d’initialisation du beano Peuvent être ordonnés
Etapes de mise à disposition d’un bean1. Création du bean par constructeur ou appel de méthode statique2. Valorisation des propriétés du bean3. Résolution des références vers d’autres beans (wiring)4. Appel de la méthode postProcessBeforeInitialization() de chaque post-processor5. Appel des méthodes d’initialisation du bean (@PostConstruct, afterPropertiesSet)6. Appel de la méthode postProcessAfterInitialization() de chaque post-processor7. Le bean est prêt à être utilisé par le conteneur
Les post-processeurs de bean (1/5)
Les post-processeurs de bean (2/5)
Chargement en mémoire de
la définition des beans
XML
Conf Spring Java
Classes annotée
s
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 *
postProcessBeforeInitializati
on
Pour chaque BeanPostProcessor
*
InitalisationBean prêtà l’emploi
postProcessAfterInitialization
afterPropertiesSet@PostConstruct
*
Possibilités multiples :1. modifier un bean2. renvoyer un proxy3. ajouer un intercepteur
Usage des post-processeurs par Spring et des frameworks tiers
Les post-processeurs de bean (3/5)
Post-Processeur DescriptionAutowiredAnnotationBeanPostProcessor Active l’auto-wiring via les annotations @Autowired
ou @InjectCommonAnnotationBeanPostProcessor Active la détection des annotations de la JSR-250AsyncAnnotationBeanPostProcessor Active la détection de l’annotation @AsyncScheduledAnnotationBeanPostProcessor Active la détection de l’annotation @ScheduledJmsListenerAnnotationBeanPostProcessor
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
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 { }
Mise en œuvre d’un AliasPostProcessor
Les post-processeurs de bean (5/5)@Target(TYPE)@Retention(RUNTIME)@Documentedpublic @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; }
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)
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>
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); } }}
Points de vigilance avec @Transactional (1/2) L’annotation @Transactional permet de délimiter des transactions
Point 1 : Commit ou Rollback ?
@Transactionalpublic void addMovie(Movie movie) throws BusinessException { movieDao.save(movie); throw new BusinessException();}
@Transactionalpublic 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
Points de vigilance avec @Transactional (2/2) Point 2: le film est-il sauvegardé dans une transaction ?
@Servicepublic 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
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)
Intercepteur transactionnel (2/2)MovieControlle
rJdkDynamicAopProx
yTransactionIntercept
or MovieService
addMovieinvoke
addMovie
Ouverture d’une transaction car
addMovie annoté
addThenIndexMovieaddThenIndexMovie addMovie
Pas de transaction car on ne repasse pas par le
proxy
Levée d’ambiguité avec @Primary (1/2) 2 implémentations d’une même interface@Servicepublic class NetflixService implements IMovieService { }@Servicepublic class ImdbService implements IMovieService { }
Spring est incapable de déterminer quel bean injecter
@Autowiredprivate IMovieService movieService;
NoUniqueBeanDefinitionException : expected single matching bean but found 2: imdbService,netflixService
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@Primarypublic 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
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)
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)
Servicesingleton
Conteneur Spring
ProxyInformationsUtilisateur
Contrôleursingleton
Jamessession
Johnsession
getName() getName()
Exemples de mise en oeuvre
Injection de beans de portée différente (3/3)
@Configurationpublic 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;… }
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
@Configurationstatic 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;} }
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 WebApplicationConte
xtDAO
Services métiers
ChildWebApplicationConte
xtContrôleurs IHM
ChildWebApplicationConte
xtContrôleurs REST
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>
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>
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")); }}
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@Componentpublic @interface Mapper { String value() default "";}
@Pointcut("within(@com.mycompagny.spring.Mapper *)")public void mapper() { /**/ }
Exemple d’utilisation en AOP dans les points de coupe :
Spring permet d’injecter une collection de beans du même type
Architecture pluggable
@Servicepublic 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
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); }
@Componentpublic class MonBean {
@Autowired private ApplicationContext applicationContext;
Depuis un bean Spring public class MonBean implements ApplicationContextAware {
@Override public void setApplicationContext( ApplicationContext applicationContext) { this.applicationContext = applicationContext; }
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