1. Introducción
Hola a todos y bienvenidos a un nuevo post de nuestro querido blog después de las vacaciones estivales! En esta ocasión vamos a tratar un tema que quería traer al blog hace bastante tiempo. Estos son los ServletContainerInitializer!.
Como todos sabemos SpringBoot utiliza de forma embebida un Tomcat, de tal forma que cuando arrancamos una aplicación se hace uso del contenedor de servlets. Desde que empecé a trabajar con esta tecnología me preguntaba cómo era posible que se pudiese desplegar una aplicación en donde no se definiese un web.xml. Por tanto como era posible que se estuviesen registrando servlet, filtros y listeners de nuestra aplicación.
Investigando un poco descubrí lo que había por debajo. Y es que SpringBoot definía por nosotros dichos recursos de forma programática de tal forma que ya no es necesario disponer de un web.xml. Si queremos definir un servlet lo hacemos programaticamente. Lo mismo ocurre con los filtros y los listener. Y todo esto existe desde la especificación 6 de Java!. Pero ¿y como se hace?
En primer lugar debemos crear dentro de nuestro classpath un fichero en META-INF/services/javax.servlet.ServletContainerInitializer
que contenga el package y el nombre de una implementación de ServletContainerInitializer
en donde se definirán los recursos de nuestra aplicación.
En dicha implementación se definirán los recursos utilizando el objeto de la clase ServletContext de la siguiente manera.
servletContext.addServlet(SERVLET_NAME, MyServlet.class); servletContext.addFilter(FILTER_NAME, MyFilter.class); servletContext.addListener(MyListener.class);
Mostrando lo que podría ser nuestra implementación
package org.jorgehernandezramirez.servlet.servletcontainerinitializer; ... public class MyServletContainerInitializer implements ServletContainerInitializer { private static final Logger LOGGER = LoggerFactory.getLogger(MyServletContainerInitializer.class); public MyServletContainerInitializer(){ super(); } @Override public void onStartup(final Set<Class<?>> handlerTypeSet, final ServletContext servletContext) throws ServletException { servletContext.addServlet(SERVLET_NAME, MyServlet.class); ... servletContext.addFilter(FILTER_NAME, MyFilter.class); ... servletContext.addListener(MyListener.class); ... } }
Os podéis descargar el código de ejemplo de mi GitHub aquí.
Tecnologías empleadas:
- Java 8
- Gradle 3.1
- Tomcat 8.5.20
- Javax Servlet Api 4.0.0
2. Injección de dependencias en el ServletContainerInitializer
Una de las características más interesantes de los ServletContainerInitializer es la capacidad de realizar inyección de dependencias. Sí, al estilo de Spring. Lo único que hace falta es definir la anotación @HandlesTypes({IHandlerType.class})
encima nuestra implementación. Donde IHandlerType
es una interfaz definida en nuestro proyecto.
@HandlesTypes({IHandlerType.class}) public class MyServletContainerInitializer implements ServletContainerInitializer { ... }
El resultado será que todas las implementaciones de IHandlerType
que se encuentren dentro del classpath serán inyectadas a través del primer parámetro método onStartUp
como un conjunto de Class
@Override public void onStartup(final Set<Class<?>> handlerTypeSet, final ServletContext servletContext) throws ServletException { .. }
Realmente me parece una característica muy importante ya que podemos implementar un sistema de clases que siga el principio abierto cerrado a la hora de registrar los recursos (servlet, filtros y listener) de forma transparente hacia nuestra implementación MyServletContainerInitializer
. Lo que pretendemos definir es un sistema de clases como el que sigue:
De tal forma que cada implementación de IHandlerType
se encargue de registrar los recursos que considere. En este caso hemos creado tres implementaciones:
- ServletRegistryHandlerType se encargará de registrar los servlet de nuestra aplicación
- FilterRegistryHandlerType se encargará de registrar los filtros de nuestra aplicación
- ListenerRegistryHandlerType se encargará de registrar los listener de nuestra aplicación
3. Definiendo Servlets, Filtros y Listeners programaticamente
Añadimos las dependencias javax.servlet-api
y logback-classic
. Además utilizamos el plugin war
.
group 'com.jorgehernandezramirez.servlet' version '1.0-SNAPSHOT' apply plugin: 'java' apply plugin: 'war' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.0' compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' }
Se muestra el fichero javax.servlet.ServletContainerInitializer que debe contener el package y el nombre de la clase de nuestro ServletContainerInitializer que queremos que se cargue en el arranque.
Se muestra la implementación al completo.
package org.jorgehernandezramirez.servlet.servletcontainerinitializer; import org.jorgehernandezramirez.servlet.servletcontainerinitializer.handlertype.IHandlerType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.annotation.HandlesTypes; import java.util.Set; @HandlesTypes({IHandlerType.class}) public class MyServletContainerInitializer implements ServletContainerInitializer { private static final Logger LOGGER = LoggerFactory.getLogger(MyServletContainerInitializer.class); public MyServletContainerInitializer(){ super(); } @Override public void onStartup(final Set<Class<?>> handlerTypeSet, final ServletContext servletContext) throws ServletException { callImplementationHandlerType(handlerTypeSet, servletContext); } private void callImplementationHandlerType(final Set<Class<?>> handlerTypeSet, final ServletContext servletContext){ if(handlerTypeSet != null){ handlerTypeSet.forEach(aClass -> { executeImplementationIHandlerTypeInstance(aClass, servletContext); }); } } private void executeImplementationIHandlerTypeInstance(Class<? extends Object> handlerTypeClass, final ServletContext servletContext){ try { final IHandlerType handlerType = (IHandlerType) handlerTypeClass.newInstance(); handlerType.execute(servletContext); } catch(Throwable throwable){ LOGGER.error("Ha ocurrido un error al execute una implementación de IHandlerType", throwable); throw new RuntimeException(throwable); } } }
Lo que intentamos hacer es recorrer todas las clase correspondiente a las implementaciones de IHandlerType
para instanciarlas e invocar al método execute(ServletContext)
Se muestra la api IHandlerType
package org.jorgehernandezramirez.servlet.servletcontainerinitializer.handlertype; import javax.servlet.ServletContext; /** * Api que va a proporcionar el método execute que deben definir las implementaciones * que van a ser inyectadas en nuestro ServletContainerInitializer */ public interface IHandlerType { /** * Método que se será llamado por nuestro ServletContainerInitializer sobre todas las * implementaciones que serán inyectadas */ void execute(ServletContext servletContext); }
Definiendo servlets
Mostramos la implementación de IHandlerType que registra los servlet en la aplicación. En nuestro caso definiremos un servlet que asociaremos al path /myservlet.
package org.jorgehernandezramirez.servlet.servletcontainerinitializer.handlertype; import org.jorgehernandezramirez.servlet.servletcontainerinitializer.MyServletContainerInitializer; import org.jorgehernandezramirez.servlet.servletcontainerinitializer.servlet.MyServlet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletContext; import javax.servlet.ServletRegistration; public class ServletRegistryHandlerType implements IHandlerType { private static final Logger LOGGER = LoggerFactory.getLogger(ServletRegistryHandlerType.class); private static final String SERVLET_NAME = "myservlet"; private static final String SERVLET_CONTEXT_PATH = "/myservlet"; public ServletRegistryHandlerType(){ super(); } @Override public void execute(final ServletContext servletContext) { LOGGER.info("Registrando servlets de nuestra app"); final ServletRegistration.Dynamic registration = servletContext.addServlet(SERVLET_NAME, MyServlet.class); registration.setLoadOnStartup(1); registration.addMapping(new String[]{SERVLET_CONTEXT_PATH}); registration.setAsyncSupported(true); } }
Servlet dummy que se hace uso en la clase anterior.
package org.jorgehernandezramirez.servlet.servletcontainerinitializer.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class MyServlet extends HttpServlet { public MyServlet(){ super(); } protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { final PrintWriter printWriter = resp.getWriter(); printWriter.println("<html>"); printWriter.println("<body>"); printWriter.println("<p>Mi Servlet</p>"); printWriter.println("</body>"); printWriter.println("</html>"); } }
Definiendo filtros
Mostramos la implementación de IHandlerType que registra los filtros en la aplicación. En nuestro caso definiremos un filtro que asociaremos al path /*.
package org.jorgehernandezramirez.servlet.servletcontainerinitializer.handlertype; import org.jorgehernandezramirez.servlet.servletcontainerinitializer.MyServletContainerInitializer; import org.jorgehernandezramirez.servlet.servletcontainerinitializer.filter.SessionCreationFilter; import org.jorgehernandezramirez.servlet.servletcontainerinitializer.servlet.MyServlet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.FilterRegistration; import javax.servlet.ServletContext; import javax.servlet.ServletRegistration; public class FilterRegistryHandlerType implements IHandlerType { private static final Logger LOGGER = LoggerFactory.getLogger(MyServletContainerInitializer.class); private static final String FILTER_NAME = "myfilter"; private static final String FILTER_CONTEXT_PATH = "/*"; public FilterRegistryHandlerType(){ super(); } @Override public void execute(final ServletContext servletContext) { LOGGER.info("Registrando filtros en nuestra app"); final FilterRegistration.Dynamic registration = servletContext.addFilter(FILTER_NAME, SessionCreationFilter.class); registration.setAsyncSupported(true); registration.addMappingForUrlPatterns(null , true, FILTER_CONTEXT_PATH); } }
Filtro que se hace uso en la clase anterior. Lo único que hace es escribir en el log el id de la sesión actual.
package org.jorgehernandezramirez.servlet.servletcontainerinitializer.filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; public class SessionCreationFilter implements Filter { private static final Logger LOGGER = LoggerFactory.getLogger(SessionCreationFilter.class); public SessionCreationFilter(){ super(); } @Override public void init(FilterConfig filterConfig) throws ServletException { LOGGER.info("Inicializando SessionCreationFilter"); } @Override public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { LOGGER.info("SessionCreationFilter! -> Id sessión {}", getSessionId(request)); chain.doFilter(request, response); } private String getSessionId(final ServletRequest request){ return ((HttpServletRequest)request).getSession(true).getId(); } @Override public void destroy() { LOGGER.info("Destruyendo SessionCreationFilter"); } }
Definiendo listeners
Mostramos la implementación de IHandlerType que registra los listener en la aplicación.
package org.jorgehernandezramirez.servlet.servletcontainerinitializer.handlertype; import org.jorgehernandezramirez.servlet.servletcontainerinitializer.MyServletContainerInitializer; import org.jorgehernandezramirez.servlet.servletcontainerinitializer.listener.MySessionListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletContext; public class ListenerRegistryHandlerType implements IHandlerType { private static final Logger LOGGER = LoggerFactory.getLogger(MyServletContainerInitializer.class); public ListenerRegistryHandlerType(){ super(); } @Override public void execute(final ServletContext servletContext) { LOGGER.info("Registrando listeners en nuestra app"); servletContext.addListener(MySessionListener.class); } }
Listener de sesión dummy que se hace uso en la clase anterior.
package org.jorgehernandezramirez.servlet.servletcontainerinitializer.listener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; public class MySessionListener implements HttpSessionListener { private static final Logger LOGGER = LoggerFactory.getLogger(MySessionListener.class); public MySessionListener(){ super(); } @Override public void sessionCreated(HttpSessionEvent httpSessionEvent) { LOGGER.info("Creando la sesión -> {}", httpSessionEvent.getSession().getId()); } @Override public void sessionDestroyed(HttpSessionEvent httpSessionEvent) { LOGGER.info("Destruyendo la sesión -> {}", httpSessionEvent.getSession().getId()); } }
4. Probando la aplicación
Para probar la aplicación debemos en primer lugar compilar nuestros fuentes
Y luego desplegar el war que se encuentra en el directorio de salida build en un tomcat.
Atacamos a la url http://localhost:8080/myservlet
y obtenemos
Vemos que en la consola ha saltado nuestro listener de sesión y el filtro antes de ejecutar el correspondiente servlet.
19:51:54.972 [http-nio-8080-exec-1] INFO org.jorgehernandezramirez.servlet.servletcontainerinitializer.listener.MySessionListener – Creando la sesión -> CF3A91259AA94604ED6BF312D1975BE0
19:51:54.976 [http-nio-8080-exec-1] INFO org.jorgehernandezramirez.servlet.servletcontainerinitializer.filter.SessionCreationFilter – SessionCreationFilter! -> Id sessión CF3A91259AA94604ED6BF312D1975BE0
20:22:36.441 [ContainerBackgroundProcessor[StandardEngine[Catalina]]] INFO org.jorgehernandezramirez.servlet.servletcontainerinitializer.listener.MySessionListener – Destruyendo la sesión -> CF3A91259AA94604ED6BF312D1975BE0