Skip to content

java

Spring Boot 2 (4)

Una parte importante de todo desarrollo moderno es la elaboración y ejecución automatizada de pruebas. Y a este respecto Spring Boot hereda todas las capacidades que ofrece el framework de Spring. Tanto para pruebas de integración, es decir, aquellas que requieren el auxilio de recursos externos a la propia aplicación, como pruebas unitarias, que pueden realizarse de forma independiente.

Para nuestro caso de uso, la construcción de servicios REST con Spring Boot 2, tiene sentido hacer pruebas para comprobar que el servidor web se levanta, mapea las rutas, y retorna un JSON que cumple con el formato esperado.

Caso de Uso

Como ejemplo utilizaremos una versión modificada de uno de los servicios REST construidos con Spring MVC en artículos anteriores, el que retorna una lista de números desde el 1 a un número dado. Teniendo en cuenta siempre que lo que interesa es probar que se cumple el contrato del servicio, no la implementación del mismo.

El primer paso es incluir las dependencias de Spring MVC en el fichero pom.xml mediante el starter de Spring Boot:

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

El segundo paso es crear en /src/main/java/{package}  la clase DTO que retornará el servicio:

import java.util.List;

public class Count {

  private List<Integer> count;

  public Count() {
  }

  public Count(List<Integer> count) {
    this.count = count;
  }

  public List<Integer> getCount() {
    return this.count;
  }
}

El tercer paso es crear en /src/main/java/{package} la clase que implementa el controlador del servicio:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CounterController {

  @GetMapping("/counter")
  public Count count(@RequestParam(name="count", defaultValue="0") Integer end) {
    var count = IntStream.range(1, end + 1)
      .boxed()
      .collect(Collectors.toUnmodifiableList());

    return new Count(count);
  }
}

Y el último paso es compilar y ejecutar la aplicación con Maven:

mvn spring-boot:run

Invocando a http://localhost:9999/counter?count=3 desde un navegador debería retornarse un JSON con un campo que contenga el array con la secuencia de números:

{"count":[1,2,3]}

Notar que el ejemplo está cambiado con respecto a artículos anteriores con objeto de que no falle si no se proporciona el parámetro de entrada, y para que retorne un JSON en vez de un array. Aún así, si se le pasa un valor que no represente un número entero, el servicio fallará con una excepción de tipo NumberFormatException, mostrándose la página de error por defecto de Spring Boot. En un sistema real explotado en producción, y expuesto de forma pública en Internet, los parámetros deben validarse siempre y este tipo de información no debería exponerse nunca por motivos de seguridad.

SpringBootTest

El primer paso para probar el servicio de ejemplo es añadir al fichero pom.xml las dependencias de Spring Test a través del starter de Spring Boot:

<dependencies>
…
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

Esta dependencia añade alguna de las librerías más populares utilizadas para la elaboración de pruebas. En particular JUnit, Hamcrest, Mockito, AssertJ y JSONassert, entre otras. Esto permite implementar las pruebas directamente con JUnit. Lo que es una ventaja en la medida que la mayoría de IDEs se integran con este framework y permiten lanzar las pruebas sin necesidad de pasar por Maven.

El segundo paso es crear en /src/test/java/{package}  una clase con un caso de prueba:

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class CounterControllerTest {

  @Autowired
  private TestRestTemplate restTemplate;

  @Test
  public void testEmptyCount() {
    var body = restTemplate.getForObject("/counter", String.class);

    assertThat(body).isEqualTo("{\"count\":[]}");
  }
}

La anotación @RunWith sirve para indicar el runner que se quiere utilizar con JUnit. La anotación @SpringBootTest sirve para indicar que la clase ejecuta casos de prueba de una aplicación de Spring Boot. Y el parámetro WebEnvironment.RANDOM_PORT indica que se quiere arrancar el servidor en un puerto aleatorio, para evitar conflictos en caso de lanzar varias pruebas en paralelo.

La instancia inyectada de la clase TestRestTemplate es registrada automáticamente como resultado de aplicar la anotación @SpringBootTest sobre la clase, y tiene la ventaja de que todas las llamadas HTTP que realice las hará contra el servidor web embebido arrancado para la prueba. Con la versión 5 de Spring se prefiere el uso de la clase WebTestClient en vez de TestRestTemplate, ya que su API sigue un estilo fluent para las aserciones, pero en la release actual sólo se registra automáticamente cuando se utiliza WebFlux y no Spring MVC.

Cuando se ejecute la prueba se arrancará el servidor web embebido, se invocará al servicio a través de restTemplate, y se comprobará que por defecto devuelve una cadena de texto que representa un JSON con un array vacío.

En un IDE moderno, como Eclipse Photon, las pruebas con JUnit se pueden ejecutar de varias formas de una manera extremadamente sencilla. Por ejemplo, pulsando el botón derecho sobre el método de la prueba y ejecutándolo a través del menú contextual.

WebMvcTest

En algunos casos tener que arrancar un servidor web puede ser un inconveniente, sobre todo cuando lo que se quiere probar es sólo un controlador de la forma más rápida posible. Para evitar esto es posible reescribir la prueba anterior utilizando las facilidades de prueba de Spring MVC, en vez de las de Spring Boot. Lo que quiere decir que se puede invocar al controlador, a la manera de Spring MCV, sin tener que levantar el servidor, que es la manera de Spring Boot.

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest(CounterController.class)
public class CounterControllerTest {

  @Autowired
  private MockMvc mvc;

  @Test
  public void testEmptyCount() throws Exception {
    mvc.perform(get("/counter"))
      .andExpect(status().isOk())
      .andExpect(content().string("{\"count\":[]}"));
  }
}

La anotación @WebMvcTest permite especificar el controlador que se quiere probar y tiene el efecto añadido que registra algunos beans de Spring, en particular una instancia de la clase MockMvc, que se puede utilizar para invocar al controlador simulando la llamada HTTP sin tener que arrancar realmente ningún servidor web.

Si se ejecuta la prueba se puede comprobar en el log que el servidor embebido no arranca, pero que el servicio es invocado y retorna el objeto JSON con un array vacío de igual forma que con el método del apartado anterior.

JSON

El inconveniente de los métodos de prueba de los apartados anteriores es que comprueban el resultado como una cadena de texto, sin ninguna estructura. Un espacio en blanco, o un orden distinto en los campos retornados, provocaría que la prueba fallase.

Para evitar estos problemas es mejor tratar el resultado como JSON y reescribir el método para que no opere con cadenas de texto.

Una forma sencilla de hacerlo es cambiar el método string por el método json:

.andExpect(content().json("{ \"count\" : [ ] }"));

La diferencia entre ambos métodos es que el primero es estricto en la comprobación que realiza entre las dos cadenas de texto, mientras que el segundo comprueba la similitud entre las dos cadenas tratándolas como si fueran representaciones en formato texto de objetos JSON. Por eso la prueba sigue ejecutándose con éxito, a pesar de los espacios añadidos a la cadena de texto utilizada para la comprobación.

Evidentemente sigue sin ser una solución correcta, ya que implica seguir escribiendo cadenas de texto, e impide utilizar las facilidades de comprobación de nombres y tipos del IDE. Una mejor solución es trabajar directamente con clases Java mapeadas como objetos JSON.

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.Collections;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest(CounterController.class)
@AutoConfigureJsonTesters
public class CounterControllerTest {

@Autowired
private MockMvc mvc;

@Autowired
private JacksonTester<Count> json;

  @Test
  public void testEmptyCount() throws Exception {
    var response = mvc.perform(get("/counter"))
      .andExpect(status().isOk())
      .andReturn().getResponse();

    var expected = json.write(new Count(Collections.emptyList())).getJson();

    assertThat(response.getContentAsString()).isEqualTo(expected);
  }
}

La anotación @AutoConfigureJsonTesters registra automáticamente una serie de beans de Spring para trabajar con librerías como Jackson, Gson y Jsonb, existiendo además anotaciones específicas para cada una de estas librerías. La instancia inyectada de la clase JacksonTester trabaja con Jackson y se utiliza para la prueba porque es la librería que se utiliza por defecto en las aplicaciones de Spring Boot.

De esta forma se evita utilizar cadenas de texto y se pueden escribir pruebas más elaboradas.

@Test
public void testCountThree() throws Exception {
  var response = mvc.perform(get("/counter?count=3"))
    .andExpect(status().isOk())
    .andReturn().getResponse();

  var expected = json.write(new Count(List.of(1, 2, 3))).getJson();

  assertThat(response.getContentAsString()).isEqualTo(expected);
}

Otra opción disponible es comparar el JSON devuelto por el servicio con un JSON almacenado en un fichero.

Resumiendo, Spring ofrece toda una variedad de configuraciones para la realización de pruebas, no sólo las básicas vistas en este artículo, sino otras más específicas para probar servicios que utilizan JPA, LDAP, Redis, MongoDB, junto con algunas otras que merece la pena explorar en la documentación oficial de referencia.

Spring Boot 2 (3)

Continuando con el ejemplo de construcción de aplicaciones básicas con Spring Boot 2, lo siguiente es añadir un servicio REST a la aplicación, ya que hoy en día es la tecnología más utilizada para construir microservicios. Tarea que puede acometerse utilizando Spring MVC, Spring WebFlux, o una implementación de JAX-RS, como Jersey o Apache CXF.

Spring MVC

Para crear un servicio REST con Spring MVC hay que añadir el starter web de Spring Boot en el fichero pom.xml:

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

Esta dependencia incluye las librerías de Spring MVC en el proyecto y permite construir aplicaciones web basadas en servlets.

Como ejemplo de servicio REST vamos a escribir un controlador que reciba un parámetro de tipo texto y retorne un JSON con un campo que contenga dicho parámetro. Es decir, el típico “saludador” (greeter), que recibe un nombre y retorna “Hello {{nombre}}!”.

Lo primero es crear en /src/main/java/{paquete} la clase que se quiere retornar como resultado de la ejecución del servicio. Es decir, el típico DTO:

public class Hello {
  private final String message;

  public Hello(String message) {
    this.message = message;
  }

  public String getMessage() {
    return this.message;
  }
}

Lo siguiente es crear en src/main/java/{paquete} la clase con el controlador, utilizando las anotaciones de Spring MVC para indicar el verbo, ruta y nombre del parámetro HTTP a través de los que se atenderán las peticiones de servicio:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

  @GetMapping("/hello")
  public Hello hello(@RequestParam String name) {
    return new Hello("Hello " + name + "!");
  }
}

La anotación @RestController hace que la respuesta del método del controlador se convierta automáticamente en un objeto de tipo JSON. La anotación @GetMapping hace que el método atienda peticiones HTTP de tipo GET. Y la anotación @RequestParam hace que el valor del parámetro HTTP con nombre “name” se asigne automáticamente al parámetro de entrada del método.

El último paso es compilar y ejecutar la aplicación utilizando el propio plugin de Maven de Spring Boot:

mvn spring-boot:run

Si todo funciona correctamente arrancará la aplicación mostrando por consola el log de ejecución, en el que debe verse la ruta mapeada por el servicio y el servidor web embebido utilizado:

Mapped "{[/hello],methods=[GET]}" onto public <paquete>.Hello <paquete>.HelloController.hello(java.lang.String)
...
Tomcat started on port(s): 9999 (http) with context path ''

El servicio arrancado de forma local se puede invocar desde un navegador a través del puerto en el que se encuentre levantado el servidor web embebido:

http://localhost:9999/hello?name=World

La respuesta será un JSON con un mensaje que contendrá el nombre pasado como parámetro:

{"message":"Hello World!"}

Spring WebFlux

Para crear un servicio REST con Spring WebFlux hay que añadir el starter de WebFlux de Spring Boot en el fichero pom.xml:

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

Esta dependencia incluye las librerías de Spring WebFlux en el proyecto y permite construir aplicaciones asíncronas no bloqueantes basadas en el paradigma reactivo.

Para convertir el ejemplo del apartado anterior en una aplicación de WebFlux sólo hay que modificar la clase del controlador, encapsulando con Mono<T> el tipo retornado. Esta clase es proporcionada por la librería Reactor y actúa como un publicador de un reactive stream que se resuelve con un único elemento o un error.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@RestController
public class HelloController {

  @GetMapping("/hello")
  public Mono<Hello> hello(@RequestParam String name) {
    var hello = new Hello("Hello " + name + "!");

    return Mono.just(hello);
  }
}

La nueva aplicación se puede compilar y ejecutar utilizando el plugin de Maven de Spring Boot de igual forma que en el apartado anterior:

mvn spring-boot:run

Si todo funciona correctamente arrancará la aplicación mostrando por consola el log de ejecución, en el que debe verse la nueva ruta mapeada y el servidor embebido, que por defecto es Netty:

Mapped "{[/hello],methods=[GET]}" onto public reactor.core.publisher.Mono<<paquete>.Hello> <paquete>.HelloController.hello(java.lang.String)

Netty started on port(s): 9999

Y de igual forma que en el apartado anterior, el servicio arrancado de forma local se puede invocar desde un navegador a través del puerto en el que se encuentre levantado el servidor web embebido:

http://localhost:9999/hello?name=World

La respuesta será un JSON con un mensaje que contendrá el nombre pasado como parámetro:

{"message":"Hello World!"}

En el caso de que el servicio REST tuviera que retornar una lista de objetos el resultado habría que encapsularlo con Flux<T>, otra clase proporcionada por la librería Reactor y que representa un publicador de reactive streams que se resuelve con 0, n elementos o error. Como ejemplo vamos a construir un nuevo controlador que admite un número como parámetro y retorna una lista de números desde 1 al número dado.

El primer paso es crear en /src/main/java/{paquete} una nueva clase para el DTO:

public class Counter {

  private Integer count;

  public Counter(Integer count) {
    this.count = count;
  }

  public Integer getCount() {
    return this.count;
  }
}

El segundo paso es crear en /src/main/java/{paquete} una nueva clase para el controlador:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;

@RestController
public class CounterController {

  @GetMapping(“/counter”)
  public Flux<Counter> count(@RequestParam Integer count) {
    return Flux.range(1, count).map(Counter::new);
  }
}

Y el último paso es el mismo de siempre: compilar, ejecutar con el plugin e invocar al servicio desde el navegador:

http://localhost:9999/counter?count=3

La respuesta será un array de objetos:

[{"count":1},{"count":2},{"count":3}]

WebFlux permite además definir las rutas (endpoints) sin utilizar anotaciones, de una forma más funcional, construyendo las rutas que se quiere que atiendan los servicios. Por ejemplo, el servicio contador anterior se puede escribir de forma equivalente creando beans de Spring de tipo RouterFunction. Beans que tienen que crearse al arrancar la aplicación, como parte del proceso de configuracion, por lo que tienen que definirse en una clase anotada con @Configuration como la siguiente:

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

import java.util.function.Function;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Flux;

@Configuration
public class CounterRouter {

  @Bean
  public RouterFunction<ServerResponse> countRouter() {
    Function<ServerRequest, Integer> count = 
      request -> Integer.valueOf(request.queryParam("count").get());

    Function<ServerRequest, Flux<Counter>> flux = 
      request -> Flux.range(1, count.apply(request)).map(Counter::new);

    HandlerFunction<ServerResponse> handler = 
      request -> ok().body(flux.apply(request), Counter.class);

    return route(GET("/counter"), handler);
 }
}

Como se observa, con este modelo es responsabilidad de la aplicación extraer los parámetros de la petición, construir el resultado y cuerpo de la respuesta, y mapear la ruta junto con el verbo. Es un enfoque más funcional evitando el uso de anotaciones y orientado a definir manejadores (handlers). En el ejemplo no se aprecia, ya que se hace todo dentro de un mismo método. En la práctica la responsabilidad estará repartida entre distintos componentes. El stream de datos normalmente procederá de algún repositorio, el manejador estará en un servicio, y el método que define la ruta sólo contendrá la parte técnica relacionada con HTTP. Notar que en este ejemplo además resulta un poco engorroso la cantidad de clases estáticas que se requiere importar.

Spring Boot 2 (2)

Hace un tiempo elaboré una breve introducción a Spring Boot con el objetivo de que pudiera ser utilizada como referencia dentro de una empresa en la que trabajaba. Con el tiempo los fuentes correspondientes acabaron publicados, y ahora con el cambio de versión de Spring Boot han quedado un poco desactualizados. Afortunadamente la documentación de Spring Boot es bastante detallada y es la mejor referencia disponible. El objetivo de este artículo y sucesivos es enmendar esas notas introductorias utilizando Spring Boot 2 para construir una serie de aplicaciones básicas usando versiones más recientes de software, como Java 10, Eclipse Photon o Tomcat 9.

Configuración Automática

Una de las ventajas de usar Spring Boot es que crear un proyecto con Maven o Gradle es muy sencillo si se utiliza Spring Initializr, una web que genera esqueletos de proyectos para Spring Boot. La web ofrece una interface muy simple que permite seleccionar el tipo de proyecto, el lenguaje de programación, la versión de Spring Boot, las dependencias con librerías de terceros, y genera automáticamente un fichero zip que contiene el proyecto listo para ser compilado y ejecutado.

Configuración Manual

Para crear un proyecto de una forma más manual con Maven hay que crear un fichero pom.xml básico como el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
  http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>{{GRUPO}}</groupId>
  <artifactId>{{NOMBRE}}</artifactId>
  <version>{{VERSION}}</version>
  <packaging>jar</packaging>

  <properties>
    <java.version>10</java.version>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  </properties>

</project>

A partir de ese fichero básico ya se puede empezar a añadir o quitar entradas para construir un tipo u otro de proyecto. Por ejemplo, para conseguir que el proyecto sea un proyecto de Spring Boot es necesario añadir org.springframework.boot:spring-boot-starter-parent como proyecto padre en el fichero pom.xml:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.0.3.RELEASE</version>
</parent>

Haciendo que org.springframework.boot:spring-boot-starter-parent sea el padre de un proyecto se consigue que de forma automática se haga referencia a una serie concreta de versiones de librerías, tanto de Spring como de terceros, que funcionan correctamente de forma conjunta con la versión usada de Spring Boot, evitando así a los proyectos tener que configurar las dependencias de manera individual.

Lo que es importante en este punto es entender que al configurar el proyecto padre no se añaden las dependencias (librerías) al proyecto, sólo se indican las versiones de las mismas que se quieren utilizar. De hecho, es incluso posible configurar el proyecto sin tener como padre a Spring Boot, importando directamente en el fichero pom.xml el conjunto de referencias de Spring Boot:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>2.0.3.RELEASE</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Las dos opciones vistas, utilizar el proyecto padre o importar las referencias, son válidas, pudiéndose utilizar una u otra dependiendo de cada caso en concreto. Para el ejemplo se continuará con la primera opción, lo importante es conocer que existen distintas opciones.

Aplicación de Consola

Aunque tradicionalmente Spring se ha asociado a la construcción de aplicaciones web, y Spring Boot a la construcción de microservicios REST, en realidad el núcleo de Spring siempre se ha podido utilizar para aplicaciones de línea de comandos.

Spring Boot facilita la creación de distintos tipos de aplicación mediante módulos Maven llamados “starters”. Así, para crear una aplicación de consola, el primer paso es añadir la dependencia org.springframework.boot:spring-boot-starter en el fichero pom.xml:

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

El siguiente paso es añadir una clase que sirva como punto de entrada a la aplicación de Spring Boot. Para ello basta con crear en /src/main/java/{paquete} una clase anotada con @SpringBootApplication y con un método estático main estándar de Java:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

La anotación @SpringBootApplication es equivalente, entre otras, a las anotaciones @Configuration y @ComponentScan de Spring.

El último paso es compilar y ejecutar la aplicación utilizando el propio plugin de Maven de Spring Boot:

mvn spring-boot:run

Si todo funciona correctamente, arrancará la aplicación de Spring Boot mostrando por consola el log de ejecución de la aplicación, donde, como mínimo, deberá verse el famoso ascii-art de Spring Boot (¡que en Spring Boot 2 se puede animar!) y la versión concreta utilizada:

Spring Boot :: (v2.0.3.RELEASE)

Lógicamente, la aplicación de consola arrancará y terminará inmediatamente, ya que no hace nada, pero cumple su función de hacer entender como construir un proyecto básico desde cero.

Aplicación web

Para construir una aplicación web con Spring Boot basada en Spring MVC hay que añadir el starter para web al fichero pom.xml:

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

Crear en /src/main/java/{paquete} una clase anotada con @EnableAutoConfiguration:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

@EnableAutoConfiguration
public class Application {

  public static void main(String[] args) {
  SpringApplication.run(Application.class, args);
  }
}

Y compilar y ejecutar la aplicación con el plugin de Maven de Spring Boot:

mvn spring-boot:run

Si todo funciona correctamente arrancará una instancia de Tomcat 8.5 y se mostrará por consola el log de ejecución, incluyendo el puerto y el contexto en que se encuentra escuchando el servidor, siendo esto último, por cierto, una novedad de Spring Boot 2:

Tomcat started on port(s): 8080 (http) with context path ''

Una vez arrancado el servidor se puede parar con la habitual combinación de teclas Control+C.

Cambiar Puerto de Tomcat

Para cambiar el puerto por defecto de arranque de Tomcat hay que crear un fichero de propiedades e indicar el nuevo puerto. El fichero de propiedades se debe ubicar en /src/main/resources, debe llamarse application, y puede estar en el formato .properties clásico de Java o en formato YAML.

Si se utiliza application.properties el fichero debe tener el siguiente contenido:

server.port = 9999

Si se utiliza application.yml el fichero debe tener el siguiente contenido:

server:
  port: 9999

Al arrancar de nuevo el servidor, Tomcat escuchará en el nuevo puerto indicado:

Tomcat started on port(s): 9999 (http) with context path ''

El uso del fichero de propiedades es una constante en Spring Boot. Por defecto el framework utiliza una serie de valores predefinidos según el criterio del equipo de Spring, de forma que se puede arrancar una aplicación sin tener que configurar ningún valor, pero deja la puerta abierta a cambiar dichos valores por parte de los desarrolladores a través del fichero de propiedades.

En lo sucesivo se sobreentiende que se utiliza el formato YAML para los ejemplos, pero usar un formato u otro es indistinto.

Cambiar Versión de Tomcat

Para cambiar la versión de Tomcat hay que añadir una propiedad en el fichero pom.xml:

<properties>
...
  <tomcat.version>9.0.10</tomcat.version>
...
</properties>

Al arrancar de nuevo el servidor, la versión de Tomcat utilizada será la nueva indicada:

Starting Servlet Engine: Apache Tomcat/9.0.10

Notar que la propiedad se añade al fichero pom.xml, y no al fichero de propiedades de la aplicación, porque es un parámetro necesario para construir la aplicación, para descargar la versión indicada de Tomcat y embeberla dentro de la aplicación, no un parámetro que se vaya a utilizar en tiempo de ejecución.

Cambiar Servidor de Aplicaciones

Para utilizar Jetty o Undertow en vez de Tomcat hay que excluir la dependencia con Tomcat del fichero pom.xml e incluir la dependencia con el nuevo servidor que se quiera utilizar.

Por ejemplo, para utilizar Jetty hay que realizar la siguiente modificación:

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

<!-- Excluye Tomcat -->
    <exclusions>
      <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
      </exclusion>
    </exclusions>

  </dependency>

<!-- Incluye Jetty -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
  </dependency>

</dependencies>

Al volver a arrancar la aplicación se levantará una instancia de Jetty en vez de Tomcat:

Jetty started on port(s) 9999 (http/1.1) with context path '/'

En todos los casos, accediendo a la url http://localhost:9999/ se puede ver la página de error 404 por defecto de Spring, ya que de momento no se ha añadido ningún servicio al servidor.

Spring Boot 2 (1)

La última versión de Spring Boot supone la primera gran actualización que recibe desde que se publicó. Las principales novedades a mi juicio son la necesidad de utilizar la versión 8 o superior de Java y el uso de la versión 5 de Spring Framework.

La necesidad de utilizar como mínimo la versión 8 de Java supone poder explotar todas las novedades que se añadieron a dicha versión, como interfaces funcionales, funciones lambda o interfaces con métodos implementados por defecto, por citar algunas.

El uso de la versión 5 de Spring Framework supone poder implementar directamente el paradigma reactivo con Spring Boot. Lo que quiere decir que se puede utilizar Netty como servidor embebido, en vez de contenedores de servlets más tradicionales como Tomcat, Jetty o Undertow.

En este punto es curioso comentar como los distintos lenguajes de programación, y las herramientas construidas en torno a ellos, se están complementando. Por ejemplo, la forma de escribir una aplicación con Angular, el popular framework para JavaScript, se parece cada vez a más a la forma en que se escriben aplicaciones en Spring. Es decir, utilizando un estilo basado en anotaciones, llamados decoradores en JavaScript, para indicar si las clases son controladores o servicios, o para enriquecerlas con cualquier otra metainformación. Por su parte, Spring ha adoptado de forma nativa en Java el modelo de servidor con un único hilo de ejecución utilizado tradicionalmente en JavaScript, como en Express, el popular servidor HTTP de node.js.

Todos los cambios y novedades que incorpora Spring Boot 2 pueden encontrarse en la página con las release notes del framework, incluida una guía de migración que incluye una referencia a un módulo que han implementando de forma explícita para facilitar dicha migración. Lo que sigue de este artículo son unas breves notas sobre un conjunto pequeño de las novedades de Spring Boot 2 que me han resultado interesantes con algunos comentarios de mi parte.

Reactive

La adopción de la versión 5 de Spring Framework permite utilizar Spring Webflux como alternativa a Spring MVC, lo que se traduce en que ahora se pueden construir aplicaciones totalmente asíncronas y no bloqueantes con Spring Boot.

Este cambio es realmente importante dentro del uso de Spring y merece la pena dedicarle un tiempo a entenderlo bien. Lo primero que hay que comprender es que el paradigma tradicional dominante en Java es atender cada petición en un hilo de ejecución (thread) distinto. De forma que si una petición accede a un recurso bloqueante, como cuando realiza una consulta a una base de datos por ejemplo, el hilo detiene su ejecución hasta que obtiene una respuesta de dicho recurso. Mientras un hilo está bloqueado no puede utilizarse para otra petición, por lo que un sistema con muchas peticiones puede llegar a bloquearse si todos sus hilos de ejecución se encuentran bloqueados.

Una implementación alternativa a la expuesta en el párrafo anterior es utilizar un único hilo de ejecución. Es decir, con un servidor que tan sólo dispone de un hilo de ejecución a través del que atiende todas las peticiones. Cuando una petición entra es atendida completamente antes de atender la siguiente, excepto cuando la petición invoca un recurso, en cuyo caso se la deja esperando hasta que el recurso responde. La que espera es la petición, no el hilo de ejecución, que puede seguir atendiendo peticiones.

Lógicamente, la clave de esta implementación es que todos los recursos deben poder ser accedidos de forma no bloqueante. Una consulta de base de datos no debe bloquear la petición, debe retornar inmediatamente y señalizar de alguna forma su resultado cuando disponga del mismo.

En Java ya existían servidores que implementaban el patrón reactivo, Spring 5 simplemente lo ha incorporado a su caja de herramientas y Spring Boot ha facilitado el uso del mismo. No obstante, la adopción de Spring 5 no implica el uso obligatorio de WebFlux con Spring Boot 2, se pueden seguir desarrollando aplicaciones con Spring MVC con Spring Boot 2 como hasta ahora.

Reactive Data

Como se ha explicado en el apartado anterior, implementar una aplicación completamente asíncrona y no bloqueante implica que todos los recursos deben ser también asíncronos y no bloqueantes. En una aplicación tradicional esto supone que el acceso a una base de datos por ejemplo debe realizarse mediante un driver no bloqueante.

Spring Boot ha añadido módulos que facilitan el acceso al API asíncrono de las bases de datos NoSQL más populares como Redis, MongoDB o Cassandra. Sin embargo, las bases de datos relacionales más tradicionales accedidas a través de JDBC o JPA aún necesitan utilizar algún tipo de driver asíncrono, normalmente no oficial, o una librería de terceros que implemente un wrapper sobre el driver original.

Por lo tanto, es importante tener en cuenta que si una aplicación va a estar ligada a uno o más recursos accedidos de forma bloqueante, por ejemplo a través de un API como JDBC o JPA, entonces una mejor opción a WebFlux puede ser utilizar Spring MVC.

Oracle tiene una propuesta en marcha para el desarrollo de un API para el acceso no bloqueante a base de datos, pero de momento no ha llegado a cristalizar.

Netty

El principal cambio de Spring Boot 2 respecto a los servidores embebidos soportados es la incorporación de Netty, el servidor asíncrono no bloqueante por antonomasia, para la implementación de aplicaciones con WebFlux.

Los servidores web tradicionales como Tomcat, Jetty y Undertow siguen siendo opciones disponibles para desarrollar aplicaciones con Spring MVC, pero con el requerimiento de implementar como mínimo la versión 3.1 del API Servlet para utilizar WebFlux, ya que fue la primera versión que incorporó la gestión de operaciones de E/S de forma no bloqueante.

A este respecto, comentar que Spring Boot 2 ha actualizado su dependencia con Tomcat a la versión 8.5, y ahora utiliza HikariCP como pool de conexiones, en vez del pool por defecto de Tomcat.

HTTP/2

HTTP/2 introdujo muchas mejoras al protocolo HTTP, como un formato binario más eficiente de procesar que el formato anterior en texto plano, la capacidad de atender múltiples peticiones a través de una misma conexión en vez de tener que establecer una nueva por cada petición, una reducción en la cantidad de paquetes necesarios para intercambiar las cabeceras HTTP mediante el uso de compresión, e incluso la posibilidad de que los servidores tomen la iniciativa en el envío de información a los clientes.

Mejoras todas ellas que se han reflejado en un aumento del rendimiento a la hora de descargar recursos estáticos de una web, como hojas de estilos e imágenes. Su explotación por parte de los servicios REST de momento parece haberse restringido a una mejora del rendimiento motivado por las propias mejoras de rendimiento en el protocolo.

Spring Boot 2 incorpora la posibilidad de utilizar HTTP/2 con Tomcat, Jetty y Undertow, aunque requiere una configuración específica en cada caso y el uso de alguna librería nativa de terceros en según que caso. La combinación de Tomcat 9 con Java 9 o superior es particularmente interesante, ya que soporta el protocolo sin requerir de ninguna librería adicional.

Actuators

Los actuators son un conjunto de utilidades que pueden añadirse de forma automática a las aplicaciones desarrolladas con Spring Boot, y a las que pueden accederse mediante JMX o HTTP. Permiten comprobar si la aplicación está levantada, consultar sus parámetros de configuración, u obtener métricas en tiempo real, por citar sólo algunos casos de uso.

En Spring Boot 2 se han unificado las rutas HTTP para acceder a dichos servicios a través de un único endpoint “\actuator”, se ha mejorado la calidad y estructura de la información retornada en los JSON, se ha adoptado HATEOAS para el autodescubrimiento de servicios mediante enlaces hipermedia en formato HAL, y se ha cambiado el sistema de métricas propio original configurado por defecto a favor de micrometer.

Esto último acerca de las métricas es más importante de lo que pueda parecer en un principio, sobre todo en el mundo actual donde priman los microservicios. Spring Boot recopila de forma automática una gran cantidad de métricas acerca del funcionamiento de las aplicaciones. Mide aspectos tales como el rendimiento de la máquina virtual de Java, la latencia de los servicios, el uso de la cache, el número de conexiones, el uso de aplicaciones específicas como Tomcat o RabbitMQ, y muchos otros parámetros. Métricas todas ellas que se pueden etiquetar y enviar a un servidor, o varios de ellos, para su procesamiento y monitorización, normalmente de forma agregada.

Para terminar, comentar que otra mejora importante es que se ha añadido a los actuators un API para exponer servicios propios de manera agnóstica, es decir, sin ligarlos a JMX o HTTP. Los beans de Spring anotados con @Endpoint, y con métodos anotados con @ReadOperation, @WriteOperation y @DeleteOperation son automáticamente expuestos por JMX, y por HTTP en el caso de encontrarse dentro de una aplicación web.

Resumiendo, en esencia Spring Boot sigue siendo el mismo que antes, la versión 2 incorpora las novedades de Spring 5 y refleja mejor el estado actual del desarrollo de aplicaciones web con Java.

Spring Cloud Netflix (y 5)

Continuando con el ejemplo práctico de uso de Spring Cloud Netflix, el último paso es montar los proyectos que arranquen los servicios de enrutamiento y monitorización.

cloud-greeter-zuul

Este proyecto es un servidor de enrutamiento basado en Zuul de Netflix OSS. Es la puerta de entrada a los servicios desde el exterior.

La estructura del proyecto es igual que en los anteriores. En el fichero pom.xml únicamente cambia la dependencia principal:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

La única clase del proyecto es el punto de entrada a la aplicación, sobre la que se añade la anotación @EnableZuulProxy  para configurar el servidor Zuul:

@EnableZuulProxy
@SpringBootApplication
public class Application {

  public static void main(final String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

El fichero de configuración del servidor es proporcionado por el servidor de configuración. El fichero es similar a los anteriores. Configura el puerto en que se levanta el servidor, las rutas a las que atiende, la configuración para localizar el servidor Eureka y la ubicación de los ficheros de log:

server:
  port: 8103

zuul:
  routes:
    greeting:
      path: /greeter/**
      serviceId: cloud-greeter-api

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8101/eureka/

logging:
  file: logs/${spring.application.name}.log
  level:
    org.springframework.cloud: DEBUG
    com.inmensia: DEBUG

La entrada zuul.routes del fichero define las rutas que atiende Zuul. En el ejemplo, una petición HTTP a la ruta “/greeter” es redirigida al servicio de nombre “cloud-greeter-api”. La resolución del nombre es realizada por Eureka.

El servidor se levanta desde línea de comandos de la forma acostumbrada:

mvn spring-boot:run

El servidor estará disponible a través de http://localhost:8103. Y para comprobar que funciona correctamente el enrutamiento se puede invocar el servicio utilizando la ruta virtual y verificar que invoca realmente al servicio solicitado:

  • http://localhost:8103/greeter/api/v1/greeting?name=World

El camino que sigue la petición consiste de varios pasos. El primero lo realiza Zuul, resolviendo la petición a través de Eureka, localizando el servicio solicitado e invocándolo. Lo que en nuestro ejemplo supone la invocación del servicio externo que actúa como gateway. El siguiente paso es la invocación del servicio interno por parte del gateway, resuelto también a través de Eureka. Y el último paso es la ejecución efectiva del servicio interno, retornándose el resultado a través de toda la cadena de servicios por la que pasa la petición.

En este punto es interesante notar que los componentes de Netflix OSS ofrecen muchas más características que las básicas contempladas en este proyecto de ejemplo. Por ejemplo, Zuul permite definir filtros, a la manera de los filtros habituales de los servidores web, permitiendo programar toda una serie de procesos totalmente personalizables sobre las peticiones, como balanceos de carga o estadísticas a medida.

cloud-greeter-turbine

Este proyecto es un servidor de monitorización basado en Turbine de Netflix OSS. Técnicamente es un agregador de streams de Server-Sent Events (SSE) en formato JSON. Lo que quiere decir que recoge eventos generados por los servidores y los agrega dejándolos disponibles para su uso, principalmente por cuadros de mando. El caso de uso principal es su uso con Hystrix, que genera eventos tales como el número de éxitos, errores y timeouts.

La estructura del proyecto es igual que en los anteriores. En el fichero pom.xml únicamente cambian las dependencias principales:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>

La única clase del proyecto es el punto de entrada a la aplicación, sobre la que se añade la anotación @EnableTurbine para configurar el servidor Turbine y @EnableHystrixDashboard para agregar eventos de Hystrix y añadir la aplicación web con el cuadro de mando:

@EnableTurbine
@EnableHystrixDashboard
@SpringBootApplication
public class Application {

  public static void main(final String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

El fichero de configuración del servidor es proporcionado por el servidor de configuración. El fichero es similar a los anteriores. Configura el puerto en que se levanta el servidor, la información a agregar, la configuración para localizar el servidor Eureka y la ubicación de los ficheros de log:

server:
  port: 8102

turbine:
  aggregator:
    clusterConfig: CLOUD-GREETER-API
    appConfig: cloud-greeter-api

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8101/eureka/

logging:
  file: logs/${spring.application.name}.log
  level:
    org.springframework.cloud: DEBUG
    com.inmensia: DEBUG

La entrada turbine.appConfig del fichero define el nombre de la aplicación de la que se quiere agregar eventos. La resolución del nombre es realizada por Eureka.
El servidor se levanta desde línea de comandos de la forma acostumbrada:

mvn spring-boot:run

El servidor estará disponible a través de http://localhost:8102. Y para comprobar que funciona correctamente el agregador se pueden invocar la aplicación web con el cuadro de mando de Hystrix:

  • http://localhost:8102/hystrix/monitor?stream=http%3A%2F%2Flocalhost%3A8300%2Fhystrix.stream
  • http://localhost:8102/hystrix/monitor?stream=http%3A%2F%2Flocalhost%3A8102%2Fturbine.stream%3Fcluster%3DCLOUD-GREETER-API

Llegado este punto se tiene un entorno configurado completo y en ejecución con Spring Cloud Netflix.

¡Feliz computación en la nube!