HATEOAS深度链接问题

外部链接(深层链接)-在Internet上,这是指向网站的超链接,该链接指向另一个网站上的页面,而不是指向该网站的主页(首页,开始页面)。 这样的链接称为外部链接(深层链接)。
维基百科
术语“深层链接”将进一步用作最接近英语的“深层链接”。 本文将重点介绍REST API,因此深层链接将意味着指向HTTP资源的链接。 例如,深层链接habr.com/en/post/426691指向habr.com上的特定文章。

HATEOAS是REST体系结构的组件,它允许通过超媒体为API客户端提供信息。 客户端知道唯一的固定地址,即API入口点; 他从服务器接收的资源中了解所有可能的操作。 资源视图包含操作或其他资源的链接; 客户端与API进行交互,从而从可用链接中动态选择一个操作。 您可以在Wikipedia上或在有关Habré的精彩文章中阅读有关HATEOAS的更多信息。

HATEOAS是REST API的下一个级别。 由于使用了超媒体,他回答了API开发过程中出现的许多问题:如何控制对服务器端操作的访问,如何摆脱客户端与服务器之间的紧密连接,必要时如何更改资源地址。 但这并不能解决与资源的深层链接应如何看的问题。

在“经典” REST实现中,客户端知道地址的结构;他知道如何通过REST API中的标识符获取资源。 例如,用户跟踪到在线商店中书籍页面的深层链接。 URL栏https://domain.test/books/1显示在浏览器的地址栏中。 客户知道“ 1”是书籍资源的标识符,要获取该标识符,您需要在REST API URL https://api.domain.test/api/books/{id}替换该标识符。 因此,在REST API中指向本书资源的深层链接如下所示: https://api.domain.test/api/books/1

在HATEOAS中,客户端不知道资源标识符或地址结构。 他不进行硬编码,而是“发现”链接。 而且,URL的结构可能会在客户端不知情的情况下发生变化,HATEOAS允许它。 由于存在这些差异,因此无法以与传统REST API相同的方式来实现深层链接。 令人惊讶的是,在Internet上搜索用于在HATEOAS中实现此类链接的配方的搜索并没有产生大量结果,只是在Stackoverflow上出现了一些令人困惑的问题。 因此,我们将考虑几种可能的选择,并尝试选择最佳选择。

竞争之外的零选择是不实施深层链接。 这可能适合某些不需要直接切换到内部资源的管理员或移动应用程序。 这完全符合HATEOAS的精神,用户只能从入口点开始按顺序打开页面,因为客户端不知道如何直接进入内部资源。 但是此选项不适用于Web应用程序-我们希望可以标记指向内部页面的链接,并且更新页面不会将我们转移回网站的主页。

因此,第一个选择是:HATEOAS API URL硬代码。 客户端知道需要深层链接的资源地址的结构,并且知道如何获取查找的资源标识符。 例如,服务器返回地址https://api.domain.test/api/books/1作为对书籍资源的引用。 客户知道“ 1”是书的标识符,并且在单击深链接时可以自行生成此URL。 这当然是一个可行的选择,但是违反了HATEOAS的原则。 地址结构和资源标识符不能再更改,否则客户端将中断,存在刚性连接。 这不是HATEOAS,这意味着该选项不适合我们。

第二个选项是在客户端URL中替换REST API URL。 对于一本书的示例,深层链接如下所示: https://domain.test/books?url=https://api.domain.test/api/books/1 。 在这里,客户端采用从服务器接收的资源链接,并将其完全替换为页面地址。 这更像HATEOAS,客户端不知道标识符和地址结构,他接收链接并按原样使用它。 单击此类深入链接时,客户端将通过REST API链接从url参数接收所需的资源。 该解决方案似乎在起作用,而且完全符合HATEOAS的精神。 但是,如果您在书签中添加了这样的链接,将来我们将无法再更改API中资源的地址(否则我们将始终不得不重定向到新地址)。 再次失去了HATEOAS的优点之一;此选项也不理想。

因此,我们希望拥有永久链接,但是这种链接可能会改变。 这种解决方案已经存在并在Internet上广泛使用-许多站点提供了指向可以共享的内部页面的短链接。 除了简洁之外,它们的优点是该站点可以更改页面的真实地址,但此类链接不会中断。 例如,Microsoft使用Windows链接来帮助表单为http://go.microsoft.com/fwlink/?LinkId=XXX页面。 多年来,对Microsoft网站进行了多次重新设计,但是旧版Windows中的链接仍然可以使用。

仍然仅是为了使该解决方案适应HATEOAS。 这是第三种选择-在REST API中使用唯一的深层链接标识符。 现在,书页的地址将如下所示: https://domain.test/books?deepLinkId=3f0fd552-e564-42ed-86b6-a8e3055e2763 。 当单击这样的深入链接时,客户端应询问服务器:哪个资源链接对应于此类deepLinkId标识符? 服务器将返回链接https://api.domain.test/api/books/1 (或者,或者立即提供资源,以免https://api.domain.test/api/books/1两次)。 如果REST API中的资源地址发生更改,则服务器将仅返回另一个链接。 记录保存在数据库中,引用标识符3f0fd552-e564-42ed-86b6-a8e3055e2763对应于书籍1的实体标识符。

为此,资源必须包含带有其深层链接标识符的deepLinkId字段,并且客户端必须在页面地址中替换它们。 此地址可以安全地添加书签并发送给朋友。 客户端独立使用某些标识符不是很好,但这可以让您保留HATEOAS对于整个API的优势。

例子


没有示例实现,本文将是不完整的。 为了测试这一概念,请考虑一个假设的在线商店目录站点示例,该站点在Spring Boot / Kotlin上具有后端,在Vue / JavaScript上具有SPA前端。 该商店出售书籍和铅笔,该站点有两个部分,您可以在其中查看产品列表并打开其页面。

“书籍”部分:



一本书的页面:



为了存储货物,定义了Spring Data JPA实体:

 enum class EntityType { PEN, BOOK } @Entity class Pen(val color: String) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() @OneToOne(cascade = [CascadeType.ALL]) val deepLink: DeepLink = DeepLink(EntityType.PEN, id) } @Entity class Book(val name: String) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() @OneToOne(cascade = [CascadeType.ALL]) val deepLink: DeepLink = DeepLink(EntityType.BOOK, id) } @Entity class DeepLink( @Enumerated(EnumType.STRING) val entityType: EntityType, @Column(columnDefinition = "uuid") val entityId: UUID ) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() } 

要创建和存储深层链接DeepLink ,将使用DeepLink实体,该实体的实例与每个域对象一起创建。 标识符本身是根据创建实体时的UUID标准生成的。 它的表包含深层链接的标识符,该链接所指向的实体的标识符和类型。

服务器的REST API是根据HATEOAS概念进行组织的,API入口点包含产品集合的链接,以及#deepLink链接,通过替换标识符来形成深层链接:

 GET http://localhost:8080/api { "_links": { "pens": { "href": "http://localhost:8080/api/pens" }, "books": { "href": "http://localhost:8080/api/books" }, "deepLink": { "href": "http://localhost:8080/api/links/{id}", "templated": true } } } 

客户端在打开“图书”部分时,在入口点的#books链接中请求资源集合:

 GET http://localhost:8080/api/books ... { "name": "Harry Potter", "deepLinkId": "4bda3c65-e5f7-4e9b-a8ec-42d16488276f", "_links": { "self": { "href": "http://localhost:8080/api/books/1272e287-07a5-4ebc-9170-2588b9cf4e20" } } }, { "name": "Cryptonomicon", "deepLinkId": "a23d92c2-0b7f-48d5-88bc-18f45df02345", "_links": { "self": { "href": "http://localhost:8080/api/books/5d04a6d0-5bbc-463e-a951-a9ff8405cc70" } } } ... 

SPA使用Vue路由器,为其定义了书页的路径{ path: '/books/:deepLinkId', name: 'book', component: Book, props: true } ,并且书本列表中的链接如下所示: <router-link :to="{name: 'book', params: {link: book._links.self.href, deepLinkId: book.deepLinkId}}">{{ book.name }}</router-link>

也就是说,当您打开特定书籍的页面时,将调用Book组件,该组件会收到两个参数: link (链接到REST API中的书籍资源, #self链接的href字段的值)和来自该资源的deepLinkId

 const Book = { template: `<div>{{ 'Book: ' + book.name }}</div>`, props: { link: null, deepLinkId: null }, data() { return { book: { name: "" } } }, mounted() { let url = this.link == null ? '/api/links/' + this.deepLinkId : this.link; fetch(url).then((response) => { return response.json().then((json) => { this.book = json }) }) } } 

Vue路由器将deepLinkId的值设置为页面/books/:deepLinkId的地址,并且组件通过来自link属性的直接链接来请求资源。 强制页面刷新时,Vue Router设置组件属性deepLinkId ,从页面地址获取它。 link属性保持为null 。 该组件检查:如果从集合中获得了直接链接,则在其上请求资源。 如果只有deepLinkId标识符deepLinkId ,则从入口点将其替换为#deepLink链接,以通过深度链接接收资源。

在后端,用于深层链接的控制器方法如下所示:

 @GetMapping("/links/{id}") fun deepLink(@PathVariable id: UUID?, response: HttpServletResponse?): ResponseEntity<Any> { id!!; response!! val deepLink = deepLinkRepo.getOne(id) val path: String = when (deepLink.entityType) { EntityType.PEN -> linkTo(methodOn(MainController::class.java).getPen(deepLink.entityId)) EntityType.BOOK -> linkTo(methodOn(MainController::class.java).getBook(deepLink.entityId)) }.toUri().path response.sendRedirect(path) return ResponseEntity.notFound().build() } 

按标识符是深层链接的本质。 根据应用程序实体的类型,将形成到控制器方法的链接,该链接将通过entityId返回其资源。 该请求被重定向到该地址。 因此,如果将来更改到实体控制器的链接,则可以简单地更改deepLink方法中链接形成的逻辑。

该示例的完整源代码可在Github上找到

Source: https://habr.com/ru/post/zh-CN445092/


All Articles