Las limitaciones de las aplicaciones monolíticas, como ser mayor tiempo de desarrollo, dificultad para ser escaladas y el hecho de que los errores individuales pueden afectar la disponibilidad de toda la aplicación llevaron a la creación de la arquitectura de microservicios.
¿Qué son los Microservicios?
Los microservicios son un enfoque arquitectónico y organizativo para el desarrollo de software en el que el software se compone de pequeños servicios independientes que se comunican a través de APIs bien definidas. Las arquitecturas de microservicios hacen que las aplicaciones sean más fáciles de escalar y más rápidas de desarrollar, lo que permite la innovación y acelera el tiempo de comercialización de nuevas funciones. Los microservicios presentan varias ventajas en comparación con las aplicaciones monolíticas:
- Facilidad de despliegue: Mientras que el despliegue de una aplicación monolítica es simple y no requiere orquestación ni gestión con otros componentes o aplicaciones, los microservicios permiten desplegar componentes de forma independiente, lo que facilita la actualización y mantenimiento de la aplicación.
- Escalabilidad y autonomía: Cada servicio que compone una arquitectura basada en microservicios puede desarrollarse, implementarse y escalar sin afectar el funcionamiento del resto de servicios, lo que brinda una mayor autonomía y flexibilidad en el desarrollo y mantenimiento de la aplicación.
- Equipos multifuncionales y autónomos: Los microservicios permiten la formación de equipos multifuncionales sin demasiado esfuerzo, ya que cada servicio es un elemento independiente, lo que facilita la gestión y el desarrollo de la aplicación.
- Mayor flexibilidad en cambios y actualizaciones: Dado que cada microservicio es independiente y puede desarrollarse, desplegarse y escalarse por separado de los demás, es más fácil realizar cambios en la aplicación sin afectar otras partes de la misma.
Crear el microservicio
En esta sección veremos los pasos necesarios para crear microservicios Spring Boot Java, desplegarlo en el servidor Apache Tomcat y acceder a los endpoints con un cliente REST. En el ejemplo desarrollaremos un microservicio que implementa un CRUD de clientes mediante las operaciones GET, POST, PUT y DELETE.
A grandes rasgos la aplicación estará compuesta por:
- Repositorio y entidades: se crearán entidades para representar el modelo de datos y estarán anotadas con anotaciones JPA para especificar el esquema de la base de datos. Se creará una interfaz de repositorio para extender la interfaz JpaRepository de Spring Data JPA para realizar operaciones CRUD.
- Capa de servicios: Se implementarán clases de servicio para contener la lógica de negocio del microservicio. Estas clases interactúan con la capa del repositorio para recuperar, manipular y persistir datos.
- Capa de controllers: Implementaremos un controlador para manejar solicitudes HTTP entrantes y asignarlas a los métodos que correspondan. Utilizaremos anotaciones como @RestController y @RequestMapping para definir los endpoints REST y los métodos HTTP.
Paso 1: crear microservicios con Spring Boot Generando el proyecto con Spring Web starter
Para generar la estructura básica de la aplicación con las dependencias necesarias:
a) acceder a la página de Spring Initializr
b) En cada sección de la página seleccionar/ingresar los valores que se indican a continuación:
- Project: Maven Project
- Language: Java
- Spring Boot: 2.6.7 (default)
Project Metadata
- Group: com.jcodepoint
- Artifact: customer-service
- Name: customer-service
- Package name: com.jcodepoint.customer-service
- Packaging: Jar
- Java: 11
Dependencias
- Clic en el botón ADD DEPENDENCIES…
- En el diálogo de dependencias buscar y agregar las siguientes dependencias:
- Spring Web
- Spring Data JPA
- H2 Database
- Lombok
Estas opciones incluirán automáticamente las siguientes dependencias en el archivo pom.xml:
<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>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
La dependencia spring-boot-starter-web incluye características como configuración automática para desarrollo web, integración con contenedores como Tomcat y soporte para crear API REST. Esta dependencia es parte de Spring Boot Starters, que son descriptores de dependencia que simplifican la gestión de dependencias en un proyecto.
c) Hacer click en GENERATE. Se descargará un archivo .zip con el proyecto generado.
Paso 2: importar el proyecto en Eclipse
En la carpeta de descargas tendríamos que encontrar el archivo customer-service.zip, que contiene todos los archivos del proyecto. Descomprimir este archivo en la carpeta de nuestro workspace.
La estructura del proyecto generado es la siguiente:
La herramienta ha generado la estructura del proyecto y los archivos necesarios para el inicio de la aplicación. Estos archivos son:
- pom.xml (Project Object Model): archivo de Maven que contiene información esencial sobre el proyecto.
- CustomerServiceApplication.java: es la clase principal del proyecto. Contiene el método main() y está marcada con la anotación @SpringBootApplication.
- CustomerServiceApplicationTests.java
- application.properties
Ahora debemos generar los archivos de proyecto para Eclipse. Desde una terminal y ubicados en el directorio raíz del proyecto ejecutar el siguiente comando:
mvn eclipse:eclipse
Hacer click derecho en el workspace de Eclipse para desplegar el menú contextual y seleccionar la opción Import…
En el diálogo que se despliega seleccionar Existing Projects into Workspace (dentro de la carpeta General) y luego click en Next:
En el siguiente diálogo, en la opción Select root directory seleccionar la carpeta raíz del proyecto. Opcionalmente, también se puede seleccionar el Working set para el proyecto:
Finalizada la importación del proyecto, se debería ver en el workspace de Eclipse como sigue:
Paso 3: implementar el API de Clientes
Nuestro ejemplo consistirá en un servicio con cuatro endpoints para implementar un CRUD de clientes para lo cual crearemos en primer lugar la clase CustomerDto como entidad cliente en nuestro modelo:
package com.jcodepoint.customerservice.dto;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.jcodepoint.customerservice.model.Customer;
public class CustomerDto implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("id")
private Integer id;
@JsonProperty("first_name")
private String firstName;
@JsonProperty("last_name")
private String lastName;
@JsonProperty("location")
private String location;
public CustomerDto() {
}
public CustomerDto(Customer customer) {
if (customer != null) {
this.setId(customer.getId());
this.setFirstName(customer.getFirstName());
this.setLastName(customer.getLastName());
this.setLocation(customer.getLocation());
}
}
// Getter & Setters
}
La anotación @JsonProperty se usa en Java con la biblioteca Jackson para especificar el nombre de la propiedad en un objeto JSON al serializar o deserializar un objeto Java. Es particularmente útil cuando el nombre de la propiedad JSON difiere del nombre del campo en el objeto Java o cuando el nombre de la propiedad JSON no está en camelCase.
Implementación del controller
En el mundo de las API y los microservicios de Restful, un controlador lleva a cabo el manejo de las solicitudes HTTP entrantes y en la provisión de respuestas adecuadas. Específicamente, la anotación @RestController en Spring Framework se utiliza para designar una clase como controlador, lo que significa que es capaz de manejar peticiones web. Cuando llega una solicitud, el controlador la procesa, interactúa con los datos necesarios y devuelve la respuesta adecuada.
Implementaremos la clase CustomerController para nuestro servicio RESTful:
package com.jcodepoint.customerservice.controller;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.jcodepoint.customerservice.dto.CustomerDto;
import com.jcodepoint.customerservice.service.CustomerService;
@RestController
public class CustomerController {
@Autowired
private CustomerService customerService;
//Implementación del método GET
//Implementación del método POST
//Implementación del método PUT
//Implementación del método DELETE
}
Método GET
Una operación GET es un tipo de petición HTTP que se utiliza para recuperar datos de un recurso específico. Se utiliza comúnmente para recuperar datos de un servidor o base de datos sin alterar el estado del recurso.
El siguiente código muestra la implementación de nuestra operación GET:
@GetMapping("/customers")
public ResponseEntity<?> getCustomers() {
List<CustomerDto> c = this.customerService.getAllCustomers();
if (c.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "No objects found"));
} else {
return ResponseEntity.ok().body(c);
}
}
La operación GET sigue las convenciones RESTful mediante el uso de la anotación @GetMapping para publicar el endpoint, utiliza la clase ResponseEntity para construir la respuesta y maneja la recuperación de datos del cliente desde la clase CustomerService.
El uso de ResponseEntity aporta flexibilidad en la construcción de diferentes tipos de respuestas según el resultado de la operación, como manejar resultados vacíos con una respuesta de petición incorrecta (BAD REQUEST) y resultados exitosos (OK) con una respuesta correcta.
Método POST
Una operación POST se refiere al método HTTP utilizado para crear un nuevo recurso. Cuando un cliente envía una solicitud POST a un servidor, normalmente incluye datos en el cuerpo de la solicitud, que luego el servidor procesa para crear un nuevo recurso. Esta operación se usa comúnmente para agregar nuevos datos, como crear un nuevo usuario, publicar un mensaje o agregar un elemento a una colección. En el contexto de los microservicios Spring Boot, la anotación @PostMapping se usa para definir un método que maneja solicitudes POST, y la anotación @RequestBody se usa para vincular el cuerpo de la solicitud a un parámetro del método, lo que permite al servidor procesar los datos entrantes y crear el nuevo recurso.
@PostMapping("/customers")
public ResponseEntity<?> addCustomer(@RequestBody CustomerDto customer) {
CustomerDto c = this.customerService.saveCustomer(customer);
if (c != null) {
return ResponseEntity.ok().build();
} else {
return ResponseEntity.badRequest().body(Map.of("message", "Object already exists"));
}
}
Método PUT
Una operación PUT es un método HTTP que se utiliza para actualizar un recurso existente o crear un nuevo recurso si no existe. Es idempotente, lo que significa que varias solicitudes idénticas deberían tener el mismo efecto que una sola solicitud. En el contexto de un microservicio Spring Boot, la anotación @PutMapping se utiliza para asignar solicitudes HTTP PUT a métodos de controlador específicos, lo que permite la actualización de recursos según el cuerpo de la solicitud proporcionado con @RequestBody y las variables de ruta, anotadas con @PathVariable.
@PutMapping("/customers/{id}")
public ResponseEntity<?> updateCustomer(@PathVariable int id, @RequestBody CustomerDto customer) {
CustomerDto c = this.customerService.updateCustomer(id, customer);
if(c != null) {
return ResponseEntity.ok().build();
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Object not found"));
}
}
Método DELETE
Una operación DELETE es un método de solicitud que se utiliza para eliminar un recurso en el servidor. Se utiliza habitualmente en las API RESTful para eliminar un recurso específico identificado mediante un identificador único, normalmente el ID del recurso. Cuando un cliente envía una solicitud DELETE al servidor, indica que el cliente desea eliminar el recurso especificado. Luego, el servidor procesa la solicitud y elimina el recurso si existe, devolviendo una respuesta adecuada al cliente.
@DeleteMapping("/customers/{id}")
public ResponseEntity<?> deleteCustomer(@PathVariable int id) {
this.customerService.deleteCustomer(id);
return ResponseEntity.ok().build();
}
Notar que:
- La anotación @RestController marca a la clase como servicio RESTful. La anotación @RestController simplifica la implementación de servicios web RESTful al combinar las funcionalidades de @Controller y @ResponseBody, lo que permite la serialización automática del objeto devuelto en HttpResponse y maneja varias API REST como solicitudes GET, POST, DELETE y PUT.
- La clase cuenta con cuatro métodos para implementar operaciones GET, POST, PUT y DELETE para consulta y alta, modificación y eliminación, respectivamente.
Implementación de la capa de servicios
La clase CustomerService en la arquitectura de microservicio Spring Boot actúa como intermediaria entre el controlador y la capa de acceso a datos (DAO). Encapsula la lógica de negocio de la aplicación, controla las transacciones y coordina las respuestas en la implementación de sus operaciones. Esta clase proporciona métodos para guardar, actualizar, recuperar y eliminar datos del cliente, asegurando la interacción adecuada con la base de datos a través de la clase CustomerRepository
. Siguiendo este patrón arquitectónico, la clase CustomerService separa efectivamente las funciones del controlador y la capa de acceso a datos, promoviendo la modularidad y la mantenibilidad dentro de la aplicación.
package com.jcodepoint.customerservice.service;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.jcodepoint.customerservice.dao.CustomerRepository;
import com.jcodepoint.customerservice.dto.CustomerDto;
import com.jcodepoint.customerservice.model.Customer;
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
public CustomerDto saveCustomer(CustomerDto customer) {
CustomerDto cdto = null;
if(!this.customerRepository.existsById(customer.getId())) {
Customer c = this.customerRepository.save(new Customer(customer));
cdto = new CustomerDto(c);
}
return cdto;
}
public CustomerDto updateCustomer(Integer id, CustomerDto customer) {
CustomerDto cdto = null;
if (this.customerRepository.existsById(id)) {
Customer c = this.customerRepository.save(new Customer(customer));
cdto = new CustomerDto(c);
}
return cdto;
}
public List<CustomerDto> getAllCustomers(){
List<Customer> result =
this.customerRepository.findAll();
List<CustomerDto> l = result.stream()
.map(customer -> new CustomerDto(customer))
.collect(Collectors.toList());
return l;
}
public void deleteCustomer(Integer id) {
this.customerRepository.deleteById(id);
}
}
Implementación de la DAO
La interfaz CustomerRepository sirve como capa de acceso a datos (DAO) en la arquitectura del microservicio. Es responsable de abstraer las interacciones con la base de datos subyacente, proporcionando métodos para operaciones CRUD relacionadas con la entidad Customer. Al extender la interfaz JpaRepository, hereda capacidades de manipulación y acceso a bases de datos. Esta interfaz actúa como un puente entre la capa de servicio y el almacenamiento de datos subyacente, encapsulando consultas y operaciones de la base de datos. Su uso de la anotación @Repository denota su función en la gestión del acceso y la persistencia de la base de datos dentro del contexto de la aplicación Spring.
package com.jcodepoint.customerservice.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.jcodepoint.customerservice.model.Customer;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Integer> {
}
La entidad JPA
La clase Customer representa la entidad en el modelo de datos del microservicio y sirve como estructura de datos persistente en la base de datos. Con la anotación @Entity, se designa como una entidad JPA, lo que permite que sea administrada por el contexto de persistencia. La clase incorpora los atributos esenciales del cliente, como identificación, nombre, apellido y ubicación. El constructor facilita la conversión de un objeto CustomerDto a una entidad Cliente, promoviendo la separación de funciones y la encapsulación. Junto con la capa DAO, esta clase forma la base para el acceso y la gestión de datos dentro de la arquitectura del microservicio.
package com.jcodepoint.customerservice.model;
import javax.persistence.Entity;
import javax.persistence.Id;
import com.jcodepoint.customerservice.dto.CustomerDto;
@Entity
public class Customer {
@Id
private Integer id;
private String firstName;
private String lastName;
private String location;
public Customer() {
}
public Customer(CustomerDto customer) {
this.setId(customer.getId());
this.setFirstName(customer.getFirstName());
this.setLastName(customer.getLastName());
this.setLocation(customer.getLocation());
}
//Getters & Setters
}
Configuración de la aplicación
La clase DbConfig
en el microservicio Spring Boot proporcionado es una clase de configuración responsable de instalar y configurar la capa de acceso a datos de la aplicación. Desde un punto de vista arquitectónico, esta clase realiza varias funciones clave:
- Configuración de fuente de datos: La clase define un método anotado con
@Bean
que configura y proporciona la fuente de datos para la aplicación. Utiliza las propiedades definidas en el archivopersistence.properties
para configurar los detalles de conexión necesarios para la base de datos. - Inyección de dependencia del entorno: Inyecta el entorno de la aplicación en la clase usando la anotación
@Autowired
, permitiendo el acceso a las propiedades y configuración del entorno. - Configuración del repositorio JPA: Habilita repositorios JPA para el paquete base especificado, permitiendo que la aplicación interactúe con la base de datos usando Spring Data JPA.
- Gestión de transacciones: Permite la gestión de transacciones de la aplicación, asegurando el manejo adecuado de las transacciones de la base de datos dentro de la capa de servicio.
package com.jcodepoint.customerservice;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableJpaRepositories(basePackages = "com.jcodepoint.customerservice.dao")
@PropertySource("persistence.properties")
@EnableTransactionManagement
public class DbConfig {
@Autowired
private Environment env;
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.user"));
dataSource.setPassword(env.getProperty("jdbc.pass"));
return dataSource;
}
}
Se utiliza la anotación @PropertySource para hacer referencia al archivo «persistence.properties», que contiene pares clave-valor para las propiedades de conexión JDBC. Las propiedades especifican el nombre de la clase del controlador, la URL de la base de datos, el nombre de usuario y la contraseña para la base de datos en memoria H2. Además, las propiedades de Hibernate definen el dialecto como H2 y configuran hbm2ddl.auto con el valor ‘create’, lo que indica que Hibernate debe crear el esquema de la base de datos al inicio. Esta configuración permite que la API REST interactúe con la base de datos H2 sin problemas, preparándola para operaciones de persistencia de datos.
jdbc.driverClassName=org.h2.Driver
jdbc.url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1
jdbc.user=tutorialuser
jdbc.pass=tutorialpass
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=create
Paso 4: ejecutar y consumir el microservicio
A los efectos de este ejemplo vamos a desplegar el microservicio en nuestro ambiente de desarrollo y a acceder a los endpoints utilizando un cliente REST.
Para levantar la aplicación ejecutamos el siguiente comando de Maven desde la terminal:
mvn spring-boot:run
El anterior comando desplegará nuestra aplicación en el servidor Apache Tomcat embebido.
Probar el método POST
En Postman, seguir los siguientes pasos:
- Crear una nueva petición:
- Haz clic en «Nuevo» y selecciona «Solicitud HTTP».
- Configura el método HTTP como POST.
- Ingresa la URL del web method, en este caso, localhost:8080/customers.
- Agrega los parámetros del cuerpo de la petición en formato JSON.
- Enviar la petición:
- Haz clic en el botón Enviar para realizar la llamada al API.
- Verificar la respuesta:
- En la parte inferior, se mostrará el panel de respuesta donde podrás verificar si el código de respuesta es satisfactorio (200 OK).
Probar el método GET
- Crear una nueva petición:
- Haz clic en «Nuevo» y selecciona «Solicitud HTTP».
- Configura el método HTTP como GET.
- Ingresa la URL del web method, en este caso, localhost:8080/customers.
- Enviar la petición:
- Haz clic en el botón Enviar para realizar la llamada al API.
- Verificar la respuesta:
- En la parte inferior, se mostrará el panel de respuesta donde podrás verificar si el código de respuesta es satisfactorio (200 OK), y el registro cargado anteriormente.
Probar el método PUT
- Crear una nueva petición:
- Haz clic en «Nuevo» y selecciona «Solicitud HTTP».
- Configura el método HTTP como PUT.
- Ingresa la URL del web method, en este caso, localhost:8080/customers/1.
- Agrega los parámetros del cuerpo de la petición en formato JSON.
- Enviar la petición:
- Haz clic en el botón Enviar para realizar la llamada al API.
- Verificar la respuesta:
- En la parte inferior, se mostrará el panel de respuesta donde podrás verificar si el código de respuesta es satisfactorio (200 OK).
Probar el método DELETE
- Crear una nueva petición:
- Haz clic en «Nuevo» y selecciona «Solicitud HTTP».
- Configura el método HTTP como DELETE.
- Ingresa la URL del web method, en este caso, localhost:8080/customers/1.
- Enviar la petición:
- Haz clic en el botón Enviar para realizar la llamada al API.
- Verificar la respuesta:
- En la parte inferior, se mostrará el panel de respuesta donde podrás verificar si el código de respuesta es satisfactorio (200 OK).
Conclusión
En este artículo hemos visto los pasos necesarios para crear un API REST. El siguiente paso es documentar nuestra API para facilitar el acceso a los usuarios.
Además, se debe considerar agregarle seguridad para restringir el acceso a los endpoints en caso de que el API sea publicada en Internet. Esto lo haremos con JSon Web Token (JWT).