En los ejemplos anteriores se explicó la configuración básica de Jersey utilizando Spring boot y la configuración de Spring data en el mismo proyecto. En este tutorial se explicará de forma simple tomando los dos proyectos anteriores como base el uso y funcionamiento de HATEOAS en REST utilizando Spring boot + Jersey.
1. Configuración, Spring boot generó starter dependencies, estas dependencias contienen todos los recursos necesarios para utilizar el módulo de spring deseado de una forma simple y manejable de una forma más simple, las versiones de estas dependencias no son requeridas ya que se heredan del proyecto padre de spring boot.Para configurar Spring HATEOAS se requiere agregar la siguiente dependencia:
<dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId> </dependency>
2. Agregando una entidad extra, en el ejemplo anterior se explicó de forma simple como utilizar Spring data para obtener información y Spring Jersey para exponerla en web services REST. En dicho ejemplo se utilizó una entidad llamada User para realizar las pruebas, en este ejemplo se modificará la entidad User y se agregará una nueva llamada Role.
/** * */ package com.raidentrance.entities; import java.io.Serializable; import javax.persistence.Basic; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; /** * @author raidentrance * */ @Entity @Table(name = "ROLE") public class Role implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID_ROLE") private Integer idRole; @Basic(optional = false) @NotNull @Size(min = 1, max = 45) @Column(name = "NAME") private String name; @Size(max = 100) @Column(name = "DESCRIPTION") private String description; private static final long serialVersionUID = 3428234636660051311L; ........ }
Ahora se modifica la entidad usuario.
/** * */ package com.raidentrance.entities; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; /** * @author raidentrance * */ @Entity @Table(name ="USER") public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "USER_ID") private Integer idUser; @Column(name = "USERNAME") private String username; @Column(name = "PASSWORD") private String password; @JoinColumn(name = "ROLE_ID", referencedColumnName = "ID_ROLE") @ManyToOne(optional = false) private Role role; ..... }
3. Actualización a scripts sql, la entidad usuario ha sido modificada y se ha agregado una entidad nueva llamada role, para soportar esto se deben realizar las siguientes modificaciones al schema de la la base de datos, del mismo modo que en el ejemplo anterior la definición del schema se realizará en el archivo schema.sql y la inserción de los datos de ejemplo en el archivo init.sql.
schema.sql
CREATE TABLE ROLE( ID_ROLE INTEGER PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(45) NOT NULL , DESCRIPTION VARCHAR(100) NULL ); CREATE TABLE USER( USER_ID INTEGER PRIMARY KEY AUTO_INCREMENT, USERNAME VARCHAR(100) NOT NULL, PASSWORD VARCHAR(100) NOT NULL, ROLE_ID INTEGER NOT NULL, FOREIGN KEY (ROLE_ID) REFERENCES ROLE (ID_ROLE) ON DELETE NO ACTION ON UPDATE NO ACTION);
init.sql
INSERT INTO ROLE (NAME,DESCRIPTION)VALUES("ADMIN","Administrator"); INSERT INTO ROLE (NAME,DESCRIPTION)VALUES("USER","Normal user"); INSERT INTO USER (USERNAME,PASSWORD,ROLE_ID)VALUES('raidentrance','superSecret',1); INSERT INTO USER (USERNAME,PASSWORD,ROLE_ID)VALUES('john','smith',1); INSERT INTO USER (USERNAME,PASSWORD,ROLE_ID)VALUES('juan','hola123',2);
HATEOAS es la abreviatura de Hypermedia as the Engine of Application state y permitirá navegar a través de los recursos REST, de tal modo que cada uno de los recursos devueltos debe contener un link al recurso completo. Con esto es posible acceder a los recursos de forma completa sin necesidad de documentación.
4. Creando Dtos con soporte para HATEOAS, lo primero que se debe hacer para agregar soporte para HATEOS es crear Dto’s con soporte a los links para realizar la navegación.
/** * */ package com.raidentrance.dto; import java.io.Serializable; import org.springframework.hateoas.ResourceSupport; /** * @author raidentrance * */ public class RoleDto extends ResourceSupport implements Serializable { private Long idRole; private String name; private String description; ........ }
/** * */ package com.raidentrance.dto; import java.io.Serializable; import org.springframework.hateoas.ResourceSupport; import com.raidentrance.entities.Role; /** * @author raidentrance * */ public class UserDto extends ResourceSupport implements Serializable { private Long idUser; private String username; private String password; private RoleDto role; .......... }
Se puede observar que los Dto’s creados heredan de la clase ResourceSupport la cuál brindará soporte para agregar links a los recursos que serán devueltos por los servicios REST.
5. Creando un AbstractAssembler, a continuación se muestra un assembler abstracto, el cuál servirá de base para generar los links para los recursos que serán devueltos por los servicios rest.
/** * */ package com.raidentrance.assembler; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.jaxrs.JaxRsLinkBuilder; import org.springframework.hateoas.mvc.ResourceAssemblerSupport; import jersey.repackaged.com.google.common.base.Preconditions; /** * @author raidentrance * */ public abstract class JaxRsResourceAssemblerSupport<T, D extends ResourceSupport> extends ResourceAssemblerSupport<T, D> { private final Class<?> controllerClass; public JaxRsResourceAssemblerSupport(Class<?> controllerClass, Class<D> resourceType) { super(controllerClass, resourceType); this.controllerClass = controllerClass; } @Override protected D createResourceWithId(Object id, T entity, Object... parameters) { Preconditions.checkNotNull(entity); Preconditions.checkNotNull(id); D instance = instantiateResource(entity); instance.add(JaxRsLinkBuilder.linkTo(controllerClass).slash(id).withSelfRel()); return instance; } }
6. Creando mapper, el siguiente paso es crear un mapper para transformar de Entidades a Dtos de forma simple, para hacer esto se hará uso de MapStruct, para más información sobre la configuración de MapStruct Mapeo de Beans con MapStruct.
/** * */ package com.raidentrance.mapper; import org.mapstruct.Mapper; import com.raidentrance.dto.RoleDto; import com.raidentrance.dto.UserDto; import com.raidentrance.entities.Role; import com.raidentrance.entities.User; /** * @author raidentrance * */ @Mapper public interface UserMapper { UserDto userEntityToUser(User entity); User userToUserEntity(UserDto dto); RoleDto roleEntityToRole(Role entity); Role roleToRoleEntity(RoleDto role); }
7. Agregando Endpoints, El primer paso es asegurar que se cuenta con los endpoints tanto para «users» como para «roles». Más adelante se detallarán los endpoints que contiene cada uno de ellos.
UserResource.java
/** * */ package com.raidentrance.resource; import java.util.ArrayList; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.raidentrance.entities.User; import com.raidentrance.repositories.UserRepository; import jersey.repackaged.com.google.common.collect.Lists; /** * @author raidentrance * */ @Component @Path("/users") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class UserResource { ...... }
RoleResource.java
/** * */ package com.raidentrance.resource; import javax.ws.rs.Consumes; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; /** * @author raidentrance * */ @Component @Path("/roles") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class RoleResource { ...... }
8. Registrando los servicios, Cómo se explicó en posts anteriores es necesario registrar cada servicio en Jersey para que funcione de forma correcta.
/** * */ package com.raidentrance.config; import org.glassfish.jersey.server.ResourceConfig; import org.springframework.stereotype.Component; import com.raidentrance.resource.RoleResource; import com.raidentrance.resource.UserResource; /** * @author raidentrance * */ @Component public class JerseyConfig extends ResourceConfig { public JerseyConfig() { register(UserResource.class); register(RoleResource.class); } }
9. Creando los assemblers , el siguiente paso es crear los assemblers, estos serán los responsables de 2 puntos transformar de entities a Dto’s y agregar los links de hateoas.
RoleAssembler.java
/** * */ package com.raidentrance.assembler; import org.mapstruct.factory.Mappers; import org.springframework.stereotype.Component; import com.raidentrance.dto.RoleDto; import com.raidentrance.entities.Role; import com.raidentrance.mapper.UserMapper; import com.raidentrance.resource.RoleResource; /** * @author raidentrance * */ @Component public class RoleAssembler extends JaxRsResourceAssemblerSupport<Role, RoleDto> { private UserMapper mapper = Mappers.getMapper(UserMapper.class); public RoleAssembler() { super(RoleResource.class, RoleDto.class); } @Override public RoleDto toResource(Role entity) { RoleDto role = createResourceWithId(entity.getIdRole(), entity); RoleDto result = mapper.roleEntityToRole(entity); result.add(role.getLinks()); return result; } }
UserAssembler.java
/** * */ package com.raidentrance.assembler; import org.mapstruct.factory.Mappers; import org.springframework.beans.factory.annotation.Autowired; import com.raidentrance.dto.RoleDto; import com.raidentrance.dto.UserDto; import com.raidentrance.entities.User; import com.raidentrance.mapper.UserMapper; import com.raidentrance.resource.UserResource; /** * @author raidentrance * */ @Component public class UserAssembler extends JaxRsResourceAssemblerSupport<User, UserDto> { @Autowired private RoleAssembler assembler; private UserMapper mapper = Mappers.getMapper(UserMapper.class); public UserAssembler() { super(UserResource.class, UserDto.class); } @Override public UserDto toResource(User entity) { UserDto resource = createResourceWithId(entity.getIdUser(), entity); UserDto result = mapper.userEntityToUser(entity); RoleDto role = assembler.toResource(entity.getRole()); result.add(resource.getLinks()); result.setRole(role); return result; } }
10. Agregando Repositorio para la entidad Role
/** * */ package com.raidentrance.repositories; import org.springframework.data.repository.CrudRepository; import com.raidentrance.entities.Role; /** * @author raidentrance * */ public interface RoleRepository extends CrudRepository<Role, Integer>{ }
11. Detallando los endpoints necesarios , A continuación se muestran los endpoints que se utilizarán para exponer los recursos y para generar la navegación entre los servicios.
UserResource.java
/** * */ package com.raidentrance.resource; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.GET; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.raidentrance.assembler.UserAssembler; import com.raidentrance.entities.User; import com.raidentrance.repositories.UserRepository; import jersey.repackaged.com.google.common.collect.Lists; /** * @author raidentrance * */ @Component @Path("/users") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class UserResource { @Autowired private UserAssembler userAssembler; @Autowired private UserRepository userRepository; @GET public Response getUsers() { List<User> users = Lists.newArrayList(userRepository.findAll()); return Response.ok(userAssembler.toResources(users)).build(); } @GET @Path("/{idUser}") public Response getById(@PathParam("idUser") Integer idUser) { User requested = userRepository.findOne(idUser); return Response.ok(userAssembler.toResource(requested)).build(); } }
RoleResource.java
/** * */ package com.raidentrance.resource; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.GET; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.raidentrance.assembler.RoleAssembler; import com.raidentrance.entities.Role; import com.raidentrance.repositories.RoleRepository; import jersey.repackaged.com.google.common.collect.Lists; /** * @author raidentrance * */ @Component @Path("/roles") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class RoleResource { @Autowired private RoleRepository roleRepository; @Autowired private RoleAssembler assembler; @GET public Response getRoles() { List<Role> role = Lists.newArrayList(roleRepository.findAll()); return Response.ok(assembler.toResources(role)).build(); } @GET @Path("/{idRole}") public Response getById(@PathParam("idRole") Integer idRole) { Role requested = roleRepository.findOne(idRole); return Response.ok(assembler.toResource(requested)).build(); } }
12. Probando todo junto ,para ejecutar la aplicación el primer punto es generar el mapper de MapStruct utilizando el goal mvn generate-sources una vez generados los mappers es necesario compilar el proyecto y el proyecto queda listo para probarlo. Para esto se ejecutará la clase principal de la aplicación SprinBootSampleApplication, con esto se iniciará un contenedor.
Probando los recursos:
[ { idUser: 1, username: "raidentrance", password: "superSecret", role: { idRole: 1, name: "ADMIN", description: "Administrator", links: [ { rel: "self", href: "http://localhost:8080/roles/1" } ] }, links: [ { rel: "self", href: "http://localhost:8080/users/1" } ] }, { idUser: 2, username: "john", password: "smith", role: { idRole: 1, name: "ADMIN", description: "Administrator", links: [ { rel: "self", href: "http://localhost:8080/roles/1" } ] }, links: [ { rel: "self", href: "http://localhost:8080/users/2" } ] }, { idUser: 3, username: "juan", password: "hola123", role: { idRole: 2, name: "USER", description: "Normal user", links: [ { rel: "self", href: "http://localhost:8080/roles/2" } ] }, links: [ { rel: "self", href: "http://localhost:8080/users/3" } ] } ]
http://localhost:8080/users/1 .
{ idUser: 1, username: "raidentrance", password: "superSecret", role: { idRole: 1, name: "ADMIN", description: "Administrator", links: [ { rel: "self", href: "http://localhost:8080/roles/1" } ] }, links: [ { rel: "self", href: "http://localhost:8080/users/1" } ] }
[ { idRole: 1, name: "ADMIN", description: "Administrator", links: [ { rel: "self", href: "http://localhost:8080/roles/1" } ] }, { idRole: 2, name: "USER", description: "Normal user", links: [ { rel: "self", href: "http://localhost:8080/roles/2" } ] } ]
http://localhost:8080/roles/1 .
{ idRole: 1, name: "ADMIN", description: "Administrator", links: [ { rel: "self", href: "http://localhost:8080/roles/1" } ] }
Puedes encontrar el código completo del ejemplo en el siguiente enlace:
https://github.com/raidentrance/spring-boot-example/tree/part3-adding-hateoas .
Autor: Alejandro Agapito Bautista
Twitter: @raidentrance
Contacto:raidentrance@gmail.com