Introducción a Arquitectura de Microservicios

Introducción

El paradigma de arquitectura monolítica puede plantear problemas en aplicaciones grandes donde resulte conveniente tener información compartimentada y distribuida ya sea por cuestiones de seguridad o por tratar de estructurar el código de forma modular con un alto nivel de independencia entre sus partes. La idea principal de la arquitectura de microservicios [1] consiste en tratar de desacoplar la lógica de la aplicación completa en microservicios encargados de realizar tareas concretas para después unir las piezas de información entregada al usuario final.

Generalmente, la implementación se realiza mediante pequeñas API independientes que utilizan recursos HTTP para comunicarse entre sí. Precisamente, una de las problemáticas inherentes a este tipo de arquitectura es garantizar que los microservicios estén bien comunicados entre sí. Por este motivo existen patrones estandarizados y librerías de uso extendido que permiten solventar estas situaciones.

En este ejemplo introductorio vamos a crear tres aplicaciones independientes sin relación directa entre ellas, cada una se ejecutará en una instancia distinta de Tomcat con puertos diferentes. Estos tres proyectos serán creados con Spring -recomiendo el uso del IDE Spring Tools 4– para simular tres servicios independientes y desacoplados. El ejemplo simulará una aplicación donde los usuarios tienen una colección de libros. Cada usuario, mediante su ID, obtiene una lista de sus libros, con los datos y rating de cada uno.

  • BookData-Service
  • BookList-Service
  • BookRating-Service

La idea es que la información esté compartimentada en bloques distintos.

  • La parte de datos del libro (titulo, autor, sinopsis) se almacena en BookData-Service
  • La parte de rating (nota media, número de votos) se almacena en BookRating-Service
  • Finalmente, BookList-Service contiene acceso a usuarios y a su colección de libros. Este servicio llama a los dos anteriores y une todas las pizas de información.
  • Cada servicio accede a tablas independientes en BBDD que no están conectadas entre sí mediante claves primarias/foráneas para incidir en el desacoplamiento de la aplicación y sus microservicios independientes.

El código completo que acompaña este artículo puede encontrarse en mi repositorio para los interesados en profundizar en los ejemplos aquí descritos. En artículos futuros se discutirán conceptos más avanzados como Service Discovery Pattern y mecanismos de seguridad adicional que permitan tener una aplicación más robusta.


BookData-Service

Comenzamos la implementación de nuestra aplicación compuesta por microservicios con el algoritmo encargado de recoger la información relevante de cada libro (título, autor, sinopsis). Para ello partimos de una aplicación creada con Spring Boot con la misma configuración que utilizamos en el artículo Fullstack REST API . Obtendremos el siguiente fichero pom.xml con las dependencias indicadas:

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

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

A continuación, crearemos la interfaz de acceso a DB para la obtención de los datos de cada libro:

@Repository
public interface BookDataRepository extends JpaRepository<BookData, Integer>{
}

Esta interfaz hace uso de una clase modelo BookData que recibe la información de la tabla correspondiente de la DB:

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="bookdata")
public class BookData{
	
	//Parameters
	@Id
	@Column(name="idbook") 
	private int idbook;
	
	@Column(name="title") 
	private String title;
	
	@Column(name="author") 
	private String author;
	
	@Column(name="summary") 
	private String summary;

	
	//Constructor
	public BookData() {}
	...
	//Getters Setters
 ...
}

Finalmente, utilizamos una clase con etiqueta @RestController que será el punto de acceso de la API a este microservicio. Esta clase será la encargada de entregar la información solicitada partiendo de la ID del libro buscado:

@RestController 
@RequestMapping("/bookdata")
public class BookDataService {
	@Autowired
	private BookDataRepository bookdataDAO;
	
	//REST: get all (GET)
	@GetMapping("/data")
	public List<BookData> findAll(){
		return bookdataDAO.findAll();
	}		
	
	//REST: get user by ID (GET)
	//@PathVariable: Annotation which indicates that a method parameter 
	//should be bound to a URI template variable.
	@GetMapping("/data/{id}")
	public ResponseEntity<BookData> getByID(@PathVariable int id) {
		//Es un Optional<T>
		Optional<BookData> u = bookdataDAO.findById(id);
		//Si está presente lo devolvemos
		if(u.isPresent()){
		    return ResponseEntity.ok(u.get());
		}
		//Si no, return null
		else{
		    return null;
		}
	}
}

Con esto, tendríamos nuestro primer microservicio.


BookRating-Service

Partiendo de las ideas anteriores, implementamos el segundo microservicio en un nuevo proyecto Spring Boot de las mismas características (tendremos las mismas dependencias Maven en el fichero pom.xml). Este algoritmo se encarga de acceder a la DB y recoger el rating y el número de votos asociado a cada libro concreto.

En este caso, la interfaz Repository es idéntica a la anterior, cambiando la clase por BookRating

@Repository
public interface BookRatingRespository extends JpaRepository<BookRating, Integer>{
}

Al igual que en el caso anterior, la clase modelo BookRating tiene los campos necesarios para almacenar la información de la DB:

@Entity
@Table(name="bookratings")
public class BookRating{
	
	//Parameters
	@Id
	@Column(name="idbook") 
	private int idbook;
	
	@Column(name="rating") 
	private float rating;
	
	@Column(name="numvotes") 
	private int numvotes;

	//Constructor
	public BookRating() {}	
	
	public BookRating(int idbook, float rating, int numvotes) {
		super();
		this.idbook = idbook;
		this.rating = rating;
		this.numvotes = numvotes;
	}

	//Getters Setters
...
}

Finalmente, creamos la clase que nos permitira recibir request y enviar la información solicitada, en este caso los ratings de cada libro:

@RestController 
@RequestMapping("/bookratings")
public class BookRatingService {
	@Autowired
	private BookRatingRespository ratingDAO;
	
	//REST: get all (GET)
	@GetMapping("/ratings")
	public List<BookRating> findAll(){
		return ratingDAO.findAll();
	}		
	
	//REST: get user by ID (GET)
	//@PathVariable: Annotation which indicates that a method parameter 
	//should be bound to a URI template variable.
	@GetMapping("/ratings/{id}")
	public ResponseEntity<BookRating> getByID(@PathVariable int id) {
		//Es un Optional<T>
		Optional<BookRating> u = ratingDAO.findById(id);
		//Si está presente lo devolvemos
		if(u.isPresent()){
		    return ResponseEntity.ok(u.get());
		}
		//Si no, return null
		else{
		    return null;
		}
	}
}

Como vemos, se trata de una clase cuya lógica es idéntica a la anterior, modificando la clase BookData por BookRating.


BookList-Service

Finalmente, procedemos a implementar el último servicio que hará uso de los dos anteriores para mostrar la información completa de la colección de libros de cada usuario. Este último proyecto incluye una dependencia adicional en el pom.xml para acceder a las distintas API:

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

Esta dependencia es necesaria para utilizar la clase WebClient cuyo uso detallaremos más abajo.

El servicio BookList accede a DB para obtener información de los usuarios y de la colección de libros de cada uno. Por tanto, debemos crear dos clases modelo que hagan referencia a estas dos tablas:

@Entity
@Table(name="user")
public class User{
	
	//Parameters
	@Id
	@Column(name="iduser") 
	private int iduser;
	
	@Column(name="name") 
	private String name;

	
	//Constructors
	public User() {}
	
	public User(int iduser, String name) {
		this.iduser = iduser;
		this.name = name;
	}
	
	//Getters Setters
   ...
}
@Entity
@Table(name="usercollection")
public class UserCollection {
	//Parameters
	@Id
	@Column(name="idcollection") 
	private int idcollection;
	
	@Column(name="iduser") 
	private int iduser;
	
	@Column(name="idbook") 
	private int idbook;

	
	//Constructors
	public UserCollection() {}

	public UserCollection(int idcollection, int iduser, int idbook) {
		this.idcollection = idcollection;
		this.iduser = iduser;
		this.idbook = idbook;
	}

	
	//Getters Setters
	...
}

Esta clase también se encargará de solicitar a los microservicios la información sobre los datos del libro y su rating, unificando toda la información en una clase final Book:

public class Book {
	private int idbook;
	private String title;
	private String author;
	private String summary;
	private float rating;
	private int numvotes;
	
	public Book() {}
	
	public Book(int idbook, BookData bd, BookRating br){
		addInformation(idbook, bd, br);
	}
	
	public void addInformation(int idbook, BookData bd, BookRating br) {
		this.idbook = idbook;
		title = bd.getTitle();
		author = bd.getAuthor();
		summary = bd.getSummary();
		rating = br.getRating();
		numvotes = br.getNumvotes();
	}
	
	//Getters Setters
    ...
}

Esta clase puede recibir como parámetros en el constructor una instancia BookData y BookRating para volcar la información después de haber sido solicitada por la API a cada uno de los microservicios. Para ello, se define el método public void addInformation(int idbook, BookData bd, BookRating br).

Para acceder a la información de los usuarios y de sus colecciones, esta clase implementa dos repositorios de forma similar a los descritos anteriormente:

@Repository
public interface UserCollectionRepository extends JpaRepository<UserCollection, Integer>{
}

@Repository
public interface UserRepository extends JpaRepository<User, Integer>{
}

Finalmente creamos un @RestController para solicitar a los microservicios la información que guardaremos en instancias de BookData y BookRating para mostrarlas al usuario final en una lista de libros:

@RestController 
@RequestMapping("/booklist")
public class BookListService {
	
	private String bookRatingsPath = "http://localhost:8081/bookratings/ratings/";
	private String bookDataPath = "http://localhost:8082/bookdata/data/";
	
	@Autowired
	private WebClient.Builder webClientBuilder;	
	@Autowired
	private UserRepository userDAO;	
	@Autowired
	private UserCollectionRepository userCollectionDAO;
	
	//REST: get all (GET)
	@GetMapping("/users")
	public List<User> findAllUsers(){
		return userDAO.findAll();
	}		
	
	//REST: get user by ID (GET)
	//@PathVariable: Annotation which indicates that a method parameter 
	//should be bound to a URI template variable.
	@GetMapping("/users/{id}")
	public List<Book> getUserByID(@PathVariable int id) {
		//Es un Optional<T>
		Optional<User> u = userDAO.findById(id);
		//Si está presente lo devolvemos
		if(u.isPresent()){			
			//Usuario encontrado
			User user = u.get();
			
			//Buscamos libros de la colección del usuario
			List<UserCollection> uc = userCollectionDAO.findAll();
			
			//Sólo mostramos aquellos libros que pertenecen al usuario
			List<Book> books = new ArrayList<Book>();
			for (UserCollection userCollection : uc) {
				if(userCollection.getIduser() == user.getIduser()) {
					//Buscar BookData
					BookData bd = webClientBuilder.build().get().uri(
							bookDataPath+userCollection.getIdbook())
							.retrieve().bodyToMono(BookData.class).block();

					//Buscar BookRating
					BookRating br = webClientBuilder.build().get().uri(
						bookRatingsPath+userCollection.getIdbook())
						.retrieve().bodyToMono(BookRating.class).block();

					//Unir las partes
					books.add(new Book(userCollection.getIdbook(), bd,br));
				}
			}		
		    return books;
		}
		//Si no, return null
		else{
		    return null;
		}
	}
}

En esta clase tenemos tres campos inyectados mediante @Autowired: los dos repositorios mencionados para acceder a la información de los usuarios, y un bean de WebClient utilizado para lanzar web request a los microservicios definidos anteriormente. El uso de WebClient [2] no es obligatorio, aunque sí recomendado, dado que en futuras versiones de Spring se va a remplazar la clase RestTemplate que se usaba hasta ahora para este tipo de request:

WebClient is an interface representing the main entry point for performing web requests. It was created as part of the Spring Web Reactive module, and will be replacing the classic RestTemplate in these scenarios. In addition, the new client is a reactive, non-blocking solution that works over the HTTP/1.1 protocol. It’s important to note that even though it is, in fact, a non-blocking client and it belongs to the spring-webflux library, the solution offers support for both synchronous and asynchronous operations, making it suitable also for applications running on a Servlet Stack.

Spring 5 WebClient, baeldung.

Toda la lógica de obtención de información se da en el método public List<Book> getUserByID donde, en primer lugar, buscamos al usuario y, en caso de existir en la DB, procedemos a solicitar la información de su colección de libros List<UserCollection> uc = userCollectionDAO.findAll(). Después, para cada uno de ellos, solicitamos la información BookData y BookRating:

			//Sólo mostramos aquellos libros que pertenecen al usuario
			List<Book> books = new ArrayList<Book>();
			for (UserCollection userCollection : uc) {
				if(userCollection.getIduser() == user.getIduser()) {
					//Buscar BookData
					BookData bd = webClientBuilder.build().get().uri(
							bookDataPath+userCollection.getIdbook())
							.retrieve().bodyToMono(BookData.class).block();

					//Buscar BookRating
					BookRating br = webClientBuilder.build().get().uri(
						bookRatingsPath+userCollection.getIdbook())
						.retrieve().bodyToMono(BookRating.class).block();

					//Unir las partes
					books.add(new Book(userCollection.getIdbook(), bd,br));
				}
			}		
		    return books;

En este ejemplo sencillo, la clase WebClient usa una uri estática que hemos indicado al inicio de la definición de la clase; cada microservicio está alojado en un puerto diferente para que las distintas instancias de Tomcat no generen conflictos:

	private String bookRatingsPath = "http://localhost:8081/bookratings/ratings/";
	private String bookDataPath = "http://localhost:8082/bookdata/data/";

Finalmente volcamos la información de los dos microservicios en un único libro que añadimos a la lista final que mostraremos al usuario: books.add(new Book(userCollection.getIdbook(), bd,br));


Configuración final

Tal y como se ha comentado, los tres microservicios están implementados como tres aplicaciones Spring Boot independientes que corren en tres instancias de Tomcat. Para evitar conflictos debemos modificar la configuración de los puertos que, por defecto, están con el 8080. Para ello, en los archivos de configuración de propiedades de BookData y BookRating indicaremos que deseamos utilizar el 8081 y el 8082.

Es importante remarcar que al tratarse de un ejemplo práctico incluimos archivos de configuración application.properties con username y password en el repositorio. En casos reales no se debe incluir información sensitiva en archivos de configuración en repositorios públicos.

BookList-Service:

spring.datasource.url=jdbc:mysql://localhost:3306/booksdb?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

BookRating-Service:

server.port=8081

spring.datasource.url=jdbc:mysql://localhost:3306/booksdb?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

BookData-Service:

server.port=8082

spring.datasource.url=jdbc:mysql://localhost:3306/booksdb?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

Conclusiones

En este artículo se ha detallado una posible implementación introductoria de microservicios mediante Spring Boot. Para ello se han construido tres aplicaciones independientes que acceden a tablas de DB que no están conectadas entre sí. Finalmente, la clase BookList solicita a los dos servicios BookData y BookRating la información compartimentada que une en una lista de libros para cada usuario. En mi repositorio público se puede acceder al código abierto que se ha implementado en este artículo.


Referencias y enlaces de interés:

[1] Introducción a los microservicios – https://decidesoluciones.es/arquitectura-de-microservicios/

[2] WebClient – Baeldung

  • Lista de reproducción de un proyecto similar creado en InteliJ IDEA. El ejemplo usa RestTemplate en lugar de WebClient. No utiliza acceso a DB para recuperar información de las clases modelo.