Giới thiệu HATEOAS

1. HATEOAS là gì?

HATEOAS (Hypermedia As The Engine Of Application State) là một trong những chuẩn được khuyến nghị cho RESTful API. Thuật ngữ “Hypermedia” có nghĩa là bất kỳ nội dung nào có chứa các liên kết (link) đến các media khác như image, movie và text.

Kiểu kiến trúc này cho phép bạn sử dụng các liên kết hypermedia trong nội dung response để client có thể tự động điều hướng đến tài nguyên phù hợp bằng cách duyệt qua các liên kết hypermedia. Nó tương tự như một người dùng web điều hướng qua các trang web bằng cách nhấp vào các link thích hợp để chuyển đến nội dung mong muốn.

HATEOAS mong muốn phía client không cần biết chút nào về cấu trúc phía server, client chỉ cần request đến một URL duy nhất, rồi từ đó mọi đường đi nước bước tiếp theo sẽ do chỉ dẫn của phía server trả về.

2. Ví dụ HATEOAS với JAX-RS

Giả sử chúng ta có một ứng dụng blog, một blog gồm nhiều bài viết, mỗi bài viết được viết bởi một tác giả nào đó. Ở mỗi bài viết có thể có nhiều bình luận và được đánh dấu một số tag. Sơ đồ class ở server như sau:

Hệ thống cung cấp các REST API cho ứng dụng blog được mô tả như sau:

  • @GET /articles : lấy danh sách thông tin tất cả các article.
  • @GET /articles/1 : lấy thông tin một article có id=1
  • @GET /articles/1/comments : lấy danh sách comment của một article có id=1
  • @GET /articles/1/tags: lấy danh sách tag của một article có id=1

Response trả về khi gọi @GET /articles :

[
    {
        "id": 1,
        "content": "HATEOAS example by maixuanviet",
        "publishedDate": "14/07/2019",
        "authorId": 1
    },
    {
        "id": 2,
        "content": "HATEOAS example by maixuanviet",
        "publishedDate": "14/07/2019",
        "authorId": 1
    },
    {
        "id": 3,
        "content": "HATEOAS example by maixuanviet",
        "publishedDate": "14/07/2019",
        "authorId": 1
    }
]

Để lấy thông tin chi tiết của article, chúng ta sẽ gọi tiếp @GET /articles/ + id của article. Tương tự chúng ta cũng cần ghép chuỗi comments, tags để có URL lấy danh sách comments, tags.

Cách thiết kế này có vấn đề là chúng ta phải biết cấu trúc resource của server để gọi cho đúng. Nếu server thay đổi cấu trúc, phía client cũng phải thay đổi theo.

Bây giờ hãy xem cách HATEOAS giải quyết vấn đề này như sau:

  • JAX-RS cung cấp 2 class: UriInfo và Link builder để tạo ra các hypermedia Link.
  • Chúng ta có thể tạo tạo thêm một thuộc tính Link trong Model class để cung cấp hypermedia Link cho client hoặc gán trực tiếp chúng trong Header.

Article.java

package com.maixuanviet.model;
 
import javax.ws.rs.core.Link;
import javax.xml.bind.annotation.XmlRootElement;
 
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
 
@Data
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement
public class Article {
 
    private Integer id;
    private String content;
    private String publishedDate;
    private Integer authorId;
     private Link self;
}

ArticleService.java

package com.maixuanviet.api;
 
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
 
import javax.annotation.security.PermitAll;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
 
import com.maixuanviet.model.Article;
 
// URI:
// http(s)://<domain>:(port)/<YourApplicationName>/<UrlPattern in web.xml>/<path>
// http://localhost:8080/RestfulWebServiceExample/rest/articles
@Path("/articles")
@PermitAll
public class ArticleService {
     
    @GET
    @Path("/")
    public Response getArticles(@Context UriInfo uriInfo) {
        List
<Article> articles = Arrays.asList(
            createArticle(1),
            createArticle(2),
            createArticle(3)
        );
         
        for (Article article : articles) {
            Link selfLink = Link.fromUri(uriInfo.getAbsolutePath().resolve(article.getId().toString())).rel("self").type("GET").build();
            article.setSelf(selfLink);
        }
         
        // http://localhost:8080/RestfulWebServiceExample/rest/articles
        Link selfLink = Link.fromUri(uriInfo.getAbsolutePath()).rel("self").type("GET").build();
         
        // http://localhost:8080/RestfulWebServiceExample/rest/articles?page=2
        Link nextLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
                .queryParam("page", "2"))
                .rel("next")
                .type("GET")
                .build();
         
        // http://localhost:8080/RestfulWebServiceExample/rest/articles?page=0
        Link prevLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
                .queryParam("page", "0"))
                .rel("prev")
                .type("GET")
                .build();
         
        return Response.ok(articles).links(selfLink, nextLink, prevLink).build();
    }
 
    @GET
    @Path("/{id}")
    public Response getArticle(@PathParam("id") int id, @Context UriInfo uriInfo) {
        Article article = createArticle(id);
 
        // http://localhost:8080/RestfulWebServiceExample/rest/articles/1
        Link selfLink = Link.fromUri(uriInfo.getAbsolutePath().resolve(article.getId().toString())).rel("self").type("GET").build();
         
        // http://localhost:8080/RestfulWebServiceExample/rest/articles/1/comments
        Link commentLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
                .path(article.getId().toString()).path("comments"))
                .rel("comments")
                .type("GET").build();
         
        // http://localhost:8080/RestfulWebServiceExample/rest/articles/1/tags
        Link tagLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
                .path(article.getId().toString()).path("tags"))
                .rel("tags")
                .type("GET").build();
         
        article.setSelf(selfLink);
        return Response.ok(article).links(selfLink, commentLink, tagLink).build();
    }
     
    private Article createArticle(Integer id) {
        Article article = new Article();
        article.setId(id);
        article.setContent("HATEOAS example by maixuanviet");
        article.setPublishedDate(LocalDate.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")));
        article.setAuthorId(1);
        return article;
    }
     
    @GET
    @Path("?page={page}")
    public Response getArticles(@QueryParam("page") int page) {
        return null;
    }
     
    @GET
    @Path("/{id}/comments")
    public Response getComments(@PathParam("id") int id) {
        return null;
    }
     
    @GET
    @Path("/{id}/tags")
    public Response getTags(@PathParam("id") int id) {
        return null;
    }
}

Mở Postman để test kết quả:

@GET http://localhost:8080/RestfulWebServiceExample/rest/articles

[
    {
        "id": 1,
        "content": "HATEOAS example by maixuanviet",
        "publishedDate": "14/07/2019",
        "authorId": 1,
        "self": {
            "uri": "http://localhost:8080/RestfulWebServiceExample/rest/1",
            "params": {
                "rel": "self",
                "type": "GET"
            },
            "type": "GET",
            "rel": "self",
            "uriBuilder": {
                "absolute": true
            },
            "rels": [
                "self"
            ],
            "title": null
        }
    },
    {
        "id": 2,
        "content": "HATEOAS example by maixuanviet",
        "publishedDate": "14/07/2019",
        "authorId": 1,
        "self": {
            "uri": "http://localhost:8080/RestfulWebServiceExample/rest/2",
            "params": {
                "rel": "self",
                "type": "GET"
            },
            "type": "GET",
            "rel": "self",
            "uriBuilder": {
                "absolute": true
            },
            "rels": [
                "self"
            ],
            "title": null
        }
    },
    {
        "id": 3,
        "content": "HATEOAS example by maixuanviet",
        "publishedDate": "14/07/2019",
        "authorId": 1,
        "self": {
            "uri": "http://localhost:8080/RestfulWebServiceExample/rest/3",
            "params": {
                "rel": "self",
                "type": "GET"
            },
            "type": "GET",
            "rel": "self",
            "uriBuilder": {
                "absolute": true
            },
            "rels": [
                "self"
            ],
            "title": null
        }
    }
]

Như bạn thấy, ở mỗi article đều cung cấp link để truy xuất thông tin chi tiết của một article. Dựa vào đây chúng ta có thể gọi API tiếp mà không cần quan tâm cấu trúc resource trên server như thế nào.

Mở tab Headers, chúng ta có thông tin truy xuất các resource khác như phân trang: next, prev.

Tương tự: @GET http://localhost:8080/RestfulWebServiceExample/rest/articles/1

{
    "id": 1,
    "content": "HATEOAS example by gpcoder",
    "publishedDate": "14/07/2019",
    "authorId": 1,
    "self": {
        "uri": "http://localhost:8080/RestfulWebServiceExample/rest/articles/1",
        "params": {
            "rel": "self",
            "type": "GET"
        },
        "type": "GET",
        "rel": "self",
        "uriBuilder": {
            "absolute": true
        },
        "rels": [
            "self"
        ],
        "title": null
    }
}

Như bạn thấy HATEOAS rất hữu dụng, tuy nhiên việc xử lý để tính toán ra những hành động, hyperlink tương ứng khá phức tạp và bandwidth dành cho nó cũng không dễ chịu chút nào. Hiện tại không có nhiều ứng cung cấp REST API với HATEOAS. Nếu không cần thiết, hãy hỗ trợ HATEOAS là optional thông qua header.

Related posts:

Java Program to Perform Preorder Non-Recursive Traversal of a Given Binary Tree
Spring Boot - Tomcat Port Number
Java Program to Implement Max-Flow Min-Cut Theorem
Loại bỏ các phần tử trùng trong một ArrayList như thế nào?
Finding Max/Min of a List or Collection
Intro to Spring Boot Starters
Java Program to Check Multiplicability of Two Matrices
Spring Cloud – Adding Angular
Java Program to Generate Random Numbers Using Probability Distribution Function
Validate email address exists or not by Java Code
Java 8 – Powerful Comparison with Lambdas
Introduction to Spring Cloud Stream
Java Program to Search for an Element in a Binary Search Tree
Spring Boot - Twilio
Java Program to Implement Binary Search Tree
Java Program to Perform Inorder Recursive Traversal of a Given Binary Tree
Java Program to Implement Max Heap
Number Formatting in Java
Introduction to Spliterator in Java
Spring Boot - Enabling HTTPS
Quick Guide to Spring MVC with Velocity
Java Program to Implement Levenshtein Distance Computing Algorithm
Introduction to Java 8 Streams
The Order of Tests in JUnit
Java Program to Implement Slicker Algorithm that avoids Triangulation to Find Area of a Polygon
Java Program to Implement Karatsuba Multiplication Algorithm
Java Program to Give an Implementation of the Traditional Chinese Postman Problem
Lập trình đa luồng với CompletableFuture trong Java 8
Hướng dẫn kết nối cơ sở dữ liệu với Java JDBC
Basic Authentication with the RestTemplate
Java Program to Construct an Expression Tree for an Postfix Expression
@Order in Spring