Salta el contingut

5. Spring Haetoas

1. HATEOAS

Hateoas (Hypermedia as the engine of application state) és un principi d'API RESTful definit per Roy Fielding. Principalment significa que el client pot moure's per tota l'aplicació només des d'URI's generals en format hipermèdia. El principi implica que l'API ha de guiar el client a través de l'aplicació retornant informació rellevant sobre els següents passos potencials, juntament amb cada resposta.

Per a la connexió entre el servidor i el client, Fielding defineix aquestes quatre característiques:

  • Identificació única de tots els recursos: tots els recursos han de poder ser identificats amb un URI (Identificador de Recurs Únic).
  • Interacció amb recursos a través de representacions: Si un client necessita un recurs, el servidor li envia una representació (per exemple, HTML, JSON o XML) perquè el client pugui modificar o eliminar el recurs original.
  • Missatges explícits: cada missatge intercanviat entre el servidor i el client ha de contenir totes les dades necessàries per entendre's mútuament.
  • HATEOAS: Aquest principi també integra una API REST. Aquesta estructura basada en hipermèdia facilita als clients l'accés a l'aplicació, ja que no necessiten conèixer res més sobre la interfície per poder accedir-hi i navegar-hi.

HATEOAS és, en resum, una de les propietats més bàsiques de les API REST i, com a tal, essencial en qualsevol servei REST.

Un valor retornat sense HATEOAS, amb dades d'un client:

JSON
{
    "idCliente": 3,
    "nif": "33333333C",
    "nombre": "Vicente",
    "apellidos": "Mondragón",
    "claveSeguridad": "1234",
    "email": "vicente.mondragon@tia.es",
    "recomendacion": {
      "idRecomendacion": 3,
      "observaciones": "Realiza muchos pedidos"
    },
    "listaCuentas": [
      {
        "idCuenta": 8,
        "banco": "1001",
        "sucursal": "1001",
        "dc": "11",
        "numeroCuenta": "1000000008",
        "saldoActual": 7500.0,
        "links": [

        ]
      },
      {
        "idCuenta": 10,
        "banco": "1001",
        "sucursal": "1001",
        "dc": "11",
        "numeroCuenta": "1000000010",
        "saldoActual": -3500.0,
        "links": [

        ]
      }
    ],
    "listaDirecciones": [
      {
        "idDireccion": 5,
        "descripcion": "calle de la creu, 2",
        "pais": "España",
        "cp": "46701"
      }
    ]
  }

Tingues en compte que:

  • Hem obtingut totes les dades del client
  • No sabem com obtenir dades de camps relacionats específics, com Direccion o Cuenta. Estan ahí però no sabem com obtenir-les

La mateixa sol·licitud amb HATEOAS:

JSON
{
  "idCliente": 3,
  "nif": "33333333C",
  "nombre": "Vicente",
  "apellidos": "Mondragón",
  "claveSeguridad": "1234",
  "email": "vicente.mondragon@tia.es",
  "links": [
    {
      "rel": "self",
      "href": "http://localhost:9090/clientes/3"
    },
    {
      "rel": "listaDirecciones",
      "href": "http://localhost:9090/clientes/3/direcciones"
    },
    {
      "rel": "listaCuentas",
      "href": "http://localhost:9090/clientes/3/cuentas"
    }
  ]
}

Com pots veure:

  • Només s'envien les dades d'un client
  • Tenim enllaços, amb URI's clares per obtenir informació específica d'aquest client

i el més important Si el servidor canvia la seva estructura, enviarà enllaços actualitzats, i el client funcionarà sense cap problema

2. Afegint HATEOAS

2.1. Llibrerires

Atenció

En aquest text, afegirem capacitats HATEOAS a una API RESTful desenvolupada al llarg de la unitat.

Només necessitem afegir aquesta dependència al nostre pom.xml, suposant que hem utilitzat un projecte iniciador de Spring:

XML
1
2
3
4
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

i ja està.

2.2. Wrappers (Envoltoris)

2.2.1. Punt d'inici

Recorda el que hem fet en el nostre projecte inicial:

  • Classes Model o DAO → preparades per guardar informació a la base de dades. Estan anotades amb Hibernate i són la base dels nostres repositoris. Per exemple, Cliente.
  • Classes DTO → preparades per transferir dades des del nostre model i cap a aquest.
  • Aquestes classes encapsulen les DAO (afegint o eliminant camps).
  • Aquestes classes tenen mètodes per convertir entre DAO i DTO.
  • És el servei qui realitza la conversió.
  • El client ens enviarà informació en aquestes classes DTO.
  • Aquestes classes poden ser utilitzades tant per una API Rest com per una aplicació web MVC.

2.2.2. Envoltori HATEOAS

Necessitem definir una nova classe per embolicar la nostra resposta HATEOAS.

Partint dels DTO's, conté tota la informació d'una classe, pròpia i relacionada (Client més Direcció més Comptes). Amb HATEOAS, com hem mostrat recentment, només necessitem la informació pròpia del Client i necessitem generar enllaços a entitats relacionades. Llavors, necessitem afegir a la informació del client la capacitat de generar i emmagatzemar enllaços. La classe que ho permet és RepresentationModel<base_class> (documentació completa aquí). Això afegirà a les nostres classes:

  • Una estructura per a guadar links
  • Mètodes per afegir, comprovar i retornar links

Per tal de fer-ho

Java
@Data @AllArgsConstructor
public class ClienteHATEOAS 
    extends RepresentationModel<ClienteDTO>
    implements Serializable{

    private static final long serialVersionUID = 1L;
    private long idCliente;
    private String nif;
    private String nombre;
    private String apellidos;
    private String claveSeguridad;
    private String email;

    public static ClienteHATEOAS fromClienteDTO2HATEOAS(ClienteDTO clienteDTO) {
        return new ClienteHATEOAS(
                clienteDTO.getIdCliente(),
                clienteDTO.getNif(),
                clienteDTO.getNombre(),
                clienteDTO.getApellidos(),
                clienteDTO.getClaveSeguridad(),
                clienteDTO.getEmail());
    }
} 

Important

  • Com que tenim una API base que funciona amb ClienteDTO, hem creat aquesta classe envoltori a partir d'ella.
  • Com que HATEOAS és només un format de resposta, pots crear-lo a partir de Cliente com a classe base, però has de definir el teu servei per retornar Cliente també.
  • És molt important crear un mètode de conversió fromClienteDTO2HATEOAS, que inclogui els camps necessaris.

Llavors, utilitzarem el mètode add(Link) en el nostre envoltori ClienteHATEOAS per afegir tants Link com sigui necessari.

Ara, la pregunta és com generar els nostres objectes Link en les nostres classes envoltori. Podríem fer-ho de manera creativa, manipulant camins en cadenes de text i composant amb mètodes complicats de subcadena i concatenació.

Però com que sabem quin mètode es crida per a cada referència, és millor crear enllaços obtenint referències al camí des dels mateixos mètodes. Per fer-ho, hem d'utilitzar aquests mètodes i crides estàtiques:

Java
1
2
3
4
5
6
7
import org.springframework.hateoas.Link;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

Link self=linkTo(methodOn(controller_class.class)
            .methodName(args))
            .withSelfRel(); // or .withRel(String Link)
  • linkTo → mètode estàtic que crea un Link des de
  • methodOn(class) → cerca en una classe de controlador un mètode
  • .methodName(args) → obté una crida real per a aquest mètode
  • I per etiquetar l'enllaç:
  • .withSelfRel() → crea un enllaç anomenat self
  • .withRel(String Link) → crea un enllaç amb el nom donat.

Exemples del nostre Cliente controller en la següent secció

Java
1
2
3
4
5
ClienteDTO elCliente=clienteService.getClienteById(idCliente);

Link self=linkTo(methodOn(ClienteController.class)
            .showClienteById(elCliente.getIdCliente()))
            .withSelfRel();

Aquest exemple:

  • Carrega un ClienteDTO del ClienteService actual.
  • Després busca a la classe ClienteController un mètode anomenat showClienteById.
  • Fa una crida interna i cerca el camí i vincula l'argument al camí (recordes @PathVariable?)
  • Finalment, obté el camí (ruta) complet amb l'argument i l'emmagatzema en el Link amb la referència self

El resultat serà alguna cosa així:

JSON
1
2
3
4
{
  "rel": "self",
  "href": "http://localhost:9090/clientes/3"
}
Java
1
2
3
4
5
ClienteDTO elCliente=clienteService.getClienteById(idCliente);

Link cuentas=linkTo(methodOn(CuentaController.class)
                .listCuentasCliente(elCliente.getIdCliente()))
                .withRel("listaCuentas");

Aquest exemple:

  • Carrega un ClienteDTO del ClienteService actual.
  • Després busca a la classe CuentaController un mètode anomenat listCuentasCliente.
  • Fa una crida interna i cerca el camí i vincula l'argument al camí (recordes @PathVariable?)
  • Finalment, obté el camí (ruta) complet amb l'argument i l'emmagatzema en el Link amb la referència self

El resultat serà alguna cosa així:

JSON
1
2
3
4
{
  "rel": "listaCuentas",
  "href": "http://localhost:9090/clientes/3/cuentas"
}

3.3. Afegint enllaços al nostre envoltori i exemple complet

Un cop hem creat els enllaços, necessitem afegir-los a la nostra última classe envoltori. Simplement utilitzarem el mètode add() per fer-ho. En el següent mètode, es rep un embolcall ClienteHATEOAS (amb només dades de ClienteDTO) i s'afegeixen tants enllaços com vulguem:

Java
private void addLinks(ClienteHATEOAS elCliente) {
  // self
  Link self=linkTo(methodOn(ClienteController.class)
    .showClienteById(elCliente.getIdCliente()))
    .withSelfRel();

  elCliente.add(self);

  // direcciones
  Link direcciones=linkTo(methodOn(DireccionController.class)
      .listDireccionesCliente(elCliente.getIdCliente()))
      .withRel("listaDirecciones");
  elCliente.add(direcciones);

  //cuentas
  Link cuentas=linkTo(methodOn(CuentaController.class)
      .listCuentasCliente(elCliente.getIdCliente()))
      .withRel("listaCuentas");
  elCliente.add(cuentas);
}

El mètode del controlador per obtenir un Cliente serà (comentat):

Java
@GetMapping("/clientes/{idCliente}")
  public ResponseEntity<ClienteHATEOAS> showClienteById(@PathVariable Long idCliente){

    // obtenir DTO del Servei
    ClienteDTO elCliente=clienteService.getClienteById(idCliente);

    // si no existeix, retornar no trobat
    if (elCliente==null)
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    else {

      // crear un ClienteHATEOAS a partir del DTO cridant el mètode estàtic
      ClienteHATEOAS returnCliente=ClienteHATEOAS.fromClienteDTO2HATEOAS(elCliente);

      // afegir enllaços a ClienteHATEOAS
      addLinks(returnCliente);

      // retornar    
      return new ResponseEntity<>(returnCliente,HttpStatus.OK);
    }
  } 

i el resultat serà alguna cosa així:

JSON
{
  "idCliente": 3,
  "nif": "33333333C",
  "nombre": "Vicente",
  "apellidos": "Mondragón",
  "claveSeguridad": "1234",
  "email": "vicente.mondragon@tia.es",
  "links": [
    {
      "rel": "self",
      "href": "http://localhost:9090/clientes/3"
    },
    {
      "rel": "listaDirecciones",
      "href": "http://localhost:9090/clientes/3/direcciones"
    },
    {
      "rel": "listaCuentas",
      "href": "http://localhost:9090/clientes/3/cuentas"
    }
  ]
}

4. Treball pendent

Ara, fent una petició simple a un Cliente tenim accès a tota la informació i següents accions disponibles en les dades de la resposta

Això permet al servidor poder evolucionar sense tindre que modificar eels clients que li fan peticions, ja que qualssevol modificació sera inmediatamnet notificada als usuaris de les API en les pròpies respostes.

Ara et toca a tu completar el projecte desenvoluat afegint les classes necessàries i afegint el HATEOAS als teus models.