0. Índice de contenidos.
- 1. Introducción.
- 2. Entorno.
- 3. Configuración.
- 4. Configuración de la base de datos.
- 5. Configuración de MyBatis.
- 6. Implementación de la capa de persistencia de nuestro CRUD.
- 7. Implementación del recurso REST de nuestro CRUD.
- 8. Referencias.
- 9. Conclusiones.
1. Introducción.
El objetivo que perseguimos con este tutorial es integrar el uso de Jersey en una aplicación con Spring Boot. Para ello, la mejor manera de conocer una tecnología es construir un prototipo con ella, así que eso es justamente lo que vamos a hacer. Vamos a construir una aplicación sencilla que gestione “cursos” a través de un API REST, se trata de construir un CRUD sobre una simple tabla (Cursos), haciéndolo a través de servicios web REST. Aunque es bastante sencillo, el ejemplo resalta las anotaciones más comunes que se necesitarán para construir nuestra API REST.
Antes de continuar vamos a explicar brevemente qué es JAX-RS y Jersey:
Java API for RESTful Web Services (JAX-RS) es un modelo de programación que proporciona soporte en la creación de servicios web que cumplan los principios REST (Representational State Transfer). JAX-RS usa anotaciones, introducidas en Java SE 5, para simplificar el desarrollo y despliegue de los clientes y puntos finales de los servicios web.
El framework de Jersey no es más que la Implementación de referencia JAX-RS. Jersey proporciona su propia API que amplía el conjunto de herramientas JAX-RS con características y utilidades adicionales para simplificar aún más el servicio RESTful y el desarrollo del cliente. Jersey también expone numerosos SPI de extensión para que los desarrolladores puedan extender Jersey y que se adapte mejor a sus necesidades.
2. Entorno.
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 15′ (2.3 GHz Intel Core i7, 16GB DDR3).
- Sistema Operativo: Mac OS High Sierra 10.13.3
- Oracle Java: 1.8.0_171
- Apache Maven: 3.5.4
- Spring Boot: 2.0.3.RELEASE
- Jersey: 2.26
3. Configuración.
Vamos a crear nuestro proyecto, se recomienda la configuración mediante el sistema de gestión de dependencias de maven y, para ello, vamos a hacer uso del wizard que nos proporciona el IDE, que se va a encargar de incluir las dependencias que seleccionemos en el proceso en nuestro pom.xml, establecemos los datos principales que necesita el wizard tal y como se puede ver en la siguiente imagen:

A continuación marcamos las dependencias para Jersey, JPA y MySQL y finalizamos el proceso de creación de proyecto.

Deberíamos tener nuestro proyecto configurado en nuestro IDE con al menos las siguientes dependencias en el pom.xml:
<dependencies> <!-- Spring --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jersey</artifactId> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> </dependencies>
Esta es la configuración mínima que necesitamos para empezar a trabajar con nuestra API REST utilizando Jersey, a continuación vamos a configurar el contenedor de Jersey, para ello creamos un paquete Java para nuestras clases de configuración (por ser un poco organizados con nuestro código) y posteriormente creamos una nueva clase de configuración que vamos a llamar ‘JerseyConfiguration’ dentro de él, con el siguiente código:
package com.autentia.training.coursemanagement.configurations; import javax.ws.rs.ApplicationPath; import org.glassfish.jersey.server.ResourceConfig; import org.springframework.context.annotation.Configuration; @Configuration @ApplicationPath("/rest") public class JerseyConfiguration extends ResourceConfig { public JerseyConfiguration() { packages("com.autentia.training.coursemanagement.web.resources"); } }
El servlet Jersey se registra y asigna al path “/*” de forma predeterminada, en nuestro ejemplo hemos querido cambiar dicha asignación a “/rest”, para ello hemos agregado la anotación @ApplicationPath a nuestra clase de configuración. También hemos configurado nuestro contenedor de Jersey para registrar los recursos mediante la definición del paquete donde residirán los mismos, pero podríamos registrar los recursos individualmente como se puede ver a continuación:
package com.autentia.training.coursemanagement.configurations; import javax.ws.rs.ApplicationPath; import org.glassfish.jersey.server.ResourceConfig; import org.springframework.context.annotation.Configuration; import com.autentia.training.coursemanagement.web.resources.CourseResource; @Configuration @ApplicationPath("/rest") public class JerseyConfiguration extends ResourceConfig { public JerseyConfiguration() { register(CourseResource.class); } }
4. Configuración de la base de datos.
Antes de comenzar con nuestro CRUD vamos a configurar una base de datos h2 (ya que estamos en un entorno de pruebas) donde podremos mantener nuestra tabla de cursos, lo primero de todo es configurar nuestro fichero pom.xml añadiendo las siguientes dependencias:
<dependencies> ... <!-- Runtime --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> ... </dependencies>
Modificamos nuestro fichero application.properties para añadir la siguiente configuración:
### Enable H2 Console Access spring.h2.console.enabled=true spring.h2.console.path=/h2-console ### Define H2 Datasource configurations spring.datasource.platform=h2 spring.datasource.url=jdbc:h2:mem:app_db;DB_CLOSE_ON_EXIT=FALSE #spring.datasource.url = jdbc:h2:file:~/h2/app_db;DB_CLOSE_ON_EXIT=FALSE spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver
Creamos un nuevo fichero que vamos a llamar ‘schema.sql’ en src/main/resources para que Spring Boot se encargue de crear y poblar nuestra tabla ‘Cursos’, para ello añadimos las siguientes sql:
DROP TABLE IF EXISTS COURSES; CREATE TABLE COURSES ( ID BIGINT(11) NOT NULL AUTO_INCREMENT, TITLE VARCHAR(250) NOT NULL, LEVEL VARCHAR(80) NOT NULL, HOURS INT(3) NULL, TEACHER VARCHAR(250) NOT NULL, STATE INT(1) NOT NULL, PRIMARY KEY (ID), UNIQUE KEY (TITLE) ); INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('Aumenta el rendimiento de tus tests de integración con BBDD usando un pool de conexiones dbcp2 con BasicDataSource de Apache', 'Intermedio', 25, 'Alberto Barranco Ramó', 1); INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('Aplicando TDD en concursos', 'Básico', 10, 'Alejandro Acebes Cabrera', 1); INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('Ejecutando test de SoapUI Open Source en JUnit en un proyecto Maven', 'Básico', 20, 'Alberto Moratilla Ocaña', 1); INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('Primeros pasos con Concordion', 'Intermedio', 15, 'Daniel Rodríguez Hernández', 0); INSERT INTO COURSES (TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES('TDD: Outside-In vs Inside-Out', 'Avanzado', 25, 'Jose Luis Rodríguez Villapecellín', 1);
5. Configuración de MyBatis.
Ahora vamos con la capa de persistencia, MyBatis en nuestro proyecto, en este apartado no me voy a explayar mucho ya que podéis encontrar más información en los tutoriales de Adictos, para ello deberíamos tener incluida la siguiente dependencia en el pom.xml:
<dependencies> ... <!-- MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> ... </dependencies>
Modificamos nuestro fichero application.properties para añadir la siguiente configuración:
mybatis.typeAliasesPackage=com.autentia.training.coursemanagement.model.entities
Por último añadimos nuestra clase de aplicación para Spring Boot (‘CourseManagementApplication’) para añadir la anotación de configuración ‘MapperScan’ para indicar el paquete donde iremos añadiendo nuestras clases ‘Mapper’ de MyBatis:
@SpringBootApplication @MapperScan("com.autentia.training.coursemanagement.model.mappers") public class CourseManagementApplication { public static void main(String[] args) { SpringApplication.run(CourseManagementApplication.class, args); } }
6. Implementación de la capa de persistencia de nuestro CRUD.
Debido a que este punto no es el foco principal de este tutorial, esta parte voy a ir más rápido dejando los ejemplos de código sin comentar mucho para no hacer este tutorial muy extenso, en nuestra web de Adictos podéis encontrar tutoriales que os ayuden a entender esta parte mejor.
Entidad ‘Curso’:
package com.autentia.training.coursemanagement.model.entities; import java.io.Serializable; /** * Entidad para la gestión de los cursos * @author Autentia * @version 1.0 */ public class Course implements Serializable { package com.autentia.training.coursemanagement.model.entities; import java.io.Serializable; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; /** * Entidad para la gestión de los cursos * @author Autentia * @version 1.0 */ public class Course implements Serializable { private static final long serialVersionUID = 1L; /** * Identificador del curso */ private Long id; /** * Titulo del curso */ private String title; /** * Nivel del curso */ private String level; /** * Número de horas del curso */ private Integer numberOfHours; /** * Profesor del curso */ private String teacher; /** * Estado del curso */ private Boolean state; /** * Devuelve el identificador del curso * @return id */ public Long getId() { return id; } /** * Establece el identificador del curso * @param id */ public void setId(Long id) { this.id = id; } /** * Devuelve el título del curso * @return title */ public String getTitle() { return title; } /** * Establece el título del curso * @param title */ public void setTitle(String title) { this.title = title; } /** * Devuelve el nivel del curso * @return level */ public String getLevel() { return level; } /** * Establece el nivel del curso * @param levelId */ public void setLevel(String level) { this.level = level; } /** * Devuelve el número de horas del curso * @return numberOfHours */ public Integer getNumberOfHours() { return numberOfHours; } /** * Establece el número de horas del curso * @param numberOfHours */ public void setNumberOfHours(Integer numberOfHours) { this.numberOfHours = numberOfHours; } /** * Devuelve el profesor del curso * @return the teacher */ public String getTeacher() { return teacher; } /** * Establece el profesor del curso * @param teacher */ public void setTeacher(String teacher) { this.teacher = teacher; } /** * Devuelve el estado del curso * @return the state */ public Boolean getState() { return state; } /** * Establece el estado del curso * @param state */ public void setState(Boolean state) { this.state = state; } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this); } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return ToStringBuilder.reflectionToString(this); } } }
Mapper para nuestra entidad ‘Curso’:
package com.autentia.training.coursemanagement.model.mappers; import java.util.List; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import com.autentia.training.coursemanagement.model.entities.Course; /** * Mapper de MyBatis para la gestión de la entidad 'Course' * @author Autentia * @version 1.0 */ @Mapper public interface CourseMapper { @Insert("INSERT INTO COURSES(TITLE, LEVEL, HOURS, TEACHER, STATE) VALUES(#{title}, #{level}, #{numberOfHours}, #{teacher}, #{state})") @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="ID") public int insert(Course course); @Update("UPDATE COURSES SET TITLE = #{title}, LEVEL = #{level}, HOURS = #{numberOfHours}, TEACHER = #{teacher}, STATE = #{state} WHERE ID=#{id}") public int update(Course course); @Delete("DELETE FROM COURSES WHERE ID=#{id}") public int deleteById(Long id); @Select("SELECT c.ID, c.TITLE, c.LEVEL, c.HOURS, c.TEACHER, c.STATE FROM COURSES c") @Results(value = { @Result(property = "id", column = "ID"), @Result(property = "title", column = "TITLE"), @Result(property = "level", column = "LEVEL"), @Result(property = "numberOfHours", column = "HOURS"), @Result(property = "teacher", column = "TEACHER"), @Result(property = "state", column = "STATE") }) public List getAll(); @Select("SELECT c.ID, c.TITLE, c.LEVEL, c.HOURS, c.TEACHER, c.STATE FROM COURSES c WHERE c.ID = #{id}") @Results(value = { @Result(property = "id", column = "ID"), @Result(property = "title", column = "TITLE"), @Result(property = "level", column = "LEVEL"), @Result(property = "numberOfHours", column = "HOURS"), @Result(property = "teacher", column = "TEACHER"), @Result(property = "state", column = "STATE") }) public Course getById(@Param("id") Long id); }
Interfaz de servicio para nuestra entidad ‘Curso’:
package com.autentia.training.coursemanagement.model.services; import java.util.List; import com.autentia.training.coursemanagement.model.entities.Course; import com.autentia.training.coursemanagement.model.exceptions.EntityNotFoundException; public interface CourseService { public Course create(Course course); public Course update(Long id, Course course) throws EntityNotFoundException; public void delete(Long id) throws EntityNotFoundException; public List findAll(); public Course findOne(Long id) throws EntityNotFoundException; }
Implementación MyBatis del servicio para nuestra entidad ‘Curso’:
package com.autentia.training.coursemanagement.model.services.mybatis; import java.util.List; import org.springframework.stereotype.Service; import com.autentia.training.coursemanagement.model.entities.Course; import com.autentia.training.coursemanagement.model.exceptions.EntityNotFoundException; import com.autentia.training.coursemanagement.model.mappers.CourseMapper; import com.autentia.training.coursemanagement.model.services.CourseService; /** * Implementación de MyBatis del servicio para la gestión de la entidad 'Course' * @author Autentia * @version 1.0 */ @Service public class MyBatisCourseService implements CourseService { private CourseMapper courseMapper; public MyBatisCourseService(final CourseMapper courseMapper) { this.courseMapper = courseMapper; } @Override public Course create(Course course) { this.courseMapper.insert(course); return course; } @Override public Course update(Long id, Course course) throws EntityNotFoundException { Course courseBD = this.courseMapper.getById(id); if (courseBD == null) { throw new EntityNotFoundException("No existe el registro a modificar"); } courseBD.setTitle(course.getTitle()); courseBD.setLevel(course.getLevel()); courseBD.setNumberOfHours(course.getNumberOfHours()); courseBD.setTeacher(course.getTeacher()); courseBD.setState(course.getState()); this.courseMapper.update(courseBD); return courseBD; } @Override public void delete(Long id) throws EntityNotFoundException { if (this.courseMapper.getById(id) == null) { throw new EntityNotFoundException("No existe el registro a eliminar"); } this.courseMapper.deleteById(id); } @Override public List findAll() { return this.courseMapper.getAll(); } @Override public Course findOne(Long id) throws EntityNotFoundException { Course course = this.courseMapper.getById(id); if (course == null) { throw new EntityNotFoundException("No existe el registro en el sistema"); } return course; } }
7. Implementación del recurso REST de nuestro CRUD.
Hasta ahora todo lo que hemos hecho ha sido crear y configurar el proyecto, además de desarrollar nuestra capa de persistencia y negocio. Ahora vamos a comenzar con el objetivo de nuestro tutorial, vamos a crear nuestro servicio REST que, basándonos en el acrónimo CRUD, inserte un curso en nuestra tabla ‘COURSES’:
package com.autentia.training.coursemanagement.web.resources; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import com.autentia.training.coursemanagement.model.entities.Course; import com.autentia.training.coursemanagement.model.exceptions.EntityNotFoundException; import com.autentia.training.coursemanagement.model.services.CourseService; /** * Servicio REST para la gestión de la entidad 'Course' * @author Autentia * @version 1.0 */ @Component @Path("/course") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class CourseResource { private Logger log = LoggerFactory.getLogger(this.getClass()); private CourseService courseService; public CourseResource(CourseService courseService) { this.courseService = courseService; } @POST public Response create(Course course) { if(course.getTitle() == null || course.getLevel() == null || course.getTeacher() == null || course.getState() == null) { return Response.status(500).entity("Falta por rellenar campos obligatorios").build(); } return Response.ok().entity(courseService.create(course)).build(); } }
La anotación @Path identifica la plantilla de ruta de la URI a la que responde el recurso, esta anotación puede especificarse a nivel de clase o método de un recurso. El valor de la anotación @Path es una plantilla de ruta de la URI parcial en relación con la URI base del servidor en el cual se implementa el recurso, la raíz del contexto de la aplicación y el patrón de URL a la que responde el runtime de JAX-RS.
La anotación @POST indica que el método anotado responde a las peticiones HTTP POST del API REST, para usar en llamadas de tipo ‘CREATE’.
A continuación os muestro un ejemplo de invocación desde postman con la confirmación de la creación del recurso mediante nuestro API REST:
NOTA: Recordar que para evitar errores hay que añadir en la cabecera de la petición el ‘Content-Type’ y establecerlo a ‘application/json’

El siguiente paso lógico sería realizar un servicio que se encargue de la consulta de nuestro recurso, para ello añadimos en nuestro recurso los siguientes métodos:
... public class CourseResource { ... @GET public Response getAll() { return Response.ok().entity(courseService.findAll()).build(); } @GET @Path("/{id}") public Response get(@PathParam("id") Long id) { Course course = null; try { course = courseService.findOne(id); } catch (EntityNotFoundException e) { log.error(e.getMessage()); return Response.status(404).build(); } return Response.ok().entity(course).build(); } ... }
La anotación @GET indica que el método anotado responde a las peticiones HTTP GET del API REST, para usarse en llamadas del tipo ‘READ’, además podemos observar que en nuestro segundo método hemos añadido la anotación @Path de nuevo para indicar que queremos añadir a nuestra URI base una parte más, en este campos queremos indicar el identificador del recurso que será enviado como parámetro a nuestro método.
A continuación os muestro un ejemplo de invocación desde postman con el resultado de la consulta general mediante nuestro API REST:

Y aquí os muestro un ejemplo de invocación desde postman con el resultado de la consulta por identificador mediante nuestro API REST:

Continuamos con el siguiente paso, la modificación de nuestro recurso, para ello añadimos en nuestra clase el siguiente método:
... public class CourseResource { ... @PUT @Path("/{id}") public Response update(@PathParam("id") Long id, Course course) { Course courseUpdated = null; try { if(course.getTitle() == null || course.getLevel() == null || course.getTeacher() == null || course.getState() == null) { return Response.status(500).entity("Falta rellenar campos obligatorios").build(); } courseUpdated = courseService.update(id, course); } catch (EntityNotFoundException e) { log.error(e.getMessage()); return Response.status(404).build(); } return Response.ok().entity(courseUpdated).build(); } ... }
La anotación @PUT indica que el método anotado responde a las peticiones HTTP PUT del API REST, para usarse en llamadas del tipo ‘UPDATE’.
A continuación os muestro un ejemplo de invocación desde postman con la confirmación de la modificación del recurso mediante nuestro API REST:
NOTA: Recordar el ‘Content-Type’, se establece a ‘application/json’

Por último vamos a realizar la eliminación de nuestro recurso, para ello añadimos en nuestra clase el siguiente método:
... public class CourseResource { ... @DELETE @Path("/{id}") public Response delete(@PathParam("id") Long id) { try { courseService.delete(id); } catch (EntityNotFoundException e) { log.error(e.getMessage()); return Response.status(404).build(); } return Response.ok().entity("Curso eliminado con éxito!").build(); } ... }
La anotación @DELETE indica que el método anotado responde a las peticiones HTTP DELETE del API REST, para usarse en llamadas del tipo ‘DELETE’.
A continuación os muestro un ejemplo de invocación desde postman con la confirmación de la eliminación del recurso mediante nuestro API REST:

8. Referencias.
- Código fuente
- https://www.adictosaltrabajo.com/
- https://www.adictosaltrabajo.com/?s=springboot
- https://www.adictosaltrabajo.com/?s=jersey
- https://www.adictosaltrabajo.com/?s=mybatis
- http://download.oracle.com/otn-pub/jcp/jaxrs-2_0-fr-eval-spec/jsr339-jaxrs-2.0-final-spec.pdf
- https://docs.oracle.com/javaee/7/tutorial/jaxrs.htm
- https://github.com/jax-rs
- https://jersey.github.io/
- http://javaarm.com/file/glassfish/jersey/doc/userguide/Jersey-2.26-User-Guide.htm
- https://github.com/jersey
- http://blog.mybatis.org/
- http://www.mybatis.org/spring/index.html
- http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
9. Conclusiones.
En este tutorial hemos visto cómo crear un sencillo CRUD mediante API REST de Jersey. Algunos se preguntarán porque usar JAX-RS con Jersey en lugar del estandar de Spring. La respuesta sería porque Jersey es la implementación del estándar de JEE, por ello estamos menos acoplados a Spring y nuestro código podría ejecutarse sin mayores problemas en cualquier servidor JEE.
Espero que os haya gustado y que os haya servido, con esa intención ha sido creado.
Un saludo.
Javi