Tu primera API com Spring Boot y Docker

Tu primera API com Spring Boot y Docker

¿Por qué usar Spring Boot?

Spring Boot es un framework de Java ampliamente usado para desarrollar aplicaciones web, especialmente las REST API.

Uno de los recursos más importantes que posee esta herramienta es su configuración automática muy al estilo Plug and Play.

Algunas de sus funciones:

  • Manejo de dependencias.
  • Tomcat, Jetty y Undertow integrados y disponibles de manera nativa.
  • Disponibilidad de funciones como métricas, health checks, logs, etc.
  • No requiere escribir código XML para configuraciones.

Esa última es una de mis preferidas. Si estás en el proceso de crear una aplicación Java web sin usar algún framework, sabes que deben atenderse varios detalles, entre ellos las propiedades y configuraciones en XML. Spring Boot abstrae esta complejidad al permitir que se realicen configuraciones a través de anotaciones de código.

Es importante resaltar también la facilidad para colocar las aplicaciones en producción. Como se mencionó anteriormente, métricas, health checks, logs y otras funciones están listas para uso. Asimismo, tenemos las bibliotecas que funcionan solo con añadir la dependencia en el archivo pom.xml. Construcción de APIs, bibliotecas para pruebas, seguridad y ORM están fácilmente disponibles.

¡Manos a la obra!

Supondré que ya entiendes lo básico de Java, algunos conceptos REST y de bases de datos relacionadas. Ahora que ya fueron detalladas las razones para usar Spring Boot, hablaremos un poco de la aplicación que vamos a construir con la herramienta.

El alcance de lo anterior será el siguiente: Nuestro cliente quisiera implementar una wishlist para los productos de su e-commerce. Con esa meta y conversando con él,  escribimos esta lista de requisitos:

  • El sistema necesita las entidades propietarias:
  • Customer
  • Product
  • Wishlist
  • Será necesario conservar la información en una base de datos MySQL.
  • Las funciones para el consumidor serán:
  • Registro en el sistema.
  • Añadir un producto a su lista en dicho sistema.
  • Tendremos un CRUD prácticamente completo para todas nuestras entidades.
  • Contenedorización en Docker. El cliente necesita esto para implementar la aplicación más fácilmente.

Usaremos la versión de pruebas de IntelliJ IDEA Ultimate para codificar el proyecto. Puedes descargar la aplicación aquí.

Vamos a generar la estructura del proyecto con Spring Initializr. Esa herramienta crea todo el esqueleto de un proyecto Spring Boot según las dependencias deseadas.

Al abrir la página de la aplicación, completemos la información del lado izquierdo:

Aquí una breve descripción de todos los campos que completaremos/seleccionaremos:

  • Project: Define el tipo de proyecto. Se refiere al Dependencies Manager que será usado. En este tutorial, trabajaremos con Maven Project.
  • Language: Spring Boot permite el uso de tres lenguajes de programación. Java es el patrón y trabajaremos con él.
  • Spring Boot: Relativa a la versión del programa. En esta oportunidad, avanzaremos con la última disponible: 2.7.1.
  • Group: Dominio del paquete base de la aplicación.
  • Artifact: Nombre de la aplicación.
  • Description: Descripción de la aplicación.
  • Package name: Paquete de Java principal de la aplicación.
  • Packaging: Tipo de packaging que se empleará. La interfaz permite la selección de jar o war.
  • Java: Versión de Java que se utilizará.

Esa es la primera etapa. Ahora elegiremos las dependencias:

Ingresa en add dependencies y busca cada una de las dependencias:

  • Spring Data JPA: Reduce el código boilerplate necesario para el JPA que a su vez se ocupa de la complejidad de acceder y mapear entidades relacionales. Esa dependencia permitirá que no escribamos el código MySQL en la mano. El Hibernate que viene con él administrará las operaciones en nuestra base de datos a un nivel superior.
  • Spring WEB: Usa MVC. REST y Tomcat como web server patrón.
  • MySQL driver: Permitirá la conexión con la base de datos MySQL.
  • Spring security: Empleada para definir el aceso a ciertas rutas de API.
  • Lombok: Permite la reducción de código boilerplate repetitivo como getters, setters, construtores, etc.

Ahora solo necesitamos generar el proyecto haciendo clic en Generate y el zip se descargará en tu navegador.

Exporta el proyecto para el IntelliJ y ábrelo en la IDE:

Clic en Open y selecciona la carpeta zip (que acabamos de descargar) ya descomprimida.

IntelliJ abrirá y descargará todas las dependencias que seleccionamos para el proyecto.

Demos una mirada a algunas carpetas y archivos llave generados anteriormente, ahora abiertos en la IDE:

  • Nuestro código se creará en el siguiente directorio: src/main/java/com/community/wishlist/
  • En el mismo directorio tenemos el archivo Java src/main/java/com/community/wishlist/WishlistApplication.java el cual será el archivo maestro de nuestro proyecto. Éste será ejecutado cuando corramos nuestra aplicación. Ten en cuenta a @SpringBootApplication.
  • Nuestras pruebas serán escritas en el directorio: src/test/java/com/community/wishlist
  • El archivo pom.xml en la raíz del proyecto contiene información sobre el proyecto (llenamos varias en la sección project metadata en Spring Initilizr) así como las configuraciones para construir la aplicación. Actualmente, nuestro archivo luce de la siguiente forma:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.community</groupId>
<artifactId>wishlist</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>wishlist</name>
<description>A ecommerce wishlist</description>
<properties>
<java.version>17</java.version>
</properties>
<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-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>

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

</project>

En el futuro, éste se modificará para añadir las dependencias necesarias.

Crearemos todos nuestros modelos/entidades conforme a lo anteriormente planeado. Crea un nuevo paquete llamado model dentro de com.community.wishlist.

A continuación, crearemos nuestras clases Java para los modelos:

  • Customer.
  • Product.
  • Wishlist.

¡Excelente! Ahora tenemos los siguientes archivos:

Los archivos están creados. Pese a que están todos iguales, sin sus atributos y comportamiento, continuaremos definiéndolos. Tendremos:

com/community/wishlist/model/Customer.java

public class Customer {

    private Long id;
    private String name;
    private String email;
    private String password;

}

Para el customer, es decir, el usuario que agregará un producto a su wishlist, tendremos los siguientes atributos principales:

  • Id.
  • Name.
  • Email.
  • Password.

public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private String password;
}

Esas palabras clave son las anotaciones de Spring Boot que comentamos al inicio del texto.

  • @Id: Indica que ese atributo es el identificador único del modelo.
  • @GeneratedValue: Indica que ese atributo será generado.

Usaremos el Lombok que seleccionamos como una de nuestras dependencias:

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private String password;

}


Esa serie de anotaciones genera todo este código durante el tiempo de compilación:

public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private String password;
    public Customer(){

    }

    public Customer(Longid, String name, String email, String password) {
        this.id =id;
        this.name =name;
        this.email =email;
        this.password =password;
    }

@Override
public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

@Override
public boolean equals(Objectobj) {
        if (this ==obj)
            return true;
        else if (obj== null)
            return false;
        else if (getClass() !=obj.getClass())
            return false;
        Customer other = (Customer)obj;
        if (id == null)
            return other.id == null;
        else return id.equals(other.id);
    }

    public Long getId() {
        return id;
    }

    public void setId(Longid) {
        this.id =id;
    }

    public String getName() {
        return name;
    }

    public void setName(Stringname) {
        this.name =name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(Stringemail) {
        this.email =email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(Stringpassword) {
        this.password =password;
    }

}


Bien. Más simple y limpio usando Llombok, ¿verdad?

Para la clase Product tendremos los siguientes atributos:

  • Id.
  • Price.
  • Brand.
  • Title.

Vamos a crearlos en el archivo Product y definir ya las anotaciones de Lombok:

com/community/wishlist/model/Product.java

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private float price;
    private String brand;
    private String title;
}


Para la clase Wishlist tendremos los siguientes atributos:

  • Id.
  • CustomerId.
  • Products.

Los crearemos en el archivo Wishlist para definir ya las anotaciones de Lombok:

com/community/wishlist/model/Wishlist.java

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@Entity
public class Wishlist {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne
    @JoinColumn(name = "customerId")
    private Customer customer;
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "productId")
    Set<Product> products = new LinkedHashSet<>();
}


¡Perfecto! Ahora tenemos creados todos nuestros modelos en la aplicación.  Más tarde, haremos que hibernate cree todas las tablas automáticamente.

Creación del Repository

El siguiente paso es crear las clases de repository. Éstas serán responsables de abstraer el código necesario para las capas de acceso a los datos. De esta forma, el desarrollador no necesita escribir clases concretas con las implementaciones de sus métodos y ni siquiera crear una @Bean de Spring. Al implementar esa interfaz ya tenemos todas las funciones comunes en varias bases de datos como CREATE, READ, UPDATE y DELETE.

Requeriremos un repository para cada uno de nuestros modelos. Al final, haremos operaciones con todos ellos.

Usaremos el directorio src/main/java/com/community/wishlist/repository:

CustomerRepository.java

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Optional<Customer> findByEmail(String email);
}

WishlistRepository.java

public interface WishlistRepository extends JpaRepository<Wishlist, Long> {
    Optional<Wishlist> findByCustomerId(Long customerId);
}

ProductRepository.java

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Tratamiento de excepciones

Antes de definir una de nuestras nuevas capas, es preciso hablar un poco sobre el tratamiento de excepciones. En Spring Boot tenemos la opción de enganchar una excepción específica que definimos a un status de HTTP. Por ejemplo, puedes crear una clase de excepción llamada EntityAlreadyExistsException que se asociará al HTTP status code 422 (unprocessable entity). Usaremos esa excepción en el caso de un usuario que ya tiene un e-mail registrado intente registrarse de nuevo.

Crea el package com.community.wishlist.exception y, dentro de él, indica:

EntityAlreadyExistsException.java

@ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY)
public class EntityAlreadyExistsException extends Exception {

    public EntityAlreadyExistsException(Stringmessage) {
        super(message);
    }
}

Otra excepción que debemos considerar es aquélla para el caso de un recurso no encontrado. Ese caso será abordado cuando el usuario intente, por ejemplo, actualizar o leer una entidad que no exista.

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends Exception {
    public ResourceNotFoundException(Stringmessage) {
        super(message);
    }
}


Todavía necesitamos otros dos archivos, uno para representar nuestro modelo de respuesta cuando una de esas excepciones fuesen activadas y otro para hacer el wiring de nuestro error con la respuesta que dará Spring Boot si eso ocurre.

Nuestro modelo de respuesta será el siguiente:

ErrorResponse.java

@Getter
@Setter
@AllArgsConstructor
public class ErrorResponse {
    private Date timestamp;
    private String status;
    private String message;
    private String details;
}

Y nuestro ControllerAdvice:

GlobalExceptionHandler.java

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> resourceNotFoundException(
            ResourceNotFoundExceptionex, WebRequestrequest) {
        ErrorResponse errorDetails =
                new ErrorResponse(new Date(), HttpStatus.NOT_FOUND.toString(),ex.getMessage(),request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }

@ExceptionHandler(EntityAlreadyExistsException.class)
    public ResponseEntity<?> entityAlreadyExistsException(
            ResourceNotFoundExceptionex, WebRequestrequest) {
        ErrorResponse errorDetails =
                new ErrorResponse(new Date(), HttpStatus.UNPROCESSABLE_ENTITY.toString(),ex.getMessage(),request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.UNPROCESSABLE_ENTITY);
    }

@ExceptionHandler(Exception.class)
    public ResponseEntity<?> globalExceptionHandler(Exceptionex, WebRequestrequest) {
        ErrorResponse errorDetails =
                new ErrorResponse(new Date(), HttpStatus.INTERNAL_SERVER_ERROR.toString() ,ex.getMessage(),request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

En el archivo definimos la respuesta en caso de surgir alguna de las dos excepciones anteriormente creadas, sumada a la hipótesis de si una excepción genérica es activada.

¡Ahora tenemos nuestras excepciones listas para ser usadas en nuestra próxima capa!

La capa intermedia entre el endpoint y el la base de datos

En su momento creamos nuestros modelos y repositorios, pero ¿ingresaremos a los repositorios que acabamos de construir?  Usaremos una capa de services que será llamada por el controlador. El primero a su vez llamará a los repositorios que creamos. Algo así:

Controller → Service → Repository

Vamos a comenzar declarando el servicio de customer en src/main/java/com/community/wishlist/repository/CustomerService.java:

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;

    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public Customer create(Customer customer) throws EntityAlreadyExistsException {
        this.findByEmail(customer.getEmail()).orElseThrow(() -> new EntityAlreadyExistsException(""));
        return this.customerRepository.save(customer);
    }

    public Optional<Customer> findById(Long id) {
        return this.customerRepository.findById(id);
    }

    public List<Customer> getAll() {
        return new ArrayList<>(customerRepository.findAll());

    }

    public Customer update(Customer newCustomer, Long id) throws ResourceNotFoundException {
        Optional<Customer> optionalCustomer = this.findById(id);
        Customer customer = optionalCustomer.orElseThrow(() -> new ResourceNotFoundException(""));
        customer.setEmail(newCustomer.getEmail());
        customer.setName(newCustomer.getName());
        return customer;
    }
   
    public void delete(Long id) throws ResourceNotFoundException {
        Optional<Customer> optionalCustomer = this.findById(id);
        Customer customer = optionalCustomer.orElseThrow(() -> new ResourceNotFoundException(""));
        this.customerRepository.delete(customer);
    }

    public Optional<Customer> findByEmail(String email) {
        return this.customerRepository.findByEmail(email);
    }
}


Nota que estamos levantando nuestras excepciones creadas en el caso de que suceda un error específico (new ResourceNotFoundException("") e new ResourceNotFoundException("")).

Definimos en el archivo el CRUD de aplicación en nuestro servicio. En cada uno de los métodos, el servicio llama el repository para realizar las acciones en la base de datos.

Veamos las definiciones de las anotaciones que usamos:

  • @Service: Indica que la clase hace operaciones basadas en la regla del negocio de aplicación. El spring automáticamente detectará cuándo hacer el classpath scanning.
  • @Transactional: Indica que todo el método debe ser ejecutado de forma atómica.

En resumen, lo que hicimos fue:

  • Definir el customerRepository que se usará para las operaciones de base de datos.
  • Indicamos el constructor de clase con el customerRepository como argumento. Eso es importante para que Spring Boot inyecte esa dependencia en la clase.
  • Precisamos las funciones:

     create: crea un customer. Es el famoso regístrese en los sitios o plataformas.

     readById: buscará al cliente por su id y lo devolverá.

     update: actualizará el email y nombre del customer. El newCustomer como             argumento es el objeto que fue enviado por el controller.

     delete: elimina al customer de la base de datos.

Cada una de ellas llama el repository y realiza las operaciones en la base de datos.

Vamos a indicar también el ProductService:

@Service
public class ProductService {
    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Product create(Product product) {
        return this.productRepository.save(product);
    }

    public Optional<Product> findById(Long id) {
        return this.productRepository.findById(id);
    }

    public List<Product> getAll() {
        return new ArrayList<>(productRepository.findAll());

    }

    public Product update(Product newProduct, Long id) throws ResourceNotFoundException {
        Optional<Product> optionalProduct = this.findById(id);
        Product product = optionalProduct.orElseThrow(() -> new ResourceNotFoundException(""));
        product.setTitle(newProduct.getTitle());
        product.setPrice(newProduct.getPrice());
        return product;
    }

    @Transactional
    public void delete(Long id) throws ResourceNotFoundException {
        Optional<Product> optionalProduct = this.findById(id);
        Product product = optionalProduct.orElseThrow(() -> new ResourceNotFoundException(""));
        this.productRepository.delete(product);
    }

}

y Ahora el WishlistService:

@Service
public class WishlistService {
    private final WishlistRepository wishlistRepository;

    public WishlistService(WishlistRepository wishlistRepository) {
        this.wishlistRepository = wishlistRepository;
    }
    public Wishlist create(Wishlist wishlist) {
        return this.wishlistRepository.save(wishlist);
    }

    public Optional<Wishlist> findByCustomerId(Long customerId) {
        return this.wishlistRepository.findByCustomerId(customerId);
    }

    public Wishlist addProduct(Set<Product> newProducts, Long id) throws ResourceNotFoundException {
        Wishlist wishlist = this.wishlistRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(""));
        wishlist.getProducts().addAll(newProducts);
        return wishlist;
    }

    public void delete(Long id) {
        Wishlist wishlist = this.wishlistRepository.getOne(id);
        this.wishlistRepository.delete(wishlist);
    }
}


Controllers: la exposición de las rutas da aplicación

Como al ejecutar un PUT a localhost:8080/wishlist/1, ¿cómo se harían las adiciones de productos a la wishlist? Aquí es donde entran los controladores de aplicaciones. Vamos a crear un controlador para cada entidad, por lo que mantenemos las URL separadas respetando el marco REST.

Los próximos archivos serán creados en: src/main/java/com/community/wishlist/controller/

Generaremos el siguiente archivo y explicaremos su estructura y anotaciones enseguida:

CustomerController.java

public class CustomerController {
    private final CustomerService customerService;

    public CustomerController(CustomerService customerService) {
        this.customerService = customerService;
    }

    @GetMapping
    public List<Customer> getAll() {
        return customerService.getAll();
    }

    @PostMapping
    @Transactional
    public ResponseEntity<Customer> create(@RequestBody @Valid Customer newCustomer, UriComponentsBuilder uriBuilder) throws EntityAlreadyExistsException {
       Customer customer = customerService.create(newCustomer);
        URI uri = uriBuilder.path("/customer/{id}").buildAndExpand(newCustomer.getId()).toUri();
        return ResponseEntity.created(uri).body(customer);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Customer> read(@PathVariable Long id) {
        Optional<Customer> customer = customerService.findById(id);
        return customer.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    @Transactional
    public ResponseEntity<Customer> update(@PathVariable Long id, @RequestBody @Valid Customer newCustomer) throws ResourceNotFoundException {
       Customer customer = customerService.update(newCustomer, id);
        return ResponseEntity.ok(customer);
    }

    @DeleteMapping("/{id}")
    @Transactional
    public ResponseEntity<?> delete(@PathVariable Long id) throws ResourceNotFoundException {
        customerService.delete(id);
        return ResponseEntity.noContent().build();
    }

  • @RestController: Indica que trabajamos con un Controller. Combina las anotaciones @Controller y @ResponseBody. Asimismo, señala que ésta es la capa que controla las requisiciones. Es dirigida a quien debe responderlas.
  • @RequestMapping: Infiere el mapeado HTTP para la data URL. en este caso, ese controller responderá al URL: nossoenderecoweb.com.br/customer/
  • @PostMapping, @GetMapping, @PutMapping, @DeleteMapping: Indica la requisición usando el dato verbo (get, post, put, delete). Es decir, al enviar una requisición GET para el url nossoenderecoweb.com.br/customer/, el código de método getAll será ejecutado.

Como se puede percibir arriba, definimos el CRUD completo para la entidad Customer. La anotación hecha a la vinculación de operación HTTP con la función manejará esta solicitud y devolverá el resultado esperado.

Es importante notar que usamos la capa de service para acceder a la información de nuestro modelo. Nuestro controller no ingresa a la base de datos ni ejecuta reglas de negocio. Apenas recibe la requisición, llama a la capa responsable y responde lo necesario.

Es momento de definir nuestros otros controllers:

ProductController.java

public class ProductController {
    private final ProductService productService;
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public List<Product> getAll() {
        return productService.getAll();
    }

    @PostMapping
    @Transactional
    public ResponseEntity<Product> create(@RequestBody @Valid Product newProduct, UriComponentsBuilder uriBuilder) {
        Product product = productService.create(newProduct);
        URI uri = uriBuilder.path("/product/{id}").buildAndExpand(newProduct.getId()).toUri();
        return ResponseEntity.created(uri).body(product);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> read(@PathVariable Long id) {
        Optional<Product> product = productService.findById(id);
        return product.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    @Transactional
    public ResponseEntity<Product> update(@PathVariable Long id, @RequestBody @Valid Product newProduct) throws ResourceNotFoundException {
        Product product = productService.update(newProduct, id);
        return ResponseEntity.ok(product);
    }

    @DeleteMapping("/{id}")
    @Transactional
    public ResponseEntity<?> delete(@PathVariable Long id) throws ResourceNotFoundException {
        productService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

WishlistController.java

@RestController
@RequestMapping("/wishlist")
public class WishlistController {
    private final WishlistService wishlistService;

    public WishlistController(WishlistService wishlistService) {
        this.wishlistService = wishlistService;
    }

    @PostMapping
    @Transactional
    public ResponseEntity<Wishlist> create(@RequestBody @Valid Wishlist newWishlist, UriComponentsBuilder uriBuilder) {
        Wishlist wishlist = wishlistService.create(newWishlist);
        URI uri = uriBuilder.path("/wishlist/{id}").buildAndExpand(newWishlist.getId()).toUri();
        return ResponseEntity.created(uri).body(wishlist);
    }

    @GetMapping("/{customerId}")
    public ResponseEntity<Wishlist> read(@PathVariable Long customerId) {
        Optional<Wishlist> wishlist = wishlistService.findByCustomerId(customerId);
        return wishlist.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    @Transactional
    public ResponseEntity<Wishlist> addProduct(@PathVariable Long id, @RequestBody @Valid Set<Product> newProducts) throws ResourceNotFoundException {
        Wishlist wishlist = wishlistService.addProduct(newProducts, id);
        return ResponseEntity.ok(wishlist);
    }

    @DeleteMapping("/{id}")
    @Transactional
    public ResponseEntity<?> delete(@PathVariable Long id){
        wishlistService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

Algunas configuraciones adicionales

Necesitamos definir algunas configuraciones para que nuestra aplicación funcione como deseamos. Podemos hacerlas en el archivo src/main/resources/application.properties. Tendremos lo siguiente:

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url =jdbc:mysql://mysql-service:3306/wishlist?useSSL=false
spring.datasource.username=root
spring.datasource.password=ourpassword

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto=update

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
logging.level.org.hibernate.SQL= DEBUG
logging.level.org.hibernate.type=TRACE
server.port = 8080

spring.mvc.pathmatch.matching-strategy=ant_path_matcher


Ya tenemos nuestras configuraciones de base de datos, Url, usuario y contraseña. Como también las configuraciones de hibernate, por cierto. Me gustaría destacar en especial al spring.jpa.hibernate.ddl-auto=update. La palabra clave update hará que hibernate compare nuestras entidades definidas en el package com.community.wishlist.model con lo que existe en nuestra base de datos. Mejor dicho: será esa keyword la que habilitará las modificaciones en nuestra base de datos cuando iniciemos la aplicación.

Swagger

Nuestra aplicación está lista para ejecutarse. Pero si no definimos ninguna respuesta para mostrar en nuestra URL raíz, mostrará la siguiente página:

Agregaremos a nuestro proyecto una página de documentación que permitirá que veamos todas las requisiciones, respuestas y entidades de nuestra aplicación.

Añade en el archivo pom.xml:

<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger2</artifactId>
  <version>2.9.2</version>
</dependency>
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
  <version>2.9.2</version>
  <scope>compile</scope>
</dependency>


Crea un paquete
com.community.wishlist.config.swagger en el archivo SpringFoxConfig.java:

@Configuration
@EnableSwagger2
public class SpringFoxConfig {
@Bean
public Docket productApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.community.wishlist"))
                .build()
                .apiInfo(metaData());
    }

    private ApiInfo metaData() {
        return new ApiInfoBuilder()
                .title("Spring Boot REST API")
                .description("\\"Spring Boot REST API\\"")
                .version("1.0.0")
                .license("Apache License Version 2.0")
                .licenseUrl("<https://www.apache.org/licenses/LICENSE-2.0\\>"")
                .build();
    }
}


Ese archivo hará todo el wiring con el spring para mostrar nuestra documentación que será generada automáticamente. Cuando ingresemos a http://localhost:8080/swagger-ui.html con el server up, veremos la siguiente página:

A todas éstas, ¡¿qué son containers?!

Se puede pensar en container como un lugar aislado que tu aplicación ejecuta. Casi como un sistema operativo que solo sirve para ejecutar tu aplicación. Es importante tener en cuenta que este "sistema operativo" contiene solo los archivos binarios y las dependencias necesarias para ejecutar su aplicación, lo que lo hace extremadamente liviano. Los contenedores comparten el mismo kernel que otros y se pueden iniciar en cuestión de segundos.

Debido a que este entorno está aislado, facilita que tu software se envíe con todas las dependencias necesarias que serán exactamente iguales (en versiones y librerías, por ejemplo). Esto reduce la fricción de entornos como desarrollo, control de calidad y producción (ese viejo problema de: “¡en mi máquina funciona!”). Con el contenedor, todas las computadoras tendrán exactamente el mismo entorno, con las mismas dependencias y binarios con las mismas versiones. Es como si ahora funcionara siempre, porque de forma simplificada es una simulación de la misma máquina que se puede ejecutar en cualquier lugar: en tu computadora, en la nube de tu compañero de trabajo o de la empresa.

Dockerizando nuestra aplicación

Bien, ahora insertemos nuestros archivos para que la aplicación esté en contenedores. Habrá dos:

  • Dockerfile: contiene todas las instrucciones necesarias para subir nuestra imagen. Cuando se realice la construcción, todos los comandos en este archivo se ejecutarán automáticamente.

# Definição de build para a imagem do Spring boot
FROM openjdk:8-jdk-alpine as build

WORKDIR /app

COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .

RUN chmod +x ./mvnw
# Faça o download das dependencias do pom.xml
RUN ./mvnw dependency:go-offline -B

COPY src src

RUN ./mvnw package -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

# Definição de produção para a imagem do Spring boot
FROM openjdk:8-jre-alpine as production
ARG DEPENDENCY=/app/target/dependency

# Copiar as dependencias para o build artifact
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app

# Rodar a aplicação Spring boot
ENTRYPOINT ["java", "-cp", "app:app/lib/*","com.community.wishlist.WishlistApplication"]

  • Docker-compose.yml: nuestro archivo de inicio docker-compose. Es importante tener en cuenta que este archivo simple es nuestro orquestador de contenedores.

# spring-boot-docker/docker-compose.yml
version: "3.7"
services:
  mysql-service:
    image: mysql:5.7
    networks:
      - spring-boot-mysql-network
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=ourpassword
      - MYSQL_DATABASE=wishlist
  web-service:
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    networks:
      - spring-boot-mysql-network
    depends_on:
      - mysql-service
networks:
  spring-boot-mysql-network:
    driver: bridge


Nota que en nuestro archivo definimos dos servicios:

  • mysql-service: nuestra base de datos mySQL que hibernate usará.
  • En environment usamos las mismas credenciales del archivo application.properties
  • web-service: nuestra API rest Spring boot
  • Ella depende de que el servicio de MYSQL funcione antes que ella.

También tenemos la declaración y uso en los servicios de nuestra red que será la red interna de compose. Ten en cuenta que la URL de la base de datos en application.properties está formada por mysql-service. Esta configuración la realiza la red anterior.

Ambos se crearán en la carpeta raíz de la aplicación.

Ahora ejecuta el comando docker-compose up en tu terminal y verás cómo se activa toda nuestra aplicación y base de datos:

¡Ahora nuestra aplicación es funcional! Puedes ir a la página de Swagger e intentar ejecutar algunas operaciones.

Este es nuestro file tree final:

¡Y eso es todo! Acabas de construir una API en Java Spring Boot desde cero. Tenemos todas nuestras rutas funcionando para las entidades y el cliente estará feliz de que pudimos cumplir con todos los requisitos. ¡El sistema de wishlist permitirá a los clientes guardar sus productos deseados y volver más tarde para comprarlos!

Por supuesto, en el aspecto técnico podríamos haber hecho mucho más, pero este tutorial sería aún más largo. Una lista de posibles detalles que puedes implementar:

  • Pruebas unitarias.
  • Cambio de contraseña por correo electrónico al cliente usando spring-boot-starter-mail.
  • Verificación de contraseña segura en el registro de clientes.
  • Uso de DTO y clases de formulario para personalizar los campos recibidos y enviados en respuesta a las solicitudes.
  • Rutas que requieren autenticación. Por el momento todas nuestras rutas son públicas.
  • Imagina que pudiéramos recibir alguna información de una API existente de nuestro cliente, como productos. Entonces, ¿cómo crear un servicio que acceda a esta otra API para usar en la nuestra?
  • Múltiples wishlists. Por el momento el usuario solo puede tener uno

Espero que te diviertas probando las sugerencias anteriores. Si deseas ver el código completo o simplemente descargarlo y ejecutarlo con un docker-compose up, entra en:

https://github.com/IanPedroV/wishlist


⚠️
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Revelo.

Revelo Content Network da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.