Learn to build hateoas links for REST resources using RepresentationModel and RepresentationModelAssemblerSupport in a Spring boot application.
1. Spring HATEOAS 1.x Changes
Spring HATEAOS 1.x module has gone through some major changes including package structure and class names, as compared to older version. Let’s discuss some important classes now used in 1.x.
1.1. Important classes
- RepresentationModel – is a container for a collection of Links and provides APIs to add those links to the model.
- EntityModel – represents
RepresentationModel
containing only single entity and related links.public class ActorModel extends RepresentationModel<ActorModel> { // attributes }
- CollectionModel – is a wrapper for a collection of entities (entity as well as collection). To create collection models, use it’ constructors (e.g. CollectionModel(List) or CollectionModel(Iterable)) or
toCollectionModel()
provided by model assemblers.public class AlbumModelAssembler extends RepresentationModelAssemblerSupport<AlbumEntity, AlbumModel> { @Override public CollectionModel<AlbumModel> toCollectionModel(Iterable<? extends AlbumEntity> entities) { CollectionModel<AlbumModel> actorModels = super.toCollectionModel(entities); } }
- PagedModel – is similar to CollectionModel with underlying pageable collection of entities.
- RepresentationModelAssembler – It’s implementation classes (such as RepresentationModelAssemblerSupport) provides methods to convert a domain object into a RepresentationModel.
- WebMvcLinkBuilder – It helps to ease building
Link
instances pointing to Spring MVC controllers.Link lnk = WebMvcLinkBuilder .linkTo(WebMvcLinkBuilder.methodOn(WebController.class) .getAllAlbums()) .withSelfRel();
- Link – represents a single link added to representation model.
- LinkRelationProvider – provides API to add link relations (
"rel"
type) inLink
instances.
The recommeded way to work with representation model is :
- extend the domain class with
RepresentationModel
- create instances of this class
- populate the properties and enrich it with links
1.2. Creating links
In Spring web MVC and webflux applications, we can use WebMvcLinkBuilder
to create links pointing to controller classes and it’s methods.
//Controller class @Controller class EmployeeController { @GetMapping("/employees") HttpEntity<CollectionModel<EmployeeModel>> getAllEmployees() { … } @GetMapping(value = "/employees/{id}") HttpEntity<EmployeeModel> getEmployeeById(@PathVariable Long id) { … } } //Create link import static org.sfw.hateoas.server.mvc.WebMvcLinkBuilder.*; //Method 1 Link link = linkTo(methodOn(EmployeeController.class) .getAllEmployees()) .withRel("employees"); // Method 2 Method method = WebController.class.getMethod("getActorById", Long.class); Link link = linkTo(method, 2L).withSelfRel();
1.3. HAL – Hypertext application language
JSON Hypertext Application Language or HAL is one of the simplest and most widely adopted hypermedia media types
By default, Spring hateoas generated responses are in application/hal+json
format. It is the default mediatype even if we pass application/json
as well.
In HAL, the _links
entry is a JSON object. The property names are link relations and each value is single or multiple links.
"_links": { "self": { "href": "http://localhost:8080/api/actors/1" } }
2. Spring HATEOAS RepresentationModel Example
To demonstrate the usage of representation models, we will create a Spring web MVC application providing albums and their actors’ basic information. An album can have many actors. Similarily, an actor can be associated with multiple albums.
We will fetch create 4 REST apis to provide:
/api/actors
– List of all actors/api/actors/{id}
– An actor given by id./api/albums
– List of all albums./api/albums/{id}
– An album given by id.
Responses of all APIs will have links added using spring hateoas classes.
2.1. Dependencies
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> <relativePath /> </parent> <groupId>com.springexamples</groupId> <artifactId>boot-hateoas</artifactId> <name>boot-hateoas</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> </project>
2.2. JPA entities and repositories
@Data @Builder @AllArgsConstructor @NoArgsConstructor @ToString(exclude = "albums") @Entity @Table(name="actor") public class ActorEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String firstName; private String lastName; private String birthDate; @ManyToMany(cascade=CascadeType.ALL) @JoinTable( name = "actor_album", joinColumns = @JoinColumn(name = "actor_id"), inverseJoinColumns = @JoinColumn(name = "album_id")) private List<AlbumEntity> albums; }
@Data @Builder @AllArgsConstructor @NoArgsConstructor @Entity @ToString(exclude = "actors") @Table(name="album") public class AlbumEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String description; private String releaseDate; @ManyToMany(mappedBy = "albums",fetch = FetchType.EAGER) private List<ActorEntity> actors; }
public interface ActorRepository extends JpaRepository<ActorEntity, Long>{ }
public interface AlbumRepository extends JpaRepository<AlbumEntity, Long>{ }
The corresponding data is loaded in H2 inmemory database using given files and configuration.
spring.datasource.url=jdbc:h2:mem:test spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=none
CREATE TABLE actor ( id INT PRIMARY KEY, first_name VARCHAR(255) NULL, last_name VARCHAR(255) NULL, birth_date VARCHAR(255) NULL ); CREATE TABLE album ( id INT PRIMARY KEY, title VARCHAR(255) NULL, description VARCHAR(255) NULL, release_date VARCHAR(255) NULL ); CREATE TABLE actor_album ( actor_id INT, album_id INT );
INSERT INTO actor VALUES ('1', 'John', 'Doe', '10-Jan-1952'); INSERT INTO actor VALUES ('2', 'Amy', 'Eugene', '05-07-1985'); INSERT INTO actor VALUES ('3', 'Laverne', 'Mann', '11-12-1988'); INSERT INTO actor VALUES ('4', 'Janice', 'Preston', '19-02-1960'); INSERT INTO actor VALUES ('5', 'Pauline', 'Rios', '29-08-1977'); INSERT INTO album VALUES ('1', 'Top Hits Vol 1', 'Top hits vol 1. description', '10-03-1981'); INSERT INTO album VALUES ('2', 'Top Hits Vol 2', 'Top hits vol 2. description', '10-03-1982'); INSERT INTO album VALUES ('3', 'Top Hits Vol 3', 'Top hits vol 3. description', '10-03-1983'); INSERT INTO album VALUES ('4', 'Top Hits Vol 4', 'Top hits vol 4. description', '10-03-1984'); INSERT INTO album VALUES ('5', 'Top Hits Vol 5', 'Top hits vol 5. description', '10-03-1985'); INSERT INTO album VALUES ('6', 'Top Hits Vol 6', 'Top hits vol 6. description', '10-03-1986'); INSERT INTO album VALUES ('7', 'Top Hits Vol 7', 'Top hits vol 7. description', '10-03-1987'); INSERT INTO album VALUES ('8', 'Top Hits Vol 8', 'Top hits vol 8. description', '10-03-1988'); INSERT INTO album VALUES ('9', 'Top Hits Vol 9', 'Top hits vol 9. description', '10-03-1989'); INSERT INTO album VALUES ('10', 'Top Hits Vol 10', 'Top hits vol 10. description', '10-03-1990'); INSERT INTO actor_album VALUES (1, 1); INSERT INTO actor_album VALUES (1, 2); INSERT INTO actor_album VALUES (2, 3); INSERT INTO actor_album VALUES (2, 4); INSERT INTO actor_album VALUES (3, 5); INSERT INTO actor_album VALUES (3, 6); INSERT INTO actor_album VALUES (4, 7); INSERT INTO actor_album VALUES (4, 8); INSERT INTO actor_album VALUES (5, 9); INSERT INTO actor_album VALUES (5, 10);
2.3. Model classes
These are DTO objects which will be returned from controller classes as representation models.
@Data @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = false) @JsonRootName(value = "actor") @Relation(collectionRelation = "actors") @JsonInclude(Include.NON_NULL) public class ActorModel extends RepresentationModel<ActorModel> { private Long id; private String firstName; private String lastName; private String birthDate; private List<AlbumModel> albums; }
@Data @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = false) @JsonRootName(value = "album") @Relation(collectionRelation = "albums") @JsonInclude(Include.NON_NULL) public class AlbumModel extends RepresentationModel<AlbumModel> { private Long id; private String title; private String description; private String releaseDate; private List<ActorModel> actors; }
2.4. Representation model assemblers
These assemblers will be used to convert the JPA entity classes to DTO objects (entity and collection representations). i.e.
ActorEntity
toActorModel
AlbumEntity
toAlbumModel
Here, we are using RepresentationModelAssemblerSupport class which implements RepresentationModelAssembler
interface. It provides toModel()
and toCollectionModel()
methods.
@Component public class AlbumModelAssembler extends RepresentationModelAssemblerSupport<AlbumEntity, AlbumModel> { public AlbumModelAssembler() { super(WebController.class, AlbumModel.class); } @Override public AlbumModel toModel(AlbumEntity entity) { AlbumModel albumModel = instantiateModel(entity); albumModel.add(linkTo( methodOn(WebController.class) .getActorById(entity.getId())) .withSelfRel()); albumModel.setId(entity.getId()); albumModel.setTitle(entity.getTitle()); albumModel.setDescription(entity.getDescription()); albumModel.setReleaseDate(entity.getReleaseDate()); albumModel.setActors(toActorModel(entity.getActors())); return albumModel; } @Override public CollectionModel<AlbumModel> toCollectionModel(Iterable<? extends AlbumEntity> entities) { CollectionModel<AlbumModel> actorModels = super.toCollectionModel(entities); actorModels.add(linkTo(methodOn(WebController.class).getAllAlbums()).withSelfRel()); return actorModels; } private List<ActorModel> toActorModel(List<ActorEntity> actors) { if (actors.isEmpty()) return Collections.emptyList(); return actors.stream() .map(actor -> ActorModel.builder() .id(actor.getId()) .firstName(actor.getFirstName()) .lastName(actor.getLastName()) .build() .add(linkTo( methodOn(WebController.class) .getActorById(actor.getId())) .withSelfRel())) .collect(Collectors.toList()); } }
@Component public class ActorModelAssembler extends RepresentationModelAssemblerSupport<ActorEntity, ActorModel> { public ActorModelAssembler() { super(WebController.class, ActorModel.class); } @Override public ActorModel toModel(ActorEntity entity) { ActorModel actorModel = instantiateModel(entity); actorModel.add(linkTo( methodOn(WebController.class) .getActorById(entity.getId())) .withSelfRel()); actorModel.setId(entity.getId()); actorModel.setFirstName(entity.getFirstName()); actorModel.setLastName(entity.getLastName()); actorModel.setBirthDate(entity.getBirthDate()); actorModel.setAlbums(toAlbumModel(entity.getAlbums())); return actorModel; } @Override public CollectionModel<ActorModel> toCollectionModel(Iterable<? extends ActorEntity> entities) { CollectionModel<ActorModel> actorModels = super.toCollectionModel(entities); actorModels.add(linkTo(methodOn(WebController.class).getAllActors()).withSelfRel()); return actorModels; } private List<AlbumModel> toAlbumModel(List<AlbumEntity> albums) { if (albums.isEmpty()) return Collections.emptyList(); return albums.stream() .map(album -> AlbumModel.builder() .id(album.getId()) .title(album.getTitle()) .build() .add(linkTo( methodOn(WebController.class) .getAlbumById(album.getId())) .withSelfRel())) .collect(Collectors.toList()); } }
2.5. REST Controller
The REST controller having APIs is :
@RestController public class WebController { @Autowired private AlbumRepository albumRepository; @Autowired private ActorRepository actorRepository; @Autowired private ActorModelAssembler actorModelAssembler; @Autowired private AlbumModelAssembler albumModelAssembler; @GetMapping("/api/actors") public ResponseEntity<CollectionModel<ActorModel>> getAllActors() { List<ActorEntity> actorEntities = actorRepository.findAll(); return new ResponseEntity<>( actorModelAssembler.toCollectionModel(actorEntities), HttpStatus.OK); } @GetMapping("/api/actors/{id}") public ResponseEntity<ActorModel> getActorById(@PathVariable("id") Long id) { return actorRepository.findById(id) .map(actorModelAssembler::toModel) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @GetMapping("/api/albums") public ResponseEntity<CollectionModel<AlbumModel>> getAllAlbums() { List<AlbumEntity> albumEntities = albumRepository.findAll(); return new ResponseEntity<>( albumModelAssembler.toCollectionModel(albumEntities), HttpStatus.OK); } @GetMapping("/api/albums/{id}") public ResponseEntity<AlbumModel> getAlbumById(@PathVariable("id") Long id) { return albumRepository.findById(id) .map(albumModelAssembler::toModel) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } }
2.6. Run the application
Run the application as Spring boot application and observe the outputs.
@SpringBootApplication public class SpringBootHateoasApplication { public static void main(String[] args) { SpringApplication.run(SpringBootHateoasApplication.class, args); } }
{ "_embedded": { "albums": [ { "id": 1, "title": "Top Hits Vol 1", "description": "Top hits vol 1. description", "releaseDate": "10-03-1981", "actors": [ { "id": 1, "firstName": "John", "lastName": "Doe", "_links": { "self": { "href": "http://localhost:8080/api/actors/1" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/1" } } }, { "id": 2, "title": "Top Hits Vol 2", "description": "Top hits vol 2. description", "releaseDate": "10-03-1982", "actors": [ { "id": 1, "firstName": "John", "lastName": "Doe", "_links": { "self": { "href": "http://localhost:8080/api/actors/1" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/2" } } }, { "id": 3, "title": "Top Hits Vol 3", "description": "Top hits vol 3. description", "releaseDate": "10-03-1983", "actors": [ { "id": 2, "firstName": "Amy", "lastName": "Eugene", "_links": { "self": { "href": "http://localhost:8080/api/actors/2" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/3" } } }, { "id": 4, "title": "Top Hits Vol 4", "description": "Top hits vol 4. description", "releaseDate": "10-03-1984", "actors": [ { "id": 2, "firstName": "Amy", "lastName": "Eugene", "_links": { "self": { "href": "http://localhost:8080/api/actors/2" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/4" } } }, { "id": 5, "title": "Top Hits Vol 5", "description": "Top hits vol 5. description", "releaseDate": "10-03-1985", "actors": [ { "id": 3, "firstName": "Laverne", "lastName": "Mann", "_links": { "self": { "href": "http://localhost:8080/api/actors/3" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/5" } } }, { "id": 6, "title": "Top Hits Vol 6", "description": "Top hits vol 6. description", "releaseDate": "10-03-1986", "actors": [ { "id": 3, "firstName": "Laverne", "lastName": "Mann", "_links": { "self": { "href": "http://localhost:8080/api/actors/3" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/6" } } }, { "id": 7, "title": "Top Hits Vol 7", "description": "Top hits vol 7. description", "releaseDate": "10-03-1987", "actors": [ { "id": 4, "firstName": "Janice", "lastName": "Preston", "_links": { "self": { "href": "http://localhost:8080/api/actors/4" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/7" } } }, { "id": 8, "title": "Top Hits Vol 8", "description": "Top hits vol 8. description", "releaseDate": "10-03-1988", "actors": [ { "id": 4, "firstName": "Janice", "lastName": "Preston", "_links": { "self": { "href": "http://localhost:8080/api/actors/4" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/8" } } }, { "id": 9, "title": "Top Hits Vol 9", "description": "Top hits vol 9. description", "releaseDate": "10-03-1989", "actors": [ { "id": 5, "firstName": "Pauline", "lastName": "Rios", "_links": { "self": { "href": "http://localhost:8080/api/actors/5" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/9" } } }, { "id": 10, "title": "Top Hits Vol 10", "description": "Top hits vol 10. description", "releaseDate": "10-03-1990", "actors": [ { "id": 5, "firstName": "Pauline", "lastName": "Rios", "_links": { "self": { "href": "http://localhost:8080/api/actors/5" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/10" } } } ] }, "_links": { "self": { "href": "http://localhost:8080/api/albums" } } }
{ "id": 1, "title": "Top Hits Vol 1", "description": "Top hits vol 1. description", "releaseDate": "10-03-1981", "actors": [ { "id": 1, "firstName": "John", "lastName": "Doe", "_links": { "self": { "href": "http://localhost:8080/api/actors/1" } } } ], "_links": { "self": { "href": "http://localhost:8080/api/actors/1" } } }
Similarly, you can see the output of other APIs as well.
3. Conclusion
In this spring hateoas tutorial, we learned :
- Important classes in spring hateoas module
- What are representation models
- How to create entity and collection models
- How to insert links in model using contoller method references
- and finally, how to use representation model assemblers
Drop me your questions in comments.
Happy Learning !!
Reference : Spring hateoas docs
Leave a Reply