HATEOAS Deep Link Problem

Links externos (links diretos) - na Internet, é o posicionamento de um hiperlink em um site que aponta para uma página em outro site, em vez de apontar para a página inicial (inicial, inicial) desse site. Esses links são chamados de links externos (links diretos).
Wikipedia
O termo "links diretos" será usado ainda mais como o mais próximo do idioma inglês "links diretos". Este artigo se concentrará na API REST, portanto, links diretos significarão links para recursos HTTP. Por exemplo, o link direto habr.com/en/post/426691 aponta para um artigo específico em habr.com.

HATEOAS é um componente da arquitetura REST que permite fornecer aos clientes da API informações através da hipermídia. O cliente conhece o único endereço fixo, o ponto de entrada da API; ele aprende todas as ações possíveis com os recursos recebidos do servidor. As visualizações de recursos contêm links para ações ou outros recursos; o cliente interage com a API, selecionando dinamicamente uma ação nos links disponíveis. Você pode ler mais sobre o HATEOAS na Wikipedia ou neste maravilhoso artigo sobre Habré.

HATEOAS é o próximo nível da API REST. Graças ao uso da hipermídia, ele responde muitas perguntas que surgem durante o desenvolvimento da API: como controlar o acesso a ações no lado do servidor, como se livrar da forte conectividade entre o cliente e o servidor, como alterar os endereços dos recursos, se necessário. Mas ele não fornece uma resposta para a pergunta de como os links profundos com os recursos devem parecer.

Na implementação REST "clássica", o cliente conhece a estrutura dos endereços e sabe como obter um recurso por identificador na API REST. Por exemplo, um usuário segue um link direto para uma página de livro em uma loja online. A barra de URL https://domain.test/books/1 exibida na barra de endereço do navegador. O cliente sabe que "1" é o identificador do recurso do livro e, para obtê-lo, é necessário substituí-lo no URL da API REST https://api.domain.test/api/books/{id} . Portanto, o link https://api.domain.test/api/books/1 para o recurso deste livro na API REST é semelhante a: https://api.domain.test/api/books/1 .

No HATEOAS, o cliente não conhece identificadores de recursos ou estrutura de endereço. Ele não codifica, mas "descobre" os links. Além disso, a estrutura das URLs pode mudar sem o conhecimento do cliente, o HATEOAS permite. Devido a essas diferenças, os links diretos não podem ser implementados da mesma maneira que a API REST clássica. Surpreendentemente, uma pesquisa na Internet de receitas para implementar esses links no HATEOAS não produziu um grande número de resultados, apenas algumas perguntas desconcertantes sobre Stackoverflow. Portanto, consideraremos várias opções possíveis e tentaremos escolher a melhor.

A opção zero fora da competição não é implementar links diretos. Isso pode ser adequado para alguns administradores ou aplicativos móveis que não exigem a capacidade de alternar diretamente para recursos internos. Isso é completamente dentro do espírito do HATEOAS, o usuário pode abrir páginas apenas sequencialmente, a partir do ponto de entrada, porque o cliente não sabe como acessar diretamente o recurso interno. Mas essa opção não é adequada para aplicativos da Web - esperamos que o link para a página interna possa ser marcado como favorito e a atualização da página não nos transfira de volta para a página principal do site.

Portanto, a primeira opção: o código da URL da API HATEOAS. O cliente conhece a estrutura dos endereços dos recursos para os quais os links diretos são necessários e sabe como obter o identificador de recurso para a pesquisa. Por exemplo, o servidor retorna o endereço https://api.domain.test/api/books/1 como uma referência ao recurso de livro. O cliente sabe que "1" é o identificador do livro e pode gerar esse URL sozinho ao clicar no link direto. Esta é certamente uma opção de trabalho, mas viola os princípios da HATEOAS. A estrutura de endereços e o identificador de recursos não podem mais ser alterados; caso contrário, o cliente interromperá, haverá uma conexão rígida. Isso não é HATEOAS, o que significa que a opção não é adequada para nós.

A segunda opção é substituir a URL da API REST na URL do cliente. Por exemplo, com um livro, o link https://domain.test/books?url=https://api.domain.test/api/books/1 será assim: https://domain.test/books?url=https://api.domain.test/api/books/1 . Aqui, o cliente pega o link do recurso recebido do servidor e o substitui inteiramente no endereço da página. É mais como o HATEOAS, o cliente não sabe sobre identificadores e estrutura de endereços, ele recebe um link e o usa como está. Ao clicar em um link tão profundo, o cliente receberá o recurso desejado por meio do link da API REST do parâmetro url. Parece que a solução está funcionando e dentro do espírito do HATEOAS. Mas se você adicionar esse link aos seus favoritos, no futuro não poderemos mais alterar o endereço do recurso na API (ou sempre teremos que redirecionar para um novo endereço). Mais uma vez, uma das vantagens do HATEOAS é perdida; essa opção também não é ideal.

Portanto, queremos ter links permanentes, que, no entanto, podem mudar. Essa solução existe e é amplamente usada na Internet - muitos sites fornecem links curtos para páginas internas que podem ser compartilhadas. Além da brevidade, a vantagem é que o site pode alterar o endereço real da página, mas esses links não quebram. Por exemplo, a Microsoft usa os links do Windows para ajudar as páginas do formulário http://go.microsoft.com/fwlink/?LinkId=XXX . Ao longo dos anos, os sites da Microsoft foram redesenhados várias vezes, mas os links nas versões mais antigas do Windows continuam funcionando.

Resta apenas adaptar esta solução ao HATEOAS. E esta é a terceira opção - usando identificadores de link direto exclusivos na API REST. Agora, o endereço da página do livro será semelhante a: https://domain.test/books?deepLinkId=3f0fd552-e564-42ed-86b6-a8e3055e2763 . Ao clicar em um link tão profundo, o cliente deve perguntar ao servidor: qual link de recurso corresponde a um identificador deepLinkId ? O servidor retornará o link https://api.domain.test/api/books/1 (bem, ou imediatamente um recurso, para não ir duas vezes). Se o endereço do recurso na API REST for alterado, o servidor simplesmente retornará outro link. Um registro é salvo no banco de dados de que o identificador de referência 3f0fd552-e564-42ed-86b6-a8e3055e2763 corresponde ao identificador de entidade do livro 1.

Para isso, os recursos devem conter um campo deepLinkId com os identificadores de seus links deepLinkId e o cliente deve substituí-los no endereço da página. Este endereço pode ser marcado com segurança e enviado aos amigos. Não é muito bom que o cliente trabalhe independentemente com determinados identificadores, mas isso permite preservar as vantagens do HATEOAS para a API como um todo.

Exemplo


Este artigo não seria completo sem um exemplo de implementação. Para testar o conceito, considere um exemplo de um site hipotético de catálogo de loja online com um back-end no Spring Boot / Kotlin e um front-end do SPA no Vue / JavaScript. A loja vende livros e lápis, o site possui duas seções nas quais você pode ver a lista de produtos e abrir suas páginas.

Seção "Livros":



Uma página de livro:



Para armazenamento de mercadorias, as entidades JPA do Spring Data são definidas:

 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() } 

Para criar e armazenar DeepLink link DeepLink , a entidade DeepLink é DeepLink , uma instância criada com cada objeto de domínio. O próprio identificador é gerado de acordo com o padrão UUID no momento em que a entidade foi criada. Sua tabela contém o identificador do link direto, o identificador e o tipo da entidade à qual o link leva.

A API REST do servidor é organizada de acordo com o conceito HATEOAS, o ponto de entrada da API contém links para coleções de produtos e um link #deepLink para formar links #deepLink substituindo um identificador:

 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 } } } 

O cliente, ao abrir a seção "Livros", solicita uma coleção de recursos no link #books no ponto de entrada:

 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" } } } ... 

O SPA usa o Vue Router, para o qual o caminho da página do livro é definido { path: '/books/:deepLinkId', name: 'book', component: Book, props: true } , e os links na lista de livros são assim: <router-link :to="{name: 'book', params: {link: book._links.self.href, deepLinkId: book.deepLinkId}}">{{ book.name }}</router-link> .

Ou seja, quando você abre a página de um livro específico, o componente Book é chamado, que recebe dois parâmetros: link (link para o recurso de livro na API REST, valor do campo href do link #self ) e deepLinkId do recurso).

 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 }) }) } } 

O Vue Router define o valor de deepLinkId como o endereço da página /books/:deepLinkId , e o componente solicita o recurso por link direto da propriedade link . Ao forçar uma atualização de página, o Vue Router define a propriedade do componente deepLinkId , obtendo-a do endereço da página. A propriedade do link permanece null . O componente verifica: se houver um link direto obtido da coleção, o recurso será solicitado. Se apenas o identificador deepLinkId estiver deepLinkId , ele será substituído no link #deepLink partir do ponto de entrada para receber o recurso pelo link #deepLink .

No back-end, o método do controlador para links diretos é assim:

 @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() } 

Por identificador é a essência do link direto. Dependendo do tipo de entidade do aplicativo, um link é formado para o método do controlador, que retorna seu recurso por entityId . A solicitação é redirecionada para este endereço. Portanto, se no futuro o link para o controlador da entidade mudar, será possível alterar simplesmente a lógica da formação do link no método deepLink .

O código fonte completo para o exemplo está disponível no Github .

Source: https://habr.com/ru/post/pt445092/


All Articles