Spring Boot y manejo de contexto

Spring Boot y manejo de contexto

Introducción

El equipo de desarrollo de CCBill revisó recientemente algunas de las aplicaciones web centrales de la empresa. El objetivo era modernizarlos y aplicar mejor la arquitectura de microservicios para permitir una implementación, escalado y resiliencia más fáciles. Teníamos aplicaciones creadas con Spring Framework con implementación WAR tradicional y nuestro objetivo era transformarlas en aplicaciones Spring Boot con contenedor de servlets integrado (Tomcat) y empaquetado jar. También queríamos aprovechar la API de Servlet 3.0+ y pasar por completo a la configuración programática (Java). Una de las aplicaciones tenía varios servlets de despachador definidos en web.xml archivo, lo que significa pocos servlets y sus contextos dentro de la aplicación. Dado que mi tarea era crear contextos para la aplicación Spring (Boot) utilizando la configuración de Java, comencé a buscar algunos recursos útiles sobre el tema. La documentación oficial para este tema en particular era bastante escasa y me tomó un tiempo encontrar una solución. Frustrado por la falta de artículos web que cubran el tema, decidí escribir este artículo y, con suerte, llenar ese vacío. Además, creé aplicaciones de muestra en GitHub que demuestran completamente los ejemplos discutidos aquí. Este blog y sus aplicaciones de muestra utilizan Spring Boot 2.x. Para comprobar las muestras de Spring Boot 1.x primavera-arranque-1.x etiqueta.

¿Por qué múltiples contextos?

En la mayoría de los casos, las aplicaciones Spring (Boot) modernas solo tienen un contexto. La arquitectura de microservicio normalmente reduce la necesidad de una estructura de aplicación compleja, pero algunas situaciones pueden requerir contextos adicionales. En mi caso, había una aplicación existente con más de un contexto que tenía que transformarse. Al tener múltiples contextos, es posible aprovechar la posibilidad de proporcionar una configuración diferente y anular una del contexto principal si es necesario. Incluso es posible tener diferentes clases o cargadores de recursos para evitar posibles conflictos. Por ejemplo, si se necesitan ciertos beans (configuración) solo cuando se procesa una solicitud de una ruta determinada (p. Ej. / foo) y se debe evitar interferir con otros beans del resto de la aplicación: se puede crear un servlet, darle un contexto web y colocar allí todos los beans necesarios y configuraciones específicas. Además, se requiere un contexto adicional si es necesario exponer algunas funcionalidades a través de un puerto adicional. Crear un contexto separado y agregarle beans puede ser muy útil al construir una biblioteca que será parte de las aplicaciones existentes. Colocar los frijoles en un contexto separado evitará la invalidación del frijol:
En caso de múltiples clases de @Configuration, los métodos de @Bean definidos en clases posteriores anularán los definidos en clases anteriores.
Los contextos se pueden organizar en jerarquía. Un contexto puede tener solo un contexto principal, pero puede haber varios contextos secundarios. Los beans del contexto principal son visibles en el contexto secundario, pero no al revés. De esa manera, un contexto secundario puede usar beans y la configuración del contexto principal y anular solo lo necesario. Un ejemplo de eso puede ser una seguridad definida en el contexto principal y usada / anulada en contextos secundarios.

Pruebas de integración

Spring y Spring Boot proporcionan muchas utilidades para las pruebas de integración. Es posible establecer una configuración de contexto mínima para una prueba específica. En lugar de activar la configuración completa para el contexto (o múltiples contextos), es posible ejecutar las pruebas solo con la configuración requerida. De esa manera, las pruebas comenzarán más rápido y ahorrará unos segundos, pero esto debe evitarse. En mi opinión, el entorno de integración debería imitar el del tiempo de ejecución porque eso ayuda a que las pruebas sean más confiables. Además de esto, los servidores de compilación dedicados y los discos más rápidos minimizan el impacto de la duración de la ejecución de la prueba. Hay casos específicos en los que probar contextos de forma aislada tiene sentido y debe aplicarse solo cuando no tienen nada en común.

Maneras de crear múltiples contextos

1. Constructor de aplicaciones de primavera

Los contextos se pueden crear usando Spring Boot's API de construcción fluidaConstructor de aplicaciones de primavera es un constructor para PrimaveraAplicación y Contexto de aplicación instancias, proporcionando soporte de jerarquía de contexto. Aquí está el ejemplo:
public static void main(String[] args) {      new SpringApplicationBuilder().sources(ParentCtxConfig.class)            .child(ChildFirstCtxConfig.class)            .sibling(ChildSecondCtxConfig.class)            .run(args);
ParentCtxConfigChildFirstCtxConfig y NiñoSegundoCtxConfig son clases anotadas con @SpringBootAplicación que es una anotación para las clases de configuración que también activa la configuración automática y el escaneo de componentes. El código de ejemplo generará tres instancias de PrimaveraAplicación y contexto de aplicación apropiado para cada instancia. Producirá un contexto principal (configurado con ParentCtxConfig) y dos contextos web secundarios (ChildFirstCtxConfig y NiñoSegundoCtxConfig). Tenga en cuenta que el contexto principal no es un contexto web. Es un tipo de AnnotationConfigApplicationContextAnotationConfigApplicationContext y los contextos secundarios son de tipo AnotaciónConfigServletWebServerApplicationContext. El primer tipo se inicia usando WebServer implementación, por lo que este ejemplo producirá dos instancias de Tomcat integrado (servidor web predeterminado). Debe tenerse en cuenta que todos los contextos de la aplicación (y sus instancias de Tomcat) se ejecutarán en una sola máquina virtual. Todos los contextos se configuran desde las mismas ubicaciones. Esto significa que el archivo de configuración predeterminado se utilizará para configurarlos todos, por lo que es importante asegurarse de que no contenga configuraciones que no se puedan compartir entre contextos. Ejemplo de eso es un puerto de servidor. Necesitamos asegurarnos de que dos contextos secundarios estén usando puertos diferentes. Podemos hacer eso con la propiedad server.port que tiene diferentes valores para cada contexto web. Una forma de lograr esto es tener un archivo de propiedades dedicado para cada contexto e importarlos usando @PropertySource en la clase de configuración relevante. Además, hay algunas otras formas de configurar el contexto. por favor refiérase a documentación para eso. Una ventaja de este enfoque es que tenemos contextos secundarios independientes separados. Eso nos dará la oportunidad de probarlos de forma aislada, ya que no se mezclan de ninguna manera. Pero necesitamos tener un contexto principal en su lugar y una jerarquía adecuada como en el ejemplo. Usaremos @ContextJerarchy y @ContextConfiguración para definir clases de configuración y establecer una jerarquía. La configuración del contexto principal estará en una clase separada que probará la extensión de las clases. Para el primer contexto del ejemplo, configuraremos una prueba como esta:
@RunWith(SpringRunner.class) 
@ContextHierarchy(  
    @ContextConfiguration(name = "child", classes = ChildFirstCtxConfig.class)    
) 
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
    @DirtiesContext(classMode = ClassMode.AFTER_CLASS)
        public class ChildFirstCtxControllerTests extends ParentCtxDefinition {         
            @Autowired        
            TestRestTemplate restTemplate;         
            @Test        
            public void testChildFirst() throws Exception {                 
                restTemplate.getForObject("/", Map.class);    
            }
        }
Dónde ParentCtxDefinición configura el contexto principal de la siguiente manera:
@ContextConfiguration(name = "parent", classes = ParentCtxConfig.class) 
@SpringBootTest(webEnvironment = WebEnvironment.NONE) 
public class ParentCtxDefinition {      
    … 
}
Esta configuración creará un contexto principal no web (WebEnvironment.NONE) configurado con ParentCtxConfig y un contexto web secundario se inició en un puerto aleatorio (WebEnvironment.RANDOM_PORT) y se configuró con ChildFirstCtxConfig. La relación padre-hijo se logra con @ContextJerarchy. Una cosa importante a tener en cuenta aquí es que es fundamental tener un nombre diferente en @ContextConfiguración ya que determina el nivel de jerarquía. Las configuraciones con el mismo nombre de nivel se fusionarán / anularán. Cuando estaba escribiendo la primera versión de esta publicación, estaba trabajando con Spring Boot 1.4.3 y no pude configurar las pruebas de la forma descrita. Pronto me di cuenta de que @ContextJerarchy (Spring class) no es compatible con Spring Boot, así que abrí la cuestión y el soporte se agrega en pocos días como parte del hito 1.5.0. Hay algunas soluciones propuestas en desbordamiento de pila para aquellos que están atascados con algunas versiones anteriores. Como alternativa, se puede usar un único contexto configurado con clases de configuración padre e hijo, lo que hará que todos los beans requeridos estén presentes pero sin la jerarquía de contexto. Este enfoque se puede utilizar cuando se trabaja en una aplicación web directamente, pero no es adecuado para bibliotecas. Además, debe tenerse en cuenta que impone la creación de instancias de contenedor de servlets separadas. El código fuente se puede encontrar en GitHub.

2.Bean de registro de servlet

DespachadorServlet es un servlet central en Spring MVC que envía solicitudes a controladores y manejadores. Tiene lo suyo WebApplicationContextWebApplicationContext que se convierte en un hijo del contexto raíz de la aplicación cuando se registra el servlet.
Una aplicación web puede definir cualquier número de DispatcherServlets. Cada servlet operará en su propio espacio de nombres, cargando su propio contexto de aplicación con mapeos, manejadores, etc. Sólo se compartirá el contexto de la aplicación raíz como lo carga ContextLoaderListener, si lo hay. A partir de Spring 3.1, DispatcherServlet ahora puede inyectarse con un contexto de aplicación web, en lugar de crear uno propio internamente. Esto es útil en entornos Servlet 3.0+, que admiten el registro programático de instancias de servlet. Consulte el javadoc DispatcherServlet (WebApplicationContext) para obtener más detalles.
Dentro de web.xml los servlets se registraron con algo como esto:
    <servlet>    
    <servlet-name>childCtxServlet</servlet-name>    
    <servlet- class>org.springframework.web.servlet.DispatcherServlet</servlet-class>    
    <init-param>        
        <param-name>contextConfigLocation</param-name>        
        <param-value>WEB-INF/spring/childCtxServlet-context.xml</param-value>    
    </init-param>    
    <load-on-startup>2</load-on-startup> 
</servlet> 
<servlet-mapping> 
   <servlet-name>childCtxServlet</servlet-name>   
    <url-pattern>/child/*</url-pattern> 
</servlet-mapping>
Para una configuración programática, hay un ServletRegistrationBean:
@Bean 
public ServletRegistrationBean childCtxServlet() {    
    DispatcherServlet dispatcherServlet = new DispatcherServlet();    
    AnnotationConfigWebApplicationContext applicationContext = new  AnnotationConfigWebApplicationContext();  
    applicationContext.register(ChildCtxConfig.class);    
    ...    
    dispatcherServlet.setApplicationContext(applicationContext);    
    ServletRegistrationBean servletRegistrationBean = new  ServletRegistrationBean(dispatcherServlet, "/child/*");    
    ...    
    return servletRegistrationBean; 
}
ChildCtxConfig es la clase anotada con @Configuración y @EnableWebMvc. Anotaciones como @ComponenteScan, @PropertySource etc. también pueden ser útiles. La ruta del servlet en el ejemplo anterior es /niño lo que significa que todos los puntos finales en el contexto estarán bajo esa ruta (p. ej. / niño / algo). En este punto, no es necesario establecer el contexto raíz como padre para un nuevo contexto. DespachadorServlet hará eso y realizará alguna otra inicialización como set id, namespace, inicializar fuentes de propiedades, agregar detectores de contexto, llamar a refresh (), etc. Obviamente, el nuevo contexto secundario no se reiniciará esta vez. En otras palabras, no habrá nuevas WebServer instancia, ya que el nuevo contexto se ejecutará dentro de la instancia principal del contenedor de servlets en su lugar. La configuración de la prueba de integración en este caso es simple, ya que no es necesario crear manualmente una jerarquía de contexto. Se creará cuando se procese la configuración para el contexto raíz. Las pruebas se pueden anotar así:
@RunWith(SpringRunner.class) 
@SpringBootTest(classes = ParentCtxConfig.class, webEnvironment =  WebEnvironment.RANDOM_PORT) 
public class ChildCtxControllerTests { 
… 
}
ParentCtxConfig.clase es una clase de configuración del ejemplo. Se puede omitir en caso de que esté en el mismo paquete que la clase de prueba, por ejemplo. Los contextos padre e hijo no se pueden probar de forma aislada, ya que el hijo siempre se crea en ParentCtxConfig y depende de los padres. El código fuente está activado GitHub.

3. AnnotationConfigEmbeddedWebApplicationContext

La siguiente forma de crear contextos es complicada. Basado en el código Spring Boot Actuator y algunos experimentos, descubrí que AnotaciónConfigServletWebServerApplicationContext se puede crear en la clase de configuración y agregar como un bean. Si se configura así, este contexto web se iniciará mediante una nueva instancia de Servidor web - diferente de otras instancias de contenedor de servlets en la aplicación. Ejemplo:
@Bean 
public AnnotationConfigEmbeddedWebApplicationContextchildContext(ApplicationContext parentContext) {        
    AnnotationConfigEmbeddedWebApplicationContext childContext = new AnnotationConfigEmbeddedWebApplicationContext();        
    // if we want this context to be child of root context; optional        
    childContext.setParent(parentContext);        
    ...        
    childContext.register(ChildCtxConfig.class);        
    childContext.refresh();        
    return childContext;
}
ChildCtxConfig es la clase de configuración anotada con @Configuration (o @SpringBootConfiguration) y otras anotaciones necesarias como @EnableWebMvc, @ComponenteScan, @PropertySource etc. También importa algunas clases de configuración automática de Spring Boot como: PropiedadMarcador de posiciónAutoConfiguración, ServletWebServerFactoryAutoConfiguración, DispatcherServletAutoConfiguración, etc. Mi consejo para aquellos que quieran experimentar más con este enfoque es comenzar con algunos conjuntos mínimos de clases de configuración automática (ver el ejemplo de GitHub) y agregar nuevos si es necesario. Por ejemplo, SeguridadAutoConfiguración y SeguridadFiltroAutoConfiguración ayudará a configurar Spring Security. Al crear AnotaciónConfigServletWebServerApplicationContext como un frijol, hay una opción para que el nuevo contexto se convierta en un elemento secundario del contexto de la aplicación. En ese caso, una instancia de DespachadorServlet con todo setDetectAllXXX () métodos establecer en falso debe crearse en el nuevo contexto. Por lo general, DispatcherServlet también buscará manejadores de solicitudes y adaptadores de contextos ancestrales, pero queremos evitar esto para que la configuración principal no se filtre. Además, las variantes compuestas de Mapeo de controlador y Adaptador de controlador debe ser creado. Solo necesitan descubrir los controladores existentes y delegarles llamadas. Es posible que haya otros beans que deban crearse manualmente según la aplicación. Básicamente, todos los ServletContextAware Los componentes deben estar en el contexto secundario. La configuración de la prueba de integración es como en el ejemplo anterior, ya que la jerarquía se logrará cuando se procese el contexto raíz. Al igual que en el tiempo de ejecución, si el nuevo contexto será hijo del contexto raíz o no, depende de la configuración. Especificando Entorno web.RANDOM_PORT en este caso es engorroso. Si hay una relación padre-hijo, el puerto del hijo debe descubrirse manualmente ya que Plantilla de descanso de prueba es consciente del contexto raíz solamente (el ejemplo está en GitHub). De lo contrario, un contexto separado tenderá a comenzar en el puerto configurado, ya que el entorno no se hereda. Si no existe una relación padre-hijo entre contextos, el configurado con ChildCtxConfig podría probarse de forma aislada del resto. El problema es que el contexto se configurará con ubicaciones predeterminadas, lo que significa que el archivo de propiedades predeterminado configurará el contexto. Esto no sucede en el tiempo de ejecución "normal", por lo que se requiere precaución para las pruebas aisladas. El código fuente del ejemplo se puede encontrar en GitHub. El enfoque con jerarquía padre-hijo descrito anteriormente se puede ver en acción en Spring Boot Actuator cuando está configurado para usar un puerto diferente de la aplicación en sí. La propiedad para eso es gestión.servidor.puerto y su implementación está en las siguientes clases:
org.springframework.boot.actuate.autoconfigure.web.servlet.WebMvcEndpointChildContextConfiguration 
			org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration 
			org.springframework.boot.actuate.autoconfigure.web.server.ManagementWebServerFactoryCustomizer

Conclusión

En los ejemplos dados anteriormente, describí tres formas de crear contextos y establecer una jerarquía. Los contextos se crean de diferentes maneras y pueden tener múltiples propósitos basados ​​en eso. Dependiendo del caso de uso y los requisitos, puede elegir cuál se adapta mejor al propósito. Además de las tres formas descritas anteriormente, puede haber otras formas de crear contextos a través de Spring Boot. Se pueden crear y / o registrar servlets usando ciertos inicializadores de contexto / servlet como AplicaciónContextInitializer por ejemplo, pero ese enfoque requiere que se registre un inicializador. Sin embargo, al final, todo se reduce a algunas de las formas descritas. Hay mucho más sobre contextos en Spring: diferentes tipos de contexto, eventos de contexto como ContextRefreshedEventContextRefreshedEvent, ContextStartedEventContextStartedEvent etc, Consciente del contexto de la aplicación interfaz, Spring TestContext Framework, etc. Para la mayoría de las cosas, la documentación oficial de Spring / Spring Boot y Guías de inicio son grandes recursos. Sin embargo, en algunos casos (como el de esta publicación), tendrás que hacer tu propia investigación y experimentar. Importe la fuente Spring en su IDE favorito y comience a jugar. ¡Buena suerte!