REST, el principio HATEOAS y Spring HATEOAS

9
56629

REST, el principio HATEOAS y Spring HATEOAS.

0. Índice de contenidos.


1. Introducción

Poco podemos decir de REST que no hayamos comentado ya. Su simplicidad hace que muchos desarrolladores opten por esta solución por encima de SOAP a la hora de exponer las diferentes funcionalidades (mejor dicho, recursos) de sus plataformas. La simplicidad es, sin lugar a dudas, su gran arma pero, ¿abusamos de ella?, ¿diseñamos verdaderas APIs REST o simples interfaces sobre HTTP?, ¿conocemos realmente los principios de diseño de servicios RESTful?.

En este tutorial intentaremos explicar qué es el principio HATEOAS, de obligado cumplimiento para cualquier API REST que se enorgullezca de serlo y veremos, mediante un ejemplo, el soporte que nos proporciona Spring para conseguirlo gracias a Spring HATEOAS.


2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.2 Ghz Intel Core I7, 8GB DDR3).
  • Sistema Operativo: Mac OS Mountain Lion 10.8
  • Entorno de desarrollo: Intellij Idea 11.1 Ultimate.
  • Apache Tomcat 7.0.47
  • Maven 3.0.3
  • Java 1.7.0_45
  • Spring 3.2.4.RELEASE
  • Spring HATEOAS 0.8.0.RELEASE
  • H2 database 1.3.170


3. ¿Qué es HATEOAS?.

HATEOAS es un acrónimo de Hypermedia As The Engine Of Application State (hipermedia como motor del estado de la aplicación). Significa algo así como que, dado un punto de entrada genérico de nuestra API REST, podemos ser capaces de descubrir sus recursos basándonos únicamente en las respuestas del servidor. Dicho de otro modo, cuando el servidor nos devuelva la representación de un recurso (JSON, XML…) parte de la información devuelta serán identificadores únicos en forma de hipervínculos a otros recursos asociados.

Lo vamos a entender más fácilmente con este ejemplo. Imaginemos que tenemos un API de un concesionario de coches donde nuestros clientes, evidentemente, compran coches. Supongamos que queremos obtener los datos del cliente con id igual a 78. Haríamos una petición de este estilo:

Request URL: http://miservidor/concesionario/api/v1/clientes/78
Request Method: GET
Status Code: 200 OK

Y obtendríamos algo como:

{
    "id": 78,
    "nombre": "Juan",
    "apellido": "García",
    "coches": [
    	{
    		"id": 1033
    	},
    	{
    		"id": 3889
    	}
    ]
}

Con esto ya sabemos que nuestro cliente compró dos coches pero, ¿cómo accedemos a la representación de esos dos recursos?. Sin consultar la documentación del API no tenemos forma de obtener la URL que identifique de forma única a cada uno de los coches. Además, aunque supiésemos conformar la URL de acceso a los recursos, cualquier cliente que quisiese consumir los recursos debería tener la responsabilidad de construir dicha URL. Por último, ¿qué ocurriría si la URL cambiase?, habría que cambiar todos los clientes que consumen los recursos.

Siguiendo el principio HATEOAS la respuesta sería algo como:

{
    "id": 78,
    "nombre": "Juan",
    "apellido": "García",
    "coches": [
    	{
    		"coche": "http://miservidor/concesionario/api/v1/clientes/78/coches/1033"
    	},
    	{
    		"coche": "http://miservidor/concesionario/api/v1/clientes/78/coches/3889"
    	}
    ]
}

De esta forma, ya sabemos dónde debemos ir a buscar los recursos relacionados (coches) con nuestro recurso original (cliente) gracias a la respuesta del servidor (hypertext-driven). Sin seguir este principio, nuestra API nunca seguirá el verdadero estilo arquitectónico REST. Y no lo digo solo yo, lo dice su principal promotor Roy Fielding.

Spring HATEOAS es un pequeño módulo perteneciente al «ecosistema Spring» que nos ayudará a crear APIs REST que respeten el principio HATEOAS.


4. Añadiendo links a la representación de nuestros recursos con Spring HATEOAS.

Añadir links a nuestros recursos (mejor dicho, a sus representaciones) es muy sencillo con Spring HATEOAS. La única condición de que debemos cumplir es que el objeto u objetos que devolvamos en la respuesta extiendan de org.springframework.hateoas.ResourceSupport. Una vez que cumplan con esto ya podremos añadir a nuestro recurso los links que apunten a las URLs de otros recursos de nuestra API con los que queramos relacionarlo.

Supongamos que tenemos una clase PersonWrapper que representará un recurso «persona»:

@XmlRootElement(name = "person")
public class PersonWrapper extends ResourceSupport {

    private String name;

    private int age;

    private PersonWrapper() {}

    public PersonWrapper(Person person) {
        this.name = person.getName();
        this.age = person.getAge();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

En nuestro controlador PersonController tendremos un método que devolverá un recurso persona que coincida con el nombre solicitado. Algo como lo siguiente:

@Controller
public class PersonController {

    @RequestMapping(value = "/persons/{name}", method = RequestMethod.GET)
    public @ResponseBody PersonWrapper getPerson(@PathVariable String name) {
        final Person person = getPersonByName(name);
        final PersonWrapper wrapper = new PersonWrapper(person);
        return wrapper;
    }
    
}    

A través de la URI /persons/pepe obtendríamos la representación (XML, JSON o lo que sea) de la persona con nombre igual a pepe.

Ahora supongamos que queremos añadir un link a nuestro recurso que apunte a sí mismo. La forma de hacerlo es muy sencilla. La clase ResourceSupport de la que heredamos viene con un método add que recibe como parámetro un objeto de tipo org.springframework.hateoas.Link que representará un enlace a cualquier recurso. Dicho enlace sigue el estandar Atom para links.

Añadir el link a nuestro recurso que apunte a sí mismo es muy fácil. Tan fácil como invocar a este nuevo método antes de devolver el recurso:

private void addSelfLink(PersonWrapper resource){
    final PersonWrapper person = methodOn(PersonController.class).getPerson(resource.getName());
    final Link link = linkTo(person).withSelfRel();
    resource.add(link);
}

Creo que el código habla por sí solo, pero por si no queda claro vamos a explicarlo. Lo que hacemos es crear un link que apunta recurso que daría como resultado la invocación del método correspondiente en el controlador correspondiente. Nótese que tanto methodOn como linkTo son métodos estáticos de la clase org.springframework.hateoas.mvc.ControllerLinkBuilder. El resultado tras añadir el link nos daría una respuesta como la que sigue:

Respuesta en formato JSON:

{
    "links": [{
        "rel": "self",
        "href": "http://localhost:8080/myapi/v1/persons/pepe"
    }],
    "name": "pepe",
    "age": 25
}

Respuesta en formato XML:


	<atom:link rel="self" href="http://localhost:8080/myapi/v1/persons/pepe"/>
	25
	luis

¿Existen otras formas de añadir links a nuestros recursos?

Pues la respuesta es sí y no. No porque todas pasan irremediablemente (al menos que yo sepa) por devolver un elemento del tipo ResourceSupport. Y sí porque Spring HATEOAS nos porporciona mecanismos para devolver estos elementos de tipo ResourceSupport de una manera más elegante. Al menos, existen estas dos alternativas:

  • Hacer uso de la clase org.springframework.hateoas.Resource que ya extiende de ResourceSupport y que nos permite no tener que crear nosotros los envoltorios puesto que Resource ya es, en sí mismo, un envoltorio. Yo, personalmente, tuve una mala experiencia usando esta clase con representaciones XML. Aquí os dejo un enlace para que que quiera saber más.
  • Hacer uso de la clase org.springframework.hateoas.mvc.ResourceAssemblerSupport, que básicamente es un conversor de POJOS que usemos en nuestro modelo de datos a objetos del tipo ResourceSupport. Probablemente esta es la mejor solución porque nos permite separar el proceso de conversión e inserción de links en una clase independiente (principio de responsabilidad única).


5. ¿Vemos un ejemplo?.

A continuación veremos un ejemplo que nos ayudará a asimilar mejor todos estos conceptos. Para ello crearemos un API REST muy futbolera :-). Estará compuesta de tres recursos distintos: equipos, estadios y jugadores, que estarán relacionados de la siguiente forma:

  • Un equipo juega en un estadio.
  • Un equipo está compuesto por un conjunto de jugadores.
  • Cada jugador solo puede jugar en un equipo.
  • En un estadio solo juega un equipo.
Resource Descripción
GET /teams Devuelve el listado de equipos
POST /teams Da de alta un equipo. Es necesario enviar en el cuerpo de la peticón los siguientes campos: name, foundationYear y rankingPosition.
GET /teams/:id Devuelve el equipo cuyo identificador coincida con :id
GET /teams/:id/stadium Devuelve el estadio del equipo cuyo identificador coincida con :id
POST /teams/:id/stadium Da de alta el estadio del equipo cuyo identificador coincida con :id. Es necesario enviar en el cuerpo de la petición los siguientes campos: capacity, name y city.
GET /teams/:id/players Devuelve los jugadores del equipo cuyo identificador coincida con :id
POST /teams/:id/players Da de alta un jugador perteneciente al equipo cuyo identificador coincida con :id. Es necesario enviar en el cuerpo de la petición los siguientes campos: name, goals, country y age.
GET /teams/:id/players/:id_player Devuelve los datos del jugador que juegue en un equipo cuyo identificador coincida con :id y con un identificador de jugador que coincida con :id_player.

Además, todas las peticiones y respuestas (body) soportarán json y xml (application/json y application/xml media types).


5.1. Respuestas sin links.

Si no seguimos el principio HATEOAS tendríamos el siguiente método en nuestro controlador que se encargaría de recibir las peticiones GET solicitando un equipo con un identificador dado.

@RequestMapping(value = "/teams/{id}", method = RequestMethod.GET)
public @ResponseBody Team getById(@PathVariable int id) {
	LOG.trace("Recibida solicitud para devolver el equipo con id {}", id);
    final Team team = teamDao.getById(id);
    if (team == null) {
        LOG.warn("El equipo con id {} no existe", id);
        throw new ResourceNotFoundException();
    }
    
    return team;
}

Tras realizar una la petición solicitando el equipo con id = 5000

Request URL: http://localhost:8080/soccer/api/teams/5000
Request Method: GET
Status Code: 200 OK

Obtendríamos un respuesta de este tipo:

{
    "teamId": 5000,
    "name": "Real Madrid C.F.",
    "foundationYear": 1902,
    "rankingPosition": 1
}

Como dijimos anteriormente, un equipo tiene asociado un estadio y un conjunto de jugadores, pero como cliente del API ¿cómo puedo saber cómo acceder a estos elementos asociados?. Sin seguir el principio HATEOAS, la única forma posible es consultar la documentación para saber que un equipo tiene un estadio y un conjunto de jugadores además de las URL´s donde residen los mismos. Además, esas URLs las deberíamos construir nosotros.


5.2. Respuestas que siguen el principio HATEOAS.

Vamos a modificar ligeramente nuestro código para que, haciendo uso de Spring-HATEOAS, podamos devolver los links que apuntan tanto al estadio como al listado de jugadores asociados a un equipo. Haremos lo siguiente:

  • Nuestra clase team ahora extenderá de ResourceSupport y heredará el soporte para que podamos añadir links asociados al equipo.
  • Añadimos los links del estadio y del listado de jugadores al equipo apuntando directamente a los métodos de sus correspondientes controladores, de esta forma, si cambiase la URL del recurso o su método HTTP no tendríamos que tocar nada en nuestro código.

OJO!!! probablemente ésta no sería la mejor forma en lo que a términos de diseño se refiere puesto que estamos mapeando directamente la salida de un DAO (Team) a la representación del recurso (se acabó el bajo acoplamiento). Lo ideal sería hacer uso de ResourceAssemblerSupport y devolver un wrapper. Sin embargo optamos por esta otra opción por simplicidad en el código.

Quedaría algo así (recordemos que ahora Team extiende de ResourceSupport):

@RequestMapping(value = "/teams/{id}", method = RequestMethod.GET)
public @ResponseBody Team getById(@PathVariable int id) {
    LOG.trace("Recibida solicitud para devolver el equipo con id {}", id);
    final Team team = teamDao.getById(id);
    if (team == null) {
        LOG.warn("El equipo con id {} no existe", id);
        throw new ResourceNotFoundException();
    }
    addTeamLinks(team);
    return team;
}

private void addTeamLinks(Team team) {
    addSelfLink(team);
    addStadiumLink(team);
    addPlayerLink(team);
}

private void addSelfLink(Team team) {
    team.add(linkTo(methodOn(TeamController.class).getById(team.getTeamId())).withSelfRel());
}

private void addStadiumLink(Team team) {
    team.add(linkTo(methodOn(StadiumController.class).getByTeamId(team.getTeamId())).withRel("stadium"));
}

private void addPlayerLink(Team team) {
    team.add(linkTo(methodOn(PlayerController.class).getTeamPlayers(team.getTeamId())).withRel("players"));
}

Y, tras la misma petición que hicimos en el punto anterior, el resultado sería este:

{
    "links": [{
        "rel": "self",
        "href": "http://localhost:8080/soccer/api/teams/5000"
    }, {
        "rel": "stadium",
        "href": "http://localhost:8080/soccer/api/teams/5000/stadium"
    }, {
        "rel": "players",
        "href": "http://localhost:8080/soccer/api/teams/5000/players"
    }],
    "teamId": 5000,
    "name": "Real Madrid C.F.",
    "foundationYear": 1902,
    "rankingPosition": 1
}

Como se puede apreciar, con una simple petición para obtener un equipo, podemos saber dónde residen sus recursos asociados. Si quisiésemos que la respuesta fuese XML, el resultado sería:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

    <atom:link rel="self" href="http://localhost:8080/soccer/api/teams/5000" />
    <atom:link rel="stadium" href="http://localhost:8080/soccer/api/teams/5000/stadium" />
    <atom:link rel="players" href="http://localhost:8080/soccer/api/teams/5000/players" />
    5000
    Real Madrid C.F.
    1902
    1

Y, evidentemente, accediendo a alguno de los recursos marcados por los enlaces, obtendríamos respuestas válidas:

Request URL: http://localhost:8080/soccer/api/teams/5000/players
Request Method: GET
Status Code: 200 OK
{
    "players": [{
        "links": [{
            "rel": "self",
            "href": "http://localhost:8080/soccer/api/teams/5000/players/7000"
        }, {
            "rel": "team",
            "href": "http://localhost:8080/soccer/api/teams/5000"
        }],
        "playerId": 7000,
        "name": "Cristiano Ronaldo",
        "goals": 172,
        "age": 28,
        "country": "Portugal",
        "teamId": 5000
    }, {
        "links": [{
            "rel": "self",
            "href": "http://localhost:8080/soccer/api/teams/5000/players/7001"
        }, {
            "rel": "team",
            "href": "http://localhost:8080/soccer/api/teams/5000"
        }],
        "playerId": 7001,
        "name": "Xabi Alonso",
        "goals": 12,
        "age": 32,
        "country": "España",
        "teamId": 5000
    }, 
    // etc, etc, etc...
    ]
}

Podéis ver y descargar el CÓDIGO FUENTE COMPLETO DEL EJEMPLO AQUÍ.

6. Referencias.


7. Conclusiones.

En este tutorial hemos visto que, aunque REST destaca por su sencillez, es necesario que tengamos presentes ciertos principios de diseño a la hora de modelar nuestras APIs. HATEOAS es uno de los principios que debemos seguir.

Gracias al principio HATEOAS facilitamos el descubrimiento de los recursos que componen nuestra API delegando en el servidor la manera en la que enlazaremos con ellos, una gran ventaja a la hora de mantener diferentes versiones de nuestra API y evitando problemas a nuestros clientes a la hora de solicitar recursos 🙂

Espero que este tutorial os haya sido de ayuda. Un saludo.

Miguel Arlandy

marlandy@autentia.com

Twitter: @m_arlandy

9 COMENTARIOS

  1. Hola, antes que nada gracias por el tuto, muy bueno, mmm tengo una duda, por qué mencionas que al mapear directamente la salida de un dao se acabaria el bajo acoplamiento?.

  2. Hola V1ct0r,

    Los DAO´s devuelven objetos que son representaciones de filas de una base de datos (o de cualquier otra fuente de datos). Si el día de mañana cambia el modelo de datos es muy probable que el objeto que devuelva nuestro DAO también cambie (ej: hemos añadido un campo nuevo en una tabla). Si ese objeto es parte del \»contrato\» de nuestra API (ya que lo devolvemos directamente), forzosamente también cambiará el contrato. Esto es debido a que la capa de exposición (controlador del API REST) y la lógica de negocio (de acceso a datos) están fuertemente acopladas (= mala práctica).

    Espero haber contestado a tu pregunta. Saludos!

  3. Gracias por la respuesta, tengo una ultima duda, disculpa las molestias, recién comienzo con estas tecnologías. Bien, estamos de acuerdo que es mejor esto:

    {
    \»id\»: 78,
    \»nombre\»: \»Juan\»,
    \»apellido\»: \»García\»,
    \»coches\»: [
    {
    \»coche\»: \»http://miservidor/concesionario/api/v1/clientes/78/coches/1033\»
    },
    {
    \»coche\»: \»http://miservidor/concesionario/api/v1/clientes/78/coches/3889\»
    }
    ]
    }

    que esto (por lo ya explicado por ti en este tuto):

    {
    \»id\»: 78,
    \»nombre\»: \»Juan\»,
    \»apellido\»: \»García\»,
    \»coches\»: [
    {
    \»id\»: 1033
    },
    {
    \»id\»: 3889
    }
    ]
    }

    Mi duda es, supongo que en este caso \»coches\» es una coleccion, seguramente coches es una entidad(clase y tabla en bd) además tiene mas campos, por ejemplo modelo, color etc.
    tu solo traes la referncia (el id), tanto en la respuesta sin uso de HATEOAS y como en la respuesta usando HATEOAS por qué?, por eficiencia, tú que recomiendas? traer las respuestas con
    todos los campos de las coleciones anidadas o solo con los id´s, obviamente en cualquier caso con su link de referencia, esta duda surge por que estoy haciendo una aplicacion con
    estas tecnologias, en la parte de persistencia uso hibernate (\»= recien comienzo con este\»), me parecio magico el traer datos con las anotaciones @ManyToOne, @ManyToMany etc, lo cual
    para mi resulto ventajoso, esto es lo que me genera duda, sera bueo ocupar estas anotaciones para traer respuestas por ejemplo json algo así

    {
    \»id\»: 78,
    \»nombre\»: \»Juan\»,
    \»apellido\»: \»García\»,
    \»coches\»: [
    {
    \»coche\»: \»http://miservidor/concesionario/api/v1/clientes/78/coches/1033\»

    \»links\»: [{
    \»rel\»: \»self\»,
    \»href\»: \»http://miservidor/concesionario/api/v1/clientes/78/coches/1033\»
    }],
    \»modelo\»: \»Ford Fiesta\»,
    \»color\»: \»Rojo\»
    },

    {
    \»coche\»: \»http://miservidor/concesionario/api/v1/clientes/78/coches/1033\»

    \»links\»: [{
    \»rel\»: \»self\»,
    \»href\»: \»http://miservidor/concesionario/api/v1/clientes/78/coches/3889\»
    }],
    \»modelo\»: \»Ford Lobo\»,
    \»color\»: \»Azul\»
    }
    ]
    }

    O es mejor traer respuestas de este tipo (solo referenciando los id´s), con esto se perderia la magia de las anotaciones de hibernate.

    {
    \»id\»: 78,
    \»nombre\»: \»Juan\»,
    \»apellido\»: \»García\»,
    \»coches\»: [
    {
    \»coche\»: \»http://miservidor/concesionario/api/v1/clientes/78/coches/1033\»
    },
    {
    \»coche\»: \»http://miservidor/concesionario/api/v1/clientes/78/coches/3889\»
    }
    ]
    }

  4. Hola V1ct0r,

    Eso ya es cómo tú quieras diseñar tu API. Yo, personalmente, devolvería únicamente el enlace al recurso.
    Lo que NUNCA debes hacer es definir un contrato (llámalo API REST, WSDL, o lo que sea) que esté condicionado por la implementación que lleve debajo (principio de diseño de servicios de BAJO ACOPLAMIENTO). El que pierdas la magia de Hibernate no debe ser una excusa que condicione el diseño de tu API. Por lo que, independientemente de si usas Hibernate y esas \»anotaciones mágicas\» de JPA, debes pensar antes en QUÉ voy a exponer en mi API (diseño el contrato) y luego en CÓMO lo voy a hacer para realizar la lógica necesaria (implementación).

    Suerte con tu API 🙂
    Saludos.

  5. Hola Miguel, muy buen tutorial.

    Me surge la siguiente duda, si yo estoy esperando del lado del cliente recibir un objeto serializado en json, para deserializarlo nuevamente, como me afecta el hecho de haber agregado estas URis al Json respuesta ? Tengo soporte del lado del cliente para poder parsear estos resultados sin tener que agregar mucho codigo ?

    Sds,

  6. Hola Miguel, soy nuevo en esto de las API’s, lei todo y trate de entenderlo y admito que no lo entendí del todo en algunas partes me perdí, por lo menos capte la idea me parece, me gustaría que me sujierieas que conceptos tendría que estudiar para entender mejor el funcionamiento y programación de las API’s. Gracias.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad