Skip to content

java

Spring Cloud Netflix (4)

Continuando con el ejemplo práctico de uso de Spring Cloud Netflix, el siguiente paso es montar los proyectos que implementen los microservicios que contengan la lógica de negocio propia de la aplicación.

cloud-greeter-service

Este proyecto es un microservicio construido con Spring Boot que utiliza el cliente del servidor de configuración y el cliente de Eureka.

La estructura del proyecto es idéntica que la de los anteriores. En el fichero pom.xml únicamente cambian las dependencias:

<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<!-- Springfox -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>

Las dependencias específicas de Spring Boot configuran el proyecto como una aplicación web, con un servidor Undertow embebido, con las utilidades ofrecidas por defecto por el Actuator de Spring Boot.

Las dependencias específicas de Spring Cloud configuran el proyecto para hacer uso del cliente del servidor de configuración y del cliente de Eureka.

Las dependencias específicas de Springfox configuran el proyecto para utilizar Swagger, que es una herramienta utilizada para describir los servicios REST de una aplicación mediante anotaciones Java que se aplican sobre las propias clases que implementan dichos servicios. La dependencia springfox-swagger2 ofrece el API con las anotaciones. La dependencia springfox-swagger-ui añade una interface gráfica a la aplicación en forma de página web que lista los servicios REST disponibles en la aplicación, y que además permite invocarlos directamente desde el navegador. Es una forma fácil de documentar los servicios REST y permite poder probarlos rápidamente.

El proyecto se compone de varias clases organizadas por paquetes. El punto de entrada a la aplicación es la clase Application anotada con @SpringBootApplication para configurarla como una aplicación de Spring Boot, con la anotación @EnableEurekaClient  para hacer que utilice el cliente de Eureka, y con la clásica anotación @ComponentScan de Spring para indicar el paquete donde localizar las clases que debe gestionar el propio Spring para implementar patrones como la inyección de dependencias.

@EnableEurekaClient
@SpringBootApplication
@ComponentScan(basePackages = { "com.inmensia.greeter" })
public class Application {

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

Dentro del paquete config se encuentra el detalle de la configuración específica para Swagger. Dentro del paquete controller el servicio REST que expone la lógica de negocio. Y dentro del paquete model se encuentran las clases POJO de entrada y salida del servicio REST.

La clase más destacada es el controlador que implementa el servicio REST:

@Api
@RestController
@RequestMapping("/api/v1")
public class GreetingController {

  private static final Logger LOGGER =
    LoggerFactory.getLogger(GreetingController.class);

  @RequestMapping(
    method = RequestMethod.POST,
    value = "/greeting")
  @ApiOperation(value = "greeting", 
    response = GreetingResponse.class)
  @ApiResponses(value = { @ApiResponse(
    code = 200,
    message = "Success", 
    response = GreetingResponse.class) })
  public GreetingResponse greeting(
    @ApiParam(value = "request", required = true)
    @RequestBody(required = true)
    final GreetingRequest request,
    final HttpServletRequest httpRequest) {

    LOGGER.debug("Received {}", request);

    return new GreetingResponse(
      httpRequest.getServerPort(), 
      "Hello " + request.getName() + "!");
  }
}

Sobre esta clase llama la atención el gran número de anotaciones utilizadas, la mitad de ellas de Spring, para definir el servicio, y la otra mitad de Swagger, para describir el servicio, y aunque este último cumple su función, en determinados casos puede resultar bastante intrusivo y repetitivo.

El servicio se limita a recoger un JSON con un campo de tipo texto y responder otro JSON con otro campo de tipo de texto. Recibe un nombre y retorna un texto en forma de saludo para el nombre dado. Adicionalmente en el JSON de salida retorna el número del puerto en el que está levantado el servicio, dato utilizado sólo a modo de ejemplo, para comprobar el balanceado de carga de una forma sencilla, en una aplicación real no sería necesario.

El fichero de configuración de la aplicación es cloud-greeter-service.yml y se encuentra en el repositorio del proyecto cloud-greeter-config:

port: ${CLOUD_GREETER_SERVICE_PORT}

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

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

En este fichero se define el servidor Eureka a utilizar. La definición a base de regiones, zonas y demás es herencia de la nomenclatura utilizada por Amazon, la plataforma utilizada por Netflix.

De igual forma que con los anteriores, el servicio se puede arrancar como cualquier otra aplicación de Spring Boot desde línea de comandos. En este caso especificando como parámetro de entrada el puerto en que debe levantarse el servidor de aplicaciones, ya que lo normal es que se quiera levantar más de una instancia de un mismo servicio para garantizar una alta disponibilidad y tolerancia a fallos.

mvn spring-boot:run "-DCLOUD_GREETER_SERVICE_PORT=8200"
mvn spring-boot:run "-DCLOUD_GREETER_SERVICE_PORT=8201"

Un servidor arrancará en el puerto 8200 y otro en el puerto 8201, y estarán disponibles a través de la rutas http://localhost:8200 y http://localhost:8201 respectivamente.

Accediendo a las rutas http://localhost:8200/swagger-ui.html y http://localhost:8201/swagger-ui.html se mostrarán las interfaces gráficas de Swagger para cada uno de los servidores.

Cada servidor, haciendo uso del cliente Eureka embebido, se comunicará automáticamente con el servidor Eureka para registrarse como servicio.

La última clase de proyecto está dentro del paquete de pruebas y hace uso de las capacidades de Spring Boot para probar servicios REST.

cloud-greeter-api

Este proyecto es un microservicio construido con Spring Boot que invoca al microservicio del proyecto anterior. El propósito de este proyecto es ilustrar la creación de un gateway. Es decir, un servicio que se limita a invocar a otros. Útil para exponer servicios de forma pública, como un API REST por ejemplo, evitando exponer los servicios que acceden a los recursos privados, como por ejemplo bases de datos.

La estructura del proyecto es idéntica que la del anterior, solo que en el fichero pom.xml se añade una nueva dependencia con Hystrix, otra librería que forma parte de Netflix OSS. Hystrix ayuda a controlar las llamadas entre los distintos servicios de un sistema. Se sitúa como una capa intermedia entre los servicios, mide la latencia, controla los errores, y permite a los servicios definir la lógica que debe ejecutarse en caso de error.

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

El fichero de configuración es proporcionado por el servidor de configuración de igual forma que en los proyectos anteriores.

server:
  port: ${CLOUD_GREETER_API_PORT}

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

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

cloud:
  greeter:
    api:
    greeterServiceUrl: http://cloud-greeter-service/api/v1/greeting

La última entrada del fichero de configuración define la ruta donde se encuentra el servicio que invocará el gateway. Como se observa, la ruta es virtual, es el nombre del servicio con el que se encuentra registrado en Eureka. La cadena “cloud-greeter-service” se sustituye por la URL real cuando se invoca el servicio.

La clase principal del proyecto que define el punto de entrada a la aplicación configura una aplicación Spring Boot que hace uso del cliente Eureka igual que en el anterior proyecto. La única diferencia es la anotación @EnableCircuitBreaker. El nombre de la anotación deriva del nombre del patrón Circuit Breaker que implementa Hystrix.

La clase del controlador que implementa el servicio REST público expuesto por el gateway tiene la misma interface que el microservicio privado del proyecto anterior. Los parámetros que recibe el público se le pasan tal cual al privado, y el resultado que se recibe del privado se publica tal cual por el público.

@Api
@RestController
@RequestMapping("/api/v1")
public class ApiController {

  private static final Logger LOGGER =
    LoggerFactory.getLogger(ApiController.class);

  private final GreeterService greeterService;

  @Autowired
  public ApiController(final GreeterService greeterService) {
    this.greeterService = greeterService;
  }

  @ApiOperation(
    value = "greeting",
    response = ApiResponse.class)
  @ApiResponses(value = { @ApiResponse(
    code = 200,
    message = "Success", 
    response = GreetingResponse.class) })
  @RequestMapping(
    method = RequestMethod.GET, 
    value = "/greeting")
  public GreetingResponse greeting(
    @ApiParam(value = "name", required = true) 
    @RequestParam(name = "name") final String name) {

    LOGGER.debug("Name: {}", name);

    final GreetingResponse response = greeterService.greeting(name);

    LOGGER.debug("Port: {}", response.getPort());
    LOGGER.debug("Message: {}", response.getMessage());

    return response;
  }
}

La invocación del servicio privado se realiza mediante una clase de servicio que utiliza la conocida clase RestTemplate de Spring. Una alternativa sería utilizar Ribbon, otra librería de Netflix OSS, que funciona mediante anotaciones en interfaces, sin implementación, de forma similar a los repositorios de Spring Data.

@ConfigurationProperties(prefix = "cloud.greeter.api")
@Service
public class GreeterServiceImpl implements GreeterService {

  private static final Logger LOGGER =
    LoggerFactory.getLogger(GreeterServiceImpl.class);

  private final RestTemplate restTemplate;

  private String greeterServiceUrl;

  @Autowired
  public GreeterServiceImpl(final RestTemplate restTemplate) {
    this.restTemplate = restTemplate;
  }

  public void setGreeterServiceUrl(final String greeterServiceUrl) {
    this.greeterServiceUrl = greeterServiceUrl;
  }

  @Override
  @HystrixCommand(fallbackMethod = "fallbackGreeting")
  public GreetingResponse greeting(final String name) {
    LOGGER.debug("Name: {}", name);

    if ("Hell".equals(name)) {
      throw new NullPointerException();
    }

    final GreetingRequest request = new GreetingRequest(name);

    final GreetingResponse response = restTemplate.postForObject(
      greeterServiceUrl,  request, GreetingResponse.class);

    LOGGER.debug("Port: {}", response.getPort());
    LOGGER.debug("Message: {}", response.getMessage());

    return response;
  }

  public GreetingResponse fallbackGreeting(final String name) {
    LOGGER.debug("Failed");

    return new GreetingResponse(null, "Failed!");
  }
}

Para demostrar el uso de Hystrix, el método que implementa el servicio está anotado con @HystrixCommand(fallbackMethod = «fallbackGreeting»). Lo que instruye a Hystrix a invocar el método fallbackGreeting de la clase cuando se produzca un error. Esto da la oportunidad a los servicios de ejecutar un comportamiento alternativo, o un comando compensatorio si es necesario deshacer alguna operación.

Para comprobar el funcionamiento de Hystrix, el método que implementa el servicio eleva un NullPointerException si se le pasa como nombre el valor “Hell”. En esos casos el servicio fallará y Hystrix llamará al método configurado en la anotación.

De la forma acostumbrada, uno o más servicios se pueden ejecutar desde línea de comandos indicando el puerto en que se quieren levantar:

mvn spring-boot:run "-DCLOUD_GREETER_API_PORT=8300"
mvn spring-boot:run "-DCLOUD_GREETER_API_PORT=8301"

Los servicios levantandos serán accesibles desde los puertos indicados, así como sus aplicaciones de Swagger correspondientes.

Para terminar, comentar que el cliente Eureka recupera la lista de servicios levantados y la guarda en local, de forma que no interroga al servidor Eureka cada vez que se invoca a un servicio, lo que mejora el rendimiento, y tiene el beneficio añadido de que, si se produce una caída del servidor Eureka, el cliente aún tendrá en local la ubicación de los servicios, lo que aumenta la disponibilidad y la tolerancia a fallos.

Spring Cloud Netflix (3)

Continuando con el ejemplo práctico de uso de Spring Cloud Netflix, el siguiente paso es montar los proyectos que arranquen el servidor de configuración y el de descubrimiento de servicios.

cloud-greeter-config

Este proyecto es un servidor de configuración basado en Spring Cloud Config. Suministra la configuración al resto de servicios de una forma centralizada.

Cuando un servicio arranca se conecta con este servidor y le pide sus propiedades de configuración. Las propiedades se pueden configurar para obtenerse desde distintas fuentes, como por ejemplo desde un servidor git, lo que permite tener un control de versiones sobre los ficheros de configuración de igual forma que se tiene habitualmente sobre el código fuente.

El servidor se implementa como una aplicación de Spring Boot. Y de hecho esto es una constante para todos los módulos. Cada módulo es una aplicación de Spring Boot. Lo que quiere decir que cada uno de ellos es una aplicación independiente que contiene embebido su propio servidor de aplicaciones. Una vez compilados se pueden arrancar y parar de forma individual, de forma que al final se tendrán tantos servidores de aplicaciones levantados como servidores y servicios se tengan.

El fichero pom.xml del proyecto se limita a heredar del proyecto padre y añadir las dependencias de Spring Boot y Spring Cloud Config:

<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>

  <parent>
    <groupId>com.inmensia</groupId>
    <artifactId>cloud-greeter-parent</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <relativePath>../cloud-greeter-parent</relativePath>
  </parent>

  <artifactId>cloud-greeter-config</artifactId>
  <packaging>jar</packaging>

  <dependencies>

    <!-- Spring Cloud -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-config-server</artifactId>
    </dependency>

    <!-- Spring Boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

  </dependencies>

  <build>
    <plugins>

      <!-- Spring Boot -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring-boot.version}</version>
      </plugin>

    </plugins>

  </build>

</project>

La dependencia spring-boot-starter-undertow hace que se utilice Undertow como servidor de aplicaciones, un servidor embebible que rinde bien con pocos recursos, algo necesario para este proyecto que levanta muchos servicios de forma local. Aunque se podría utilizar cualquier otro servidor, como Jetty o Apache por ejemplo.

La dependencia spring-boot-starter-actuator es una utilidad de Spring Boot que expone una serie de servicios REST para la aplicación de forma automática. Estos servicios implementan tareas habituales, evitando que los desarrolladores tengan que implementarlos por su cuenta, como por ejemplo un servicio que atiende la ruta “/health” que se puede utilizar para comprobar si el servidor está levantado y retorna un JSON con información acerca del sistema.

La única clase del proyecto es el punto de entrada a la aplicación:

@EnableConfigServer
@SpringBootApplication
public class Application {

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

El servidor se arranca como una aplicación de Spring Boot ordinaria gracias a la anotación @SpringBootApplication. Y el servidor de configuración se activa con la anotación @EnableConfigServer. Así de simple, no hay más. No es de extrañar que Spring Boot se haya convertido en un referente para la construcción de microservicios para las empresas con equipos de desarrolladores Java que necesitan obtener resultados de una forma rápida.

El único fichero de configuración del proyecto es application.yml, que es el propio fichero de configuración cargado por defecto por Spring Boot:

server:
  port: 8100

spring:
  application:
    name: cloud-greeter-config
      profiles:
        active: native

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

---

spring:
  profiles: native
  cloud:
    config:
      server:
        native:
          searchLocations: ${CLOUD_GREETER_CONFIG_PATH}

En el fichero se configura el puerto en que se debe levantar el servidor de aplicaciones, el nombre de la aplicación, la ubicación de los ficheros de logs, y el repositorio con los ficheros de configuración de los servicios. La ubicación del repositorio se realiza mediante una variable de entorno, de forma que pueda cambiarse fácilmente. Otra forma más habitual de hacerlo es utilizar varios profiles y definir el nombre del profile a utilizar en una variable de entorno.

El servidor se puede arrancar como cualquier otra aplicación de Spring Boot. Por ejemplo desde línea de comandos, en el directorio donde se encuentre el módulo, utilizando el plugin de Spring Boot para Maven:

mvn spring-boot:run "-DCLOUD_GREETER_CONFIG_PATH=../../../cloud-greeter-config"

El servidor arrancará en el puerto 8100 y estará disponible a través de la ruta http://localhost:8100, pudiéndose comprobar su estado a través de la ruta expuesta por Spring Boot Actuator:

  • http://localhost:8100/health

cloud-greeter-eureka

Este proyecto es un servidor de descubrimiento de servicios basado en Eureka. Mantiene el registro centralizado de los servicios disponibles y garantiza una alta disponibilidad de los mismos, o al menos una alta posibilidad de encontrar una instancia de los mismos en funcionamiento a pesar de las incidencias que puedan producirse, como cortes de red o caídas de servidores.

La estructura del proyecto es idéntica que la del anterior. En el fichero pom.xml únicamente cambian las dependencias principales:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-eureka-server</artifactId>
  </dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-config-client</artifactId>
</dependency>

La dependencia spring-cloud-config-client es para utilizar el cliente de Spring Cloud Config. Es decir, que el servidor Eureka también recoge su configuración del servidor de configuración. De hecho, la idea es que todas las aplicaciones que se levanten, tanto los servicios que implementen lógica de negocio, como los servidores que les dan soporte, utilicen el servidor de configuración.

La única clase del proyecto es el punto de entrada a la aplicación:

@EnableEurekaServer
@SpringBootApplication
public class Application {

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

El servidor se arranca como una aplicación de Spring Boot ordinaria gracias a la anotación @SpringBootApplication. Y el servidor Eureka se activa con la anotación @EnableEurekaServer.

El único fichero de configuración del proyecto es boostrap.yml, que es el propio fichero de configuración cargado por defecto por Spring Boot cuando una aplicación se configura para obtener sus propiedades desde un servidor de configuración:

spring:
  application:
    name: cloud-greeter-eureka
  cloud:
    config:
     failFast: true
     uri: http://localhost:8100

En el fichero se configura el nombre de la aplicación y la URL donde se encuentra el servidor de configuración.

El servidor Eureka obtiene su fichero de configuración del servidor de configuración. Fichero que se llama cloud-greeter-eureka.yml y se encuentra en el repositorio del proyecto cloud-greeter-config junto con el resto de ficheros de configuración. El servidor de configuración simplemente casa el nombre de la aplicación con el nombre del fichero. Si existe un fichero con el nombre de la aplicación supone que es el fichero de configuración de dicha aplicación y se lo sirve.

server:
  port: 8101

eureka:
  instance:
    hostname: localhost
    client:
      registerWithEureka: false
      fetchRegistry: false

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

En el fichero se configura el puerto en que se debe levantar el servidor de aplicaciones, la ubicación de los ficheros de logs, y los parámetros propios del servidor Eureka. Notar que uno de los parámetros sirve para evitar que el propio servidor se registre contra si mismo.

El servidor se puede arrancar como cualquier otra aplicación de Spring Boot. Por ejemplo desde línea de comandos, en el directorio donde se encuentre el módulo, utilizando el plugin de Spring Boot para Maven:

mvn spring-boot:run

El servidor arrancará en el puerto 8101 y estará disponible a través de la ruta http://localhost:8101. Al acceder a la URL se abre un panel de control con los servicios registrados. A medida que se levanten nuevos servicios estos irán apareciendo en el listado.

En buena lógica, los servidores pueden configurarse en cluster para asegurar su disponibilidad y que no representen un punto único de fallo.

Spring Cloud Netflix (2)

Hace ya un tiempo desarrollé un ejemplo mostrando un caso de uso sencillo de Spring Cloud Netflix. El proyecto acabó sirviendo de referencia e introducción a esta tecnología para los equipos de desarrollo de una empresa para la que trabajaba y el código fuente publicado en GitHub en dos repositorios, uno con los fuentes de los servicios y otro con los ficheros de configuración:

El ejemplo se compone de una estructura muy sencilla, sin distracciones, con la intención de fomentar las buenas prácticas en el uso de Maven para la configuración de proyectos con múltiples módulos, de forma que las dependencias comunes están declaradas en un proyecto padre del que todos heredan y cada proyecto sólo incluye las dependencias propias.

El objetivo del proyecto es mostrar las piezas mínimas necesarias para realizar una instalación con Spring Cloud Netflix, desarrollando tanto los servicios propios, que ejecutan la lógica de negocio, como los servicios de la plataforma, necesarios para que los servicios propios se puedan configurar de forma remota, se puedan invocar directamente entre ellos, o de forma externa a través de una ruta pública, se puedan ejecutar de forma balanceada, y se puedan monitorizar.

El directorio raíz del proyecto contiene el fichero pom.xml que incluye todos los módulos en los que se encuentra estructurado:

<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.inmensia</groupId>
  <artifactId>cloud-greeter</artifactId>
  <version>0.1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>modules/cloud-greeter-parent</module>
    <module>modules/cloud-greeter-config</module>
    <module>modules/cloud-greeter-eureka</module>
    <module>modules/cloud-greeter-service</module>
    <module>modules/cloud-greeter-api</module>
    <module>modules/cloud-greeter-turbine</module>
    <module>modules/cloud-greeter-zuul</module>
  </modules>
</project>
  • cloud-greeter-parent: Proyecto padre del que todos heredan. Contiene la configuración común y declara las dependencias compartidas por todos.
  • cloud-greeter-config: Proyecto correspondiente al servicio de configuración. Arranca una instancia del servidor Spring Cloud Config que todos los servicios utilizan para obtener su configuración de forma remota.
  • cloud-greeter-eureka: Proyecto que arranca el servidor Eureka utilizado para el registro y descubrimiento de servicios.
  • cloud-greeter-service: Este proyecto contiene un servicio de ejemplo. Expone un servicio REST que recibe un JSON con un nombre y retorna un JSON con una cadena de texto que contiene el nombre recibido dentro del típico “Hello <<nombre>>!”.
  • cloud-greeter-api: Este proyecto contiene un servicio que invoca al servicio del proyecto anterior. La idea es de que este servicio es expuesto de forma pública mientras el anterior sólo se puede invocar de forma interna.
  • cloud-greeter-zuul: Proyecto que arranca el servidor Zuul que actúa como enrutador. Recibe las peticiones de llamadas a los servicios y las enruta hacia los servicios levantados aplicando balanceo de carga.
  • cloud-greeter-turbine: Proyecto que arranca el servidor Turbine que expone una aplicación web a través de la que se puede monitorizar el estado de los servicios.

cloud-greeter-parent

Como ya se ha comentado, este proyecto contiene las partes comunes, un fichero pom.xml con la configuración del proyecto y las dependencias compartidas:

<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>com.inmensia</groupId>
  <artifactId>cloud-greeter-parent</artifactId>
  <version>0.1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>

    <spring-boot.version>1.5.3.RELEASE</spring-boot.version>
    <spring-cloud.version>Dalston.SR1</spring-cloud.version>
    <springfox.version>2.7.0</springfox.version>

    <maven-compiler-plugin.version>3.6.1</maven-compiler-plugin.version>
  </properties>

  <dependencyManagement>
    <dependencies>

    <!-- Spring Boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>${spring-boot.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>

    <!-- Spring Cloud -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>

    <!-- Springfox -->
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>${springfox.version}</version>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger-ui</artifactId>
      <version>${springfox.version}</version>
    </dependency>

    </dependencies>
  </dependencyManagement>
  
  <build>
    <pluginManagement>
      <plugins>

        <!-- Forces the JDK version -->
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>${maven-compiler-plugin.version}</version>
          <configuration>
            <source>${java.version}</source>
            <target>${java.version}</target>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

</project>

Las únicas dependencias declaradas son Spring Boot, Spring Cloud y Springfox. Spring Boot facilita la creación de microservicios, Spring Cloud la creación de servicios en la nube, y Springfox la documentación de los servicios REST.

Springfox quizás sea menos conocido, pero es simplemente una capa que se coloca por encima de Swagger y tiene el añadido de que es capaz de recuperar información de las anotaciones de librerías como Spring o Jackson, en vez de forzar a los desarrolladores a utilizar las anotaciones de Swagger, evitando así la dependencia y la duplicación de esfuerzos.

Spring Cloud Netflix (1)

Todas las empresas desarrollan una o varias actividades que representan su fuente principal de ingresos. Y esos ingresos se dividen en partidas de dinero que deben dedicarse a gastos e inversiones. Para las empresas de gran tamaño una de esas partidas habitualmente se destina al mantenimiento de sus sistemas de información. Sistemas que tradicionalmente han gestionado en una infraestructura propia. Sus propias máquinas, sus propias aplicaciones, sus propios equipos, sus propios problemas.

Para algunas empresas la información es parte fundamental de su actividad. Mantener esa información en un entorno propio les resulta algo natural. No obstante, se plantea la cuestión de si realmente necesitan hacerse responsable de gestionar absolutamente todos sus sistemas. No sólo los de misión crítica, sino los de uso más común. El esfuerzo humano, material y económico necesario para mantener en funcionamiento toda una infraestructura propia puede llegar a ser bastante elevado.

Desde hace más de dos décadas, gigantes como Amazon, Google y Microsoft comenzaron a ofrecer sus infraestructuras para que pudieran ser explotadas por otras empresas. Una oferta que se ha materializado en distintos servicios. Servicios de almacenamiento, servicios de computación, servicios de aplicaciones, … La idea es que las empresas no tengan que invertir en sistemas virtuales, distribuidos, replicados, escalables, tolerantes a fallos y con alta disponibilidad. Conceptos técnicos todos ellos que no se encuentran habitualmente dentro del dominio de su negocio.

Aunque el paradigma ha ido evolucionando con los años, por lo general se acepta que si un sistema o información no se encuentra alojado dentro de una infraestructura propia se dice que está en la nube. Si se almacena un archivo, como una fotografía, en un servidor ajeno se dice que está en la nube. Si se crea un fichero, como un documento, a través de una aplicación alojada en un servidor ajeno se dice que se usa un servicio en la nube. Si se ejecuta una aplicación propia en un servidor ajeno se dice que usa la infraestructura de la nube. Y en general, simplificando bastante, si se realiza cualquier acción que implica el acceso a un servidor ajeno a través de una red se considera que se está trabajando en la nube. Entendiendo en todo caso que dicho acceso se realiza normalmente a través de Internet, e independientemente de que algunas empresas mantengan grandes infraestructuras propias que denominan nubes privadas.

Una de las razones que motivó la aparición de la nube fue la necesidad de disponer de una gran capacidad de cómputo por parte de las empresas que empezaban a ofertar servicios para millones de potenciales clientes conectados a un mismo tiempo. Las empresas necesitaban arquitecturas con una alta escalabilidad horizontal bajo demanda utilizando las capacidades de una infraestructura externa ofrecida por algún proveedor. Las soluciones que surgieron de esa necesidad originó la creación de arquitecturas que se han popularizado hasta convertirse en el marco de referencia para el desarrollo de aplicaciones hoy en día. Evolucionando desde aplicaciones monolíticas basadas en base de datos relaciones hasta arquitecturas de microservicios y base de datos NoSQL.

Una de las compañías que tuvo que acometer la tarea de desarrollar una arquitectura en la nube fue Netflix, el popular servicio de streaming de series, películas y documentales. El código fuente de los servicios básicos que componen su plataforma desarrollada en Java se encuentra disponible de forma abierta bajo licencia Apache 2.0. Sus soluciones se desarrollaron en una época temprana y con unas necesidades muy concretas para la infraestructura de Amazon sobre la que se ejecuta, por lo que no reflejan el estado actual del desarrollo de microservicios ejecutados sobre contenedores. No obstante, la plataforma abierta de Netflix supone un punto de entrada muy interesante para los desarrolladores que quieran conocer el principio de funcionamiento de este tipo de arquitecturas.

Netflix Open Source Software (Netflix OSS) es el conjunto de librerías y herramientas desarrollados por Netflix que posibilita la ejecución de servicios en la nube. Proporciona una serie de componentes básicos que exponen todas las características esperadas de un entorno en la nube.

Spring Cloud es un framework desarrollado en Java por Spring que expone los patrones comunes implementados por los sistemas distribuidos. Proporciona tanto una serie de capas de abstracción a través de interfaces genéricas como una serie de implementaciones concretas de dichas interfaces con librerías propias o de terceros.

Spring Cloud Netflix es una implementación de las interfaces propuestas por Spring Cloud utilizando las librerías de Netflix OSS y Spring Boot.

El objetivo de todos estos frameworks es que los desarrolladores puedan montar un sistema de servicios distribuidos ejecutándose sobre una o más máquinas. Permiten a los desarrolladores centrarse en la lógica de negocio y no en los detalles de la infraestructura necesaria para la ejecución de los servicios. Entendiendo por servicio una aplicación propia, preferentemente estructurada en microservicios. Es decir, dividida en componentes que realizan una única tarea dentro de un dominio específico y que pueden ser invocados por el resto de servicios independientemente del lenguaje de programación en el que se encuentren implementados.

En la práctica, de cara a explotar un conjunto de servicios, se consideran necesarias, o al menos deseables, una serie de características. Estas características tienen como propósito facilitar la configuración, despliegue, comunicación y monitorización de dichos servicios. El enfoque de Spring Cloud Netflix al respecto es que nos responsabilicemos de tener una aplicación distinta arrancada, o varias instancias de las mismas, para cubrir cada una de estas características. Además de responsabilizarnos de que nuestras aplicaciones se comuniquen con ellas para obtener provecho de dichas características. Es decir, somos responsables de todo el software, tanto de infraestructura como de nuestros propios servicios. Spring Cloud Netflix proporciona las aplicaciones necesarias para el servidor y las librerías necesarias para que nuestros servicios se comuniquen con ellas.

Algunas de las características cubiertas por Spring Cloud Netflix son las siguientes:

– Configuración. Cuando se levanta un servicio es habitual que necesite una serie de parámetros de configuración locales propios. La cuestión no es tanto que estos parámetros se distribuyan como parte del servicio, sino que se puedan establecer y modificar, incluso en tiempo de ejecución, para facilitar la explotación del sistema.

Spring Cloud proporciona un servidor de configuración propio que denomina Spring Cloud Config. Y Netflix OSS por su parte proporciona Archaius, su propio servidor de configuración. Utilizar uno u otro es indistinto, Spring Cloud Netflix actúa como puente presentando a las aplicaciones una misma interface para ambos.

– Descubrimiento. En un sistema distribuido se debe conocer el número de instancias levantadas de un servicio, y un servicio debe poder invocar una instancia de otro servicio independientemente de donde se encuentre ejecutándose.

Netflix OSS proporciona Eureka, un servidor de descubrimiento de servicios. Cuando un servicio se levanta lo notifica a Eureka, que lleva el registro de los servicios levantados y permite que los servicios puedan invocarse los unos a los otros a través de su registro.

– Enrutamiento. Los servicios propios de las aplicaciones normalmente son expuestos públicamente a través de determinadas rutas. Rutas que no deben venir impuestas por la infraestructura donde se encuentren en ejecución.

Netflix OSS proporciona Zuul, un servidor que, en otras muchas cosas, permite definir las rutas en las que los servicios son publicados. Cuando un servicio se invoca, Zuul resuelve la petición redirigiéndola a alguno de los servicios registrados en Eureka.

– Monitorización. En un sistema distribuido con múltiples servicios sobre múltiples máquinas es importante disponer de herramientas que muestren el estado en tiempo real de todos ellos.

Netflix OSS proporciona Turbine, una aplicación cliente que muestra en un cuadro de mando el estado de distintas métricas. Se puede configurar fácilmente para que muestre el estado de los distintos servicios registrados en Eureka. Aunque técnicamente es un agregador de streams de Server-Sent Event (SSE), por lo que es bastante abierto.

Por último, comentar que hoy en día el enfoque de Spring Cloud Netflix, que obliga a los desarrolladores a conocer todos los detalles del funcionamiento del servidor y ligar sus aplicaciones con un framework concreto, ha evolucionado hacia arquitecturas en las que los desarrolladores no tienen que responsabilizarse del software del servidor ni añadir dependencias de terceros en las aplicaciones propias, permitiendo centrarse exclusivamente en la lógica de negocio.

Collectors en Java

Las clases que implementan la interface Collector de Java son las encargadas de convertir en conjuntos finitos de información las secuencias de elementos, potencialmente infinitas, generadas por los streams. Son operaciones terminales de reducción dentro de la cadena de procesamiento de un stream. Acumulan los datos producidos y los almacenan en algún tipo de colección, o generan un único valor a partir de ellos. Se utilizan por ejemplo para crear listas a partir de streams, o calcular valores máximos o mínimos. En la práctica son más conocidas por “¡Ah, eso que se pone al final! La cosa esa del .collect(Collectors.toList())” (sic).

Los recolectores fueron introducidos en Java 8 y han sido ampliados en las versiones 9 y 10. La interface básica es java.util.stream.Collector<T, A, R>, que ofrece cuatro métodos básicos con los que se construyen los recolectores. Estos métodos hacen uso del concepto de interface funcional de Java y no implementan las acciones directamente, sino que retornan una función que es la que realmente implementa la acción. Es como ir a una ventanilla para hacer una gestión y que te respondan “sí, es aquí, pero vaya a esa otra ventanilla”.

Las funciones básicas expuestas por los recolectores son muy genéricas, indican cómo construir un contenedor intermedio, cómo añadir elementos a dicho contenedor, cómo combinar dos contenedores para generar un resultado parcial, y cómo calcular el resultado final a partir de los parciales:

  • Supplier<A> supplier(): Este método retorna una función que crea un contenedor intermedio de tipo A. Un contenedor puede ser de cualquier clase que se quiera. No tiene que implementar ninguna interface concreta. Puede ser una Collection clásica de Java, pero también puede ser un StringBuilder por ejemplo.
  • BiConsumer<A, T> accumulator(): Este método retorna la función que acumula elementos de entrada del tipo T en un contenedor intermedio de tipo A.
  • BinaryOperator<A> combiner(): Este método retorna la función que combina dos contenedores intermedios de tipo A y crea un resultado parcial también de tipo A.
  • Function<A, R> finisher(): Este método retorna la función que calcula el resultado final de tipo R a partir de los resultados parciales de tipo A. El tipo R no tiene que implementar ninguna interface concreta.

Una forma muy directa de implementar un recolector es utilizar uno de los dos métodos estáticos of de la interface Collector. Estos métodos admiten como parámetros de entrada las cuatro funciones básicas y las características del recolector. Siendo estas características una combinación de valores del enumerado Collector.Characteristics. Donde el valor CONCURRENT indica que el proceso de recolección puede ejecutarse de forma concurrente, el valor UNORDERER indica que el resultado no depende del orden original de los elementos de entrada, y el valor IDENTITY_FINISH indica que la función de finalización puede omitirse retornándose directamente el contenedor intermedio. Es el mismo principio de definición de características usado para los Spliterators, nombre inspirado seguramente en alguna de las películas de Parque Jurásico.

A modo de ejemplo, el recolector del código siguiente se crea especificando directamente las funciones como parámetros del método Collector.of. Su propósito es retornar la suma de todos los elementos de tipo Integer de una fuente de entrada. La primera función crea un array con un entero que se puede utilizar como contenedor intermedio, la segunda función suma a un contenedor intermedio un elemento de entrada, la tercera función suma al contenido de un contenedor el contenido de otro, y la cuarta función retorna el resultado final de un contenedor.

var collector = Collector.of(

// supplier
  () -> new int[1],

// accumulator
  (int[] result, Integer element) -> result[0] += element,

// combiner
  (int[] result, int[] partial) -> {
    result[0] += partial[0];
    return result;
  },

// finisher
  (int[] result) -> result[0],

// characteristics
  Collector.Characteristics.UNORDERED
);

El recolector anterior puede aplicarse directamente en un stream de enteros para obtener la suma de todos los elementos:

var sum = Stream.of(1, 2, 3).collect(collector);

Como es de suponer, los recolectores se pueden crear de una manera más tradicional definiendo una clase que implementa la interface Collector, sin necesidad de utilizar expresiones lambdas como en el código de ejemplo anterior. No obstante, normalmente no suele ser necesario escribirlos, ya que Java proporciona de forma nativa implementaciones de una gran cantidad de recolectores de uso habitual a través de métodos estáticos de la clase java.util.stream.Collectors.

Los recolectores más comunes de la clase Collectors son los que crean colecciones, como los clásicos métodos toList, toSet y toMap que existen desde la versión 8 de Java, más los que se añadieron en la versión 10 como toUnmodifiableList, toUnmodifiableSet y toUnmodifiableMap, alguno más específico como toConcurrentMap, o el más genérico toCollection:

stream.collect(Collectors.toList());

stream.collect(Collectors.toUnmodifiableSet());

stream.collect(Collectors.toCollection(ArrayList::new));

Otros recolectores de gran utilidad ofrecidos por Collectors son los que realizan reducciones, es decir, calculan un único resultado a partir de los elementos de entrada. Ejemplos de este tipo de recolectores son counting, que cuenta el número de elementos, joining, que concatena todos los elementos como una cadena de texto, maxBy y minBy, que calculan el máximo y mínimo, summingInt, summingLong y summingDouble, que calculan la suma de los elementos, y averagingInt, averagingLong y averagingDouble, que calculan la media de los elementos. Un caso particular es summarizingInt, summarizingLong y summarizingDouble, que calcula todos los valores anteriores a un mismo tiempo, es decir que retorna un objeto que contiene el total de elementos, el máximo, el mínimo y la media.

empleados.collect(Collectors.counting());

empleados.collect(Collectors.averagingDouble(Empleado::getSueldo));

Otro tipo interesante de recolectores son los de agrupación, que como su propio nombre indica permiten agrupar los elementos de entrada en base a un determinado criterio. La salida de estos recolectores es un Map, donde la clave es el criterio de agrupación y el valor la colección de elementos agrupados.

empleados.collect(Collectors.groupingBy(Empleado::getDepartamento));

empleados.collect(Collectors.groupingBy(
  Empleado::getDepartamento,
  Collectors.counting()));

La clase Collectors  expone más factorías de recolectores aparte de las aquí citadas, por lo que es recomendable echar un vistazo a la documentación del API.

Como nota final, comentar que he decidido traducir collector como recolector, en vez de dejarlo en el inglés original, como se suele hacer con otras palabras, como stream por ejemplo, por la similitud con la palabra en español, pero evitando traducirlo directamente como colector, que para mí tiene otro sentido.

¡Feliz día de la recolección!