Skip to content

Latest commit

 

History

History
1416 lines (1179 loc) · 37.9 KB

spring-hateoas.adoc

File metadata and controls

1416 lines (1179 loc) · 37.9 KB

Spring HATEOAS

Arthur Noseda

Arthur Noseda

Marié, 2 enfants

Développeur Java depuis 2003

Tout commence par un besoin

Tout commence par un besoin

Comment concevoir l’API REST d’un processus…​

Comment concevoir l’API REST d’un processus…​

Sans que le client ne connaisse à l’avance les états et les transitions.

Sans que le client ne connaisse à l’avance les états et les transitions.

Disclaimer

Vous n’avez peut-être pas besoin de respecter les contraintes HATEOAS.

REST APIs must be hypertext-driven

Selon Roy Fielding
  • Roy Fielding est l’un des principaux auteurs de la spécification HTTP.

  • Membre fondateur de la fondation Apache

  • A défini le style d’architecture representational state transfer

  • …​

REST APIs must be hypertext-driven

RESTistential Crisis over Hypermedia APIs (InfoQ)

Getting hyper about hypermedia APIs

Getting hyper about hypermedia APIs

David Heinemeier Hansson, le créateur de Ruby on Rails, démonte un certain nombre d’idées reçues à propos d’hypermedia.

Demo

background

LDVELH

REST in Practice

LDVELH

[*] --> Draw
state NextRound <<choice>>
state HeroWoundLuckTested <<choice>>
state MonsterWoundLuckTested <<choice>>

Draw --> NextRound : Next Round

NextRound --> HeroWound : [har < mar && hs > 0]
NextRound --> HeroDied : [har < mar && hs <= 0]
NextRound --> MonsterWound : [har > mar && ms > 0]
NextRound --> MonsterDied : [har > mar && ms <= 0]
NextRound --> Draw : [har == mar]

HeroWound --> NextRound : Next Round
HeroWound --> HeroWoundLuckTested : Test Luck

HeroWoundLuckTested --> NextRound : [hs > 0]
HeroWoundLuckTested --> HeroDied : [hs <= 0]

MonsterWound --> NextRound : Next Round
MonsterWound --> MonsterWoundLuckTested : Test Luck

MonsterWoundLuckTested --> NextRound : [ms > 0]
MonsterWoundLuckTested --> MonsterDied : [ms <= 0]

HeroDied --> [*]
MonsterDied --> [*]

RESTful Web APIs

RESTful Web APIs

Richardon & Amundsen

Leonard Richardson Mike Amundsen

REST in Practice

REST in Practice

Webber, Parastatidis & Robinson

Jim Webber Savas Parastatidis Ian Robinson

Jim Webber, Savas Parastatidis & Ian Robinson

Modèle de maturité de Richardson

Modèle de maturité de Richardson

Le talk de 2008 à la QCon

An amazing technology stack

Au moment de présenter "Justice Will Take Us Millions Of Intricate Moves", Richardson travaillait sur l’application Launchpad pour Canonical.

Modèle de maturité hypermédia

Jason Desrosiers redécoupe le niveau 3 en 4

  • HMM 0 : application/json contenant des URL

  • HMM 1 : type de médias formalisant ce que sont les liens

  • HMM 2 : HMM 1 + type de médias formalisant comment mettre à jour une ressource

  • HMM 3 : HMM 2 + type de médias utilisant un vocabulaire dont la sémantique est décrite et partagée

Hypermedia ?

Définition

Système de présentation de l’information reposant sur des hyperliens qui permettent de passer d’un document multimédia à un autre.
— Office québécois de la langue française

HATEOAS ?

Hypermedia As The Engine Of Application State

Définition d’HATEOAS

L’état de la ressource détermine la liste des liens et fait partie de la représentation

Qui produit des API hypermedia ?

Qui produit des API hypermedia ?

Producteurs d’hypermedia

  • Spring

  • Microsoft

  • Amazon

  • Adidas

  • Camunda

  • SlimPay

  • NRK TV

  • …​

Spring HATEOAS

The usual suspects

Une longue incubation

  • 2000 - La dissertation de Roy Fielding

  • 2008 - Maturity Heuristic (Richardson)

  • 19/07/2012 - Spring HATEOAS 0.1.0.RELEASE

  • 30/09/2019 - Spring HATEOAS 1.0.0.RELEASE

Le titre exact de la dissertation de Roy Fielding : Architectural Styles and the Design of Network-based Software Architectures

Mais un produit vivant

Export gitk

Caractéristiques

  • Compatible avec Spring MVC et Spring WebFlux

  • i18n

  • Support de HAL, HAL-FORMS, Collection+JSON, ALPS, UBER out of the box

  • Projets communautaires pour JSON:API et Siren

start.spring.io

start.spring.io > Spring HATEOAS

start.spring.io

start.spring.io > Spring REST Repositories

start.spring.io

start.spring.io > Spring REST Docs

Modèle de programmation

  • EntityModel

  • CollectionModel

  • Link

  • Affordance

getTodos

@GetMapping
public CollectionModel<EntityModel<Todo>> getTodos() {
  return CollectionModel.of(todoRepository.findAll().stream()
      .map(this::toRepresentation)
      .collect(Collectors.toList()),
          linkTo(methodOn(TodoController.class)
              .getTodos()).withSelfRel()
          .andAffordance(afford(methodOn(TodoController.class)
              .createTodo(null))));
}

getTodo

@GetMapping("/{id}")
public EntityModel<Todo> getTodo(@PathVariable("id") UUID id) {
  return todoRepository.findById(id)
      .map(this::toRepresentation)
      .orElseThrow(this::notFound);
}

private EntityModel<Todo> toRepresentation(Todo todo) {
  return EntityModel.of(todo,
      linkTo(methodOn(TodoController.class)
          .getTodo(todo.getId())).withSelfRel()
          .andAffordance(afford(methodOn(TodoController.class)
              .updateTodo(todo.getId(), null)))
          .andAffordance(afford(methodOn(TodoController.class)
              .deleteTodo(todo.getId()))));
}

Quel media-type choisir ?

Hypertext Application Language

IANA : application/hal+json et application/hal+xml

  • Le plus simple et le plus populaire

  • Rien de prévu pour les modifications

L’Internet Assigned Numbers Authority supervise entre autres l’allocation globale des adresses IP, la gestion de la zone racine dans les DNS et les types de médias.

HAL

{
  "_embedded" : {
    "todos" : [ {
      "id" : "5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
      "title" : "Go on a Treasure Hunt",
      "completed" : false,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
        }
      }
    }, ... ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/todos"
    }
  }
}

HAL

{
  "id" : "5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
  "title" : "Go on a Treasure Hunt",
  "completed" : false,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
    }
  }
}

De la normalisation des relations

IANA Link Registrations

Définies en tant que constantes

Définies en tant que constantes

IANA Link Registrations dans Spring HATEOAS

HAL-FORMS

IANA : application/prs.hal-forms+json

  • Ajoute la notion de formulaire (au sens HTML) à HAL

prs signifie personal.

HAL-FORMS

{
  "_embedded" : {
    "todos" : [ {
      "id" : "5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
      "title" : "Go on a Treasure Hunt",
      "completed" : false,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
        }
      },
      "_templates" : {
        "default" : {
          "method" : "PUT",
          "properties" : [ {
            "name" : "completed",
            "readOnly" : true
          }, {
            "name" : "title",
            "readOnly" : true,
            "type" : "text"
          } ]
        },
        "deleteTodo" : {
          "method" : "DELETE",
          "properties" : [ ]
        }
      }
    }, ... ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/todos"
    }
  },
  "_templates" : {
    "default" : {
      "method" : "POST",
      "properties" : [ {
        "name" : "title",
        "readOnly" : true,
        "type" : "text"
      } ]
    }
  }
}

HAL-FORMS

{
  "id" : "5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
  "title" : "Go on a Treasure Hunt",
  "completed" : false,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
    }
  },
  "_templates" : {
    "default" : {
      "method" : "PUT",
      "properties" : [ {
        "name" : "completed",
        "readOnly" : true
      }, {
        "name" : "title",
        "readOnly" : true,
        "type" : "text"
      } ]
    },
    "deleteTodo" : {
      "method" : "DELETE",
      "properties" : [ ]
    }
  }
}

HAL Explorer

HAL Explorer

HAL Explorer

Collection+JSON

IANA : application/vnd.collection+json

vnd signifie vendor.

Collection+JSON

{
  "links": [
    {
      "rel": "self",
      "href": "http://localhost:8080/todos"
    }
  ],
  "content": [
    {
      "id": "6f0cd0d2-a870-4c84-a76a-498014779c21",
      "title": "Go on a Treasure Hunt",
      "completed": false,
      "links": [
        {
          "rel": "self",
          "href": "http://localhost:8080/todos/6f0cd0d2-a870-4c84-a76a-498014779c21"
        }
      ]
    },
    ...
  ]
}

Collection+JSON

{
  "id": "6f0cd0d2-a870-4c84-a76a-498014779c21",
  "title": "Go on a Treasure Hunt",
  "completed": false,
  "links": [
    {
      "rel": "self",
      "href": "http://localhost:8080/todos/6f0cd0d2-a870-4c84-a76a-498014779c21"
    }
  ]
}

UBER

application/vnd.amundsen-uber+json

  • Pas enregistré auprès de l’IANA

UBER

{
  "uber": {
    "version": "1.0",
    "data": [
      {
        "name": "self",
        "rel": [
          "self",
          "getTodos"
        ],
        "url": "http://localhost:8080/todos"
      },
      {
        "name": "createTodo",
        "rel": [
          "createTodo"
        ],
        "url": "http://localhost:8080/todos",
        "action": "append",
        "model": "title={title}"
      },
      {
        "data": [
          {
            "name": "self",
            "rel": [
              "self",
              "getTodo"
            ],
            "url": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
          },
          {
            "name": "updateTodo",
            "rel": [
              "updateTodo"
            ],
            "url": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
            "action": "replace",
            "model": "completed={completed}&title={title}"
          },
          {
            "name": "deleteTodo",
            "rel": [
              "deleteTodo"
            ],
            "url": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
            "action": "remove",
            "model": ""
          },
          {
            "name": "todo",
            "data": [
              {
                "name": "completed",
                "value": false
              },
              {
                "name": "id",
                "value": "5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
              },
              {
                "name": "title",
                "value": "Go on a Treasure Hunt"
              }
            ]
          }
        ]
      },
      ...
    ]
  }
}

UBER

{
  "uber": {
    "version": "1.0",
    "data": [
      {
        "name": "self",
        "rel": [
          "self",
          "getTodo"
        ],
        "url": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
      },
      {
        "name": "updateTodo",
        "rel": [
          "updateTodo"
        ],
        "url": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
        "action": "replace",
        "model": "completed={completed}&title={title}"
      },
      {
        "name": "deleteTodo",
        "rel": [
          "deleteTodo"
        ],
        "url": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
        "action": "remove",
        "model": ""
      },
      {
        "name": "todo",
        "data": [
          {
            "name": "completed",
            "value": false
          },
          {
            "name": "id",
            "value": "5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
          },
          {
            "name": "title",
            "value": "Go on a Treasure Hunt"
          }
        ]
      }
    ]
  }
}

Formats spécialisés

Application-Level Profile Semantics (ALPS)

application/alps+json et application/alps+xml

ALPS

@GetMapping(value = "/profile", produces = ALPS_JSON_VALUE)
public Alps alps() {
  return Alps.alps()
      .descriptor(
          List.of(
              Descriptor.builder()
                  .id("id")
                  .type(Type.SEMANTIC)
                  .doc(Doc.builder().value("The identifier").build())
                  .build(),
              Descriptor.builder()
                  .id("title")
                  .type(Type.SEMANTIC)
                  .doc(Doc.builder().value("The title").build())
                  .build(),
              // ici les autres propriétés
          )
      )
      .build();
}

ALPS

{
  "version": "1.0",
  "descriptor": [
    {
      "id": "id",
      "type": "SEMANTIC",
      "doc": {
        "value": "The identifier"
      }
    },
    {
      "id": "title",
      "type": "SEMANTIC",
      "doc": {
        "value": "The title"
      }
    },
    ...
  ]
}

Problem Details for HTTP APIs

IANA : application/problem+json

Problem

return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Problem.create()
    .withType(URI.create("http://example.com/todos/cannot-update-completed"))
    .withTitle("Cannot update completed")
    .withStatus(HttpStatus.FORBIDDEN)
    .withInstance(URI.create(String.format("/todos/%s", id))));

Problem

{
  "type": "http://example.com/todos/cannot-update-completed",
  "title": "Cannot update completed",
  "status": 403,
  "instance": "/todos/8bce30c8-d975-49f8-b90c-c250a8c78362"
}

Extensions communautaires

JSON:API

IANA : application/vnd.api+json

JSON:API

{
  "data": [
    {
      "id": "5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
      "type": "todos",
      "attributes": {
        "title": "Go on a Treasure Hunt",
        "completed": false
      },
      "links": {
        "self": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
      }
    },
    ...
  ],
  "links": {
    "self": "http://localhost:8080/todos"
  }
}

JSON:API

{
  "data": {
    "id": "5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
    "type": "todos",
    "attributes": {
      "title": "Go on a Treasure Hunt",
      "completed": false
    }
  },
  "links": {
    "self": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
  }
}

Siren

IANA : application/vnd.siren+json

Siren

{
  "class": [
    "collection"
  ],
  "entities": [
    {
      "class": [
        "entity"
      ],
      "rel": [
        "item"
      ],
      "properties": {
        "id": "5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
        "title": "Go on a Treasure Hunt",
        "completed": false
      },
      "links": [
        {
          "rel": [
            "self"
          ],
          "href": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
        }
      ],
      "actions": [
        {
          "name": "updateTodo",
          "method": "PUT",
          "href": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
          "type": "application/x-www-form-urlencoded",
          "fields": [
            {
              "name": "completed",
              "type": "text"
            },
            {
              "name": "title",
              "type": "text"
            }
          ]
        },
        {
          "name": "deleteTodo",
          "method": "DELETE",
          "href": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
        }
      ]
    },
    ...
  ],
  "links": [
    {
      "rel": [
        "self"
      ],
      "href": "http://localhost:8080/todos"
    }
  ],
  "actions": [
    {
      "name": "createTodo",
      "method": "POST",
      "href": "http://localhost:8080/todos",
      "type": "application/x-www-form-urlencoded",
      "fields": [
        {
          "name": "title",
          "type": "text"
        }
      ]
    }
  ]
}

Siren

{
  "class": [
    "entity"
  ],
  "properties": {
    "id": "5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
    "title": "Go on a Treasure Hunt",
    "completed": false
  },
  "links": [
    {
      "rel": [
        "self"
      ],
      "href": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
    }
  ],
  "actions": [
    {
      "name": "updateTodo",
      "method": "PUT",
      "href": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca",
      "type": "application/x-www-form-urlencoded",
      "fields": [
        {
          "name": "completed",
          "type": "text"
        },
        {
          "name": "title",
          "type": "text"
        }
      ]
    },
    {
      "name": "deleteTodo",
      "method": "DELETE",
      "href": "http://localhost:8080/todos/5f4bb924-e930-4b2d-ae1c-4bdb501752ca"
    }
  ]
}

Autres hypermédias

Spring Boot Actuator

Spring Actuator

Spring Boot Actuator

$ curl -I http://localhost:8080/actuator
HTTP/1.1 200
Content-Type: application/vnd.spring-boot.actuator.v3+json
Content-Length: 1735
Date: Tue, 22 Mar 2022 20:42:01 GMT

Spring Boot Actuator

Merci