Herramientas de usuario

Herramientas del sitio


clase:daw:dws:1eval:spring

02 - Spring-Boot

Spring Boot es un framework de desarrollo de aplicaciones Java que simplifica el proceso de construcción y despliegue de aplicaciones. Proporciona un conjunto de características y herramientas que facilitan la configuración inicial, la gestión de dependencias y la implementación de aplicaciones de manera rápida y sencilla.

Spring Boot se basa en el framework Spring, aprovechando su potencia y modularidad, pero reduciendo la complejidad al proporcionar una configuración automática y por defecto. Esto permite a los desarrolladores centrarse en la lógica de negocio de sus aplicaciones en lugar de preocuparse por la configuración y la integración de diferentes componentes.

Crear un proyecto

Para crear un nuevo proyecto Spring Boot contamos con una herramienta muy útil que nos automatiza el proceso: Spring Initializr. Podemos crear el proyecto directamente desde su web, configurándolo a nuestra medida y añadiendo las dependencias necesarias. Una vez terminado el proceso, generamos el proyecto, lo que nos descargará un zip con el proyecto ya configurado.

Tendremos que elegir el tipo de proyecto (Gradle o Maven), el lenguaje (Java, Kotlin o Groovy) y la versión de Spring Boot. Además, añadiremos los metadatos (artefacto, grupo, descripción…), así como el tipo de empaquetado (que, en nuestro caso, será jar) y la versión de Java a utilizar.

En la sección de la derecha, podremos añadir las dependencias que queramos. Por ahora, vamos a añadir sólo la dependencia Spring Web, que nos permite crear webs (incluido Api Rest) y lleva un servidor Tomcat embebido.

Una vez lo tengamos todo, le damos al botón Generate y nos generará un archivo comprimido con el proyecto ya creado y configurado.

Iniciar el servidor

Al crear el proyecto, Spring Boot crea una clase que será la encargada de iniciarlo:

package com.cipfpmislata.basic_web;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BasicWebApplication {

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

}

Si ejecutamos el proyecto, veremos que Spring Boot ha levantado el servidor Tomcat en el puerto por defecto (8080). Si accedes desde un navegador a http://localhost:8080/, deberías ver una página genérica de error (tranquilo, esto quiere decir que todo ha funcionado correctamente):

Cambiar puerto por defecto en Spring Boot

Para cambiar el puerto en el que escucha el servidor embebido de Spring Boot, la manera más rápida y fácil es añadiendo la siguiente línea al archivo application.properties de la carpeta resources:

server.port=8081;

Si abres ahora el navegador, verás como podrás acceder a tu aplicación cargando la url http://localhost:8081/:

Beans

Dentro de la infraestructura de Spring tenemos algo llamado Spring Container, que vendría siendo el núcleo del framework. El contenedor crea objetos, los une ,los configura y administra su ciclo de vida completamente. Los Beans son objetos que puede manejar el contendor de Spring.

Para crear un Bean Spring utiliza anotaciones mediante el símbolo arroba (@). Por ejemplo, vamos a crear un controlador para responder a las rutas de nuestra aplicación. Para ello, crea la clase MainController:

package com.cipfpmislata.basic_web;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class MainController {

    public void index() {
        System.out.println("Método index del controlador Main ejecutándose");
    }
}

Fíjate que en la línea anterior a la definición de la clase hemos añadido la anotación @RestController (asegúrate de importar la librería org.springframework.web.bind.annotation.RestController para que funcione). Con esa notación, le indicamos a Spring que queremos crear un Bean de tipo controlador de Rest. Existen varios tipos de Beans en Spring Boot. Durante el curso, iremos usando algunos de ellos.

Rutas

Para que Spring sepa qué método ejecutar según la ruta que introduzca el usario en el navegador, vamos a utilizar otro tipo de anotaciones delante de cada método: @GetMapping:

package com.cipfpmislata.basic_web;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;

@RestController
public class MainController {

    @GetMapping("/")
    public void index() {
        System.out.println("Método index del controlador Main ejecutándose");
    }
}

Con ésto le estamos indicando a Spring que cada vez que se acceda a la ruta raíz de nuestra web (/) se ejecute el método MainController de nuestra aplicación. Si abrimos el navegador y accedemos a la ruta raíz (http://localhost:8080/) deberíamos ver en la terminal la frase “Método index de MainController ejecutándose”:

Añádiendo más rutas

Vamos a añadir la ruta /about con la información de nuestra organización. Lo único que tenemos que hacer es crear un nuevo método en nuestro controlador e indicarle la ruta a la que tiene que reaccionar:

package com.cipfpmislata.basic_web;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;

@RestController
public class MainController {

    @GetMapping("/")
    public void index() {
        System.out.println("Método index del controlador Main ejecutándose");
    }

    @GetMapping("/about")
    public void about(){
        System.out.println("Método about de MainController ejecutándose");
    }    
}

Ten en cuenta que no tenemos que juntar todas las rutas en un mismo controlador. En una aplicación web, normalmente tenemos varios controladores, cada uno con su propias rutas. Gracias a Spring Boot, para que todo funcione correctamente solo tenemos que añadir anotaciones válidas (@RestController, @Controller…) a cada uno de nuestros controladores y definir las rutas para que se haga cargo el framework.
Si las rutas de nuestros controladores tienen todas la misma raíz (/categorias, /categorias/id_categoria, /categorias/id_categoria/productos…) podemos utilizar la anotación @RequestMapping delante de la clase para establecer esa ruta común:

@RequestMapping("/categorias")
@RestController
public class CategoryController {
...

Por ahora, solo estamos utilizando el método HTTP GET mediante la anotación @GetMapping, que sirve para recuperar información. Si queremos añadir, modificar o borrar datos, también tenemos otras anotaciones como @PostMapping, @PutMappint, @DeleteMapping

Rutas con parámetros variables

¿Qué pasa si nuestras rutas son del estilo /productos/id_product? Obviamente, hacer una ruta por cada producto es inviable. Para eso, tenemos que definir una ruta con un parámetro variable (id_product). La manera de hacerlo mediante Spring es utilizando las llaves para definir parámetros variables en nuestras rutas y usando la notación @PathVariable en la cabecera de nuestro método para recoger ese parámetro.

Por ejemplo, crea el controlador ProductController y añade lo siguiente:

package com.cipfpmislata.basic_web;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@RestController
public class ProductController {
    
        @GetMapping("/products/{id}")
        public void getById(@PathVariable("id") int id){
            System.out.println("Ruta: /productos/" + id);
        }
}  

Si abrimos la ruta, por ejemplo, /products/2, veremos como nuestro método recoge el valor de dicho id:

Si accedes a la ruta /products/14, verás como cambia el id del producto de manera correcta:

Si al parámetro lo llamas igual que la variable de entrada del método, te puedes ahorrar escribir su nombre entre paréntesis en la anotación @PathVariable:
    @GetMapping("/productos/{id}")
    public void getById(@PathVariable int id){
            System.out.println("Ruta: /productos/" + id);
    }

Inyección de dependencias automáticas

Vamos a ver otra funcionalidad muy útil de Spring: la inyección de dependencias automática.

La inyección de dependencias (DI) es un principio fundamental en el diseño de software y se refiere a la práctica de suministrar las dependencias de un objeto desde el exterior, en lugar de que el objeto las cree por sí mismo. En lugar de que un objeto se encargue de instanciar y gestionar sus propias dependencias, se le proporcionan desde fuera, lo que promueve la modularidad, la reutilización y la separación de responsabilidades en el código.

Spring lo hace mediante la inversión de control (IoC), donde un contenedor es responsable de administrar las dependencias y las proporciona automáticamente cuando se necesitan.

Vamos a ver un ejemplo para entenderlo mejor. Tenemos una interfaz llamada ProductService con un método getAll():

public interface ProductService {

    void getAll();
}

Además, implementamos la interfaz mediante la clase ProductServiceImpl. El método mostrará “Getting all products” en el log:

public class ProductServiceImpl implements ProductService{

    @Override
    public void getAll() {
        LoggerFactory.getLogger(ProductService.class).warn("Getting all products");
    }
}

Vamos a crear ahora un método en nuestro controlador de productos que llame al método getAll() del servicio recién creado. Este método se ejecutará cuando accedamos a la ruta localhost:8080/products:

@RequestMapping(ProductController.URL)
@RestController
public class ProductController {
    public static final String URL = "/products";
    private final ProductService productService = new ProductServiceImpl();

    @GetMapping
    public ResponseEntity<Void> getAll() {
        productService.getAll();
        return ResponseEntity.ok()
                .body("Hello World!");
    }
}

Básicamente lo que hemos hecho es añadir un atributo de tipo ProductService e inicializarlo con una instancia de tipo ProductServiceImpl (Recuerda como funciona el polimorfismo). Después, ejecutamos su método getAll().

Lo que devuelve el método es una respuesta http 200 (ok) con el contenido “Hello World!”. Ya iremos viendo el formato y código de las respuestas en APIs más adelante.

Si accedemos a la ruta, deberíamos ver el mensaje “Getting all products” en el log.

Vamos a hacer ahora que sea el propio Spring el que se encargue de inyectar la dependencia y crear el objeto de forma automática. Para eso, utilizamos la etiqueta @Autowired en el atributo:

@RequestMapping(ProductController.URL)
@RestController
public class ProductController {
    public static final String URL = "/products";
    @Autowired
    private final ProductService productService;

    @GetMapping
    public ResponseEntity<Void> getAll() {
        productService.getAll();
        return ResponseEntity.ok()
                .body("Hello World!");
    }
}

Si lo dejamos así, vemos que Spring nos muestra un error:

Field productService in es.cesguiro.ProductController required a bean of type 'es.cesguiro.ProductService' that could not be found

Ésto ocurre porque Spring sólo es capaz de manejar objetos que sean de tipo Bean, con lo que tenemos que convertir nuestro servicio en uno de ellos:

@Service
public class ProductServiceImpl implements ProductService{

    @Override
    public void getAll() {
        LoggerFactory.getLogger(ProductService.class).warn("Getting all products");
    }
}

Ahora ya no debería mostrarse el error y funcionar todo.

En este caso, hemos utilizado la anotación @Service en nuestro servicio. En realidad, podríamos utilizar cualquier anotación que convierta la clase en un Bean (@Component, @Controller, @Repository), aunque no es lo recomendable, ya que en un futuro podría cambiar la funcionalidad de cada etiqueta y sería mejor nombrar a cada Bean como lo qué es (controlador, servicio, repositorio…)

La inyección de dependencias la podemos hacer sobre atributos de clases directamente (como en el ejemplo anterior), en el constructor, en un setter… ¿Dónde es más adecuado hacerlo? Si te fijas, IntelliJ (o el editor que estés utilizando) se queja diciendo que la inyección en los atributos no es recomendable. Utilizar @Autowired en los atributos de clase no es recomendable principalmente por dos razones relacionadas con pruebas unitarias:

  • Dificultad para Mocking: Cuando se utiliza @Autowired en los atributos de clase, Spring realiza la inyección de dependencias automáticamente durante la inicialización del bean. Esto puede dificultar la escritura de pruebas unitarias efectivas porque tus pruebas tendrían que depender de instancias reales de los beans inyectados.
  • Control Explícito en las Pruebas: En cambio, al utilizar inyección de dependencias a través de constructores o métodos setters explícitos en lugar de @Autowired en los atributos, tienes un mayor control sobre lo que se inyecta en las pruebas. Puedes usar mocks o stubs específicos para simular el comportamiento de las dependencias y probar componentes de manera aislada.

Por lo tanto, vamos a modificar nuestro controlador para hacer la DI en el constructor:

@RequestMapping(ProductController.URL)
@RestController
public class ProductController {
    public static final String URL = "/products";
    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public ResponseEntity<Void> getAll() {
        productService.getAll();
        return ResponseEntity.ok()
                .body("Hello World!");
    }
}

Además, desde la versión 4.3 de Spring la anotación @Autowired ya no es necesaria explícitamente para la inyección de dependencias si se aplica únicamente en un constructor. Spring automáticamente detecta y utiliza el constructor para realizar la inyección de dependencias.

Por último, si utilizamos Lombok, podremos incluso ahorrarnos escribir el código del constructor con la etiqueta @RequiredArgsConstructor:

@RequestMapping(ProductController.URL)
@RestController
@RequiredArgsConstructor
public class ProductController {
    public static final String URL = "/products";
    private final ProductService productService;

    @GetMapping
    public ResponseEntity<Void> getAll() {
        productService.getAll();
        return ResponseEntity.ok()
                .body("Hello World!");
    }
}

De esta forma, nuestro código queda más limpio y simple y podemos inyectar nuestras dependencias mockeadas en los tests sin problemas:

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @MockBean
    private ProductService productService;

    @Test
    void getAll() throws Exception {
        doNothing().when(productService).getAll();
        mockMvc.perform(get("/products"))
                .andExpect(status().isOk())
                .andExpect(content().string("Hello World!"));;
    }
}

¿Qué pasa si tenemos más de una implementación de una interfaz? En ese caso, podemos utilizar la etiqueta Qualifier para indicar qué implementación concreta queremos inyectar.
Spring se puede configurar (creación de Beans, inyección de dependencias…) de varias formas. Las 2 formas habituales son mediante anotaciones, como haremos nosotros durante el curso, o documentos XML.
2023/07/19 08:15 · cesguiro
clase/daw/dws/1eval/spring.txt · Última modificación: 2024/06/20 13:32 por cesguiro