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.
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.
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):
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/:
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.
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”:
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"); } }
@RequestMapping("/categorias") @RestController public class CategoryController { ...
¿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:
@GetMapping("/productos/{id}") public void getById(@PathVariable int id){ System.out.println("Ruta: /productos/" + id); }
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.
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:
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!"));; } }