Primer Ejercicio: Spring Boot + Web Services
Aún en desarrollo..
Inicio
Para este mini tutorial se verá cómo implementar una aplicación de servicios web de tipo REST(ful), básica, valiéndonos de una aplicación modelo provista por la guía del la página del Spring Framework y luego se ampliarán sus capacidades para poder acceder a los registros de una base de datos local.
Porqué Spring Boot?
- Desarrollo rápido
- Múltiples dependencias manejadas por nosotros (en lugar de nosotros)
- No requiere configuración XML (aunque puede)
- Aplicaciones auto-contenidas
- Framework Maduro
- Bien documentado
Nuevo proyecto Spring Boot
1- Crear el proyecto
-
Desde el menú Archivo/File del STS, seleccionar [File] -> [New] -> [Spring Boot] -> [Import Spring Getting Started Content].
-
Seleccionar el proyecto “Rest Service”, marcar la opción [Maven] del conjunto “Build Type” y del conjunto “Code Sets”, dejar marcada la opción [complete].
-
Cuando haya finalizado la descarga de los archivos requeridos, se haya generado la estructura de directorios y haya construido/compilado el proyecto por primera vez, nos aparecerá el proyecto listo para ejecutar y probar el servicio web de ejemplo que trae por defecto y la estructura del mismo será como en la imagen de a continuación:
Img 1.01: Estructura inicial del primer ejercicio.
2- Resolver las dependencias del proyecto
En este paso lo que hacemos es asegurarnos de que las dependencias del proyecto estén descargadas a nuestro repositorio local (en nuestro equipo), a modo de que puedan ser referenciados por el programa que estaremos preparando.
- Click derecho sobre el proyecto, del menú contextual seleccionar las opciones [Maven] -> [Update project…].
- Si están actualizadas y/o se resolvieron todas las dependencias, podemos pasar al siquiente punto.
3- Probar el proyecto base
-
Click derecho sobre el nodo principal del proyecto, del menú contextual, seleccionar las opciones [Run As] -> [Spring Boot App].
-
Si no hay inconveniente, deberíamos ver en la consola algo parecido a esto:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.0.1.RELEASE) 2018-04-27 17:19:28.294 INFO 17464 --- [ main] hello.Application : Starting Application on PY-WSSCONTIC13 with PID 17464 (E:\CDGS\PROYECTOS_JFP_VS\CAPACITACIONES\gs-rest-service-complete\target\classes started by cgaray in E:\CDGS\PROYECTOS_JFP_VS\CAPACITACIONES\gs-rest-service-complete) 2018-04-27 17:19:28.294 INFO 17464 --- [ main] hello.Application : No active profile set, falling back to default profiles: default 2018-04-27 17:19:28.354 INFO 17464 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6156496: startup date [Fri Apr 27 17:19:28 PYT 2018]; root of context hierarchy 2018-04-27 17:19:28.962 INFO 17464 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2018-04-27 17:19:28.977 INFO 17464 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2018-04-27 17:19:28.977 INFO 17464 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.29 2018-04-27 17:19:28.982 INFO 17464 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [C:\Program Files\Java\jdk1.8.0_152\bin;C:\Windows\Sun\Java\bin;C:\Windows\system32;C:\Windows;c:\oracle\instantclient_11_2;E:\CDGS\no_instalables\gradle\gradle-4.0.1\bin;C:\app\Administrador\product\11.2.0\client_1;C:\app\Administrador\product\11.2.0\client_1\bin;C:\Python36\Scripts\;C:\Python36\;e:\CDGS\no_instalables\maven-3.3.3\bin;c:\Program Files\java\jdk1.8.0_152\bin;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\ProgramData\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\110\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\ManagementStudio\;C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\;C:\Program Files\Microsoft SQL Server\120\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\120\DTS\Binn\;C:\Program Files\Microsoft SQL Server\120\DTS\Binn\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\dotnet\;C:\Program Files\TortoiseSVN\bin;C:\Program Files\cURL\bin;C:\Users\cgaray\AppData\Local\GitHubDesktop\bin;.] 2018-04-27 17:19:29.042 INFO 17464 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2018-04-27 17:19:29.042 INFO 17464 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 693 ms 2018-04-27 17:19:29.127 INFO 17464 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/] 2018-04-27 17:19:29.132 INFO 17464 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*] 2018-04-27 17:19:29.132 INFO 17464 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] 2018-04-27 17:19:29.132 INFO 17464 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*] 2018-04-27 17:19:29.132 INFO 17464 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*] 2018-04-27 17:19:29.207 INFO 17464 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2018-04-27 17:19:29.362 INFO 17464 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6156496: startup date [Fri Apr 27 17:19:28 PYT 2018]; root of context hierarchy 2018-04-27 17:19:29.393 INFO 17464 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String) 2018-04-27 17:19:29.398 INFO 17464 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest) 2018-04-27 17:19:29.398 INFO 17464 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) 2018-04-27 17:19:29.413 INFO 17464 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2018-04-27 17:19:29.413 INFO 17464 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2018-04-27 17:19:29.513 INFO 17464 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup 2018-04-27 17:19:29.538 INFO 17464 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2018-04-27 17:19:29.543 INFO 17464 --- [ main] hello.Application : Started Application in 1.441 seconds (JVM running for 2.319)
Como se puede ver, lo primero que aparece es el logo de Spring hecho con ascii, debajo del logo tenemos la versión del Spring Boot, en este caso corresponde a la versión 2.0.1.-
A continuación tenemos, datos del equipo donde se está ejecutando el programa, el ID de Proceso, el directorio de las clases y el usuario bajo cuya sesión se está ejecutando.
El inicio del servicio del Tomcat y su puerto de escucha de peticiones http(8080), las reglas de filtrado de direcciones expuestas, las direcciones expuestas, el registro de beans, el mensaje de finalización del arranque del Tomcat y finalmente, el tiempo total ocupado en el proceso de arrranque de la aplicación.
Si hubieran otras líneas, que no fueran errores, corresponderían, bien a mensajes de loggings, mensajes de la propia aplicación, mensajes de las librerías utilizadas o mensajes de errores durante el inicio o posteriores.
-
Suponiendo que el arranque haya sido completamente normal, podremos probar el servicio web expuesto inicialmente, para ellos, ingresamos en la barra de direcciones de nuestro navegador web favorito la dirección correspondiente a nuestro equipo local, en el puerto de escucha del servicio Tomcat http://localhost:8080/greeting y nos tendría que retornar la siguiente cadena
JSON
:{"id":1,"content":"Hello, World!"}
Donde:
El atributo
id
corresponde al identificador único de la respuesta y el atributocontent
hace referencia al contenido del saludo.Obs.:
-
Este resultado corresponde a la respuesta por defecto del servicio, es decir, cuando no recibe datos como parte de la solicitud. Si nos fijamos en el código de la clase
GreetingController
y en su métodogreeting
, veremos que acepta un parámetro a través de laurl
y cuyo nombre esname
, y también vemos que tiene un valor por defecto para el mismoWorld
. -
Con la anotación
@RequestMapping
estamos indicando al framework que el método debe responder a la llamada en unaurl
con un sufijo asignado, que en este caso es/greeting
y cuyo métodohttp
corresponde alGET
, que no se especifica, pero que es el valor por defecto. -
Sabemos que el parámetro se espera sea pasado como parte de la
url
a través de la anotación@RequestParam
(parámetro de petición), entonces podemos probar incluirlo como parte de la dirección ingresada en el navegador web e indicando un nombre al cual se realizará el saludo en la respuesta devuelta. Por ejemplo si pasamos como parámetroname=diego
la respuesta sería esta:{"id":3,"content":"Hello, diego!"}
-
-
Analizar las clases y sus correspondientes funciones:
-
Application
-
Greeting
-
GreetingController
-
Modificar el proyecto
En esta parte lo que haremos será comenzar a modificar el programa e ir probando los diferentes métodos de pasos de datos desde el cliente hacia los servicios web.
1- Agregar un nuevo método de despedida
-
Crea la clase
Farewell
que extienda deGreeting
-
Agregar el constructor requerido.
-
Refactorizar (renombrar la clase
GreetingController
aMainController
para hacer que el sea genérico. -
Refactorizar la variable final
template
agreet
y crear la variable finalfare
, similar pero con la cadena “Goodbye, %s!”. -
Crear el método
farewell
que devuelva como resultado una instancia de la claseFarewell
y que reciba como parámetros, de URLname
y tenga un valor por defecto a elección. -
Escribir el resultado del método, similar al método
greeting
. -
Agregar la anotación correspondiente al método, podemos probar utilizar la anotación
@GetMapping
. -
Probar el nuevo servicio expuesto invocando a su URL local.
Tarea 1-01: Investigar diferencias entre @GetMapping
y @RequestMapping
.
2- Pruebas unitarias automáticas
Si bien no es el objetivo de esta sección aún podemos fijarnos que en el directorio de nuestro proyecto existe una rama correspondiente a los códigos de pruebas en scr/test/java
y que contiene un archivo de clase llamado MainControllerTests
.
Img 1-02: Directorio de códigos de programas de pruebas automáticas.
Abriendo el archivo MainControllerTests
podemos ver que contiene dos métodos que son para ejecutar pruebas sobre el servicio que originalmente venía con el proyecto base, el primero noParamGreetingShouldReturnDefaultMessage()
es para probar la llamada al servicio greeting
tal y como está, o sea, sin pasarle parámetro alguno.
El segundo método paramGreetingShouldReturnTailoredMessage()
es para probar el mismo servicio pero pasando la cadena “Spring Community” como valor del parámetro name
.
Ambos métodos ejecutan la llamada al servicio web y también se encargan de validar el resultado devuelto por el mismo (sino no sería una prueba completa).
Los métodos se ejecutan, por defecto, en orden alfabético y sólo si tienen la anotación @Test
.
Este este es el mecanismo más común para hacer pruebas unitarias con JUnit y Spring, y que lo que hace es, cuando se ejecuta la compilación, sí la opción de ejecutar pruebas está marcada, al final del proceso, las corre y devuelve el resultado de ejecución de las mismas. Nos ahorra el tener que hacerlas en forma manual y también a reducir la posibilidad de poner códigos con errores en producción.
Tarea 1-03: Escribir los métodos de pruebas del servicio Farewell
para los casos de paso de parámetro y sin el mismo.
3- Crear una Base de Datos en Memoria para simular ABM
Primer truco: Ahorrar líneas de código manuales. El proyecto Lombok provee una librería que nos ayuda a reducir la cantidad de líneas de código que debemos escribir para las clases POJOs, esto tiene evidentes ventajas a la hora de iniciar un proyecto debido al tiempo que nos ahorramos.
Instalación: Procedemos a descargar desde el sitio oficial e instalarlo según la guía ofrecida.
Implementación: Una vez terminada la instalación, procedemos a incluir la librería en nuestro proyecto siguiendo estos pasos:
Localizamos el archivo
pom.xml
y lo abrimos.En la pestaña Dependencias hacemos click en el botón [Add…]
En la ventana emergente de selección de dependencia, en el cuadro de búsqueda ingresamos la cadena “projectlombok” e inmediatamente comenzará a listar aquellas que coincidan, que para este caso será una sola.
Seleccionamos el único ítem que devuelve como resultado y hacemos click en el botón [OK]
Obs.: En caso de que no se encuentre la dependencia, podemos agregarla manualmente al archivo
POM.xml
descargando el fragmento correspondiente desde aluno de los repositorios abiertos en la red, como por ejemplohttps://mvnrepository.com
.El STS comenzará a descargar las librerías correspondientes (en caso de que no las tengan aún disponibles en su repositorio local), compilará de vuelta el proyecto y quedará listo para utilizarse.
-
Creamos el paquete
hello.modelos
-
Creamos la clase
Alumno
con los atributosid
,nombre
,documento
yactivo
, sin los getters y setters. - Aplicamos las ventajas del proyecto Lombok:
-
A nivel de clase añadimos la anotación
@Data
, verificamos que importe desde la librería correspondiente y nos fijamos en la pestaña Outline del STS. Qué vemos? -
Agregamos igualmente la anotación
@NoArgsConstructor
. -
Luego creamos un constructor con todos los atributos “seteables” y lo anotamos con
@Builder
. -
Por último
@EqualsAndHashCode
, y le indicamos que debe utilizar el atributodocumento
. -
Cuántas líneas de código nos hemos ahorrado?.
- Debería quedar algo así:
@Data
@NoArgsConstructor
@EqualsAndHashCode(of = { "documento" })
public class Alumno {
long id;
String nombre;
long documento;
boolean activo = false;
@Builder
public Alumno(Long id, String nombre, long documento, boolean activo) {
super();
this.documento = documento;
this.activo = activo;
}
}
-
Creamos la clase
Materia
con los atributosid
,nombre
,activa
. Seguimos los mismos pasos que conAlumno
. - Creamos la clase
Aula
, con los atributosid
,nombre
, una lista dematerias
y otra dealumnos
, ambas de tipo final.- Notar que para las listas no se crean los setters correspondientes. Porqué?
- Cuál es la ventaja que las listas sean de tipo final?
-
Creamos el paquete
hello.servicios
. - Dentro del nuevo paquete, creamos la clase
BaseDatos
, de tipo final, con los atributos siguientes:- Una lista de instancias de la clase
Aula
, otra deMateria
y una más deAlumno
, todas de tipo final - Las listas de instancias no deben aceptar instancias repetidas, deben ser únicas.
- Creamos unas instancias de
AtomicInteger
para utilizarlos como generadores de IDs para cada colección. - Antes continuar, crearemos una super clase
Entidad
, que implemente la interfacejava.io.Serializable
y que haga de padre a nuestras clases de datos actuales. Ésta clase deberá tener un atributoid
de tipo genérico y otronombre
de tipoString
, ambas deben ser visibles hasta sus clases de implementación. Ya veremos las ventajas que brindan. - Modificamos las entidades originales para que extiendan de
Entidad
y resolvemos los warnings que nos aparezcan. - Hagamos que la clase
BaseDatos
sea un Singleton, para reducir las probabilidades de inconsistencias de datos:
- Creamos una instancia estática de sí misma como atributo.
- “Privatizamos” el constructor de la clase.
- Creamos el método
getInstancia()
que retorne la referencia a la instancia estática. - Agregamos los métodos para manejar los datos contenidos: -
Persistir (altas y modificaciones)
-
Remover
-
Listar (con y sin criterios de filtrado)
-
La clase debería de quedar más o menos parecida a lo que vemos aquí.
- Una lista de instancias de la clase
Tarea 1-004: Probemos nuestra aplicación hasta este punto escribiendo los métodos en la clafse de testing.
- Para cada entidad.
- Para la base de datos.
- Para los métodos de persistencia y listado (con y sin criterios).
4- Crear los servicios correspondientes
Creamos los métodos correspondientes a los servicios requeridos para poder realizar el ABM y Listado de cada una de las entidades creadas GET, POST, PUT
y DELETE
.
Los métodos deben agregarse a la clase MainController
y pueden verse como éste conjunto.
Tarea 1-005: Crear los métodos correspondientes para el manejo de datos de Materias y Aulas.
Fin del primer ejercicio
Hasta aquí ya se tiene todo lo necesario, sólo faltaría enlazarlo a una base de datos.
Referencias
- Guía Spring Rest Services.
- Ejemplos completos de aplicaciones con Spring (en modo boot).