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.

org.jorgehernandezramirez.servlet.servletcontainerinitializer.MyServletContainerInitializer

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.

org.jorgehernandezramirez.servlet.servletcontainerinitializer.MyServletContainerInitializer

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

gradle clean build

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