Giới thiệu HATEOAS

Bài viết được sự cho phép của tác giả Giang Phan

1. HATEOAS là gì?

HATEOAS (Hypermedia AThe Engine OApplication 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ề.

  "Mục tiêu và thách thức của Chatbot là hiểu được cảm xúc của người dùng và có cảm xúc riêng"
  10 ngôn ngữ phát triển nhanh nhất theo GitHub thống kê năm 2024

Xem thêm việc làm SSIS hấp dẫn trên TopDev

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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
    {
        "id": 1,
        "content": "HATEOAS example by gpcoder",
        "publishedDate": "14/07/2019",
        "authorId": 1
    },
    {
        "id": 2,
        "content": "HATEOAS example by gpcoder",
        "publishedDate": "14/07/2019",
        "authorId": 1
    },
    {
        "id": 3,
        "content": "HATEOAS example by gpcoder",
        "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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.gpcoder.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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package com.gpcoder.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.gpcoder.model.Article;
// URI:
// http(s)://<domain>:(port)/<YourApplicationName>/<UrlPattern in web.xml>/<path>
@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);
        }
        
        Link selfLink = Link.fromUri(uriInfo.getAbsolutePath()).rel("self").type("GET").build();
        
        Link nextLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
                .queryParam("page", "2"))
                .rel("next")
                .type("GET")
                .build();
        
        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);
        Link selfLink = Link.fromUri(uriInfo.getAbsolutePath().resolve(article.getId().toString())).rel("self").type("GET").build();
        
        Link commentLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
                .path(article.getId().toString()).path("comments"))
                .rel("comments")
                .type("GET").build();
        
        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 gpcoder");
        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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
[
    {
        "id": 1,
        "content": "HATEOAS example by gpcoder",
        "publishedDate": "14/07/2019",
        "authorId": 1,
        "self": {
            "params": {
                "rel": "self",
                "type": "GET"
            },
            "type": "GET",
            "rel": "self",
            "uriBuilder": {
                "absolute": true
            },
            "rels": [
                "self"
            ],
            "title": null
        }
    },
    {
        "id": 2,
        "content": "HATEOAS example by gpcoder",
        "publishedDate": "14/07/2019",
        "authorId": 1,
        "self": {
            "params": {
                "rel": "self",
                "type": "GET"
            },
            "type": "GET",
            "rel": "self",
            "uriBuilder": {
                "absolute": true
            },
            "rels": [
                "self"
            ],
            "title": null
        }
    },
    {
        "id": 3,
        "content": "HATEOAS example by gpcoder",
        "publishedDate": "14/07/2019",
        "authorId": 1,
        "self": {
            "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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "id": 1,
    "content": "HATEOAS example by gpcoder",
    "publishedDate": "14/07/2019",
    "authorId": 1,
    "self": {
        "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.

Tài liệu tham khảo:

Bài viết gốc được đăng tải tại gpcoder.com
Có thể bạn quan tâm:
Xem thêm tuyển dụng CNTT hấp dẫn trên TopDev