Contextos da Web Spring

Contextos da Web Spring

1. Introdução

Ao usar o Spring em um aplicativo Web, temos várias opções para organizar os contextos do aplicativo que conectam tudo.

Neste artigo, vamos analisar e explicar as opções mais comuns que o Spring oferece.

2. O contexto do aplicativo Web raiz

Cada webapp Spring tem um contexto de aplicativo associado que está vinculado ao seu ciclo de vida: o contexto do aplicativo da web raiz.

Este é um recurso antigo anterior ao Spring Web MVC, portanto, não está vinculado especificamente a nenhuma tecnologia de estrutura da web.

O contexto é iniciado quando o aplicativo é iniciado e é destruído quando para, graças a um ouvinte de contexto de servlet. Os tipos mais comuns de contextos também podem ser atualizados em tempo de execução, embora nem todas as implementações deApplicationContext tenham esse recurso.

O contexto em um aplicativo da web é sempre uma instância deWebApplicationContext. Essa é uma interface que estendeApplicationContext com um contrato para acessarServletContext.

De qualquer forma, os aplicativos geralmente não devem se preocupar com os detalhes de implementação:the root web application context is simply a centralized place to define shared beans.

2.1. OContextLoaderListener

O contexto do aplicativo da web raiz descrito na seção anterior é gerenciado por um ouvinte da classeorg.springframework.web.context.ContextLoaderListener, que faz parte do módulospring-web.

By default, the listener will load an XML application context from /WEB-INF/applicationContext.xml. No entanto, esses padrões podem ser alterados. Podemos usar anotações Java em vez de XML, por exemplo.

Podemos configurar esse ouvinte no descritor do webapp (arquivoweb.xml) ou programaticamente em ambientes Servlet 3.x.

Nas seções a seguir, examinaremos cada uma dessas opções em detalhes.

2.2. Usandoweb.xmle um contexto de aplicativo XML

Ao usarweb.xml, configuramos o ouvinte como de costume:


    
        org.springframework.web.context.ContextLoaderListener
    

Podemos especificar um local alternativo da configuração do contexto XML com o parâmetrocontextConfigLocation:


    contextConfigLocation
    /WEB-INF/rootApplicationContext.xml

Ou mais de um local, separado por vírgulas:


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

Podemos até usar padrões:


    contextConfigLocation
    /WEB-INF/*-context.xml

Em qualquer caso,only one context is defined, combinando todas as definições de bean carregadas dos locais especificados.

2.3. Usandoweb.xmle um contexto de aplicativo Java

Também podemos especificar outros tipos de contextos além do padrão baseado em XML. Vejamos, por exemplo, como usar a configuração de anotações Java.

Usamos o parâmetrocontextClass para informar ao ouvinte qual tipo de contexto instanciar:


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

Every type of context may have a default configuration location. No nosso caso,AnnotationConfigWebApplicationContext não tem um, então temos que fornecê-lo.

Podemos, assim, listar uma ou mais classes anotadas:


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

Ou podemos dizer ao contexto para verificar um ou mais pacotes:


    contextConfigLocation
    org.example.bean.config

E, claro, podemos misturar e combinar as duas opções.

2.4. Configuração Programática com Servlet 3.x

Version 3 of the Servlet API has made configuration through the web.xml file completely optional. Bibliotecas podem fornecer seus fragmentos da web, que são peças de configuração XML que podem registrar ouvintes, filtros, servlets e assim por diante.

Além disso, os usuários têm acesso a uma API que permite definir programaticamente todos os elementos de um aplicativo baseado em servlet.

O módulospring-web faz uso desses recursos e oferece sua API para registrar componentes do aplicativo quando ele é iniciado.

O Spring verifica o classpath do aplicativo em busca de instâncias da classeorg.springframework.web.WebApplicationInitializer. Esta é uma interface com um único método,void onStartup(ServletContext servletContext) throws ServletException, que é invocado na inicialização do aplicativo.

Vejamos agora como podemos usar esse recurso para criar os mesmos tipos de contextos de aplicativo da web raiz que vimos anteriormente.

2.5. Usando Servlet 3.x e um contexto de aplicativo XML

Vamos começar com um contexto XML, assim como na Seção 2.2.

Implementaremos o métodoonStartup mencionado anteriormente:

public class ApplicationInitializer implements WebApplicationInitializer {

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

Vamos dividir a implementação linha por linha.

Primeiro, criamos um contexto raiz. Como queremos usar XML, ele deve ser um contexto de aplicativo baseado em XML e, como estamos em um ambiente da web, ele deve implementarWebApplicationContext também.

A primeira linha, portanto, é a versão explícita do parâmetrocontextClass que encontramos anteriormente, com o qual decidimos qual implementação de contexto específico usar:

XmlWebApplicationContext rootContext = new XmlWebApplicationContext();

Então, na segunda linha, dizemos ao contexto de onde carregar suas definições de bean. Novamente,setConfigLocations é o análogo programático do parâmetrocontextConfigLocation emweb.xml:

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

Por fim, criamos umContextLoaderListener com o contexto raiz e o registramos no contêiner de servlet. Como podemos ver,ContextLoaderListener tem um construtor apropriado que pega umWebApplicationContexte o disponibiliza para o aplicativo:

servletContext.addListener(new ContextLoaderListener(rootContext));

2.6. Usando Servlet 3.x e um contexto de aplicativo Java

Se quisermos usar um contexto baseado em anotação, poderíamos alterar o trecho de código na seção anterior para torná-lo instanciar umAnnotationConfigWebApplicationContext.

No entanto, vamos ver uma abordagem mais especializada para obter o mesmo resultado.

The WebApplicationInitializer class that we’ve seen earlier is a general-purpose interface. Acontece que o Spring fornece algumas implementações mais específicas, incluindo uma classe abstrata chamadaAbstractContextLoaderInitializer.

Sua tarefa, como o nome indica, é criar umContextLoaderListener e registrá-lo no contêiner de servlet.

Temos apenas que dizer como construir o contexto raiz:

public class AnnotationsBasedApplicationInitializer
  extends AbstractContextLoaderInitializer {

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

Aqui podemos ver que não precisamos mais registrarContextLoaderListener, o que nos poupa de um pouco de código clichê.

Observe também o uso do métodoregister que é específico paraAnnotationConfigWebApplicationContext em vez dosetConfigLocations mais genérico: invocando-o, podemos registrar classes individuais anotadas de@Configuration com o contexto , evitando assim a varredura de pacotes.

3. Contextos do servlet do despachante

Agora vamos nos concentrar em outro tipo de contexto de aplicativo. Desta vez, estaremos nos referindo a um recurso que é específico do Spring MVC, ao invés de parte do suporte de aplicativo da web genérico do Spring.

Spring MVC applications have at least one Dispatcher Servlet configured (mas possivelmente mais de um, falaremos sobre esse caso mais tarde). Este é o servlet que recebe solicitações de entrada, as envia para o método do controlador apropriado e retorna a visualização.

Each DispatcherServlet has an associated application context. Beans definidos em tais contextos configuram o servlet e definem objetos MVC como controladores e resolvedores de visualização.

Vamos ver como configurar o contexto do servlet primeiro. Veremos alguns detalhes detalhados posteriormente.

3.1. Usandoweb.xmle um contexto de aplicativo XML

DispatcherServlet é normalmente declarado emweb.xml com um nome e um mapeamento:


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


    normal-webapp
    /api/*

Se não especificado de outra forma, o nome do servlet é usado para determinar o arquivo XML a ser carregado. Em nosso exemplo, usaremos o arquivoWEB-INF/normal-webapp-servlet.xml.

Também podemos especificar um ou mais caminhos para arquivos XML, de maneira semelhante aContextLoaderListener:


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

3.2. Usandoweb.xmle um contexto de aplicativo Java

Quando queremos usar um tipo diferente de contexto, procedemos comoContextLoaderListener, novamente. Ou seja, especificamos um parâmetrocontextClass junto com umcontextConfigLocation adequado:


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

3.3. Usando Servlet 3.x e um contexto de aplicativo XML

Novamente, veremos dois métodos diferentes para declarar programaticamente umDispatcherServlet e aplicaremos um a um contexto XML e o outro a um contexto Java.

Então, vamos começar com um contexto de aplicativo genéricoWebApplicationInitializere XML.

Como vimos anteriormente, temos que implementar o métodoonStartup. No entanto, desta vez, criaremos e registraremos um servlet de despachante também:

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/*");

Podemos facilmente traçar um paralelo entre o código acima e os elementos de configuraçãoweb.xml equivalentes.

3.4. Usando Servlet 3.x e um contexto de aplicativo Java

Desta vez, vamos configurar um contexto baseado em anotações usando uma implementação especializada deWebApplicationInitializer:AbstractDispatcherServletInitializer.

Essa é uma classe abstrata que, além de criar um contexto de aplicativo da web raiz como visto anteriormente, nos permite registrar um servlet de despachante com boilerplate mínimo:

@Override
protected WebApplicationContext createServletApplicationContext() {

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

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

Aqui podemos ver um método para criar o contexto associado ao servlet, exatamente como vimos antes para o contexto raiz. Além disso, temos um método para especificar os mapeamentos do servlet, como emweb.xml.

4. Contextos pais e filhos

Até agora, vimos dois tipos principais de contextos: o contexto do aplicativo da web raiz e os contextos do servlet do distribuidor. Então, podemos ter uma pergunta:are those contexts related?

Acontece que sim, eles são. Na verdade,the root context is the parent of every dispatcher servlet context. Assim, os beans definidos no contexto do aplicativo da web raiz são visíveis para cada contexto de servlet do distribuidor, mas não vice-versa.

Portanto, normalmente, o contexto raiz é usado para definir os beans de serviço, enquanto o contexto do dispatcher contém os beans que estão especificamente relacionados ao MVC.

Observe que também vimos maneiras de criar o contexto do servlet do distribuidor programaticamente. Se definirmos manualmente seu pai, o Spring não substituirá nossa decisão e esta seção não se aplicará mais.

Em aplicativos MVC mais simples, é suficiente ter um único contexto associado ao único servlet do distribuidor. Não há necessidade de soluções excessivamente complexas!

Ainda assim, o relacionamento pai-filho se torna útil quando temos vários servlets de expedidor configurados. Mas quando devemos nos preocupar em ter mais de um?

Em geral,we declare multiple dispatcher servletswhen we need multiple sets of MVC configuration. Por exemplo, podemos ter uma API REST junto com um aplicativo MVC tradicional ou uma seção não segura e segura de um site:

image

Observação: quando estendemosAbstractDispatcherServletInitializer (consulte a seção 3.4), registramos um contexto de aplicativo da web raiz e um único servlet de despachante.

Portanto, se quisermos mais de um servlet, precisamos de várias implementaçõesAbstractDispatcherServletInitializer. No entanto, só podemos definir um contexto raiz, ou o aplicativo não iniciará.

Felizmente, o métodocreateRootApplicationContext pode retornarnull. Assim, podemos ter implementações de umAbstractContextLoaderInitializere muitosAbstractDispatcherServletInitializer que não criam um contexto raiz. Nesse cenário, é aconselhável ordenar os inicializadores com@Order explicitamente.

Além disso, observe queAbstractDispatcherServletInitializer registra o servlet com um determinado nome (dispatcher) e, claro, não podemos ter vários servlets com o mesmo nome. Portanto, precisamos substituirgetServletName:

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

5. Um exemplo de contexto pai e filho

Suponha que tenhamos duas áreas de nosso aplicativo, por exemplo, uma pública que seja acessível ao mundo e outra segura, com configurações diferentes do MVC. Aqui, vamos apenas definir dois controladores que geram uma mensagem diferente.

Além disso, suponha que alguns dos controladores precisem de um serviço que mantenha recursos significativos; um caso onipresente é persistência. Então, queremos instanciar esse serviço apenas uma vez, para evitar dobrar seu uso de recursos e porque acreditamos no princípio de Não se Repita!

Vamos agora prosseguir com o exemplo.

5.1. O serviço compartilhado

Em nosso exemplo de olá mundo, optamos por um serviço mais simples e acolhedor, em vez de persistência:

package com.example.contexts.services;

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

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

Declararemos o serviço no contexto do aplicativo da web raiz, usando a verificação de componentes:

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

Podemos preferir o XML:

5.2. Os controladores

Vamos definir dois controladores simples que usam o serviço e emitem uma saudação:

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() + "

";

Como podemos ver, os controladores estão em dois pacotes diferentes e imprimem mensagens diferentes: um diz "normal" e o outro "seguro".

5.3. Os contextos do servlet do despachante

Como dissemos anteriormente, teremos dois contextos de servlet de dispatcher diferentes, um para cada controlador. Então, vamos defini-los, em 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, se preferirmos, em XML:





5.4. Juntando tudo

Agora que temos todas as peças, precisamos dizer ao Spring para conectá-las. Lembre-se de que precisamos carregar o contexto raiz e definir os dois servlets do distribuidor. Embora tenhamos visto várias maneiras de fazer isso, agora vamos nos concentrar em dois cenários, um Java e um XML. Let’s start with Java.

Definiremos umAbstractContextLoaderInitializer para carregar o contexto raiz:

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

Então, precisamos criar os dois servlets, portanto, definiremos duas subclasses deAbstractDispatcherServletInitializer. Primeiro, o "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";
}

Em seguida, o "seguro", que carrega um contexto diferente e é mapeado para um caminho diferente:

@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";
}

E pronto! Acabamos de aplicar o que tocamos nas seções anteriores.

We can do the same with web.xml, novamente apenas combinando as peças que discutimos até agora.

Defina um contexto de aplicativo raiz:


    
        org.springframework.web.context.ContextLoaderListener
    

Um contexto de expedidor "normal":


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


    normal-webapp
    /api/*

E, finalmente, um contexto "seguro":


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


    secure-webapp
    /s/api/*

6. Combinando múltiplos contextos

Existem outras maneiras além de pai-filho de combinar vários locais de configuração,to split big contexts and better separate different concerns. Já vimos um exemplo: quando especificamoscontextConfigLocation com vários caminhos ou pacotes, o Spring constrói um único contexto combinando todos os definições de bean, como se fossem escritas em um único arquivo XML ou classe Java, na ordem.

No entanto, podemos obter um efeito semelhante com outros meios e até usar abordagens diferentes juntas. Vamos examinar nossas opções.

Uma possibilidade é a varredura de componente, que explicamosin another article.

6.1. Importando um contexto para outro

Como alternativa, podemos fazer com que uma definição de contexto importe outra. Dependendo do cenário, temos diferentes tipos de importações.

Importando uma classe@Configuration em Java:

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

Carregando algum outro tipo de recurso, por exemplo, uma definição de contexto XML, em Java:

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

Por fim, incluindo um arquivo XML em outro:

Assim, temos muitas maneiras de organizar os serviços, componentes, controladores etc. que colaboram para criar nosso aplicativo incrível. E o legal é que os IDEs entendem todos eles!

7. Aplicativos da Web Spring Boot

Spring Boot automatically configures the components of the application, então, geralmente, há menos necessidade de pensar em como organizá-los.

Ainda assim, por baixo do capô, o Boot usa recursos do Spring, incluindo aqueles que vimos até agora. Vamos ver algumas diferenças dignas de nota.

Aplicativos da web Spring Boot em execução em um contêiner integradodon’t run any WebApplicationInitializer por design.

Se for necessário, podemos escrever a mesma lógica em umSpringBootServletInitializer ou aServletContextInitializer, dependendo da estratégia de implantação escolhida.

No entanto, para adicionar servlets, filtros e ouvintes, como visto neste artigo, não é necessário fazer isso. In fact, Spring Boot automatically registers every servlet-related bean to the container:

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

Os objetos definidos são mapeados de acordo com as convenções: os filtros são mapeados automaticamente para / *, ou seja, para cada solicitação. Se registrarmos um único servlet, ele será mapeado para /, caso contrário, cada servlet será mapeado para seu nome de bean.

Se as convenções acima não funcionarem para nós, podemos definir umFilterRegistrationBean,ServletRegistrationBean, orServletListenerRegistrationBean. Essas classes nos permitem controlar os bons aspectos do registro.

8. Conclusões

Neste artigo, demos uma visão detalhada das várias opções disponíveis para estruturar e organizar um aplicativo da web Spring.

Deixamos alguns recursos de fora, principalmente osupport for a shared context in enterprise applications, que, no momento da escrita, ainda émissing from Spring 5.

A implementação de todos esses exemplos e trechos de código pode ser encontrada emthe GitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.