User Tools

Site Tools


apuntes:servicios_web

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
apuntes:servicios_web [2018/02/15 18:12] – [Consumir un servicio web desde Django] Santiago Faciapuntes:servicios_web [2021/10/21 06:56] (current) Santiago Faci
Line 1: Line 1:
-===== Servicios Web =====+====== Creación de servicios web. Spring Boot ======
  
 ==== ¿Qué son los servicios web? ==== ==== ¿Qué son los servicios web? ====
Line 76: Line 76:
 </code> </code>
  
-===== Spring framework =====+===== Desarrollo de servicios web con Spring Boot =====
  
-<figure> +En el punto anterior sobre [[https://datos.codeandcoke.com/apuntes:spring-web#ejecucion_de_la_aplicacion_web|Creación de aplicaciones web. Spring Boot]] vimos cómo comenzar el desarrollo de una aplicación web interactiva. Ahora se trata de implementar un proyecto muy similar, puesto que se puede considerar una aplicación web pero en este caso implementaremos un Controlador REST en lugar de un controlador web.
-{{ spring-logo.png }} +
-<caption>Framework Spring</caption></figure>+
  
-[[http://www.spring.io|Spring]] es un framework de Java para el desarrollo de aplicaciones web. En nuestro caso, lo que queremos construir es una pequeña aplicación web que nos permita disponer de servicios web para comunicar nuestra aplicación con una Base de Datos y que podamossi asi lo queremosproporcionar algo de lógica en el lado servidor cuando sea necesario. Para eso utilizaremos //Spring Boot// que es una parte de este framework que facilita bastante el trabajo para casos como el que a nosotros nos interesa.+Podemos seguir el mismo guión que en el punto anterior hasta el momento en que se define el @Controller y se empieza a trabajar con las plantillas HTML. Ahora se trata de implementar servicios web por lo que definiremosen su lugaruna clase que hará de @RestController y no habrá plantillas HTML puesto que la comunicación se hará utilizando JSON (conversión que Spring Boot hará automáticamente) y no será usuario-máquina sino máquina-maquina.
  
-Para eso, lo primero que haremos será utilizar el [[http://start.spring.io|Spring Initializr]] para preparar el proyecto inicial sobre el que luego diseñaremos nuestra pequeña aplicación web. Para ello podemos seguir el videotutorial que aparece al final de este apartado. +Partimos entonces de un proyecto de aplicación con [[https://start.spring.io|Spring Initializr]]:
- +
-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 ====
Line 94: Line 90:
 <file java application.properties> <file java application.properties>
 # Configuración para el acceso a la Base de Datos # Configuración para el acceso a la Base de Datos
-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:mysql://localhost:3306/opiniones +spring.datasource.url=jdbc:mysql://localhost:3306/myshoponline 
-spring.datasource.username=root +spring.datasource.username=myshopuser 
-spring.datasource.password=+spring.datasource.password=mypassword
 spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.driverClassName=com.mysql.jdbc.Driver
 </file> </file>
  
-Sobre el fichero ''build.gradle'' tendremos que realizar algunos cambios:+Hay que tener en cuenta que la propiedad //spring.jpa.hibernate.ddl-auto// se utiliza para que la base de datos se genere automáticamente en cada arranque de la aplicación. Esto nos interesará cuando estemos en desarrollo pero no cuando queramos desplegarla en producción. Por lo que tendremos que tener cuidado y controlar el valor de dicha propiedad. 
  
-<file java build.gradle> +  * //none//Para indicar que no queremos que genere la base de datos 
-. . . +  * //update//Si queremos que la genere de nuevo en cada arranque 
-apply plugin: 'java' +  * //create//Si queremos que la cree pero que no la genere de nuevo si ya existe
-apply plugin'idea' +
-apply plugin'spring-boot' +
-apply plugin'war'+
  
-jar { +==== Definir la base de datos ====
-  baseName 'opiniones' +
-  version '0.0.1' +
-}+
  
-repositories { +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 atributosy la tabla de MySQL (y sus columnas) a la hora de realizar consultas, inserciones, modificaciones o borrados. E incluso a la hora de crear las tablas, puesto que bastará con definir nuestro modelo de clases con las anotaciones apropiadas para que Spring pueda crearlas en base a éstas (y porque tenemos la opción //spring.jpa.hibernate.ddl-auto=updat//e en el fichero de configuración). Cuando ya no queramos que Spring genere automáticamente la base de datos en cada arranque (por ejemplo, en producción), tendremos que poner cambiar esa opción a valor //none//.
-  mavenCentral() +
-}+
  
-dependencies { +Simplemente tendremos que crear la base de datosY 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).
-  compile('org.springframework.boot:spring-boot-starter-web') +
-  compile('org.springframework.boot:spring-boot-starter-data-jpa') +
-  compile('mysql:mysql-connector-java:5.1.16'+
-  providedRuntime("org.springframework.boot:spring-boot-starter-tomcat"+
-}+
  
-configurations { +<code sql> 
-  providedRuntime +CREATE DATABASE myshoponline; 
-} +CREATE USER myshopuser IDENTIFIED BY 'mypassword'; 
-</file>+GRANT ALL PRIVILEGES ON myshoponline.* TO myshopuser; 
 +</code>
  
-Ahora, modificaremos la clase principal ''Application''. De esta forma podremos iniciar nuestro servidor directamente desde la consola de IntelliJ o bien contenida dentro de un servidor de aplicaciones como //Tomcat//, aunque no lo haremos así en este caso.+Usaremos //myshopuser// como usuario y //mypassword// como usuario y contraseña en el fichero de configuración (//application.properties//) del proyecto.
  
-Conviene prestar atención a los comentarios que he dejado en esta clasedonde se explica cómo lanzar la aplicación servidor una vez que este lista.+Asísimplemente tenemos que crear la clase con los atributos y métodos que queramos y añadir las anotaciones que orientarán a //Hibernate// para saber a qué tabla corresponden los objetos de la clase y a qué columnas sus atributos.
  
-<file java Application.java>+Usaremos, además, la librería [[https://projectlombok.org/|lombok]] para la generación automática de //getters//, //setters// y constructores. 
 + 
 +<code java
 +import lombok.*; 
 + 
 +import javax.persistence.*; 
 +import java.time.LocalDateTime;
 /** /**
- Clase que lanza la aplicación + Producto de la tienda online
- * +
- * Cómo compilar/ejecutar la aplicación: +
-  - Si se hacen cambios en el build.gradle conviene ejecutar (desde la terminal): +
-      - ./gradlew idea +
-      - ./gradlew build +
-  - Una vez compilado se pueden ejecutar por dos vias +
-      - ./gradlew bootRun +
-      - También se puede ejecutar el jar (con java -jar) que se genera en la carpeta 'build/libs' según el fichero 'build.gradle' +
- * +
-  El proyecto parte de un proyecto base creado con la herramienta Spring Initializr, +
-  disponible en https://start.spring.io/. Conviene seleccionar ya de inicio las dependencias de Web, JPA y MySQL +
-  De todas formas se pueden añadir luego a gradle y sincronizar el proyecto como se indica más arriba+
  *  *
  * @author Santiago Faci  * @author Santiago Faci
- * @version curso 2015-2016+ * @version curso 2021
  */  */
-@SpringBootApplication +@Data 
-public class Application extends SpringBootServletInitializer {+@AllArgsConstructor 
 +@NoArgsConstructor 
 +@Entity(name = "products") 
 +public class Product {
  
-  public static void main(String[] args{ +    @Id 
-    SpringApplication.run(Application.class, args); +    @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 = "creation_date") 
 +    private LocalDateTime creationDate
 +} 
 +</code>
  
-  @Override +> **Recordad que todas las anotaciones Java en el ejemplo anterior son clases que pertenecen al paquete 'javax.persistence'. Tened cuidado de no importar las mismas clases que existen en otros paquetes, aunque estén relacionados con Spring**
-  protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { +
-    return application.sources(applicationClass); +
-  }+
  
-  private static Class<Application> applicationClass Application.class; +==== El acceso a la base de datos ====
-+
-</file>+
  
-==== Definir la Base de Datos ====+Ahora creamos la ''interface'' donde se definirán los métodos que permitirán acceder a la Base de Datos. En este caso nos basta con definir las cabeceras de los mismos, puesto que se trata de una ''interface''. Será el framework el que se encargue de su implementación. En este caso hemos definido métodos para obtener todas las puntuaciones y otro para obtener las que tengan una puntuación determinada. Además, podremos contar con que tenemos las operaciones que nos permiten registrar/modificar (''save'') y eliminar (''delete'') información de la Base de Datos.
  
-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, insercionesmodificaciones o borrados.+<code java> 
 +/**  
 + * Repositorio de Productos 
 + * @author Santiago Faci 
 + * @version curso 2021 
 + */ 
 +@Repository 
 +public interface ProductRepository extends CrudRepository<ProductLong> {
  
-A continuación se muestra el script ''SQL'' que creará la tabla de la base de datos que usaremos para este ejemplo. Y más adelante la clase Java que se utilizará para hacer el mapeo con dicha tabla.+    Set<Product> findAll(); 
 +    Set<Product> findByCategory(String category); 
 +
 +</code>
  
-<file sql opiniones.sql> +==== Implementación de la lógica de negocio: Los Services ====
-CREATE DATABASE IF NOT EXISTS opiniones; +
-USE opiniones;+
  
-CREATE TABLE IF NOT EXISTS opiniones ( +Los Services serán la capa de nuestra aplicación web donde implementaremos toda la lógica de negocio. 
-  id INT UNSIGNED PRIMARY KEY, +
-  titulo VARCHAR(50) NOT NULL, +
-  texto VARCHAR(50), +
-  fecha DATETIME, +
-  puntuacion INT UNSIGNED +
-); +
-</file>+
  
-Así, simplemente tenemos que crear la clase con los atributos y métodos que queramos y añadir las anotaciones que orientarán a //Hibernate// para saber a qué tabla corresponden los objetos de la clase y a qué columnas sus atributos.+Definiremos una interface con todos los métodos que necesitemos:
  
 <code java> <code java>
-/** +public interface ProductService {
- * Opinion que los usuarios tienen sobre un monumento +
- * Se deben definir las anotaciones que indican la tabla y columnas a las que +
- * representa esta clase y sus atributos +
- * +
- * @author Santiago Faci +
- * @version curso 2015-2016 +
- */ +
-@Entity +
-@Table(name = "opiniones"+
-public class Opinion {+
  
-  @Id +    Set<Product> findAll()
-  @GeneratedValue +    Set<Product> findByCategory(String category)
-  private int id+    Optional<Product> findById(long id); 
-  @Column +    Product addProduct(Product product)
-  private String titulo+    Product modifyProduct(long id, Product newProduct)
-  @Column +    void deleteProduct(long id);
-  private String texto+
-  @Column +
-  private Date fecha+
-  @Column +
-  private int puntuacion; +
- +
-  // Constructor +
-  // Getters y Setters +
-  . . .+
 } }
 </code> </code>
  
-==== El Acceso a la Base de Datos ==== +Que implementaremos en la clase //ProductServiceImpl//
- +
-Ahora creamos la ''interface'' donde se definirán los métodos que permitirán acceder a la Base de Datos. En este caso nos basta con definir las cabeceras de los mismos, puesto que se trata de una ''interface''. Será el framework el que se encargue de su implementación. En este caso hemos definido métodos para obtener todas las puntuaciones y otro para obtener las que tengan una puntuación determinada. Además, podremos contar con que tenemos las operaciones que nos permiten registrar/modificar (''save'') y eliminar (''delete'') información de la Base de Datos.+
  
 <code java> <code java>
-/** +@Service 
- * Clase que hace de interfaz con la Base de Datos +public class ProductServiceImpl implements ProductService {
- * Al heredar de CrudRepository se asumen una serie de operaciones +
- * para registrar o eliminar contenido (save/delete) +
- * Se pueden añadir operaciones ya preparadas como las que hay de ejemplo ya hechas +
- * +
- @author Santiago Faci +
- * @version curso 2015-2016 +
- */ +
-public interface OpinionRepository extends CrudRepository<Opinion, Integer> {+
  
-  List<Opinion> findAll(); +    @Autowired 
-  List<OpinionfindByPuntuacion(int puntuacion);+    private ProductRepository productRepository; 
 + 
 +    @Override 
 +    public Set<ProductfindAll() { 
 +        return productRepository.findAll(); 
 +    } 
 + 
 +    @Override 
 +    public Set<ProductfindByCategory(String category) { 
 +        return productRepository.findByCategory(category); 
 +    } 
 + 
 +    @Override 
 +    public Optional<Product> findById(long id) { 
 +        return productRepository.findById(id); 
 +    } 
 + 
 +    @Override 
 +    public Product addProduct(Product product) { 
 +        return productRepository.save(product); 
 +    } 
 + 
 +    @Override 
 +    public Product modifyProduct(long id, Product newProduct) { 
 +        Product product = productRepository.findById(id) 
 +                .orElseThrow(() -> new ProductNotFoundException(id)); 
 +        newProduct.setId(product.getId()); 
 +        return productRepository.save(newProduct); 
 +    } 
 + 
 +    @Override 
 +    public void deleteProduct(long id) { 
 +        productRepository.findById(id) 
 +                .orElseThrow(() -> new ProductNotFoundException(id)); 
 +        productRepository.deleteById(id); 
 +    }
 } }
 </code> </code>
  
-==== Implementación del Controller ====+==== Implementación del controller ====
  
-Por últimocrearemos la clase que hará de ''Controller'' de la aplicaciónEn ella introduciremos los métodos con las operaciones que queremos que nuestros usuarios puedan realizar, programaremos la lógica que necesitemos y accederemos a los datos a través del ''OpinionRepository'' que hemos creado en el paso anterior.+Antes de continuares muy conveniente leerse el siguiente artículo sobre los diferentes [[https://www.dariawan.com/tutorials/rest/http-methods-spring-restful-services/|métodos HTTP en servicios web REST]]. Explica muy claramente cómo deben ser las diferentes operaciones que se pueden llevar cabo sobre los recursos del sistema.
  
-En este caso hemos creado tres operaciones:+Y a continuacuón, en esta ocasión definiremos un controlador REST. En él se han definido diferentes endpoints que permite ejecutar las siguientes operaciones:
  
-  * getOpiniones()Devuelve todas las opiniones de la base de datos +  * Obtener todos los productosMétodo GET que devuelve toda la colección 
-  * getOpiniones(int puntuacion)Devuelve las opiniones que tienen una determinada puntuacion +  * Obtener todos los productos de una categoría determinada: Método GET que devuelve la colección filtrada por el campo categoria 
-  * addOpinion(String titulo, String texto, int puntuacion)Registra una nueva opinión en la base de datos+  * Obtener un producto determinadoMétodo GET que devuelve un objeto determinado utilizando un parámetro Path  
 +  * Registrar un nuevo productoMé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
  
-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 ejemplosi quisieramos obtener todas las opiniones que tienen una determinada puntuación utilizaríamos la siguiente URL: http://localhost:8082/opiniones_puntuacion?puntuacion=4 (cambiando ''localhost'' por la IP o nombre del servidor que corresponda en cada caso)Más adelante se verá cómo hacerlo desde una aplicación Android pero es posible probar nuestro servidor accediendo a estas URLs directamente desde el navegador, de forma que podamos comprobar que todo funciona correctamente antes de seguir.+Como veremos, algunas de las operaciones devuelven un error controlado (mediante un gestor de excepciones que se ha definido al final del controladorcuando el producto solicitado no existeEn esos casosse devuelve además una respuesta definida en la clase ''Response'' para notificar el código de error y mensaje de negocio, a parte del [[https://developer.mozilla.org/es/docs/Web/HTTP/Status|código de estado HTTP correspondiente]].
  
-  * http://localhost:8080/opiniones +Para entender el siguiente fragmento de código conviene tener en cuenta lo siguiente
-  * http://localhost:8080/opiniones_puntuacion +  * Cada método anotado define un endpoint que podrá ser invocado por otra aplicación 
-  * http://localhost:8080/opiniones_puntuacion?puntuacion=4 +  * Las anotaciones @GetMapping, @PostMapping, @PutMapping y @DeleteMapping definen el método (GET, POST, PUT, DELETE) y la URL de dicho endpoint.  
-  * http://localhost:8080/add_opinion?titulo=eltitulo&texto=eltexto&puntuacion=10+  * Si el endpoint debe utilizar ''Path Params'' vendrán definidos en la URL y también en el método como ''@PathVariable'' 
 +  * Si el endpoint debe utilizar ''Query Params'' vendrán definidos solamente en el método como ''@RequestParam'' 
 +  * Si el endpoint debe utilizar ''Body Params'' vendrán definidos solamente en el método como ''@RequestBody'' 
 +  * La respuesta será siempre un objeto ''ResponseEntity'' que contedrá la información de respuesta y un código HTTP
  
 <code java> <code java>
-/** 
- * Controlador para las opiniones 
- * Contendrá todos los métodos que realicen operaciones sobre opiniones de los usuarios 
- * 
- * @author Santiago Faci 
- * @version curso 2015-2016 
- */ 
 @RestController @RestController
-public class OpinionController {+public class ProductController {
  
-  @Autowired +    @Autowired 
-  private OpinionRepository repository;+    private ProductService productService;
  
-  /** +    @GetMapping("/products") 
-   * Obtiene todas las opiniones de los usuarios +    public ResponseEntity<Set<Product>getProducts(@RequestParam(value = "category", defaultValue = "") String category) { 
-   @return +        Set<Product> products = null; 
-   */ +        if (category.equals("")) 
-  @RequestMapping("/opiniones") +            products = productService.findAll(); 
-  public List<OpiniongetOpiniones() {+        else 
 +            products = productService.findByCategory(category);
  
-    List<OpinionlistaOpiniones = repository.findAll(); +        return new ResponseEntity<>(products, HttpStatus.OK); 
-    return listaOpiniones; +    }
-  }+
  
-  /** +    @GetMapping("/products/{id}") 
-   * Obtiene todas las opiniones con una puntuacion determinada +    public ResponseEntity<ProductgetProduct(@PathVariable long id) { 
-   @param puntuacion +        Product product = productService.findById(id) 
-   * @return +                .orElseThrow(() -> new ProductNotFoundException(id));
-   */ +
-  @RequestMapping("/opiniones_puntuacion") +
-  public List<OpiniongetOpiniones(int puntuacion) {+
  
-    List<OpinionlistaOpiniones = repository.findByPuntuacion(puntuacion); +        return new ResponseEntity<>(product, HttpStatus.OK); 
-    return listaOpiniones; +    }
-  }+
  
-  /** +    @PostMapping("/products") 
-   * Registra una nueva opinión en la Base de Datos +    public ResponseEntity<Product> addProduct(@RequestBody Product product{ 
-   * @param titulo +        Product addedProduct = productService.addProduct(product); 
-   * @param texto +        return new ResponseEntity<>(addedProductHttpStatus.OK)
-   * @param puntuacion +    }
-   */ +
-  @RequestMapping("/add_opinion") +
-  public void addOpinion(@RequestParam(value = "titulo", defaultValue = "nada"String titulo, +
-                         @RequestParam(value = "texto" , defaultValue = "nada mas"String texto, +
-                         @RequestParam(value = "puntuacion"defaultValue = "-1"int puntuacion) {+
  
-    Opinion opinion = new Opinion(); +    @PutMapping("/products/{id}"
-    opinion.setTitulo(titulo); +    public ResponseEntity<Product> modifyProduct(@PathVariable long id, @RequestBody Product newProduct{ 
-    opinion.setTexto(texto); +        Product product = productService.modifyProduct(id, newProduct); 
-    opinion.setFecha(new Date(System.currentTimeMillis())); +        return new ResponseEntity<>(product, HttpStatus.OK); 
-    opinion.setPuntuacion(puntuacion);+    }
  
-    repository.save(opinion); +    @DeleteMapping("/products/{id}"
-  }+    public ResponseEntity<Response> deleteProduct(@PathVariable long id) { 
 +        productService.deleteProduct(id); 
 +        return new ResponseEntity<>(Response.noErrorResponse(), HttpStatus.OK); 
 +    } 
 + 
 +    @ExceptionHandler(ProductNotFoundException.class) 
 +    @ResponseBody 
 +    @ResponseStatus(HttpStatus.NOT_FOUND) 
 +    public ResponseEntity<Response> handleException(ProductNotFoundException pnfe) { 
 +        Response response = Response.errorResonse(NOT_FOUND, pnfe.getMessage()); 
 +        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); 
 +    }
 } }
 </code> </code>
  
-==== Ejecución del servidor ====+Como se puede ver, al final del controlador, se ha definido un método que sirve de ejemplo para ver cómo tratar las excepciones que se puedan producir. En este caso será necesario implementar la clase ''ProductNotFoundException'' que define la excepción para los casos en los que no se encuentra el objeto requerido.
  
-Una vez terminado todo, para lanzar el servidor tenemos dos opciones: +<code java> 
-  * Desde el propio IDE, ejecutando ''./gradlew bootRun'' (o bien ''gradlew bootRun'' si estamos en Windows) +public class ProductNotFoundException extends RuntimeException {
-  * Utilizando el jar que podemos generar con el comando ''./gradlew jar build'' (''gradlew jar build'' en Windows) y ejecutarlo con el comando ''java -jar''. El ''.jar'' generado lo podremos encontrar en la carpeta ''build/libs''+
  
-{{ youtube>TBIzVT5dHC4 }\\+    public ProductNotFoundException() { 
 +        super(); 
 +    }
  
-===== Consumir un servicio web desde Django =====+    public ProductNotFoundException(String message) { 
 +        super(message); 
 +    }
  
-Puesto que la idea es consumir un servicio web que nos ofrecerá una serie de datos en formato JSON y nos interesaría convertirlos fácilmente a objetos Python, instalaremos el paquete //djangorestframework// que nos permitirá utilizar los objetos //Serializer// que realizarán esta tarea por nosotros.+    public ProductNotFoundException(long id) { 
 +        super("Product not found: " + id); 
 +    } 
 +
 +</code>
  
-<code bash+También necesitaremos implementar la clase ''Response'' que usamos como respuesta genérica cuando lo que hay que responder no es información sino que es la confirmación de que una operación se ha ejecutado correctamente o bien un error porque algo no ha ocurrido como se esperaba. 
-santi@zenbook:$ pip3 install djangorestframework+ 
 +<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, NO_MESSAGE)); 
 +    } 
 + 
 +    public static Response errorResonse(int errorCode, String errorMessage) { 
 +        return new Response(new Error(errorCode, errorMessage)); 
 +    } 
 +}
 </code> </code>
  
-Una vez instalado el paquete tendremos que añadirlo al fichero ''settings.py'' de nuestro proyecto:+==== TrazabilidadLogs de aplicación ====
  
-<file python settings.py> +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.
-. . . +
-INSTALLED_APPS = [ +
-    . . . +
-    'rest_framework' +
-+
-. . . +
-</file>+
  
-En nuestro caso vamos a concectar con un servicio web que ofrece datos sobre opiniones de usuarios de forma que obtenemosentre otrosun titulo, un texto y una puntuaciónEntonces crearemos un modelo de datos ''Opinion'' para almacenar dicha información en forma de objeto.+Por defectocuando ejecutamos la aplicación en modo desarrollo, y también ocurre asi cuando se hace en producción, Spring Boot lanza por pantalla las trazas de ejecución con una configuración predeterminadaPero tenemos la opción de configurar como queremos que sean esas trazas y si queremos que también se genere un log físico en disco, usando la librería logback (que es la sucesora de la ya conocida librería log4j).
  
-<file python models.py> +Para eso, simplemente tenemos que crear un fichero llamado ''logback-spring.xml'' en la carpeta ''resources'' del proyectoY a continuación se muestra un ejemplo de cómo tendría que quedar ese fichero para tener una traza por consola al ejecutar y el comando y, al mismo tiempo, que esa traza quedara almacenada en un fichero en disco de forma que éste rotara cada día o cada vez que llegara a un tamaño determinado (fijado en 10 MB).
-. . +
-class Opinion(models.Model)+
-    titulo = models.CharField(max_length=200) +
-    texto = models.CharField(max_length=200) +
-    puntuacion = models.IntegerField +
-. . . +
-</file>+
  
-Ahora, en un fichero que llamaremos ''serializer.py'' (tendremos que añadirlo a la misma altura que ficheros como ''views.py'' o ''models.py''), añadiremos la información para que éste serialize la información en JSON al modelo definido anteriormente.+<code xml> 
 +<?xml version="1.0" encoding="UTF-8"?> 
 +<configuration> 
 +    <!-- Propiedades que se usará para indicar dónde almacenar los logs y cómo se llama el fichero --> 
 +    <property name="LOG_DIR" value="logs" /> 
 +    <property name="LOG_NAME" value="myshop" />
  
-<file python serializer.py+    <!-- Configuración del log que aparece por consola: Console appender --> 
-from rest_framework import serializers +    <appender name="Console" 
-from .models import Opinion+              class="ch.qos.logback.core.ConsoleAppender"
 +        <layout class="ch.qos.logback.classic.PatternLayout"> 
 +            <!-- Configuración de la traza --> 
 +            <Pattern> 
 +                %white(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %-60.60yellow(%C{20}): %msg%n%throwable 
 +            </Pattern> 
 +        </layout> 
 +    </appender>
  
 +    <!-- Configuración para que se almacene el log en un fichero: File Appender -->
 +    <appender name="RollingFile"
 +              class="ch.qos.logback.core.rolling.RollingFileAppender">
 +        <file>${LOG_DIR}/${LOG_NAME}.log</file>
 +        <encoder
 +                class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
 +            <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
 +        </encoder>
  
-class OpinionSerializer(serializers.ModelSerializer)+        <!-- Política de rotado de logsdiario y cuando el fichero llegue a los 10 MB --> 
-    class Meta: +        <rollingPolicy 
-        fields ('titulo', 'texto', 'puntuacion') +                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> 
-        model = Opinion +            <fileNamePattern>${LOG_DIR}/${LOG_NAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern> 
-</file>+            <timeBasedFileNamingAndTriggeringPolicy 
 +                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> 
 +                <maxFileSize>10MB</maxFileSize> 
 +            </timeBasedFileNamingAndTriggeringPolicy> 
 +        </rollingPolicy> 
 +    </appender>
  
-<file python views.py> +    <!-- Define el nivel de log para cada appender --
-import requests +    <root level="info"> 
-. . . +        <appender-ref ref="RollingFile" /> 
-def pelicula(request, pelicula_id): +        <appender-ref ref="Console" /> 
-    pelicula Pelicula.objects.get(pk=pelicula_id) +    </root> 
-    . . . +</configuration> 
-    ws requests.get('http://localhost:8080/opiniones?pelicula=' + pelicula.titulo) +</code>
-    json ws.json() +
-    serializer = OpinionSerializer(data=json, many=True) +
-    if serializer.is_valid(): +
-        lista_opiniones = serializer.save() +
-        context = {'lista_opiniones': lista_opiniones, 'pelicula': pelicula} +
-        return render(request, 'peliculas/index.html', context) +
-    else: +
-        print(serializer.errors)+
  
-    return render(request, 'peliculas/index.html'+Asi es como quedaría el fichero log resultante con las trazas de ejecución de la aplicación:
-. . . +
-</file>+
  
-En el caso de que no queramos guardarlo en la Base de Datos, podemos simplemente obtener los datos como una lista de 'OrderedDict' para visualizarlos:+<code bash> 
 +santi@zenbook:$ cd logs  
 +santi@zenbook:$ ls -la 
 +total 56 
 +drwxr-xr-x   3 santi  staff    96B Mar  2 21:36 . 
 +drwxr-xr-x  14 santi  staff   448B Mar  2 21:36 .. 
 +-rw-r--r--   1 santi  staff    28K Mar  2 21:51 myshop.log 
 +</code>
  
-<code python>+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 ''LoggerFactory'' que permite obtener una instancia de un objecto ''Logger'' para registrar trazas y dejar asi constancia de cualquier evento de importancia. 
 + 
 +<code java> 
 +private final Logger logger = LoggerFactory.getLogger(ProductController.class); 
 +</code> 
 + 
 +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("/products"
 +    public ResponseEntity<Set<Product>> getProducts(@RequestParam(value = "category", defaultValue = "") String category) { 
 +        logger.info("inicio getProducts"); 
 +        Set<Product> products = null; 
 +        if (category.equals("")) 
 +            products = productService.findAll(); 
 +        else 
 +            products = productService.findByCategory(category); 
 + 
 +        logger.info("fin getProducts"); 
 +        return new ResponseEntity<>(products, HttpStatus.OK); 
 +    } 
 +</code> 
 + 
 +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> handleException(ProductNotFoundException pnfe) { 
 +    Response response = Response.errorResonse(NOT_FOUND, pnfe.getMessage()); 
 +    logger.error(pnfe.getMessage(), pnfe); 
 +    return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); 
 +
 +</code> 
 + 
 +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:47:55,813 ERROR [http-nio-8081-exec-2] c.s.m.c.ProductController                       : Product not found: 2 
 +com.sanvalero.myshop.exception.ProductNotFoundException: Product not found: 2 
 + at com.sanvalero.myshop.service.ProductServiceImpl.lambda$deleteProduct$1(ProductServiceImpl.java:49) 
 + at java.base/java.util.Optional.orElseThrow(Optional.java:408) 
 + at com.sanvalero.myshop.service.ProductServiceImpl.deleteProduct(ProductServiceImpl.java:49) 
 + at com.sanvalero.myshop.controller.ProductController.deleteProduct(ProductController.java:60) 
 + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 
 + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 
 + at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 
 + at java.base/java.lang.reflect.Method.invoke(Method.java:566) 
 + at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) 
 + at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) 
 + at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) 
 + at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894) 
 + at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) 
 + at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) 
 + at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1060) 
 + at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:962) 
 + at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) 
 + at org.springframework.web.servlet.FrameworkServlet.doDelete(FrameworkServlet.java:931) 
 + at javax.servlet.http.HttpServlet.service(HttpServlet.java:658) 
 + at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) 
 + at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
 . . . . . .
-    if serializer.is_valid(): 
-        lista_opiniones = serializer.data 
-        // Visualiza el titulo de la primera opinion obtenida 
-        print(lista_opiniones[0]['titulo']) 
 . . . . . .
 </code> </code>
  
-----+===== Relaciones entre clases en el modelo de datos ===== 
 +===== 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://www.getpostman.com|Postman]], que es una aplicación destinada exclusivamente a testear APIs.
  
-{{ ejercicio.png?75}}+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.
  
-===== Ejercicios =====+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)
  
-  - Realiza un servicio web que proporcione una API para notificar sobre eventos que tienen lugar en una ciudadSe dispondrá de una Base de Datos con los datos de eventos (nombre, descripción, fecha, lugar, plazas_disponibles) y la API ofrecerá un servicio para que otras aplicaciones puedan obtener información de dichos eventos. Dicha API proporcionará el siguiente servicio: +<figure> 
-    - Listado de todos los eventos  +{{ getProducts.png }} 
-    - Listado con los eventos que disponen de plazas +<caption>Obtiene todos los productos</caption> 
-    - Información de un evento concreto a partir del nombre del mismo +</figure>
-  - Amplia el ejercicio anterior para que sea posible registrar y eliminar eventos+
  
-----+<figure> 
 +{{ getProductsByCategory.png }} 
 +<caption>Obtiene todos los productos de la misma categoría</caption> 
 +</figure>
  
-===== Proyectos de Ejemplo =====+ 
 +<figure> 
 +{{ addProduct.png }} 
 +<caption>Registra un nuevo producto</caption> 
 +</figure> 
 + 
 + 
 +<figure> 
 +{{ modifyProduct.png }} 
 +<caption>Modifica un producto existente</caption> 
 +</figure> 
 + 
 + 
 +<figure> 
 +{{ deleteProduct.png }} 
 +<caption>Elimina un producto</caption> 
 +</figure> 
 + 
 + 
 +<figure> 
 +{{ deleteProductError.png }} 
 +<caption>Devuelve un error porque no existe el producto que se pide eliminar</caption> 
 +</figure>
  
 ---- ----
  
-===== Prácticas =====+===== 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á: origen, destino, precio, numero de escalas y compañia. Deberá ofrecer las siguientes operaciones: 
 +    - 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, categoría, ¿piscina?, localidad) y de las habitaciones de los mismos (tamaño, 1 ó 2 personas, precio/noche, ¿incluye desayuno?, ¿ocupada?). Deberá ofrecer, sobre esos datos, las siguientes operaciones: 
 +    - Búsqueda de hotel por localidad o categoría 
 +    - Búsqueda de habitaciones de un hotel por tamaño y precio (rango minimo->máximo). Solo mostrará aquellas habitaciones que estén marcadas como libres 
 +    - 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://www.github.com/codeandcoke/spring-web|repositorio spring-web]] de GitHub. 
 + 
 +Los proyectos que se vayan haciendo en clase estarán disponibles en el [[http://www.github.com/codeandcoke/datos-ejercicios|repositorio datos-ejercicios]], también en GitHub. 
 + 
 +Para manejaros con Git recordad que tenéis una serie de videotutoriales en [[https://entornos-desarrollo.codeandcoke.com/apuntes:git|La Wiki de Entornos de Desarrollo]]
  
 ---- ----
  
-(c) 2018 Santiago Faci+(c) 2016-2021 Santiago Faci
apuntes/servicios_web.1518718320.txt.gz · Last modified: 2019/01/04 13:22 (external edit)