Spring Boot series: actuator, monitoriza tu aplicación

Retomando mi serie de miniposts sobre Spring Boot, hoy voy a explicar una funcionalidad muy interesante que podemos añadir fácilmente a cualquier aplicación Spring Boot, es Spring Boot Actuator, con el que podremos monitorizar e interactuar con nuestra aplicación.

Concepto:
Actuator es un subproyecto de Spring Boot que añade una serie de «endpoints» para monitorizar, auditar e incluso gestionar tu aplicación fácilmente. Como todo en Spring Boot, tiene una configuración por defecto y apenas tenemos que hacer nada para tenerlo funcionando desde el primer día (aparte de añadirlo en las librerías del proyecto, con maven o lo que corresponda).
En este post explicaré como añadir actuator a un proyecto Spring Boot, y comentaré algunos de sus endpoints y otras posibles personalizaciones. Como proyecto base he usado el que ya creé en el post sobre Spring Boot, lo podéis descargar de mi github, aquí.

Entorno usado:
Java JDK 1.8
Maven 3.2.1
Git 1.9.4
IDE Intellij 14 Ultimate version

Pasos:

1. Lo primero, editaremos nuestro pom.xml de maven, para añadir en nuestras dependencias el paquete «starter» de actuator:

...
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
...

2. Y… ya está. Con esto, Spring Boot nos autoconfigura todos los endpoints de actuator con los parámetros por defecto. Para probarlo, arrancamos con ‘mvn spring-boot:run’ o desde la tool window de maven en Intellij, y accedemos, por ejemplo, a http://localhost:8080/health/ desde un navegador o cliente Rest. Nos devolverá un json como este:

    {
       "status": "UP"
    }

Por defecto tenemos varios endpoints disponibles para ver todo tipo de información, siempre devuelta en formato json:

– health: información del estado de la aplicación (muy básico, como vemos en el ejemplo).
– env: información de variables de entorno y propiedades.
– beans: beans registrados en el contexto de Spring.
– trace: trazas de los últimos accesos.

Para más información sobre los endpoints, podéis consultar la documentación oficial de Spring Boot.
En los siguientes pasos vamos a personalizar y cambiar alguna configuración de estos endpoints.

3. Cambiar la url base de todos los endpoints: Probablemente nos gustaría tener todos estos endpoints bajo una url común en plan http://localhost:8080/adminpath/el-endpoint-que-sea. Es muy fácil, simplemente creamos un fichero de propiedades bajo la carpeta ‘src/main/resources/’ (si no lo tenemos ya), y añadimos la siguiente propiedad:

management.contextPath=/actuator-admin

Si arrancamos ahora, y accedemos, por ejemplo, a http://localhost:8080/actuator-admin/env veremos que responde. Todos nuestros endpoints cuelgan ahora de /actuator-admin.
Se pueden modificar de esta manera también el puerto (management.port) y otros parámetros.

4. Endpoint ‘info’: este endpoint por defecto no devuelve nada. Está pensado para devolver información general de nuestra aplicación, como versión, nombre… lo que nosotros queramos. Podemos personalizarlo muy fácilmente, también mediante propiedades. Vamos a añadir las siguientes propiedades a nuestro application.properties:

...
info.application.name=Spring Boot Series: actuator example app
info.application.description=Example Spring Boot app with actuator, for  my blog
info.application.version=0.1.1

Arrancamos la aplicación, y accedemos a http://localhost:8080/actuator-admin/info. Nos devolverá un json tal que así:

    {
       "application":
       {
           "version": "0.1.1",
           "description": "Example Spring Boot app with actuator, for my blog",
           "name": "Spring Boot Series: actuator example app"
       }
    }

 

5. También podemos personalizar el endpoing ‘health’. Como veis, simplemente devuelve un ‘OK’, haciendo ciertas comprobaciones que tiene por defecto. Quizás nos interese comprobar cosas concretas a nuestra aplicación, como espacio libre en disco, testear si responde una base de datos… Para hacer esto, simplemente registramos un bean que herede de HealthIndicator. Por ejemplo:

package com.edwise.springbootseries.actuator.endpoints;

// imports...

@Component
public class BetterHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        FileManager.Status diskStatus = FileManager.checkStatus();

        Health health = null;
        if (diskStatus.equals(FileManager.Status.OK)) {
            health = Health.up().build();
        } else {
            health = Health.down().withDetail("diskStatus", diskStatus).build();
        }

        return health;
    }
}

En el código, lo que hacemos es llamar a un ‘checker’ de un manager ficticio, y según lo que nos devuelva, devolvemos que nuestra aplicación está ‘UP’, o que está ‘DOWN’, añadiendo el estado devuelto por el manager.
Ahora, el endpoint nos podría devolver algo como esto: (con un 503, además, no un 200).

    {
       "status": "DOWN",
       "diskStatus": "LOW"
    }

6. Por último, también podemos crear nuestro propio endpoint y añadirlo a actuator. Para ello registramos un bean que herede de AbstractEndpoint. Por ejemplo:

package com.edwise.springbootseries.actuator.endpoints;

// imports...

@Component
public class BugsEndpoint extends AbstractEndpoint<List<Bug>> {

    @Autowired
    BugsService bugsService;

    public BugsEndpoint() {
        super("bugs");
    }


    @Override
    public List<Bug> invoke() {
        return bugsService.getAllBugs();
    }
}

Heredamos de AbstractEndpoint, poniéndole como parámetro genérico lo que vaya a devolver el endpoint. implementamos el método ‘invoke‘, que es el que lo devolverá. Y en el constructor, llamando al constructor de AbstractEndpoint, le pasamos como parámetro la url que queremos para nuestro endpoint. Para el ejemplo, sería http://localhost:8080/actuator-admin/bugs

Y hasta aquí esta pequeña revisión de Spring Boot actuator. Os recomiendo que reviséis la documentación oficial, para más opciones y posibilidades.

Si queréis bajaros el proyecto de prueba completo, lo podéis bajar directamente de mi github, como siempre 🙂

Implementar un servicio Rest con JAX-RS (Jersey)

En breve seguiré con la serie de post de Spring Boot, pero antes quería escribir uno sobre como montar un servicio REST, pero sin nada de Spring, usando JAX-RS. La idea sobre todo es por, más adelante, intentar montar Swagger encima de un proyecto ‘no Spring’. Esto último lo dejaremos para otro post, por supuesto. Ahora vamos a montar un servicio REST básico usando Jersey (implementación de JAX-RS). El servicio será prácticamente idéntico al que montamos en el post Implementar un servicio Rest con Spring

Concepto:
Ya hablé un poco sobre el concepto REST en el post de Rest con Spring.
Respecto a JAX-RS, es la especificación oficial de servicios Rest que ofrecen los chicos de Java, y Jersey es su implementación más ‘standard’ (o ‘reference implementation’).

Entorno usado:
Java JDK 1.8
Maven 3.2.1
Git 1.9.4
IDE Intellij 14 Ultimate version
Server Tomcat 8.0.14
Server GlassFish 4.1 (opcional)

Pasos:

1. Creamos un nuevo proyecto maven en Intellij con el archetype ‘quickstart’ (los pasos a seguir para la creación del proyecto en el IDE son los mismos de mi post POC con Spring Boot, los pasos 1 al 4).

2. Añadimos las dependencias maven de jersey que necesitamos, en nuestro pom:

...
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>2.13</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet</artifactId>
        </dependency>

        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
        </dependency>

        ...
    </dependencies>
...

La primera, en dependencyManagement, es la dependencia principal para montar un proyecto con jersey.
El artefacto jersey-container-servlet es el básico si queremos tener soporte servidor.
Las otras dos son dependencias jackson necesarias para el parseo de jsons.

2. Vamos a montar una aplicación web sin web.xml. Para ello crearemos la clase que hace las veces de ese web.xml, heredando de ResourceConfig:

package com.edwise.pocs.jerseyrest;

import org.glassfish.jersey.server.ResourceConfig;
import javax.ws.rs.ApplicationPath;

@ApplicationPath("api")
public class RestApplication extends ResourceConfig {

}

Por ahora es suficiente así. Con esto configuramos una aplicación web, en la ruta ‘/api’.

3. Para que maven no se queje a la hora de compilar nuestro proyecto, diciendo que no hay ‘web.xml’, necesitamos añadir la propiedad ‘failOnMissingWebXml’ a la configuración del plugin war de maven (lo cual nos obliga a añadir toda la configuración de ese plugin en nuestro pom.xml):

...
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
                <version>2.1.1</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.2</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
...

4. Comenzamos a implementar nuestro servicio en si. Para ello usamos la misma entidad User que ya usamos en el post de Rest con Spring. Podéis ver el código aquí.
Como tenemos un campo fecha en nuestro User que usa la librería joda-time, añadimos también al pom.xml las dependencias necesarias (joda-time y jackson-datatype-joda).
Podéis ver esto más en detalle en el paso 2 de aquel post.

5. Implementamos el servicio que usara nuestro controller (en jax-rs se les suele llamar ‘resource’ a los controllers, pero yo lo llamaré controller). El servicio es idéntico al usado en el post de Rest con Spring. Podéis ver el código en github, de la interfaz y del implementado (es un mock).

6. Vamos ahora con la implementación del controller:

package com.edwise.pocs.jerseyrest.resource;

import com.edwise.pocs.jerseyrest.entity.User;
import com.edwise.pocs.jerseyrest.service.UserService;

import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("users")
public class UserController {

    @Inject
    private UserService userService;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List getAllUsers() {
        return userService.findAll();
    }

    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getUserById(@PathParam("id") long userId) {
        Response response;
        User user = userService.findById(userId);
        if (user == null) {
            response = Response.status(Response.Status.NOT_FOUND).build();
        } else {
            response = Response.ok(user).build();
        }
        return response;
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response insertUser(User user) {
        userService.save(user);

        return Response.status(Response.Status.CREATED).build();
    }

    @PUT
    @Path("/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response updateUser(@PathParam("id") long userId, User user) {
        Response response;
        User userOld = userService.findById(userId);
        if (userOld != null) {
            userService.update(userOld.copyFrom(user));
            response = Response.status(Response.Status.NO_CONTENT).build();
        } else {
            response = Response.status(Response.Status.NOT_FOUND).build();
        }

        return response;
    }

    @DELETE
    @Path("/{id}")
    public Response deleteUser(@PathParam("id") long userId) {
        userService.delete(userId);
        return Response.status(Response.Status.NO_CONTENT).build();
    }
}

Lo explicamos un poco, aunque es bastante sencillito:
@Inject: como el @Autowired de Spring. Para ello, el servicio tiene que estar registrado en el contexto. Lo vemos más adelante…
@Path: similar al @RequestMapping de Spring. Con el tendremos en ‘/users’ nuestro servicio REST (en ‘/api/users’, por lo que pusimos en nuestra clase RestApplication)
@GET, @POST, @PUT, @DELETE: totalmente autoexplicativas: marcan cada método de la clase con el método HTTP usado para acceder a ellos (en Spring esto lo hacemos también en el @RequestMapping)
@Produces y @Consumes: El tipo de dato que devuelve o necesita como entrada. También lo teniamos en el @RequestMapping en Spring)
Clase Response: para devolver la información y el httpStatus, en plan builder con fluent interface.

7. Con esto ya tendríamos todo montado, pero faltan un par de temas importantes. El primero es la inyección de dependencias (DI) en JEE. No tenemos Spring, con lo que hay que configurar la CDI de JEE. Inicialmente es suficiente con poner un fichero beans.xml vacío bajo un directorio WEB-INF o META-INF en nuestra aplicación. Creamos entonces, dentro del directorio ‘main’, la siguiente estructura: ‘webapp/WEB-INF’ y dentro creamos nuestro beans.xml.

8. Ya podríamos arrancar nuestro servidor… pero no Tomcat. Si arrancamos esto con tomcat, nos va a dar un error en el @Inject del controller donde injectamos el servicio. Esto es por que Tomcat es solo un contenedor de servlets. Para tener CDI automática necesitamos un contenedor JEE, por ejemplo, GlassFish. Con GlassFish, ya tendríamos solucionado ese primer punto. Pero vamos a intentar resolverlo para Tomcat.

9. Para Tomcat tenemos que registrar los beans que vamos a usar en nuestra clase RestApplication. Quedaría así:

package com.edwise.pocs.jerseyrest;

import com.edwise.pocs.jerseyrest.service.UserService;
import com.edwise.pocs.jerseyrest.service.impl.UserServiceMock;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;

import javax.ws.rs.ApplicationPath;

@ApplicationPath("api")
public class RestApplication extends ResourceConfig {

    public RestApplication() {
        packages("com.edwise.pocs.jerseyrest");

        register(new AbstractBinder() {
            @Override
            protected void configure() {
                bind(new UserServiceMock()).to(UserService.class);
            }
        });

    }
}

Para que el controller se registre basta la primera linea, donde le decimos en que paquetes buscar. Para el servicio, es necesario registrarlo usando un AbstractBinder, en el que ‘bindeamos’ la interfaz con la implementación que queremos asociarle.
Y con esto, ahora si, podríamos desplegarlo en un tomcat. Pero aún queda un pequeño detalle.

10. Al igual que nos paso en el post del servicio con Spring, en este tenemos también un campo fecha de tipo LocalDate, de la librería joda-time. Para poder realizar correctamente el parseo de json, necesitamos configurar correctamente el mapper que se encarga de ello. Al igual que con Spring, necesitamos registrar el JodaModule. Para ello necesitamos, por un lado, implementar un provider de json:

package com.edwise.pocs.jerseyrest.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;

import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

@Provider
@Produces(MediaType.APPLICATION_JSON)
public class JsonMapperProvider implements ContextResolver {

    final ObjectMapper objectMapper;

    public JsonMapperProvider() {
        objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JodaModule());
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    }

    @Override
    public ObjectMapper getContext(Class<?> arg0) {
        return objectMapper;
    }

}

En él, registramos el modulo JodaModule, aparte de configurar un poco como queremos el formato de las fechas.

11. Para que registre correctamente los provider de Jackson, es necesario modificar otra vez nuestra RestApplication, que quedaría ya finalmente así:

package com.edwise.pocs.jerseyrest;

import com.edwise.pocs.jerseyrest.service.UserService;
import com.edwise.pocs.jerseyrest.service.impl.UserServiceMock;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;

import javax.ws.rs.ApplicationPath;

@ApplicationPath("api")
public class RestApplication extends ResourceConfig {

    public RestApplication() {
        packages("com.edwise.pocs.jerseyrest");

        register(new AbstractBinder() {
            @Override
            protected void configure() {
                bind(new UserServiceMock()).to(UserService.class);
            }
        });

        register(JacksonFeature.class);
    }
}

En la linea resaltada, lo que hacemos es registrar una feature de Jackson. De esta manera ya si que pillará el provider, y tendremos un parseo correcto de las fechas en el servicio.

12. Creamos el war ejecutando ‘mvn clean package’ o desde la tool window de maven en Intellij. Y lo desplegamos en un tomcat (ya sea en Intellij, si tenemos la versión Ultimate, o a mano, en un tomcat aparte). Si accedemos desde un navegador o un cliente Rest a http://localhost:8080/api/users/ (o http://localhost:8080/jersey-rest-example-1.1/api/users/ si no lo tenemos desplegado en el raíz), nos devolverá algo como esto:

    [
       {
           "id": 12,
           "name": "Gandalf",
           "type": 1,
           "phone": "666554433",
           "birthDate": "1911-01-02"
       },
       {
           "id": 140,
           "name": "Aragorn",
           "type": 1,
           "phone": "661534411",
           "birthDate": "1923-07-16"
       },
       {
           "id": 45332,
           "name": "Frodo",
           "type": 2,
           "phone": "666222211",
           "birthDate": "1951-11-24"
       }
    ]

Si queréis descargarlos el proyecto completo, lo tenéis aquí. Hasta la próxima! 🙂

ACTUALIZADO: He actualizado el proyecto en github con los cambios que he hecho para el post Cómo devolver el resultado de un POST en un servicio REST.