Contextes Web Printemps

Contextes Web du printemps

1. introduction

Lorsque vous utilisez Spring dans une application Web, nous disposons de plusieurs options pour organiser les contextes d’application qui connectent le tout.

Dans cet article, nous allons analyser et expliquer les options les plus courantes proposées par Spring.

2. Le contexte de l'application Web racine

Chaque application Web Spring a un contexte d'application associé qui est lié à son cycle de vie: le contexte d'application Web racine.

Il s'agit d'une ancienne fonctionnalité antérieure à Spring Web MVC. Elle n'est donc pas spécifiquement liée à une technologie de framework Web.

Le contexte est lancé au démarrage de l’application, et il est détruit à l’arrêt, grâce à un écouteur de contexte de servlet. Les types de contextes les plus courants peuvent également être actualisés lors de l'exécution, bien que toutes les implémentations deApplicationContext n'aient pas cette capacité.

Le contexte dans une application Web est toujours une instance deWebApplicationContext. C'est une interface étendantApplicationContext avec un contrat pour accéder auxServletContext.

Quoi qu'il en soit, les applications ne devraient généralement pas se préoccuper de ces détails d'implémentation:the root web application context is simply a centralized place to define shared beans.

2.1. LesContextLoaderListener

Le contexte de l'application Web racine décrit dans la section précédente est géré par un écouteur de la classeorg.springframework.web.context.ContextLoaderListener, qui fait partie du modulespring-web.

By default, the listener will load an XML application context from /WEB-INF/applicationContext.xml. Cependant, ces valeurs par défaut peuvent être modifiées. Nous pouvons utiliser des annotations Java au lieu de XML, par exemple.

Nous pouvons configurer cet écouteur soit dans le descripteur de l'application web (fichierweb.xml) soit par programme dans les environnements Servlet 3.x.

Dans les sections suivantes, nous examinerons chacune de ces options en détail.

2.2. Utilisation deweb.xml et d'un contexte d'application XML

Lors de l'utilisation deweb.xml, nous configurons l'auditeur comme d'habitude:


    
        org.springframework.web.context.ContextLoaderListener
    

Nous pouvons spécifier un autre emplacement de la configuration du contexte XML avec le paramètrecontextConfigLocation:


    contextConfigLocation
    /WEB-INF/rootApplicationContext.xml

Ou plusieurs emplacements séparés par des virgules:


    contextConfigLocation
    /WEB-INF/context1.xml, /WEB-INF/context2.xml

On peut même utiliser des patterns:


    contextConfigLocation
    /WEB-INF/*-context.xml

Dans tous les cas,only one context is defined, en combinant toutes les définitions de bean chargées à partir des emplacements spécifiés.

2.3. Utilisation deweb.xml et d'un contexte d'application Java

Nous pouvons également spécifier d'autres types de contextes que celui par défaut basé sur XML. Voyons, par exemple, comment utiliser à la place la configuration des annotations Java.

Nous utilisons le paramètrecontextClass pour indiquer à l'auditeur quel type de contexte instancier:


    contextClass
    
        org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    

Every type of context may have a default configuration location. Dans notre cas, leAnnotationConfigWebApplicationContext n'en a pas, nous devons donc le fournir.

On peut donc lister une ou plusieurs classes annotées:


    contextConfigLocation
    
        com.example.contexts.config.RootApplicationConfig,
        com.example.contexts.config.NormalWebAppConfig
    

Ou nous pouvons indiquer au contexte d'analyser un ou plusieurs packages:


    contextConfigLocation
    org.example.bean.config

Et, bien sûr, nous pouvons mélanger et assortir les deux options.

2.4. Configuration programmatique avec Servlet 3.x

Les bibliothèquesVersion 3 of the Servlet API has made configuration through the web.xml file completely optional. peuvent fournir leurs fragments Web, qui sont des éléments de configuration XML qui peuvent enregistrer des écouteurs, des filtres, des servlets, etc.

De plus, les utilisateurs ont accès à une API qui permet de définir par programme chaque élément d'une application basée sur un servlet.

Le modulespring-web utilise ces fonctionnalités et propose son API pour enregistrer les composants de l'application au démarrage.

Spring analyse le chemin de classe de l'application pour les instances de la classeorg.springframework.web.WebApplicationInitializer. Il s'agit d'une interface avec une seule méthode,void onStartup(ServletContext servletContext) throws ServletException, qui est appelée au démarrage de l'application.

Voyons maintenant comment nous pouvons utiliser cette fonction pour créer les mêmes types de contextes d'application Web racine que ceux que nous avons vus précédemment.

2.5. Utilisation de Servlet 3.x et d'un contexte d'application XML

Commençons par un contexte XML, comme dans la section 2.2.

Nous allons mettre en œuvre la méthodeonStartup susmentionnée:

public class ApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext)
      throws ServletException {
        //...
    }
}

Décomposons la mise en œuvre ligne par ligne.

Nous créons d'abord un contexte racine. Puisque nous voulons utiliser XML, il doit s'agir d'un contexte d'application basé sur XML, et comme nous sommes dans un environnement Web, il doit également implémenterWebApplicationContext.

La première ligne est donc la version explicite du paramètrecontextClass que nous avons rencontré précédemment, avec laquelle nous décidons quelle implémentation de contexte spécifique utiliser:

XmlWebApplicationContext rootContext = new XmlWebApplicationContext();

Ensuite, à la deuxième ligne, nous indiquons le contexte dans lequel charger les définitions de bean. Encore une fois,setConfigLocations est l'analogue programmatique du paramètrecontextConfigLocation enweb.xml:

rootContext.setConfigLocations("/WEB-INF/rootApplicationContext.xml");

Enfin, nous créons unContextLoaderListener avec le contexte racine et l'enregistrons avec le conteneur de servlet. Comme nous pouvons le voir,ContextLoaderListener a un constructeur approprié qui prend unWebApplicationContext et le rend disponible à l'application:

servletContext.addListener(new ContextLoaderListener(rootContext));

2.6. Utilisation de Servlet 3.x et d'un contexte d'application Java

Si nous voulons utiliser un contexte basé sur des annotations, nous pourrions modifier l'extrait de code de la section précédente pour qu'il instancie unAnnotationConfigWebApplicationContext à la place.

Cependant, voyons une approche plus spécialisée pour obtenir le même résultat.

The WebApplicationInitializer class that we’ve seen earlier is a general-purpose interface. Il s'avère que Spring fournit quelques implémentations plus spécifiques, y compris une classe abstraite appeléeAbstractContextLoaderInitializer.

Son travail, comme son nom l'indique, est de créer unContextLoaderListener et de l'enregistrer avec le conteneur de servlet.

Il suffit de lui dire comment construire le contexte racine:

public class AnnotationsBasedApplicationInitializer
  extends AbstractContextLoaderInitializer {

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        AnnotationConfigWebApplicationContext rootContext
          = new AnnotationConfigWebApplicationContext();
        rootContext.register(RootApplicationConfig.class);
        return rootContext;
    }
}

Ici, nous pouvons voir que nous n'avons plus besoin d'enregistrer lesContextLoaderListener, ce qui nous évite un peu de code standard.

Notez également l'utilisation de la méthoderegister spécifique àAnnotationConfigWebApplicationContext au lieu dessetConfigLocations plus génériques: en l'invoquant, nous pouvons enregistrer les classes annotées individuelles@Configuration avec le contexte , évitant ainsi la numérisation des paquets.

3. Contextes de servlet de répartiteur

Concentrons-nous maintenant sur un autre type de contexte d'application. Cette fois, nous ferons référence à une fonctionnalité spécifique à Spring MVC, plutôt qu'à une partie de la prise en charge des applications Web génériques de Spring.

Spring MVC applications have at least one Dispatcher Servlet configured (mais peut-être plus d'un, nous parlerons de ce cas plus tard). C'est le servlet qui reçoit les demandes entrantes, les distribue à la méthode de contrôleur appropriée et renvoie la vue.

Each DispatcherServlet has an associated application context. Les beans définis dans de tels contextes configurent le servlet et définissent les objets MVC tels que les contrôleurs et les résolveurs de vue.

Voyons d'abord comment configurer le contexte du servlet. Nous examinerons plus en détail plus tard.

3.1. Utilisation deweb.xml et d'un contexte d'application XML

DispatcherServlet est généralement déclaré enweb.xml avec un nom et un mappage:


    normal-webapp
    
        org.springframework.web.servlet.DispatcherServlet
    
    1


    normal-webapp
    /api/*

Sauf indication contraire, le nom du servlet est utilisé pour déterminer le fichier XML à charger. Dans notre exemple, nous utiliserons le fichierWEB-INF/normal-webapp-servlet.xml.

Nous pouvons également spécifier un ou plusieurs chemins vers des fichiers XML, de la même manière queContextLoaderListener:


    ...
    
        contextConfigLocation
        /WEB-INF/normal/*.xml
    

3.2. Utilisation deweb.xml et d'un contexte d'application Java

Lorsque nous voulons utiliser un autre type de contexte, nous procédons à nouveau comme avecContextLoaderListener. Autrement dit, nous spécifions un paramètrecontextClass avec uncontextConfigLocation approprié:


    normal-webapp-annotations
    
        org.springframework.web.servlet.DispatcherServlet
    
    
        contextClass
        
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        
    
    
        contextConfigLocation
        com.example.contexts.config.NormalWebAppConfig
    
    1

3.3. Utilisation de Servlet 3.x et d'un contexte d'application XML

Encore une fois, nous examinerons deux méthodes différentes pour déclarer par programme unDispatcherServlet, et nous appliquerons l'une à un contexte XML et l'autre à un contexte Java.

Commençons donc par unWebApplicationInitializer générique et un contexte d'application XML.

Comme nous l'avons vu précédemment, nous devons implémenter la méthodeonStartup. Cependant, cette fois, nous allons également créer et enregistrer un servlet de répartiteur:

XmlWebApplicationContext normalWebAppContext = new XmlWebApplicationContext();
normalWebAppContext.setConfigLocation("/WEB-INF/normal-webapp-servlet.xml");
ServletRegistration.Dynamic normal
  = servletContext.addServlet("normal-webapp",
    new DispatcherServlet(normalWebAppContext));
normal.setLoadOnStartup(1);
normal.addMapping("/api/*");

Nous pouvons facilement établir un parallèle entre le code ci-dessus et les éléments de configurationweb.xmléquivalents.

3.4. Utilisation de Servlet 3.x et d'un contexte d'application Java

Cette fois, nous allons configurer un contexte basé sur les annotations en utilisant une implémentation spécialisée deWebApplicationInitializer:AbstractDispatcherServletInitializer.

C'est une classe abstraite qui, en plus de créer un contexte d'application Web racine comme vu précédemment, nous permet d'enregistrer un servlet de répartiteur avec un minimum de passe-partout:

@Override
protected WebApplicationContext createServletApplicationContext() {

    AnnotationConfigWebApplicationContext secureWebAppContext
      = new AnnotationConfigWebApplicationContext();
    secureWebAppContext.register(SecureWebAppConfig.class);
    return secureWebAppContext;
}

@Override
protected String[] getServletMappings() {
    return new String[] { "/s/api/*" };
}

Ici, nous pouvons voir une méthode pour créer le contexte associé au servlet, exactement comme nous l'avons vu précédemment pour le contexte racine. De plus, nous avons une méthode pour spécifier les mappages du servlet, comme dansweb.xml.

4. Contextes parent et enfant

Jusqu'à présent, nous avons vu deux principaux types de contextes: le contexte de l'application Web racine et les contextes de servlet du répartiteur. Ensuite, nous pourrions avoir une question:are those contexts related?

Il s'avère que oui, ils le sont. En fait,the root context is the parent of every dispatcher servlet context. Ainsi, les beans définis dans le contexte de l'application Web racine sont visibles pour chaque contexte de servlet du répartiteur, mais pas l'inverse.

Ainsi, généralement, le contexte racine est utilisé pour définir les beans de service, tandis que le contexte du répartiteur contient les beans spécifiquement liés à MVC.

Notez que nous avons également vu des moyens de créer le contexte du servlet du répartiteur par programmation. Si nous définissons manuellement son parent, Spring ne remplace pas notre décision et cette section ne s'applique plus.

Dans les applications MVC plus simples, il suffit d’associer un seul contexte au seul servlet du répartiteur. Nul besoin de solutions trop complexes!

Néanmoins, la relation parent-enfant devient utile lorsque plusieurs servlets de distributeur sont configurés. Mais quand devrions-nous nous donner la peine d'en avoir plus d'un?

En général,we declare multiple dispatcher servletswhen we need multiple sets of MVC configuration. Par exemple, nous pouvons avoir une API REST avec une application MVC traditionnelle ou une section non sécurisée et sécurisée d'un site Web:

image

Remarque: lorsque nous étendonsAbstractDispatcherServletInitializer (voir section 3.4), nous enregistrons à la fois un contexte d'application Web racine et un seul servlet de répartiteur.

Donc, si nous voulons plus d'un servlet, nous avons besoin de plusieurs implémentations deAbstractDispatcherServletInitializer. Cependant, nous ne pouvons définir qu’un seul contexte racine, sinon l’application ne démarrera pas.

Heureusement, la méthodecreateRootApplicationContext peut renvoyernull. Ainsi, nous pouvons avoir une implémentationAbstractContextLoaderInitializer et plusieurs implémentationsAbstractDispatcherServletInitializer qui ne créent pas de contexte racine. Dans un tel scénario, il est conseillé de commander explicitement les initialiseurs avec@Order.

Notez également queAbstractDispatcherServletInitializer enregistre la servlet sous un nom donné (dispatcher) et, bien sûr, nous ne pouvons pas avoir plusieurs servlets avec le même nom. Donc, nous devons remplacergetServletName:

@Override
protected String getServletName() {
    return "another-dispatcher";
}

5. Un exemple de contexte parent et enfant

Supposons que notre application comporte deux zones, par exemple une zone publique accessible au monde et une zone sécurisée, avec des configurations MVC différentes. Ici, nous allons simplement définir deux contrôleurs qui génèrent un message différent.

Supposons également que certains contrôleurs ont besoin d’un service qui contient d’importantes ressources. un cas omniprésent est la persistance. Ensuite, nous voulons instancier ce service une seule fois, pour éviter de doubler son utilisation des ressources, et parce que nous croyons au principe Ne vous répétez pas!

Passons maintenant à l'exemple.

5.1. Le service partagé

Dans notre exemple hello world, nous avons opté pour un service d'accueil plus simple au lieu de la persistance:

package com.example.contexts.services;

@Service
public class GreeterService {
    @Resource
    private Greeting greeting;

    public String greet() {
        return greeting.getMessage();
    }
}

Nous déclarerons le service dans le contexte de l'application Web racine, à l'aide de l'analyse des composants:

@Configuration
@ComponentScan(basePackages = { "com.example.contexts.services" })
public class RootApplicationConfig {
    //...
}

Nous pourrions préférer XML à la place:

5.2. Les contrôleurs

Définissons deux contrôleurs simples qui utilisent le service et émettent un message d'accueil:

package com.example.contexts.normal;

@Controller
public class HelloWorldController {

    @Autowired
    private GreeterService greeterService;

    @RequestMapping(path = "/welcome")
    public ModelAndView helloWorld() {
        String message = "

Normal " + greeterService.greet() + "

"; return new ModelAndView("welcome", "message", message); } } //"Secure" Controller package com.example.contexts.secure; String message = "

Secure " + greeterService.greet() + "

";

Comme on peut le constater, les contrôleurs se trouvent dans deux packages différents et impriment des messages différents: l’un dit «normal», l’autre «sécurisé».

5.3. Les contextes du servlet Dispatcher

Comme nous l'avons dit précédemment, nous allons avoir deux contextes de servlet de répartiteur différents, un pour chaque contrôleur. Alors, définissons-les, en Java:

//Normal context
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.example.contexts.normal" })
public class NormalWebAppConfig implements WebMvcConfigurer {
    //...
}

//"Secure" context
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.example.contexts.secure" })
public class SecureWebAppConfig implements WebMvcConfigurer {
    //...
}

Ou, si on préfère, en XML:





5.4. Mettre tous ensemble

Maintenant que nous avons toutes les pièces, nous avons juste besoin de dire à Spring de les câbler. Rappelons que nous devons charger le contexte racine et définir les deux servlets de répartiteur. Bien que nous ayons vu plusieurs façons de le faire, nous allons maintenant nous concentrer sur deux scénarios, un Java et un XML. Let’s start with Java.

Nous allons définir unAbstractContextLoaderInitializer pour charger le contexte racine:

@Override
protected WebApplicationContext createRootApplicationContext() {
    AnnotationConfigWebApplicationContext rootContext
      = new AnnotationConfigWebApplicationContext();
    rootContext.register(RootApplicationConfig.class);
    return rootContext;
}

Ensuite, nous devons créer les deux servlets, nous allons donc définir deux sous-classes deAbstractDispatcherServletInitializer. Premièrement, le "normal":

@Override
protected WebApplicationContext createServletApplicationContext() {
    AnnotationConfigWebApplicationContext normalWebAppContext
      = new AnnotationConfigWebApplicationContext();
    normalWebAppContext.register(NormalWebAppConfig.class);
    return normalWebAppContext;
}

@Override
protected String[] getServletMappings() {
    return new String[] { "/api/*" };
}

@Override
protected String getServletName() {
    return "normal-dispatcher";
}

Ensuite, celui «sécurisé», qui charge un contexte différent et est mappé sur un chemin différent:

@Override
protected WebApplicationContext createServletApplicationContext() {
    AnnotationConfigWebApplicationContext secureWebAppContext
      = new AnnotationConfigWebApplicationContext();
    secureWebAppContext.register(SecureWebAppConfig.class);
    return secureWebAppContext;
}

@Override
protected String[] getServletMappings() {
    return new String[] { "/s/api/*" };
}

@Override
protected String getServletName() {
    return "secure-dispatcher";
}

Et nous avons terminé! Nous venons d'appliquer ce que nous avons touché dans les sections précédentes.

We can do the same with web.xml, encore une fois simplement en combinant les pièces dont nous avons discuté jusqu'à présent.

Définir un contexte d'application racine:


    
        org.springframework.web.context.ContextLoaderListener
    

Un contexte de répartiteur «normal»:


    normal-webapp
    
        org.springframework.web.servlet.DispatcherServlet
    
    1


    normal-webapp
    /api/*

Et, enfin, un contexte «sécurisé»:


    secure-webapp
    
        org.springframework.web.servlet.DispatcherServlet
    
    1


    secure-webapp
    /s/api/*

6. Combiner plusieurs contextes

Il existe d'autres moyens que parent-enfant de combiner plusieurs emplacements de configuration,to split big contexts and better separate different concerns. Nous avons déjà vu un exemple: lorsque nous spécifionscontextConfigLocation avec plusieurs chemins ou packages, Spring construit un seul contexte en combinant tous les définitions de bean, comme si elles étaient écrites dans un seul fichier XML ou une classe Java, dans l'ordre.

Cependant, nous pouvons obtenir un effet similaire avec d'autres moyens et même utiliser différentes approches ensemble. Examinons nos options.

Une possibilité est le balayage des composants, que nous expliquonsin another article.

6.1. Importer un contexte dans un autre

Alternativement, nous pouvons avoir une définition de contexte en importer une autre. Selon le scénario, nous avons différents types d’importations.

Importation d'une classe@Configuration en Java:

@Configuration
@Import(SomeOtherConfiguration.class)
public class Config { ... }

Chargement d'un autre type de ressource, par exemple une définition de contexte XML, en Java:

@Configuration
@ImportResource("classpath:basicConfigForPropertiesTwo.xml")
public class Config { ... }

Enfin, inclure un fichier XML dans un autre:

Ainsi, nous avons de nombreuses façons d’organiser les services, composants, contrôleurs, etc., qui collaborent pour créer notre application géniale. Et la bonne chose est que les IDE les comprennent tous!

7. Applications Web Spring Boot

Spring Boot automatically configures the components of the application, donc, généralement, il est moins nécessaire de réfléchir à la manière de les organiser.

Pourtant, sous le capot, Boot utilise les fonctionnalités de Spring, y compris celles que nous avons vues jusqu'à présent. Voyons quelques différences notables.

Applications Web Spring Boot s'exécutant dans un conteneur intégrédon’t run any WebApplicationInitializer par conception.

Si cela s'avère nécessaire, nous pouvons écrire la même logique dans unSpringBootServletInitializer  ou unServletContextInitializer à la place, en fonction de la stratégie de déploiement choisie.

Toutefois, pour ajouter des servlets, des filtres et des écouteurs, comme indiqué dans cet article, il n'est pas nécessaire de le faire. In fact, Spring Boot automatically registers every servlet-related bean to the container:

@Bean
public Servlet myServlet() { ... }

Les objets ainsi définis sont mappés selon les conventions: les filtres sont automatiquement mappés sur / *, c'est-à-dire sur chaque requête. Si nous enregistrons un seul servlet, celui-ci est associé à /, sinon chaque servlet est associé à son nom de bean.

Si les conventions ci-dessus ne fonctionnent pas pour nous, nous pouvons définir à la place unFilterRegistrationBean,ServletRegistrationBean,  ouServletListenerRegistrationBean. Ces cours nous permettent de contrôler les aspects délicats de l’enregistrement.

8. Conclusions

Dans cet article, nous avons présenté une vue détaillée des différentes options disponibles pour structurer et organiser une application Web Spring.

Nous avons omis certaines fonctionnalités, notamment lessupport for a shared context in enterprise applications, qui, au moment de l’écriture, sont toujoursmissing from Spring 5.

L'implémentation de tous ces exemples et extraits de code se trouve dansthe GitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.