# Construyendo servicios REST con Spring REST se ha convertido rápidamente en el estándar de facto para crear servicios web en la web porque son fáciles de construir y de consumir. Hay una discusión mucho más amplia sobre cómo REST encaja en el mundo de los microservicios, pero, para este tutorial, veamos cómo construir servicios RESTful. ¿Por qué descansar? REST adopta los preceptos de la web, incluida su arquitectura, beneficios y todo lo demás. Esto no es sorprendente dado que su autor, Roy Fielding, estuvo involucrado en probablemente una docena de especificaciones que rigen cómo funciona la web. Que beneficios La web y su protocolo central, HTTP, proporcionan una pila de características: - Acciones adecuadas ( `GET`, `POST`, `PUT`, `DELETE`, ...) - Almacenamiento en caché - Redirección y reenvío - Seguridad (encriptación y autenticación) Todos estos son factores críticos en la construcción de servicios resilientes. Pero eso no es todo. La web está construida con muchas especificaciones pequeñas, por lo tanto, ha podido evolucionar fácilmente, sin atascarse en las "guerras de estándares". Los desarrolladores pueden recurrir a kits de herramientas de terceros que implementan estas diversas especificaciones y al instante tienen la tecnología del cliente y del servidor a su alcance. Entonces, basándose en HTTP, las API REST proporcionan los medios para construir API flexibles que pueden: - Soporta compatibilidad con versiones anteriores - API evolucionables - Servicios escalables - Servicios asegurables - Un espectro de servicios sin estado a estado Lo importante es darse cuenta de que REST, aunque ubicuo, no es un estándar, *per se* , sino un enfoque, un estilo, un conjunto de *restricciones* en su arquitectura que puede ayudarlo a construir sistemas a escala web. En este tutorial usaremos el portafolio de Spring para construir un servicio RESTful mientras aprovechamos las características sin pila de REST. ## Empezando Mientras trabajamos en este tutorial, usaremos [Spring Boot](https://spring.io/projects/spring-boot) . Vaya a [Spring Initializr](https://start.spring.io/) y seleccione lo siguiente: - Web - PSD - H2 - Lombok Luego elija "Generar proyecto". A `.zip`se descargará. Descomprimirlo. Dentro encontrará un proyecto simple basado en Maven que incluye un `pom.xml`archivo de compilación (NOTA: *puede* usar Gradle. Los ejemplos en este tutorial estarán basados en Maven). Spring Boot puede funcionar con cualquier IDE. Puede usar Eclipse, IntelliJ IDEA, Netbeans, etc. [Spring Tool Suite](https://spring.io/tools/) es una distribución IDE de código abierto basada en Eclipse que proporciona un superconjunto de la distribución Java EE de Eclipse. Incluye características que hacen que trabajar con aplicaciones Spring sea aún más fácil. De ninguna manera es obligatorio. Pero considérelo si desea ese **empuje** adicional para sus pulsaciones de teclas. Aquí hay un video que muestra cómo comenzar con STS y Spring Boot. Esta es una introducción general para familiarizarlo con las herramientas. Si elige IntelliJ IDEA como su IDE para este tutorial, debe instalar el complemento lombok. Para ver cómo instalamos complementos en IntelliJ IDEA, eche un vistazo a la [gestión de complementos](https://www.jetbrains.com/help/idea/managing-plugins.html) . Después de esto, debe asegurarse de que la casilla de verificación "Habilitar procesamiento de anotación" esté marcada en: Preferencias → Compilador → Procesadores de anotación, como se describe [https://stackoverflow.com/questions/14866765/building-with-lomboks-slf4j-and- intellij-cannot-find-symbol-log](https://stackoverflow.com/questions/14866765/building-with-lomboks-slf4j-and-intellij-cannot-find-symbol-log) ## La historia hasta ahora ... Comencemos con lo más simple que podemos construir. De hecho, para hacerlo lo más simple posible, incluso podemos dejar de lado los conceptos de REST. (Más adelante, agregaremos REST para comprender la diferencia). Nuestro ejemplo modela un servicio de nómina simple que administra a los empleados de una empresa. En pocas palabras, debe almacenar los objetos de los empleados en una base de datos H2 en memoria y acceder a ellos a través de JPA. Esto se envolverá con una capa Spring MVC para acceder de forma remota. / src main/java/com.manexware.springframework.model/Employee.java ```java package com.manexware.springframework.model; import lombok.Data; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Data @Entity class Employee { private @Id @GeneratedValue Long id; private String name; private String role; Employee() {} Employee(String name, String role) { this.name = name; this.role = role; } } ``` A pesar de ser pequeña, esta clase de Java contiene mucho: - `@Data`es una anotación de Lombok para crear todos los getters, setters, `equals`, `hash`, y `toString`métodos, basados en los campos. - `@Entity` es una anotación JPA para preparar este objeto para el almacenamiento en un almacén de datos basado en JPA. - `id`, `name`y `role`son el atributo de nuestro objeto de dominio, el primero se marca con más anotaciones JPA para indicar que es la clave principal y que el proveedor JPA completa automáticamente. - se crea un constructor personalizado cuando necesitamos crear una nueva instancia, pero aún no tenemos una identificación. Con esta definición de objeto de dominio, ahora podemos recurrir a [Spring Data JPA](https://spring.io/guides/gs/accessing-data-jpa/) para manejar las tediosas interacciones de la base de datos. Los repositorios de Spring Data son interfaces con métodos que admiten la lectura, actualización, eliminación y creación de registros en un almacén de datos back-end. Algunos repositorios también admiten paginación y clasificación de datos, según corresponda. Spring Data sintetiza implementaciones basadas en convenciones encontradas en la denominación de los métodos en la interfaz. | | Existen múltiples implementaciones de repositorio además de JPA. Puede usar Spring Data MongoDB, Spring Data GemFire, Spring Data Cassandra, etc. Para este tutorial, nos quedaremos con JPA. | | ---- | ------------------------------------------------------------ | | | | /src/main/java/com.manexware.springframework.repository/EmployeeRepository.java ```java package com.manexware.springframework.repository; import org.springframework.data.jpa.repository.JpaRepository; interface EmployeeRepository extends JpaRepository { } ``` Esta interfaz extiende los JPA de Spring Data `JpaRepository`, especificando el tipo de dominio como `Employee`y el tipo de identificación como `Long`. Esta interfaz, aunque vacía en la superficie, tiene un gran impacto dado que admite: - Crear nuevas instancias - Actualización de las existentes - Borrado - Encontrar (uno, todos, por propiedades simples o complejas) La [solución de repositorio de](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories) Spring Data hace posible eludir los detalles del almacén de datos y, en cambio, resuelve la mayoría de los problemas utilizando la terminología específica del dominio. Lo creas o no, ¡esto es suficiente para lanzar una aplicación! Una aplicación Spring Boot es, como mínimo, un `public static void main`punto de entrada y la `@SpringBootApplication`anotación. Esto le dice a Spring Boot que ayude, siempre que sea posible. nonrest / src / main / java / nómina / PayrollApplication.java ```java package payroll; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class PayrollApplication { public static void main(String... args) { SpringApplication.run(PayrollApplication.class, args); } }COPIAR ``` `@SpringBootApplication`es una metaanotación que incluye **escaneo de componentes** , **configuración automática** y **soporte de propiedades** . No profundizaremos en los detalles de Spring Boot en este tutorial, pero en esencia, activará un contenedor de servlets y servirá nuestro servicio. Sin embargo, una aplicación sin datos no es muy interesante, así que precarguemosla. La clase siguiente se cargará automáticamente por Spring: nonrest / src / main / java / nómina / LoadDatabase.java ```java package payroll; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @Slf4j class LoadDatabase { @Bean CommandLineRunner initDatabase(EmployeeRepository repository) { return args -> { log.info("Preloading " + repository.save(new Employee("Bilbo Baggins", "burglar"))); log.info("Preloading " + repository.save(new Employee("Frodo Baggins", "thief"))); }; } }COPIAR ``` ¿Qué sucede cuando se carga? - Spring Boot ejecutará TODOS los `CommandLineRunner`beans una vez que se cargue el contexto de la aplicación. - Este corredor solicitará una copia del `EmployeeRepository`que acaba de crear. - Al usarlo, creará dos entidades y las almacenará. - `@Slf4j`es una anotación de Lombok a autocreate una red basada en SLF4J `LoggerFactory`como `log`, lo que nos permite registrar estos recién creados "empleados". Haz clic derecho y **Ejecutar** `PayRollApplication` , y esto es lo que obtienes: Fragmento de la salida de la consola que muestra la precarga de datos ``` ... 2018-08-09 11: 36: 26.169 INFO 74611 --- [main] nómina.LoadDatabase: Preloading Employee (id = 1, name = Bilbo Baggins, role = robo) 2018-08-09 11: 36: 26.174 INFO 74611 --- [main] nómina.LoadDatabase: Preloading Employee (id = 2, name = Frodo Baggins, role = thief) ... ``` Este no es **todo el** registro, sino solo los bits clave de la precarga de datos. (De hecho, echa un vistazo a toda la consola. Es glorioso). ## HTTP es la plataforma Para envolver su repositorio con una capa web, debe recurrir a Spring MVC. Gracias a Spring Boot, hay poca infraestructura para codificar. En cambio, podemos centrarnos en acciones: nonrest / src / main / java / nómina / EmployeeController.java ```java package payroll; import java.util.List; 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; @RestController class EmployeeController { private final EmployeeRepository repository; EmployeeController(EmployeeRepository repository) { this.repository = repository; } // Aggregate root @GetMapping("/employees") List all() { return repository.findAll(); } @PostMapping("/employees") Employee newEmployee(@RequestBody Employee newEmployee) { return repository.save(newEmployee); } // Single item @GetMapping("/employees/{id}") Employee one(@PathVariable Long id) { return repository.findById(id) .orElseThrow(() -> new EmployeeNotFoundException(id)); } @PutMapping("/employees/{id}") Employee replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) { return repository.findById(id) .map(employee -> { employee.setName(newEmployee.getName()); employee.setRole(newEmployee.getRole()); return repository.save(employee); }) .orElseGet(() -> { newEmployee.setId(id); return repository.save(newEmployee); }); } @DeleteMapping("/employees/{id}") void deleteEmployee(@PathVariable Long id) { repository.deleteById(id); } }COPIAR ``` - `@RestController` indica que los datos devueltos por cada método se escribirán directamente en el cuerpo de la respuesta en lugar de representar una plantilla. - An `EmployeeRepository`es inyectado por el constructor en el controlador. - Tenemos rutas para cada operación ( `@GetMapping`, `@PostMapping`, `@PutMapping`y `@DeleteMapping`, lo que corresponde a HTTP `GET`, `POST`, `PUT`, y `DELETE`llamadas). (NOTA: es útil leer cada método y comprender lo que hacen). - `EmployeeNotFoundException` es una excepción utilizada para indicar cuándo se busca un empleado pero no se lo encuentra. nonrest / src / main / java / nómina / EmployeeNotFoundException.java ```java package payroll; class EmployeeNotFoundException extends RuntimeException { EmployeeNotFoundException(Long id) { super("Could not find employee " + id); } }COPIAR ``` Cuando `EmployeeNotFoundException`se lanza un, este tidbit adicional de la configuración Spring MVC se usa para representar un **HTTP 404** : nonrest / src / main / java / nómina / EmployeeNotFoundAdvice.java ```java package payroll; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; @ControllerAdvice class EmployeeNotFoundAdvice { @ResponseBody @ExceptionHandler(EmployeeNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) String employeeNotFoundHandler(EmployeeNotFoundException ex) { return ex.getMessage(); } }COPIAR ``` - `@ResponseBody` indica que este consejo se presenta directamente en el cuerpo de respuesta. - `@ExceptionHandler`configura el consejo para responder solo si `EmployeeNotFoundException`se arroja un. - `@ResponseStatus`dice emitir un `HttpStatus.NOT_FOUND`, es decir, un **HTTP 404** . - El cuerpo del consejo genera el contenido. En este caso, da el mensaje de la excepción. Para iniciar la aplicación, haga clic derecho `public static void main`en `PayRollApplication`y seleccione **Ejecutar** desde su IDE o: Spring Initializr usa envoltura maven, así que escriba esto: ``` $ ./mvnw clean spring-boot: ejecutar ``` Alternativamente, utilizando la versión de Maven instalada, escriba esto: ``` $ mvn clean spring-boot: ejecutar ``` Cuando se inicia la aplicación, podemos interrogarla de inmediato. ``` $ curl -v localhost: 8080 / empleados ``` Esto producirá: ``` * Intentando :: 1 ... * Conjunto TCP_NODELAY * Conectado al puerto localhost (:: 1) 8080 (# 0) > GET / empleados HTTP / 1.1 > Anfitrión: localhost: 8080 > Usuario-Agente: curl / 7.54.0 > Aceptar: * / * > GET / empleados / 99 HTTP / 1.1 > Anfitrión: localhost: 8080 > Usuario-Agente: curl / 7.54.0 > Aceptar: * / * > org.springframework.boot spring-boot-starter-hateoas COPIAR ``` Esta pequeña biblioteca nos dará las construcciones para definir un servicio RESTful y luego renderizarlo en un formato aceptable para el consumo del cliente. Un ingrediente crítico para cualquier servicio RESTful es agregar [enlaces](https://tools.ietf.org/html/rfc5988) a operaciones relevantes. Para hacer que su controlador sea más RESTful, agregue enlaces como este: Obtener un recurso de un solo elemento ```java @GetMapping("/employees/{id}") Resource one(@PathVariable Long id) { Employee employee = repository.findById(id) .orElseThrow(() -> new EmployeeNotFoundException(id)); return new Resource<>(employee, linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel(), linkTo(methodOn(EmployeeController.class).all()).withRel("employees")); }COPIAR ``` Declaraciones de importación relevantes ```java import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; import org.springframework.hateoas.Resource; import org.springframework.hateoas.Resources;COPIAR ``` Esto es muy similar a lo que teníamos antes, pero algunas cosas han cambiado: - El tipo de retorno del método ha cambiado de `Employee`a `Resource`. `Resource`es un contenedor genérico de Spring HATEOAS que incluye no solo los datos sino también una colección de enlaces. - `linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel()`pide que Spring HATEOAS cree un enlace al método de `EmployeeController`'s `one()`y lo marque como un enlace [propio](https://www.iana.org/assignments/link-relations/link-relations.xhtml) . - `linkTo(methodOn(EmployeeController.class).all()).withRel("employees")`le pide a Spring HATEOAS que cree un enlace a la raíz agregada `all()`y lo llame "empleados". ¿Qué queremos decir con "construir un enlace"? Uno de los tipos principales de Spring HATEOAS es `Link`. Incluye un **URI** y un **rel** (relación). Los enlaces son los que potencian la web. Antes de la World Wide Web, otros sistemas de documentos mostraban información o enlaces, pero era la vinculación de documentos CON datos lo que unía la web. Roy Fielding fomenta la creación de API con las mismas técnicas que hicieron que la web sea exitosa, y los enlaces son una de ellas. Si reinicia la aplicación y consulta el registro de empleados de **Bilbo** , obtendrá una respuesta ligeramente diferente a la anterior: Representación RESTful de un solo empleado ```javascript { "id": 1, "name": "Bilbo Baggins", "role": "burglar", "_links": { "self": { "href": "http://localhost:8080/employees/1" }, "employees": { "href": "http://localhost:8080/employees" } } }COPIAR ``` Esta salida descomprimida muestra no solo los elementos de datos que vio anteriormente ( `id`, `name`y `role`), sino también una `_links`entrada que contiene dos URI. Todo este documento está formateado con [HAL](http://stateless.co/hal_specification.html) . HAL es un peso ligero [MediaType](https://tools.ietf.org/html/draft-kelly-json-hal-08) que permite la codificación no sólo datos, sino también controles hipermedia, alertando a los consumidores a otras partes de la API que pueden desplazarse hacia. En este caso, hay un enlace "propio" (como una `this`declaración en el código) junto con un enlace a la **raíz agregada** . Para hacer que la raíz agregada TAMBIÉN sea más RESTful, desea incluir enlaces de nivel superior y TAMBIÉN incluir componentes RESTful dentro de: Obtener un recurso raíz agregado ```java @GetMapping("/employees") Resources> all() { List> employees = repository.findAll().stream() .map(employee -> new Resource<>(employee, linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(), linkTo(methodOn(EmployeeController.class).all()).withRel("employees"))) .collect(Collectors.toList()); return new Resources<>(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel()); }COPIAR ``` ¡Guauu! ¡Ese método, que solía ser, `repository.findAll()`ha crecido mucho! Vamos a desempacarlo. `Resources<>`es otro contenedor Spring HATEOAS destinado a encapsular colecciones. También, también te permite incluir enlaces. No dejes pasar esa primera declaración. ¿Cuándo significa "encapsular colecciones"? Colecciones de empleados? No exactamente. Como estamos hablando de REST, debería encapsular colecciones de **recursos** de **empleados** . Es por eso que busca a todos los empleados, pero luego los transforma en una lista de `Resource`objetos. (¡Gracias Java 8 Stream API!) Si reinicia la aplicación y busca la raíz agregada, puede ver cómo se ve. RESTful representación de una colección de recursos de empleados ```javascript { "_embedded": { "employeeList": [ { "id": 1, "name": "Bilbo Baggins", "role": "burglar", "_links": { "self": { "href": "http://localhost:8080/employees/1" }, "employees": { "href": "http://localhost:8080/employees" } } }, { "id": 2, "name": "Frodo Baggins", "role": "thief", "_links": { "self": { "href": "http://localhost:8080/employees/2" }, "employees": { "href": "http://localhost:8080/employees" } } } ] }, "_links": { "self": { "href": "http://localhost:8080/employees" } } }COPIAR ``` Para esta raíz agregada, que sirve una colección de recursos de empleados, hay un enlace **"propio" de** nivel superior . La **"colección"** aparece debajo de la sección **"_embedded"** . Así es como HAL representa colecciones. Y cada miembro individual de la colección tiene su información y enlaces relacionados. ¿Cuál es el punto de agregar todos estos enlaces? Permite evolucionar los servicios REST con el tiempo. Los enlaces existentes se pueden mantener mientras se agregan nuevos enlaces en el futuro. Los clientes más nuevos pueden aprovechar los nuevos enlaces, mientras que los clientes heredados pueden mantenerse en los enlaces antiguos. Esto es especialmente útil si los servicios se reubican y se trasladan. Mientras se mantenga la estructura de enlaces, los clientes TODAVÍA pueden encontrar e interactuar con las cosas. ## Simplificando la creación de enlaces ¿Notó la repetición en la creación de enlaces de un solo empleado? El código para proporcionar un enlace único a un empleado, así como un enlace "empleados" a la raíz agregada se mostró dos veces. Si eso te preocupa, ¡bien! Hay una solución En pocas palabras, debe definir una función que convierta `Employee`objetos en `Resource`objetos. Si bien puede codificar fácilmente este método usted mismo, hay beneficios en el futuro de la implementación de la `ResourceAssembler`interfaz de Spring HATEOAS . evolution / src / main / java / nómina / EmployeeResourceAssembler.java ```java package payroll; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceAssembler; import org.springframework.stereotype.Component; @Component class EmployeeResourceAssembler implements ResourceAssembler> { @Override public Resource toResource(Employee employee) { return new Resource<>(employee, linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(), linkTo(methodOn(EmployeeController.class).all()).withRel("employees")); } }COPIAR ``` Esta interfaz simple tiene un método: `toResource()`. Se basa en convertir un objeto que no sea de recursos ( `Employee`) en un objeto basado en recursos ( `Resource`). Todo el código que vio anteriormente en el controlador se puede mover a esta clase. Y al aplicar Spring Framework `@Component`, este componente se creará automáticamente cuando se inicie la aplicación. | | La clase base abstracta de Spring HATEOAS para todos los recursos es `ResourceSupport`. Pero por simplicidad, recomiendo usar `Resource`como mecanismo para envolver fácilmente todos los POJO como recursos. | | ---- | ------------------------------------------------------------ | | | | Para aprovechar este ensamblador, solo tiene que modificarlo `EmployeeController`inyectando el ensamblador en el constructor. Entonces el Inyectando EmployeeResourceAssembler en el controlador ```java @RestController class EmployeeController { private final EmployeeRepository repository; private final EmployeeResourceAssembler assembler; EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler assembler) { this.repository = repository; this.assembler = assembler; } ... }COPIAR ``` Desde aquí, puede usarlo en el método de empleado de un solo elemento: Obteniendo un recurso de un solo elemento usando el ensamblador ```java @GetMapping("/employees/{id}") Resource one(@PathVariable Long id) { Employee employee = repository.findById(id) .orElseThrow(() -> new EmployeeNotFoundException(id)); return assembler.toResource(employee); }COPIAR ``` Este código es casi el mismo, excepto que en lugar de crear la `Resource`instancia aquí, lo delega al ensamblador. Tal vez eso no se parece mucho? Aplicar lo mismo en el método de controlador raíz agregado es más impresionante: Obteniendo recurso raíz agregado usando el ensamblador ```java @GetMapping("/employees") Resources> all() { List> employees = repository.findAll().stream() .map(assembler::toResource) .collect(Collectors.toList()); return new Resources<>(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel()); }COPIAR ``` El código es, nuevamente, casi el mismo, sin embargo, puedes reemplazar toda esa `Resource`lógica de creación `map(assembler::toResource)`. Gracias a las referencias de métodos de Java 8, es muy fácil enchufarlo y simplificar su controlador. | | Un objetivo de diseño clave de Spring HATEOAS es hacer que sea más fácil hacer The Right Thing ™. En este escenario, agregar hipermedia a su servicio sin codificar nada. | | ---- | ------------------------------------------------------------ | | | | ¡En esta etapa, ha creado un controlador Spring MVC REST que realmente produce contenido hipermedia! Los clientes que no hablan HAL pueden ignorar los bits adicionales mientras consumen los datos puros. Los clientes que hablan HAL pueden navegar por su API habilitada. Pero eso no es lo único necesario para construir un servicio verdaderamente RESTful con Spring. ## API REST en evolución Con una biblioteca adicional y algunas líneas de código extra, ha agregado hipermedia a su aplicación. Pero eso no es lo único necesario para que su servicio sea RESTful. Una faceta importante de REST es el hecho de que no es una pila tecnológica ni un estándar único. REST es una colección de restricciones arquitectónicas que, cuando se adoptan, hacen que su aplicación sea mucho más resistente. Un factor clave de resistencia es que cuando realiza actualizaciones a sus servicios, sus clientes no sufren tiempos de inactividad. En los "viejos" días, las actualizaciones eran notorias por romper clientes. En otras palabras, una actualización del servidor requería una actualización del cliente. En la actualidad, las horas o incluso los minutos de tiempo de inactividad dedicados a una actualización pueden costar millones de dólares en ingresos perdidos. Algunas compañías requieren que presente a la gerencia un plan para minimizar el tiempo de inactividad. En el pasado, podía salirse con la actualización a las 2:00 a.m. de un domingo cuando la carga era mínima. Pero en el comercio electrónico actual basado en Internet con clientes internacionales, tales estrategias no son tan efectivas. Los servicios basados en SOAP y los servicios basados en CORBA fueron increíblemente frágiles. Fue difícil implementar un servidor que pudiera admitir clientes antiguos y nuevos. Con las prácticas basadas en REST, es mucho más fácil. Especialmente usando la pila Spring. Imagine este problema de diseño: ha implementado un sistema con este `Employee`registro basado. El sistema es un gran éxito. Has vendido tu sistema a innumerables empresas. De repente, la necesidad de que el nombre de un empleado que se divide en `firstName`y `lastName`surge. UH oh. No pensé en eso. Antes de abrir la `Employee`clase y reemplazar el campo individual `name`con `firstName`y `lastName`, deténgase y piense por un segundo. ¿Eso romperá a algún cliente? ¿Cuánto tiempo llevará actualizarlos? ¿Incluso controlas a todos los clientes que acceden a tus servicios? Tiempo de inactividad = dinero perdido. ¿La gerencia está lista para eso? Hay una vieja estrategia que precede a REST por años. > Nunca elimine una columna en una base de datos. \- Desconocido Siempre puede agregar columnas (campos) a una tabla de base de datos. Pero no te lleves uno. El principio en los servicios RESTful es el mismo. Agregue nuevos campos a sus representaciones JSON, pero no elimine ninguno. Me gusta esto: JSON que admite múltiples clientes ```javascript { "id": 1, "firstName": "Bilbo", "lastName": "Baggins", "role": "burglar", "name": "Bilbo Baggins", "_links": { "self": { "href": "http://localhost:8080/employees/1" }, "employees": { "href": "http://localhost:8080/employees" } } }COPIAR ``` Observe cómo este formato muestra `firstName`, `lastName`Y `name`? Si bien tiene una duplicación de información, el propósito es apoyar a los clientes antiguos y nuevos. Eso significa que puede actualizar el servidor sin requerir que los clientes actualicen al mismo tiempo. Un buen movimiento que debería reducir el tiempo de inactividad. Y no solo debe mostrar esta información tanto en la "forma anterior" como en la "nueva", sino que también debe procesar los datos entrantes en ambos sentidos. ¿Cómo? Simple. Me gusta esto: Registro de empleados que maneja clientes "viejos" y "nuevos" ```java package payroll; import lombok.Data; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Data @Entity class Employee { private @Id @GeneratedValue Long id; private String firstName; private String lastName; private String role; Employee() {} Employee(String firstName, String lastName, String role) { this.firstName = firstName; this.lastName = lastName; this.role = role; } public String getName() { return this.firstName + " " + this.lastName; } public void setName(String name) { String[] parts =name.split(" "); this.firstName = parts[0]; this.lastName = parts[1]; } }COPIAR ``` Esta clase es muy similar a la versión anterior de `Employee`. Repasemos los cambios: - El campo `name`ha sido reemplazado por `firstName`y `lastName`. Lombok generará getters y setters para esos. - Se define un captador "virtual" para la `name`propiedad anterior `getName()`. Utiliza los campos `firstName`y `lastName`para producir un valor. - Un colocador "virtual" de la antigua `name`propiedad también se define, `setName()`. Analiza una cadena entrante y la almacena en los campos adecuados. Por supuesto, CADA cambio a su API es tan simple como dividir una cadena o fusionar dos cadenas. Pero seguramente no es imposible llegar a un conjunto de transformaciones para la mayoría de los escenarios, ¿eh? Otro ajuste fino es garantizar que cada uno de sus métodos REST devuelva una respuesta adecuada. Actualice el método POST de esta manera: POST que maneja solicitudes de clientes "antiguos" y "nuevos" ```java @PostMapping("/employees") ResponseEntity newEmployee(@RequestBody Employee newEmployee) throws URISyntaxException { Resource resource = assembler.toResource(repository.save(newEmployee)); return ResponseEntity .created(new URI(resource.getId().expand().getHref())) .body(resource); }COPIAR ``` - El nuevo `Employee`objeto se guarda como antes. Pero el objeto resultante se envuelve usando el `EmployeeResourceAssembler`. - Spring MVC `ResponseEntity`se usa para crear un mensaje de estado **HTTP 201 Creado** . Este tipo de respuesta generalmente incluye un encabezado de respuesta de **ubicación** , y usamos el enlace recién formado. - Además, devuelva la versión basada en recursos del objeto guardado. Con este ajuste, puede usar el mismo punto final para crear un nuevo recurso de empleado y usar el `name`campo heredado : ``` $ curl -v -X POST localhost: 8080 / empleados -H 'Tipo de contenido: application / json' -d '{"name": "Samwise Gamgee", "role": "gardener"}' ``` La salida se muestra a continuación: ``` > POST / empleados HTTP / 1.1 > Anfitrión: localhost: 8080 > Usuario-Agente: curl / 7.54.0 > Aceptar: * / * > Tipo de contenido: application / json > Contenido-Longitud: 46 > replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) throws URISyntaxException { Employee updatedEmployee = repository.findById(id) .map(employee -> { employee.setName(newEmployee.getName()); employee.setRole(newEmployee.getRole()); return repository.save(employee); }) .orElseGet(() -> { newEmployee.setId(id); return repository.save(newEmployee); }); Resource resource = assembler.toResource(updatedEmployee); return ResponseEntity .created(new URI(resource.getId().expand().getHref())) .body(resource); }COPIAR ``` El `Employee`objeto construido a partir de la `save()`operación se envuelve usando `EmployeeResourceAssembler`un `Resource`objeto. Como queremos un código de respuesta HTTP más detallado que **200 OK** , usaremos el `ResponseEntity`contenedor Spring MVC . Tiene un método estático útil `created()`donde podemos conectar el URI del recurso. Al agarrar el `resource`puede obtener su enlace "self" a través de la `getId()`llamada al método. Este método produce un `Link`que puede convertir en Java `URI`. Para atar bien las cosas, se inyecta `resource`el `body()`método en sí mismo . | | En REST, la **identificación de** un recurso es el URI de ese recurso. Por lo tanto, Spring HATEOAS no le entrega el `id`campo del tipo de datos subyacente (que ningún cliente debería), sino el URI correspondiente. Y no confundir `ResourceSupport.getId()`con `Employee.getId()`. | | ---- | ------------------------------------------------------------ | | | | Es discutible si **HTTP 201 Created** lleva la semántica correcta ya que no estamos necesariamente "creando" un nuevo recurso. Pero viene precargado con un encabezado de respuesta de **ubicación** , así que ejecútelo. ``` $ curl -v -X PUT localhost: 8080 / employee / 3 -H 'Content-Type: application / json' -d '{"name": "Samwise Gamgee", "role": "ring bearer"}' * Conjunto TCP_NODELAY * Conectado al puerto localhost (:: 1) 8080 (# 0) > PUT / empleados / 3 HTTP / 1.1 > Anfitrión: localhost: 8080 > Usuario-Agente: curl / 7.54.0 > Aceptar: * / * > Tipo de contenido: application / json > Contenido-Longitud: 49 > deleteEmployee(@PathVariable Long id) { repository.deleteById(id); return ResponseEntity.noContent().build(); }COPIAR ``` Esto devuelve una respuesta **HTTP 204 Sin contenido** . ``` $ curl -v -X BORRAR localhost: 8080 / empleados / 1 * Conjunto TCP_NODELAY * Conectado al puerto localhost (:: 1) 8080 (# 0) > BORRAR / empleados / 1 HTTP / 1.1 > Anfitrión: localhost: 8080 > Usuario-Agente: curl / 7.54.0 > Aceptar: * / * > { }COPIAR ``` Con esto en su lugar, ahora puede definir un básico `OrderController`: enlaces / src / main / java / nómina / OrderController.java ```java @RestController class OrderController { private final OrderRepository orderRepository; private final OrderResourceAssembler assembler; OrderController(OrderRepository orderRepository, OrderResourceAssembler assembler) { this.orderRepository = orderRepository; this.assembler = assembler; } @GetMapping("/orders") Resources> all() { List> orders = orderRepository.findAll().stream() .map(assembler::toResource) .collect(Collectors.toList()); return new Resources<>(orders, linkTo(methodOn(OrderController.class).all()).withSelfRel()); } @GetMapping("/orders/{id}") Resource one(@PathVariable Long id) { return assembler.toResource( orderRepository.findById(id) .orElseThrow(() -> new OrderNotFoundException(id))); } @PostMapping("/orders") ResponseEntity> newOrder(@RequestBody Order order) { order.setStatus(Status.IN_PROGRESS); Order newOrder = orderRepository.save(order); return ResponseEntity .created(linkTo(methodOn(OrderController.class).one(newOrder.getId())).toUri()) .body(assembler.toResource(newOrder)); } }COPIAR ``` - Contiene la misma configuración de controlador REST que los controladores que ha construido hasta ahora. - Inyecta tanto un `OrderRepository`como un (aún no construido) `OrderResourceAssembler`. - Las dos primeras rutas Spring MVC manejan la raíz agregada, así como una `Order`solicitud de recurso de un solo elemento . - La tercera ruta Spring MVC maneja la creación de nuevos pedidos, al iniciarlos en el `IN_PROGRESS`estado. - Todos los métodos de controlador devuelven una de las `ResourceSupport`subclases de Spring HATEOAS para representar adecuadamente hipermedia (o un contenedor alrededor de ese tipo). Antes de construir el `OrderResourceAssembler`, discutamos lo que debe suceder. Usted está modelando el flujo de estados entre `Status.IN_PROGRESS`, `Status.COMPLETED`y `Status.CANCELLED`. Una cosa natural al entregar tales datos a los clientes es dejar que los clientes tomen una decisión sobre lo que pueden hacer en función de esta carga útil. Pero eso estaria mal. ¿Qué sucede cuando introduce un nuevo estado en este flujo? La colocación de varios botones en la interfaz de usuario probablemente sería errónea. ¿Qué sucede si cambiaste el nombre de cada estado, tal vez mientras codificabas el soporte internacional y mostrabas texto específico de la localidad para cada estado? Eso probablemente rompería a todos los clientes. Ingrese **HATEOAS** o **Hypermedia como el motor del estado de la aplicación** . En lugar de que los clientes analicen la carga útil, bríndeles enlaces para señalar acciones válidas. Desacoplar acciones basadas en estado de la carga útil de datos. En otras palabras, cuando **CANCELAR** y **COMPLETAR** son acciones válidas, agréguelas dinámicamente a la lista de enlaces. Los clientes solo necesitan mostrar a los usuarios los botones correspondientes cuando existen los enlaces. Esto separa a los clientes de tener que saber CUANDO tales acciones son válidas, reduciendo el riesgo de que el servidor y sus clientes se desincronicen en la lógica de las transiciones de estado. Habiendo adoptado el concepto de `ResourceAssembler`componentes Spring HATEOAS , poner esa lógica en el `OrderResourceAssembler`sería el lugar perfecto para capturar esta regla de negocio: enlaces / src / main / java / nómina / OrderResourceAssembler.java ```java package payroll; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceAssembler; import org.springframework.stereotype.Component; @Component class OrderResourceAssembler implements ResourceAssembler> { @Override public Resource toResource(Order order) { // Unconditional links to single-item resource and aggregate root Resource orderResource = new Resource<>(order, linkTo(methodOn(OrderController.class).one(order.getId())).withSelfRel(), linkTo(methodOn(OrderController.class).all()).withRel("orders") ); // Conditional links based on state of the order if (order.getStatus() == Status.IN_PROGRESS) { orderResource.add( linkTo(methodOn(OrderController.class) .cancel(order.getId())).withRel("cancel")); orderResource.add( linkTo(methodOn(OrderController.class) .complete(order.getId())).withRel("complete")); } return orderResource; } }COPIAR ``` Este ensamblador de recursos siempre incluye el enlace **propio** al recurso de un solo elemento, así como un enlace de regreso a la raíz agregada. Pero también incluye dos enlaces condicionales a `OrderController.cancel(id)`, así como `OrderController.complete(id)`(aún no definido). Estos enlaces SOLO se muestran cuando el estado del pedido es `Status.IN_PROGRESS`. Si los clientes pueden adoptar HAL y la capacidad de leer enlaces en lugar de simplemente leer los datos de JSON antiguo, pueden intercambiar la necesidad de conocimiento del dominio sobre el sistema de pedidos. Esto, naturalmente, reduce el acoplamiento entre el cliente y el servidor. Y abre la puerta para ajustar el flujo de cumplimiento de pedidos sin interrumpir a los clientes en el proceso. Para redondear el cumplimiento del pedido, agregue lo siguiente `OrderController`para la `cancel`operación: Crear una operación "cancelar" en el OrderController ```java @DeleteMapping("/orders/{id}/cancel") ResponseEntity cancel(@PathVariable Long id) { Order order = orderRepository.findById(id).orElseThrow(() -> new OrderNotFoundException(id)); if (order.getStatus() == Status.IN_PROGRESS) { order.setStatus(Status.CANCELLED); return ResponseEntity.ok(assembler.toResource(orderRepository.save(order))); } return ResponseEntity .status(HttpStatus.METHOD_NOT_ALLOWED) .body(new VndErrors.VndError("Method not allowed", "You can't cancel an order that is in the " + order.getStatus() + " status")); }COPIAR ``` Comprueba el `Order`estado antes de permitir que se cancele. Si no es un estado válido, devuelve un Spring HATEOAS `VndError`, un contenedor de errores compatible con hipermedia. Si la transición es realmente válida, hace la transición `Order`a `CANCELLED`. Y agregue esto al `OrderController`también para completar el pedido: Crear una operación "completa" en OrderController ```java @PutMapping("/orders/{id}/complete") ResponseEntity complete(@PathVariable Long id) { Order order = orderRepository.findById(id).orElseThrow(() -> new OrderNotFoundException(id)); if (order.getStatus() == Status.IN_PROGRESS) { order.setStatus(Status.COMPLETED); return ResponseEntity.ok(assembler.toResource(orderRepository.save(order))); } return ResponseEntity .status(HttpStatus.METHOD_NOT_ALLOWED) .body(new VndErrors.VndError("Method not allowed", "You can't complete an order that is in the " + order.getStatus() + " status")); }COPIAR ``` Esto implementa una lógica similar para evitar que `Order`se complete un estado a menos que se encuentre en el estado correcto. Al agregar un pequeño código de inicialización adicional a `LoadDatabase`: Actualización del precargador de bases de datos ```java orderRepository.save(new Order("MacBook Pro", Status.COMPLETED)); orderRepository.save(new Order("iPhone", Status.IN_PROGRESS)); orderRepository.findAll().forEach(order -> { log.info("Preloaded " + order); });COPIAR ``` ... puedes probar cosas! Para usar el servicio de pedido recién creado, solo realice algunas operaciones: ``` $ curl -v http: // localhost: 8080 / orders { "_incrustado": { "lista de orden": [ { "id": 3, "descripción": "MacBook Pro", "Estado: COMPLETADO", "_Enlaces": { "self": { "href": "http: // localhost: 8080 / orders / 3" }, "pedidos": { "href": "http: // localhost: 8080 / orders" } } }, { "id": 4, "descripción": "iPhone", "estado en progreso", "_Enlaces": { "self": { "href": "http: // localhost: 8080 / orders / 4" }, "pedidos": { "href": "http: // localhost: 8080 / orders" }, "cancelar": { "href": "http: // localhost: 8080 / orders / 4 / cancel" }, "completo": { "href": "http: // localhost: 8080 / orders / 4 / complete" } } } ] }, "_Enlaces": { "self": { "href": "http: // localhost: 8080 / orders" } } } ``` Este documento HAL muestra inmediatamente diferentes enlaces para cada pedido, en función de su estado actual. - El primer orden, **COMPLETADO,** solo tiene los enlaces de navegación. Los enlaces de transición de estado no se muestran. - El segundo pedido, que es **IN_PROGRESS,** además tiene el enlace **cancelar** , así como el enlace **completo** . Intenta cancelar un pedido: ``` $ curl -v -X DELETE http: // localhost: 8080 / orders / 4 / cancel > BORRAR / pedidos / 4 / cancelar HTTP / 1.1 > Anfitrión: localhost: 8080 > Usuario-Agente: curl / 7.54.0 > Aceptar: * / * > BORRAR / pedidos / 4 / cancelar HTTP / 1.1 > Anfitrión: localhost: 8080 > Usuario-Agente: curl / 7.54.0 > Aceptar: * / * > PUT / orders / 4 / complete HTTP / 1.1 > Anfitrión: localhost: 8080 > Usuario-Agente: curl / 7.54.0 > Aceptar: * / * >