apuntes:spring
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
apuntes:spring [17/07/2020 10:18] – Santiago Faci | apuntes:spring [15/11/2023 21:36] (current) – [ModelMapper] Santiago Faci | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== | + | ====== Creación de servicios web REST / APIs. Spring Boot ====== |
- | ==== ¿Qué son los servicios web? ==== | + | {{ spring-logo.png? |
+ | |||
+ | ==== ¿Qué son los servicios web REST? ==== | ||
Un servicio web es una aplicación que se encuentra en el lado servidor y permite que otra aplicación cliente conecte con ella a través de Internet para el intercambio de información utilizando el protocolo HTTP. | Un servicio web es una aplicación que se encuentra en el lado servidor y permite que otra aplicación cliente conecte con ella a través de Internet para el intercambio de información utilizando el protocolo HTTP. | ||
Line 76: | Line 78: | ||
</ | </ | ||
- | ===== Spring | + | ===== Desarrollo de una API con Spring |
- | < | + | En el punto anterior sobre [[https:// |
- | {{ spring-logo.png }} | + | |
- | < | + | |
- | [[http:// | + | Podemos seguir |
- | Para eso, lo primero que haremos será utilizar el [[http:// | + | Partimos entonces de un proyecto de aplicación con [[https:// |
- | + | ||
- | Una vez tengamos creado el proyecto inicial, podemos empezar a trabajar en él para tener nuestro servidor. En este caso se trata de crear un servidor que tendrá los servicios web necesarios para que los usuarios de una aplicación Android puedan registrar sus opiniones en nuestra Base de Datos. Así, otros usuarios podrán visualizarlas en sus terminales. | + | |
==== Configuración del servidor ==== | ==== Configuración del servidor ==== | ||
- | Lo primero de todo será editar el fichero de configuración del proyecto para personalizarlo a nuestro caso: | + | Lo primero de todo será editar el fichero de configuración del proyecto para personalizarlo a nuestro caso. En el siguiente ejemplo estaríamos configurando la aplicación para conectar con una base de datos MySQL: |
- | < | + | < |
- | # Configuración | + | # Configuracion |
- | spring.jpa.hibernate.ddl-auto=none | + | spring.jpa.hibernate.ddl-auto=update |
spring.jpa.properties.hibernate.globally_quoted_identifiers=true | spring.jpa.properties.hibernate.globally_quoted_identifiers=true | ||
+ | |||
# Puerto donde escucha el servidor una vez se inicie | # Puerto donde escucha el servidor una vez se inicie | ||
- | server.port=${port:8080} | + | server.port=8080 |
# Datos de conexion con la base de datos MySQL | # Datos de conexion con la base de datos MySQL | ||
- | spring.datasource.url=jdbc: | + | spring.datasource.url=jdbc: |
- | spring.datasource.username=root | + | spring.datasource.username=myshopuser |
- | spring.datasource.password= | + | spring.datasource.password=mypassword |
- | spring.datasource.driverClassName=com.mysql.jdbc.Driver | + | |
</ | </ | ||
- | Sobre el fichero '' | + | Pero también nos podría interesar utilizar una base de datos H2: |
- | < | + | < |
- | . . . | + | # Configuracion para el acceso a la Base de Datos |
- | apply plugin: ' | + | spring.jpa.hibernate.ddl-auto=update |
- | apply plugin: ' | + | spring.jpa.properties.hibernate.globally_quoted_identifiers=true |
- | apply plugin: ' | + | |
- | apply plugin: ' | + | |
- | jar { | + | # Puerto donde escucha el servidor una vez se inicie |
- | | + | server.port=8080 |
- | version = ' | + | |
- | } | + | |
- | + | ||
- | repositories { | + | |
- | mavenCentral() | + | |
- | } | + | |
- | dependencies { | + | # Datos de conexion con la base de datos H2 |
- | | + | spring.datasource.url=jdbc:h2:file:/ |
- | compile(' | + | spring.datasource.driverClassName=org.h2.Driver |
- | | + | spring.datasource.username=sa |
- | | + | spring.datasource.password=password |
- | } | + | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect |
- | + | spring.h2.console.enabled=true | |
- | configurations { | + | |
- | providedRuntime | + | |
- | } | + | |
</ | </ | ||
- | Ahora, modificaremos | + | Hay que tener en cuenta que la propiedad |
- | Conviene prestar atención a los comentarios | + | * //none//: Para indicar |
+ | * //update//: Si queremos que la genere de nuevo en cada arranque | ||
+ | * //create//: Si queremos que la cree pero que no la genere de nuevo si ya existe | ||
- | <file java Application.java> | + | ==== Definir |
- | /** | + | |
- | * Clase que lanza la aplicación | + | |
- | * | + | |
- | * Cómo compilar/ | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | * | + | |
- | | + | |
- | | + | |
- | | + | |
- | * | + | |
- | * @author Santiago Faci | + | |
- | * @version curso 2015-2016 | + | |
- | */ | + | |
- | @SpringBootApplication | + | |
- | public class Application extends SpringBootServletInitializer { | + | |
- | public static void main(String[] args) { | + | Hay que tener en cuenta que //Spring// utiliza por debajo el framework de // |
- | SpringApplication.run(Application.class, args); | + | |
- | } | + | |
- | @Override | + | > Hay que tener en cuenta que, si hemos configurado el proyecto para hacer uso de una base de datos H2, no será necesario realizar este paso, puesto que la base de datos se creará automáticamente la primera vez que se inicie la aplicación. Podemos omitir este apartado. |
- | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { | + | |
- | return application.sources(applicationClass); | + | |
- | } | + | |
- | private static Class< | + | En el caso de que hayamos optado por MySQl, simplemente tendremos que crear la base de datos. Y ya de paso aprovecharemos para crear un usuario con el que la aplicación web se conectará (de esa manera evitamos tener que configurar el acceso usando el usuario root). |
- | } | + | |
- | </ | + | |
- | ==== Definir la Base de Datos ==== | + | <code sql> |
+ | CREATE DATABASE myshoponline; | ||
+ | CREATE USER myshopuser IDENTIFIED BY ' | ||
+ | GRANT ALL PRIVILEGES ON myshoponline.* TO myshopuser; | ||
+ | </ | ||
- | Hay que tener en cuenta que //Spring// utiliza por debajo el framework de //Hibernate// para trabajar con la Base de Datos. Eso nos va a permitir trabajar con nuestras clases Java directamente sobre la Base de Datos, ya que será //Hibernate// quién realizará el mapeo entre el objeto Java (y sus atributos) y la tabla de MySQL (y sus columnas) a la hora de realizar consultas, inserciones, | + | Usaremos |
- | A continuación se muestra el script '' | + | Si optamos por H2, no será necesario llevar a cabo este paso ya que la base de datos será creada integramente por Spring Boot en el momento en que ejecutemos |
- | <file sql opiniones.sql> | + | ==== Definir el modelo de datos ==== |
- | CREATE DATABASE IF NOT EXISTS opiniones; | + | |
- | USE opiniones; | + | |
- | CREATE TABLE IF NOT EXISTS opiniones | + | Como Spring Boot utiliza Hibernate como libreria ORM (Object-Relationship Mapping), para definir el modelo de datos de nuestra base de datos, nos bastará con escribir las clases Java que representarán a los datos en nuestra aplicación web. A través de las anotaciones que veremos a continuación, le daremos las instrucciones a Spring acerca de cómo crear la base de datos de forma transparente para nosotros. |
- | id INT UNSIGNED PRIMARY KEY, | + | |
- | titulo VARCHAR(50) NOT NULL, | + | |
- | texto VARCHAR(50), | + | |
- | fecha DATETIME, | + | |
- | puntuacion INT UNSIGNED | + | |
- | ); | + | |
- | </ | + | |
Así, simplemente tenemos que crear la clase con los atributos y métodos que queramos y añadir las anotaciones que orientarán a // | Así, simplemente tenemos que crear la clase con los atributos y métodos que queramos y añadir las anotaciones que orientarán a // | ||
+ | |||
+ | Usaremos, además, la librería [[https:// | ||
<code java> | <code java> | ||
+ | import lombok.*; | ||
+ | |||
import javax.persistence.*; | import javax.persistence.*; | ||
+ | import java.time.LocalDateTime; | ||
/** | /** | ||
- | | + | |
- | * Se deben definir las anotaciones que indican | + | |
- | * representa esta clase y sus atributos | + | |
* | * | ||
* @author Santiago Faci | * @author Santiago Faci | ||
- | * @version curso 2015-2016 | + | * @version curso 2021 |
*/ | */ | ||
- | @Entity | + | @Data |
- | @Table(name = "opiniones") | + | @AllArgsConstructor |
- | public class Opinion | + | @NoArgsConstructor |
+ | @Entity(name = " | ||
+ | public class Product { | ||
+ | |||
+ | | ||
+ | @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
+ | private long id; | ||
+ | @Column | ||
+ | private String name; | ||
+ | @Column | ||
+ | private String description; | ||
+ | @Column | ||
+ | private String category; | ||
+ | @Column | ||
+ | private float price; | ||
+ | @Column(name = " | ||
+ | private LocalDateTime creationDate; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | > **Recordad que todas las anotaciones Java en el ejemplo anterior son clases que pertenecen al paquete ' | ||
+ | |||
+ | ==== El acceso a la base de datos ==== | ||
+ | |||
+ | Ahora creamos la '' | ||
+ | |||
+ | <code java> | ||
+ | /** | ||
+ | * Repositorio de Productos | ||
+ | * @author Santiago Faci | ||
+ | * @version curso 2021 | ||
+ | */ | ||
+ | @Repository | ||
+ | public interface ProductRepository extends CrudRepository< | ||
+ | |||
+ | Set< | ||
+ | Set< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Conviene echar un vistazo a la [[https:// | ||
+ | |||
+ | ==== Implementación de la lógica de negocio: Los Services ==== | ||
+ | |||
+ | Los Services serán la capa de nuestra aplicación web donde implementaremos toda la lógica de negocio. | ||
+ | |||
+ | Definiremos una interface con todos los métodos que necesitemos: | ||
+ | |||
+ | <code java> | ||
+ | public interface ProductService { | ||
+ | |||
+ | Set< | ||
+ | Set< | ||
+ | Optional< | ||
+ | Product addProduct(Product product); | ||
+ | Product modifyProduct(long id, Product newProduct); | ||
+ | void deleteProduct(long id); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Que implementaremos en la clase // | ||
+ | |||
+ | <code java> | ||
+ | @Service | ||
+ | public class ProductServiceImpl implements ProductService { | ||
+ | |||
+ | @Autowired | ||
+ | private ProductRepository productRepository; | ||
+ | |||
+ | @Override | ||
+ | public Set< | ||
+ | return productRepository.findAll(); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Set< | ||
+ | return productRepository.findByCategory(category); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Optional< | ||
+ | return productRepository.findById(id); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Product addProduct(Product product) { | ||
+ | return productRepository.save(product); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Product modifyProduct(long id, Product newProduct) throws ProductNotFoundException { | ||
+ | Product product = productRepository.findById(id) | ||
+ | .orElseThrow(() -> new ProductNotFoundException(id)); | ||
+ | newProduct.setId(product.getId()); | ||
+ | return productRepository.save(newProduct); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void deleteProduct(long id) throws ProductNotFoundException { | ||
+ | productRepository.findById(id) | ||
+ | .orElseThrow(() -> new ProductNotFoundException(id)); | ||
+ | productRepository.deleteById(id); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Implementación del controller ==== | ||
+ | |||
+ | Antes de continuar, es muy conveniente leerse el siguiente artículo sobre los diferentes [[https:// | ||
+ | |||
+ | Y a continuacuón, | ||
+ | |||
+ | * Obtener todos los productos: Método GET que devuelve toda la colección | ||
+ | * Obtener todos los productos de una categoría determinada: | ||
+ | * Obtener un producto determinado: | ||
+ | * Registrar un nuevo producto: Método POST que registra un nuevo producto en la base de datos | ||
+ | * Modificar un producto: Método PUT que modifica un producto | ||
+ | * Eliminar un producto: Método DELELET que elimina un producto existente | ||
+ | |||
+ | Como veremos, algunas de las operaciones devuelven un error controlado (mediante un gestor de excepciones que se ha definido al final del controlador) cuando el producto solicitado no existe. En esos casos, se devuelve además una respuesta definida en la clase '' | ||
+ | |||
+ | Para entender el siguiente fragmento de código conviene tener en cuenta lo siguiente: | ||
+ | * Cada método anotado define un endpoint que podrá ser invocado por otra aplicación | ||
+ | * Las anotaciones @GetMapping, | ||
+ | * Si el endpoint debe utilizar '' | ||
+ | * Si el endpoint debe utilizar '' | ||
+ | * Si el endpoint debe utilizar '' | ||
+ | * La respuesta será siempre un objeto '' | ||
+ | |||
+ | |||
+ | Consulta [[https:// | ||
+ | |||
+ | <code java> | ||
+ | @RestController | ||
+ | public class ProductController { | ||
+ | |||
+ | @Autowired | ||
+ | private ProductService productService; | ||
+ | |||
+ | @GetMapping("/ | ||
+ | public ResponseEntity< | ||
+ | Set< | ||
+ | if (category.equals("" | ||
+ | products = productService.findAll(); | ||
+ | else | ||
+ | products = productService.findByCategory(category); | ||
+ | |||
+ | return new ResponseEntity<> | ||
+ | } | ||
+ | |||
+ | @GetMapping("/ | ||
+ | public ResponseEntity< | ||
+ | Product product = productService.findById(id) | ||
+ | .orElseThrow(() -> new ProductNotFoundException(id)); | ||
+ | |||
+ | return new ResponseEntity<> | ||
+ | } | ||
+ | |||
+ | @PostMapping("/ | ||
+ | public ResponseEntity< | ||
+ | Product addedProduct = productService.addProduct(product); | ||
+ | return new ResponseEntity<> | ||
+ | } | ||
+ | |||
+ | @PutMapping("/ | ||
+ | public ResponseEntity< | ||
+ | Product product = productService.modifyProduct(id, | ||
+ | return new ResponseEntity<> | ||
+ | } | ||
+ | |||
+ | @DeleteMapping("/ | ||
+ | public ResponseEntity< | ||
+ | productService.deleteProduct(id); | ||
+ | return ResponseEntity.noContent().build(); | ||
+ | } | ||
+ | |||
+ | @ExceptionHandler(ProductNotFoundException.class) | ||
+ | @ResponseBody | ||
+ | @ResponseStatus(HttpStatus.NOT_FOUND) | ||
+ | public ResponseEntity< | ||
+ | Response response = Response.errorResonse(NOT_FOUND, | ||
+ | return new ResponseEntity<> | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Como se puede ver, al final del controlador, | ||
+ | |||
+ | <code java> | ||
+ | public class ProductNotFoundException extends RuntimeException { | ||
+ | |||
+ | public ProductNotFoundException() { | ||
+ | super(); | ||
+ | } | ||
+ | |||
+ | public ProductNotFoundException(String message) { | ||
+ | super(message); | ||
+ | } | ||
+ | |||
+ | public ProductNotFoundException(long id) { | ||
+ | super(" | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | También necesitaremos implementar la clase '' | ||
+ | |||
+ | <code java> | ||
+ | @Data | ||
+ | @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
+ | public class Response { | ||
+ | |||
+ | public static final int NO_ERROR = 0; | ||
+ | public static final int NOT_FOUND = 101; | ||
+ | |||
+ | public static final String NO_MESSAGE = ""; | ||
+ | |||
+ | private Error error; | ||
+ | |||
+ | @Data | ||
+ | @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
+ | static class Error { | ||
+ | private long errorCode; | ||
+ | private String message; | ||
+ | } | ||
+ | |||
+ | public static Response noErrorResponse() { | ||
+ | return new Response(new Error(NO_ERROR, | ||
+ | } | ||
+ | |||
+ | public static Response errorResonse(int errorCode, String errorMessage) { | ||
+ | return new Response(new Error(errorCode, | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== Validación con Spring Boot ===== | ||
+ | |||
+ | Spring Boot dispone de una librería dedicada exclusivamente a la validación de la entrada recibida por una operación de nuestra API. Podemos añadirla incluyendo la correspondiente dependencia en nuestro fichero '' | ||
+ | |||
+ | <code xml> | ||
+ | . . . | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | Y dispondremos de nuevas anotaciones que validarán, automáticamente, | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | |||
+ | <code java> | ||
+ | @Data | ||
+ | @NoArgsConstructor | ||
+ | @AllArgsConstructor | ||
+ | @Entity(name = "products") | ||
+ | public class Product | ||
@Id | @Id | ||
- | @GeneratedValue | + | @GeneratedValue(strategy = GenerationType.IDENTITY) |
- | private | + | private |
@Column | @Column | ||
- | private String | + | |
+ | @NotNull(message = "El campo es obligatorio" | ||
+ | | ||
@Column | @Column | ||
- | private String | + | private String |
@Column | @Column | ||
- | private | + | |
+ | | ||
@Column | @Column | ||
- | private int puntuacion; | + | |
+ | | ||
+ | @Column(name = " | ||
+ | private LocalDateTime creationDate; | ||
+ | |||
+ | @ManyToOne | ||
+ | @JoinColumn(name = " | ||
+ | private Provider provider; | ||
+ | } | ||
+ | </ | ||
- | // Constructor | + | En el método del controlador podremos utilizar la anotación '' |
- | // Getters y Setters | + | |
+ | <code java> | ||
+ | public class ProductController { | ||
+ | | ||
+ | @PostMapping(" | ||
+ | public ResponseEntity< | ||
+ | @Valid @RequestBody Product | ||
+ | Product newProduct = productService.addProduct(product, | ||
+ | return ResponseEntity.status(HttpStatus.CREATED).body(newProduct); | ||
+ | } | ||
. . . | . . . | ||
} | } | ||
</ | </ | ||
- | > **Recordad que todas las anotaciones Java en el ejemplo anterior son clases que pertenecen al paquete | + | Y el control de errores de esa validación se podrá gestionar capturando la excepción |
- | ==== El Acceso a la Base de Datos ==== | + | <code java> |
+ | public class ProductController { | ||
+ | . . . | ||
+ | @ExceptionHandler(MethodArgumentNotValidException.class) | ||
+ | public ResponseEntity< | ||
+ | Map< | ||
+ | manve.getBindingResult().getAllErrors().forEach(error -> { | ||
+ | String fieldName | ||
+ | String message | ||
+ | errors.put(fieldName, | ||
+ | }); | ||
- | Ahora creamos la '' | + | return ResponseEntity.badRequest().body(ErrorResponse.validationError(errors)); |
+ | } | ||
+ | . . . | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | La clase '' | ||
+ | |||
+ | Esa clase '' | ||
<code java> | <code java> | ||
- | /** | + | @Data |
- | * Clase que hace de interfaz con la Base de Datos | + | public |
- | * Al heredar de CrudRepository se asumen una serie de operaciones | + | |
- | * para registrar o eliminar contenido (save/ | + | |
- | * Se pueden añadir operaciones ya preparadas como las que hay de ejemplo ya hechas | + | |
- | * | + | |
- | | + | |
- | * @version curso 2015-2016 | + | |
- | */ | + | |
- | public | + | |
- | | + | |
- | | + | private String message; |
+ | private Map< | ||
+ | |||
+ | private ErrorResponse(int errorCode, String errorMessage) { | ||
+ | code = errorCode; | ||
+ | message = errorMessage; | ||
+ | errors = new HashMap<> | ||
+ | | ||
+ | |||
+ | private ErrorResponse(int code, String message, Map<String, String> errors) { | ||
+ | this.code = code; | ||
+ | this.message = message; | ||
+ | this.errors = errors; | ||
+ | } | ||
+ | |||
+ | public static ErrorResponse generalError(int code, String message) { | ||
+ | return new ErrorResponse(code, | ||
+ | } | ||
+ | |||
+ | public static ErrorResponse validationError(Map< | ||
+ | return new ErrorResponse(104, | ||
+ | } | ||
} | } | ||
</ | </ | ||
- | ==== Implementación del Controller ==== | + | Asi, si alguna de las reglas de validación se incumple al registrar un nuevo Producto, recibiremos como respuesta algo como esto: |
- | Por último, crearemos la clase que hará de '' | + | <code javascript> |
+ | { | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | } | ||
+ | } | ||
+ | </ | ||
- | En este caso hemos creado tres operaciones: | + | ===== Relaciones entre clases en el modelo de datos ===== |
- | * getOpiniones(): | + | A diferencia |
- | * getOpiniones(int puntuacion): | + | |
- | * addOpinion(String titulo, String texto, int puntuacion): | + | |
- | Cada una de las operaciones tienen una URL de mapeo que nos permite acceder a las mismas desde cualquier cliente (navegador, aplicación Java, aplicación Android). Por ejemplo, si quisieramos obtener todas las opiniones que tienen una determinada puntuación utilizaríamos | + | Con las anotaciones y atributos que definamos para cada tipo de relación, será Spring Boot quien se encargará de definir |
- | * http:// | + | === OneToMany |
- | * http:// | + | |
- | * http:// | + | Supongamos el modelo de clases de una API donde existen las clases '' |
- | * http:// | + | |
+ | Asi es como definiremos esa relación en el lado del Producto: | ||
<code java> | <code java> | ||
- | /** | + | . . . |
- | * Controlador para las opiniones | + | public class Product { |
- | * Contendrá todos los métodos que realicen operaciones sobre opiniones de los usuarios | + | . . . |
- | * | + | @ManyToOne |
- | | + | @JoinColumn(name = " |
- | * @version curso 2015-2016 | + | |
- | */ | + | . . . |
- | @RestController | + | } |
- | public class OpinionController { | + | </ |
- | @Autowired | + | Y asi en el lado del '' |
- | private OpinionRepository repository; | + | |
- | /** | + | <code java> |
- | * Obtiene todas las opiniones de los usuarios | + | . . . |
- | * @return | + | public class Provider { |
- | */ | + | . . . |
- | @RequestMapping("/opiniones") | + | @OneToMany(mappedBy = "provider") |
- | | + | |
+ | private Set< | ||
+ | . . . | ||
+ | } | ||
+ | </ | ||
- | List< | + | === ManyToMany === |
- | return listaOpiniones; | + | |
- | } | + | |
- | /** | + | Supongamos ahora que también existe |
- | * Obtiene todas las opiniones con una puntuacion determinada | + | |
- | * @param puntuacion | + | |
- | * @return | + | |
- | */ | + | |
- | @RequestMapping("/ | + | |
- | public List< | + | |
- | List<Opinion> listaOpiniones | + | En el lado del Producto la definiríamos asi: |
- | return | + | |
+ | <code java> | ||
+ | . . . | ||
+ | public class Product { | ||
+ | . . . | ||
+ | @ManyToMany | ||
+ | @JoinTable(name | ||
+ | | ||
+ | | ||
+ | private List< | ||
+ | | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Y en el lado del pedido: | ||
+ | |||
+ | <code java> | ||
+ | . . . | ||
+ | public class Order { | ||
+ | . . . | ||
+ | @ManyToMany(mappedBy = " | ||
+ | private List< | ||
+ | . . . | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== ModelMapper ===== | ||
+ | |||
+ | [[https:// | ||
+ | |||
+ | Necesitaremos mapear un objeto en otro cuando queramos " | ||
+ | |||
+ | La principal ventaja de esta librería es el ahorro en código repetitivo (boilerplate code) puesto que algunos objetos podrían tener decenas de atributos y eso resultaría en decenas de líneas que no aportan realmente valor y dificultan la lectura del código. | ||
+ | |||
+ | <code xml> | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | Debemos instanciar la librería para poder utilizarla donde corresponda: | ||
+ | |||
+ | <code java> | ||
+ | @Configuration | ||
+ | public class AppConfig { | ||
+ | |||
+ | @Bean | ||
+ | public ModelMapper modelMapper() { | ||
+ | return | ||
} | } | ||
+ | } | ||
+ | </ | ||
- | /** | + | Y ya en la capa '' |
- | * Registra una nueva opinión | + | |
- | * @param titulo | + | |
- | * @param texto | + | |
- | * @param puntuacion | + | |
- | */ | + | |
- | @RequestMapping("/ | + | |
- | public void addOpinion(@RequestParam(value = " | + | |
- | | + | |
- | | + | |
- | Opinion opinion = new Opinion(); | + | <code java> |
- | | + | public class ProductServiceImpl implements ProductService { |
- | | + | . . . |
- | | + | @Autowired |
- | | + | private ModelMapper modelMapper; |
+ | . . . | ||
+ | |||
+ | @Override | ||
+ | public Product modifyProduct(long productId, Product product) throws ProductNotFoundException { | ||
+ | | ||
+ | .orElseThrow(ProductNotFoundException:: | ||
+ | | ||
+ | | ||
- | | + | |
} | } | ||
+ | . . . | ||
} | } | ||
</ | </ | ||
- | ==== Ejecución del servidor ==== | ||
- | Una vez terminado todo, para lanzar el servidor tenemos dos opciones: | + | ===== Uso de DTOs ===== |
- | * Desde el propio IDE, ejecutando '' | + | |
- | * Utilizando el jar que podemos generar con el comando '' | + | |
- | {{ youtube> | + | Una vez definidas las clases de nuestro modelo de datos (con todos los atributos y sus relaciones), |
- | ==== Depuración | + | Es por eso que surge el concepto |
- | La primera aproximación a la hora de depurar los Servicios Web desarrollados es utilizar el navegador introduciendo las URLs esperando obtener la salida apropiada y comprobar asi que todo funciona correctamente. Es bastante cómodo pero fácil de usar y eficiente, hasta un punto. | + | <file java UserOrderDTO> |
+ | @Data | ||
+ | @NoArgsConstructor | ||
+ | @AllArgsConstructor | ||
+ | public class UserOrderDTO { | ||
+ | private long userId; | ||
+ | private String orderNumber; | ||
+ | private String[] productNames; | ||
+ | } | ||
+ | </ | ||
- | Si lo que queremos es depurar totalmente, y de una forma profesional, nuestros Servicios Web, tenemos | + | Y será en la capa '' |
< | < | ||
- | {{ postman.png }} | + | {{ dto.png }} |
- | < | + | < |
+ | |||
+ | En el siguiente ejemplo tenemos el modelo que define un Producto en una API: | ||
+ | |||
+ | <code java> | ||
+ | @Data | ||
+ | @NoArgsConstructor | ||
+ | @AllArgsConstructor | ||
+ | @Entity(name = " | ||
+ | public class Product { | ||
+ | |||
+ | @Id | ||
+ | @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
+ | private long id; | ||
+ | @Column | ||
+ | @NotBlank(message = "El campo no puede estar vacío" | ||
+ | @NotNull(message = "El campo es obligatorio" | ||
+ | private String name; | ||
+ | @Column | ||
+ | private String description; | ||
+ | @Column | ||
+ | @NotNull | ||
+ | private String category; | ||
+ | @Column | ||
+ | @Min(value = 0) | ||
+ | private int price; | ||
+ | @Column(name = " | ||
+ | private LocalDateTime creationDate; | ||
+ | |||
+ | @ManyToOne | ||
+ | @JoinColumn(name = " | ||
+ | private Provider provider; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Y a continuación el DTO que hemos decidido definir solamente con la información necesaria que se debe enviar para registrar un nuevo producto (El resto de campos se podrán calcular/ | ||
+ | |||
+ | <code java> | ||
+ | @Data | ||
+ | @NoArgsConstructor | ||
+ | @AllArgsConstructor | ||
+ | public class ProductInDTO { | ||
+ | |||
+ | @NotNull | ||
+ | private String name; | ||
+ | @NotNull | ||
+ | private String description; | ||
+ | private String category; | ||
+ | @Min(value = 0, message = "El precio debe ser mayor que cero" | ||
+ | private float price; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Asi, en el '' | ||
+ | |||
+ | <code java> | ||
+ | @PostMapping("/ | ||
+ | public ResponseEntity< | ||
+ | Product newProduct = productService.addProduct(productDTO, | ||
+ | return ResponseEntity.status(HttpStatus.CREATED).body(newProduct); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Será la capa '' | ||
+ | |||
+ | <code java> | ||
+ | @Service | ||
+ | public class ProductService { | ||
+ | . . . | ||
+ | | ||
+ | | ||
+ | . . . | ||
+ | | ||
+ | | ||
+ | | ||
+ | // Mapeamos los atributos del Dto al objeto del modelo | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | } | ||
+ | . . . | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== Trazabilidad. Logs de aplicación ===== | ||
+ | |||
+ | Si queremos mantener la trazabilidad de la ejecución de nuestra aplicación (y esto sería válido tanto para la aplicación web como para el proyecto de servicio web que estamos haciendo ahora), tenemos que configurar cómo queremos que se registren los sucesos y trazas de la ejecución. | ||
+ | |||
+ | Por defecto, cuando ejecutamos la aplicación en modo desarrollo, y también ocurre asi cuando se hace en producción, | ||
+ | |||
+ | Para eso, simplemente tenemos que crear un fichero llamado '' | ||
+ | |||
+ | <code xml> | ||
+ | <?xml version=" | ||
+ | < | ||
+ | <!-- Propiedades que se usará para indicar dónde almacenar los logs y cómo se llama el fichero --> | ||
+ | < | ||
+ | < | ||
+ | |||
+ | <!-- Configuración del log que aparece por consola: Console appender --> | ||
+ | < | ||
+ | class=" | ||
+ | <layout class=" | ||
+ | <!-- Configuración de la traza --> | ||
+ | < | ||
+ | %white(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %-60.60yellow(%C{20}): | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | <!-- Configuración para que se almacene el log en un fichero: File Appender --> | ||
+ | < | ||
+ | class=" | ||
+ | < | ||
+ | < | ||
+ | class=" | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | <!-- Política de rotado de logs: diario y cuando el fichero llegue a los 10 MB --> | ||
+ | < | ||
+ | class=" | ||
+ | < | ||
+ | < | ||
+ | class=" | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | <!-- Define el nivel de log para cada appender --> | ||
+ | <root level=" | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | Asi es como quedaría el fichero log resultante con las trazas de ejecución de la aplicación: | ||
+ | |||
+ | <code bash> | ||
+ | santi@zenbook: | ||
+ | santi@zenbook: | ||
+ | total 56 | ||
+ | drwxr-xr-x | ||
+ | drwxr-xr-x | ||
+ | -rw-r--r-- | ||
+ | </ | ||
+ | |||
+ | Hasta el momento, la mayoría de las trazas que se registran las emite el propio framework Spring Boot. Pero nosotros tenemos la oportunidad de registrar las que consideremos oportunas utilizando la clase '' | ||
+ | |||
+ | <code java> | ||
+ | private final Logger logger = LoggerFactory.getLogger(ProductController.class); | ||
+ | </ | ||
+ | |||
+ | Por ejemplo, a continuación se registran un par de trazas para que quede constancia de que se ha invocado a la operación que permite listar los productos del catálogo: | ||
+ | |||
+ | <code java> | ||
+ | @GetMapping("/ | ||
+ | public ResponseEntity< | ||
+ | logger.info(" | ||
+ | Set< | ||
+ | if (category.equals("" | ||
+ | products = productService.findAll(); | ||
+ | else | ||
+ | products = productService.findByCategory(category); | ||
+ | |||
+ | logger.info(" | ||
+ | return new ResponseEntity<> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | También en el caso de que se produzca alguna excepción, será interesante registrar una traza e incluso podremos incluir la propia excepción: | ||
+ | |||
+ | <code java> | ||
+ | @ExceptionHandler(ProductNotFoundException.class) | ||
+ | @ResponseBody | ||
+ | @ResponseStatus(HttpStatus.NOT_FOUND) | ||
+ | public ResponseEntity< | ||
+ | Response response = Response.errorResonse(NOT_FOUND, | ||
+ | logger.error(pnfe.getMessage(), | ||
+ | return new ResponseEntity<> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | A continuación podemos ver cómo quedará la traza del ejemplo anterior registrada en el log de la aplicación: | ||
+ | |||
+ | <code bash> | ||
+ | 2021-03-02 21: | ||
+ | com.sanvalero.myshop.exception.ProductNotFoundException: | ||
+ | at com.sanvalero.myshop.service.ProductServiceImpl.lambda$deleteProduct$1(ProductServiceImpl.java: | ||
+ | at java.base/ | ||
+ | at com.sanvalero.myshop.service.ProductServiceImpl.deleteProduct(ProductServiceImpl.java: | ||
+ | at com.sanvalero.myshop.controller.ProductController.deleteProduct(ProductController.java: | ||
+ | at java.base/ | ||
+ | at java.base/ | ||
+ | at java.base/ | ||
+ | at java.base/ | ||
+ | at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java: | ||
+ | at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java: | ||
+ | at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java: | ||
+ | at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java: | ||
+ | at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java: | ||
+ | at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java: | ||
+ | at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java: | ||
+ | at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java: | ||
+ | at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java: | ||
+ | at org.springframework.web.servlet.FrameworkServlet.doDelete(FrameworkServlet.java: | ||
+ | at javax.servlet.http.HttpServlet.service(HttpServlet.java: | ||
+ | at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java: | ||
+ | at javax.servlet.http.HttpServlet.service(HttpServlet.java: | ||
+ | . . . | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | <code properties> | ||
+ | . . . | ||
+ | logging.level.org.springframework=WARN | ||
+ | logging.level.org.hibernate=WARN | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | ====== Probar los Servicios Web ====== | ||
+ | |||
+ | Si antes de integrar una aplicación con un determinado servicio web, queremos probar éste para comprobar cómo funcionar, tenemos que usar aplicaciones destinadas para ese propósito, como [[https:// | ||
+ | |||
+ | Para el servicio web desarrollado a lo largo de este tema, vamos a ver cómo se definirían una serie de pruebas para todos sus endpoints utilizando Postman. | ||
+ | |||
+ | Crearemos una colección y diferentes requests que nos permitan probar todos los endpoints desarrollados en este proyecto (Pincha en la captura para aumentarla y ver cómo configurar cada uno de los casos) | ||
+ | |||
+ | < | ||
+ | {{ getProducts.png }} | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | < | ||
+ | {{ getProductsByCategory.png }} | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | |||
+ | < | ||
+ | {{ addProduct.png }} | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | |||
+ | < | ||
+ | {{ modifyProduct.png }} | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | |||
+ | < | ||
+ | {{ deleteProduct.png }} | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | |||
+ | < | ||
+ | {{ deleteProductError.png }} | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | ---- | ||
+ | |||
+ | ====== Ejercicios ====== | ||
+ | |||
+ | - Crea una aplicación que ofrezca unos servicios web para la gestión de vuelos. La aplicación tendrá una base de datos de vuelos donde almacenará: | ||
+ | - Búsqueda de vuelos, pudiendo filtrar por origen, destino y numero de escalas | ||
+ | - Registro de un nuevo vuelo | ||
+ | - Dar de baja un vuelo | ||
+ | - Dar de baja todos los vuelos a un destino determinado | ||
+ | - Modificar un vuelo\\ \\ | ||
+ | - Crea una API que ofrezca servicios web de búsqueda de hoteles. Se mantendrá un base de datos de hoteles (nombre, descripción, | ||
+ | - Búsqueda de hotel por localidad o categoría | ||
+ | - Búsqueda de habitaciones de un hotel por tamaño y precio (rango minimo-> | ||
+ | - Registrar un nuevo hotel | ||
+ | - Registrar una nueva habitación a un hotel | ||
+ | - Eliminar una habitación determinada de un hotel | ||
+ | - Modificar una habitación para indicar que está ocupada | ||
+ | ====== Proyectos de ejemplo ====== | ||
+ | |||
+ | Todos los proyectos de ejemplo de esta parte están en el [[http:// | ||
+ | |||
+ | Los proyectos que se vayan haciendo en clase estarán disponibles en el [[http:// | ||
+ | |||
+ | Para manejaros con Git recordad que tenéis una serie de videotutoriales en [[https:// | ||
---- | ---- | ||
- | (c) 2016-2020 Santiago Faci | + | (c) 2016-2023 Santiago Faci |
apuntes/spring.1594981086.txt.gz · Last modified: 17/07/2020 10:18 by Santiago Faci