11

Adding HAL links to Spring Boot 2 applications using Spring HATEOAS

 3 years ago
source link: https://tech.asimio.net/2020/04/06/Adding-HAL-Hypermedia-to-Spring-Boot-2-applications-using-Spring-HATEOAS.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Adding HAL links to Spring Boot 2 applications using Spring HATEOAS

Orlando L Otero | Apr 6, 2020

| api, hal, hateoas, hypermedia, java, microservices, restful, spring boot, sql

| 9 min read

| 0 Comments

This post has been featured on https://www.baeldung.com/java-weekly-328.

1. INTRODUCTION

HATEOAS, acronym for Hypermedia as the Engine of Application State, offers what your API consumers might do next when starting from a REST API entry point.

It includes hypermedia in the response, stateful links to related REST resources depending on business value or context. For instance, an upsell hypermedia link to upgrade to a Hotel suite instead of the room you might have in a shopping cart. A cancel hypermedia link to postpone a scheduled payment to a service provider.

This allows your API endpoints to reach the Level 3 of the famous Richardson Maturity Model. A more mature level than resources and verbs since it helps to provide API discoverability and self-documentation, to some degree.

Spring Boot - HAL - HATEOAS

This blog post covers the configuration and implementation details to include HAL representations in your API responses using Spring Boot 2.1.x.RELEASE and Spring HATEOAS 0.25.x.RELEASE.

2. REQUIREMENTS

  • Java 8+.
  • Maven 3.2+.
  • Familiarity with Spring Framework.

3. HATEOAS IMPLEMENTATION

This section helps you to include HAL (Hypertext Application Language) hyperlinks in REST response payloads. From dependencies, properties settings, to bean definitions, controller implementation, domain and response models, and mappers.

3.1. DEPENDENCIES

pom.xml:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

This is the only dependency required when used in Spring Boot applications, which brings in spring-boot-starter-web. Other dependencies found in this file provide support for the demo application to compile and run.

3.2. PROPERTIES CONFIGURATION

application.yml:

spring:
  hateoas:
    use-hal-as-default-json-media-type: false
  jackson:
    default-property-inclusion: NON_ABSENT

spring.jackson.default-property-inclusion set to NON_ABSENT configures Jackson’s ObjectMapper to leave null or in the case of Java 8+, empty Optional attributes, out of the JSON response payload. It’s shortcut to setting @JsonInclude(Include.NON_ABSENT) to every REST resource class.

spring.hateoas.use-hal-as-default-json-media-type set to false prevents sending back the Content-Type header set to application/hal+json by default.

Note: It would interesting to see how Header-based API Versioning producing application/vnd.asimio-v1+json payloads and application/json+hal would play along. Stay tuned, I might cover this functionality in a future post.
3.3. REST CONTROLLER

Let’s analyze ActorController.java endpoint implementations independently.

  • retrieveActor()
@RestController
@RequestMapping(value = "/api/actors", produces = { MediaType.APPLICATION_JSON_VALUE, MediaTypes.HAL_JSON_VALUE })
public class ActorController {
...
  @GetMapping(value = "/{id}")
  public ResponseEntity<ActorResource> retrieveActor(@PathVariable Integer id) {
    Optional<Actor> optionalActor = this.dvdRentalService.retrieveActor(id);
    return optionalActor.map(actor -> {
      ActorResource resource = ActorResourceMapper.INSTANCE.map(actor);
      return new ResponseEntity<>(resource, HttpStatus.OK);
    }).orElseThrow(() -> new ResourceNotFoundException(String.format("Actor with id=%s not found", id)));
  }
...
}

The endpoints implemented in this class produces application/json or application/hal+json unless a particular implementation specifies a different value.

The retrieveActor() method uses MapStruct-based ActorResourceMapper.java to not only map from the JPA domain entity to the REST resource, but to also add HATEOAS links.

  • retrieveActorFilms()
@RestController
@RequestMapping(value = "/api/actors", produces = { MediaType.APPLICATION_JSON_VALUE, MediaTypes.HAL_JSON_VALUE })
public class ActorController {
...
  @GetMapping(value = "/{id}/films")
  public Resources<FilmResource> retrieveActorFilms(@PathVariable Integer id) {
    Actor actor = this.dvdRentalService.retrieveActor(id)
      .orElseThrow(() -> new ResourceNotFoundException(String.format("Actor with id=%s not found", id)));
    List<FilmResource> resources = FilmActorResourceMapper.INSTANCE.map(actor.getFilmActors());
    Resources<FilmResource> result = new Resources<>(resources);
    // Adding root level links: self=/api/actors/1/films, parent=/api/actors/1 in addition to each film's self link
    this.addLinks(result, actor);
    return result;
  }

  private void addLinks(Resources<FilmResource> resources, Actor actor) {
    // parent link
    Link parentLink = ControllerLinkBuilder.linkTo(ActorController.class)
      .slash(actor)
      .withRel("parent");
    // self link
    Link selfLink = ControllerLinkBuilder.linkTo(ActorController.class)
      .slash(actor)
      .slash("films")
      .withSelfRel();
    resources.add(parentLink, selfLink);
  }
...
}

Similarly to retrieveActor() method, retrieveActorFilms() also uses a MapStruct mapper to map from the JPA entity to the endpoint response.

The difference is executing addLinks() method in the Controller class to add HAL instead of adding the links via the JPA domain model to RESTful resource model mapper.

The REST response would include these links:

"links" : [
  {
    "href" : "http://localhost:8080/api/actors/1",
    "rel" : "parent"
  },
  {
    "href" : "http://localhost:8080/api/actors/1/films",
    "rel" : "self"
  }
]

Just two different ways to accomplish the same goal: to add HAL support to REST APIs responses.

3.4. JPA AND REST RESOURCE MODELS

Actor.java:

@Entity
@Table(name = "actor", schema = "public")
// Getters/Setters
public class Actor implements Identifiable<Integer>, Serializable {
...
  @Override
  public Integer getId() {
    return this.actorId;
  }
...
}

Actor is a JPA entity, part of the application’s internal domain model. I would suggest to never expose the model directly as the API response. Any change to the internal domain model might cause breaking changes to the contracted API response and thus affecting your APIs consumers.

This JPA entity also implements Spring HATEOAS’s Identifiable marker interface used to identify objects by their id to include it in the links.

Spring HATEOAS executes the getId() method via statements like:
ControllerLinkBuilder.linkTo(ActorController.class).slash(entity);
as found in ActorResourceMapper.java; to generate hypermedia link:
/api/actors/1.

ActorResource.java:

@Relation(value = "actor", collectionRelation = "actors")
public class ActorResource extends ResourceSupport {

  @JsonProperty("id")
  private int actorId;
  private String first;
  private String last;
...
}

ActorResource is a RESTful API response. This is what your RESTful endpoint will be sending back to your consumers.

Domain model changes should have minimum impact to the API responses to prevent breaking your API clients. In case you have to make a breaking change, I would suggest to version the endpoints.

This resource model extends ResourceSupport to provide support for retrieving, adding, removing links via add(Link... links) and others methods, as included in ActorResourceMapper.java.

The @Relation configures the way Spring HATEOAS includes instances of this class in the response payload. In this case it will use actors as the name for a collection of ActorResource instead of defaulting to actorList.

3.5. RESOURCE MAPPER

ActorResourceMapper.java:

@Mapper
public interface ActorResourceMapper extends ResourceMapper<Actor, ActorResource> {

  ActorResourceMapper INSTANCE = Mappers.getMapper(ActorResourceMapper.class);

  @AfterMapping
  default void addLinks(@MappingTarget ActorResource resource, Actor entity) {
    ControllerLinkBuilder linkBuilder = ControllerLinkBuilder.linkTo(ActorController.class).slash(entity);
    Link selfLink = linkBuilder.withSelfRel();
    Link filmsLink = linkBuilder.slash("films").withRel("films");
    resource.add(selfLink, filmsLink);
  }
...
}

This is a MapStruct-based mapper that maps the internal domain model Actor to the API response ActorResource.

Stay tuned, I’ll cover Java objects mapping using MapStruct in a future post.

The addLinks() method adds two hypermedia links to the API response which would look like:

"links":[
  {
    "rel":"self",
     "href":"http://localhost:8080/api/actors/1"
  },
  {
    "rel":"films",
     "href":"http://localhost:8080/api/actors/1/films"
  }
]

FilmResourceMapper.java:

@Mapper
public interface FilmResourceMapper extends ResourceMapper<Film, FilmResource> {

  FilmResourceMapper INSTANCE = Mappers.getMapper(FilmResourceMapper.class);

  @AfterMapping
  default void addLinks(@MappingTarget FilmResource resource, Film entity) {
    // self link
    Link selfLink = ControllerLinkBuilder.linkTo(FilmController.class)
      .slash(entity)
      .withSelfRel();
    resource.add(selfLink);

    // actors links
    entity.getFilmActors().stream()
      .forEach(filmActor -> resource.add(
        ControllerLinkBuilder.linkTo(
          ControllerLinkBuilder.methodOn(ActorController.class)
            .retrieveActor(filmActor.getActor().getActorId()))
            .withRel("actors")
        ));
  }
...
}

This mapper adds a self link to the film resource requested and also adds a link to each individual actor resource that performed in the film.

The REST response would include these links:

"links" : [
  {
    "href" : "http://localhost:8080/api/films/832",
    "rel" : "self"
  },
  {
    "href" : "http://localhost:8080/api/actors/24",
    "rel" : "actors"
  },
  {
    "href" : "http://localhost:8080/api/actors/1",
    "rel" : "actors"
  },
...
]

ResourceMapper.java:

@FunctionalInterface
public interface ResourceMapper<E, R> {

  R map(E entity);

  default List<R> map(Collection<E> entities) {
    return entities.stream()
      .map(entity -> map(entity))
      .collect(Collectors.toList());
  }
}

This is a Functional Interface that other mappers implements to take advantage of the default method implementation. It maps a collection of entities to REST resources sent as the API response.

3.6. X-FORWARDED-* HEADERS

If your RESTful Web services are behind a Gateway or Edge server like Zuul or front-end proxy, you might want to replace the host, port, protocol, etc. with those of the Edge server.

spring-web dependency allows you to accomplish this behavior using ForwardedHeaderFilter Servlet Filter:

WebConfig.java:

@Configuration
public class WebConfig {
...
  @Bean
  public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
    FilterRegistrationBean<ForwardedHeaderFilter> result = new FilterRegistrationBean<>();
    result.setFilter(new ForwardedHeaderFilter());
    result.setOrder(0);
    return result;
  }
...

This ForwardedHeaderFilter bean definition is all you need to render absolute links using X-Forwarded-* headers set by the fronting load balancer.

4. SAMPLE RESPONSE PAYLOADS

  • A sample application/json, the JSON with hypermedia links response payload looks like:
curl http://localhost:8080/api/films/133 | json_pp
{
  "id": 133,
  "lang": "English",
  "title": "CHAMBER ITALIAN",
  "description": "A Fateful Reflection of a Moose And a Husband who must Overcome a Monkey in Nigeria",
  "rentalRate": 4.99,
  "rentalDuration": 7,
  "length": 117,
  "releaseYear": "2006",
  "links": [
    {
      "rel": "self",
      "href": "http://localhost:8080/api/films/133"
    },
    {
      "rel": "actors",
      "href": "http://localhost:8080/api/actors/132"
    },
    {
      "rel": "actors",
      "href": "http://localhost:8080/api/actors/148"
    },
...
  ]
}

Notice the links include one "rel": "self" and many "rel": "actors" entries.

  • While the same request but accepting application/hal+json, the JSON + HAL response looks like:
curl -H "Accept: application/hal+json" http://localhost:8080/api/films/133 | json_pp
{
  "id": 133,
  "lang": "English",
  "title": "CHAMBER ITALIAN",
  "description": "A Fateful Reflection of a Moose And a Husband who must Overcome a Monkey in Nigeria",
  "rentalRate": 4.99,
  "rentalDuration": 7,
  "length": 117,
  "releaseYear": "2006",
  "_links": {
    "self": {
      "href": "http://localhost:8080/api/films/133"
    },
    "actors": [
      {
        "href": "http://localhost:8080/api/actors/68"
      },
      {
        "href": "http://localhost:8080/api/actors/107"
      },
...
    ]
  }
}

Notice the _links object. That’s the name specified in the HAL specification for hal+json payloads.

Also notice the actors array. The default behavior is to named it actorList but that was configured in ActorResource via the @Relation annotation.

  • A third example, also accepting application/hal+json, the JSON + HAL response looks like:
curl -H "Accept: application/hal+json" http://localhost:8080/api/actors/1/films | json_pp
{
  "_links" : {
    "parent" : {
      "href" : "http://localhost:8080/api/actors/1"
    },
    "self" : {
      "href" : "http://localhost:8080/api/actors/1/films"
    }
  },
  "_embedded" : {
    "films" : [
      {
        "rentalRate" : 2.99,
        "length" : 149,
        "releaseYear" : "2006",
        "id" : 166,
        "rentalDuration" : 6,
        "_links" : {
          "self" : {
            "href" : "http://localhost:8080/api/films/166"
          }
        },
        "lang" : "English",
        "title" : "COLOR PHILADELPHIA",
        "description" : "A Thoughtful Panorama of a Car And a Crocodile who must Sink a Monkey in The Sahara Desert"
      },
...

It now includes the _embedded object.

  • Finally a sample response sending X-Forwarded-* headers:
curl -H "Accept: application/json" -H "X-Forwarded-Host: gateway.asimio.net" -H "X-Forwarded-Port: 9090" http://localhost:8080/api/films/133 | json_pp
{
  "lang": "English",
  "title": "CHAMBER ITALIAN",
  "description": "A Fateful Reflection of a Moose And a Husband who must Overcome a Monkey in Nigeria",
  "rentalRate": 4.99,
  "rentalDuration": 7,
  "length": 117,
  "releaseYear": "2006",
  "links": [
    {
      "rel": "self",
      "href": "http://gateway.asimio.net:9090/api/films/133"
    },
    {
      "rel": "actors",
      "href": "http://gateway.asimio.net:9090/api/actors/148"
    },
    {
      "rel": "actors",
      "href": "http://gateway.asimio.net:9090/api/actors/132"
    },
...

The request sends X-Forwarded-Host and X-Forwarded-Port headers replacing the host and port the RESTful application listens on with an Edge server the consuming applications should send the requests to.

5. CONCLUSION

Adding HAL representations to your REST API responses suggest your consumers what they might do next. They provide clients with some kind of statefulness. Although the links don’t specify if it’s a GET, a DELETE or a PUT request.
A REST response might include a link to upgrade from a standard Hotel room to a suite, a link to upgrade to a first-class airline seat, to add related items to an existing shopping cart, etc.

There are a number of libraries that help you to include hypermedia in your RESTful responses. If you are invested in Spring Boot, the spring-boot-starter-hateoas starter simplifies such a task.

This blog post helps you with the configuration and implementation details to get started including HAL hypermedia in your API responses. It doesn’t cover pagination but stay tuned, I’ll follow up with another blog post that adds HAL pagination support to Spring Boot applications using Spring HATEOAS.

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.

6. SOURCE CODE

Accompanying source code for this blog post can be found at:

7. REFERENCES


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK