Problema de enlace profundo de HATEOAS

Enlace externo (enlace profundo): en Internet, esta es la ubicación de un hipervínculo en un sitio que apunta a una página en otro sitio web, en lugar de apuntar a la página de inicio (inicio, inicio) de ese sitio. Dichos enlaces se denominan enlaces externos (enlaces profundos).
Wikipedia
El término "enlaces profundos" se utilizará además como el más cercano al idioma inglés "enlaces profundos". Este artículo se centrará en la API REST, por lo que los enlaces profundos significarán enlaces a recursos HTTP. Por ejemplo, el enlace profundo habr.com/en/post/426691 apunta a un artículo específico en habr.com.

HATEOAS es un componente de la arquitectura REST que permite proporcionar información a los clientes API a través de hipermedia. El cliente conoce la única dirección fija, el punto de entrada de la API; él aprende todas las acciones posibles de los recursos recibidos del servidor. Las vistas de recursos contienen enlaces a acciones u otros recursos; el cliente interactúa con la API, seleccionando dinámicamente una acción de los enlaces disponibles. Puede leer más sobre HATEOAS en Wikipedia o en este maravilloso artículo sobre Habré.

HATEOAS es el siguiente nivel de API REST. Gracias al uso de hipermedia, responde muchas preguntas que surgen durante el desarrollo de la API: cómo controlar el acceso a las acciones en el lado del servidor, cómo deshacerse de la estrecha conectividad entre el cliente y el servidor, cómo cambiar las direcciones de los recursos si es necesario. Pero no proporciona una respuesta a la pregunta de qué tan profundos deberían ser los enlaces a los recursos.

En la implementación REST "clásica", el cliente conoce la estructura de las direcciones, sabe cómo obtener un recurso por identificador en la API REST. Por ejemplo, un usuario sigue un enlace profundo a una página de libro en una tienda en línea. La barra de URL https://domain.test/books/1 muestra en la barra de direcciones del navegador. El cliente sabe que "1" es el identificador del recurso del libro y, para obtenerlo, debe sustituir este identificador en la URL de la API REST https://api.domain.test/api/books/{id} . Por lo tanto, el enlace profundo al recurso de este libro en la API REST se ve así: https://api.domain.test/api/books/1 .

En HATEOAS, el cliente no sabe acerca de los identificadores de recursos o la estructura de direcciones. No codifica, pero "descubre" enlaces. Además, la estructura de las URL puede cambiar sin el conocimiento del cliente, HATEOAS lo permite. Debido a estas diferencias, los enlaces profundos no se pueden implementar de la misma manera que la API REST clásica. Sorprendentemente, una búsqueda en Internet de recetas para implementar dichos enlaces en HATEOAS no arrojó una gran cantidad de resultados, solo algunas preguntas desconcertantes sobre Stackoverflow. Por lo tanto, consideraremos varias opciones posibles e intentaremos elegir la mejor.

La opción cero fuera de la competencia no es implementar enlaces profundos. Esto puede ser adecuado para algunos administradores o aplicaciones móviles que no requieren la capacidad de cambiar directamente a recursos internos. Esto está completamente en el espíritu de HATEOAS, el usuario puede abrir páginas solo secuencialmente, comenzando desde el punto de entrada, porque el cliente no sabe cómo ir directamente al recurso interno. Pero esta opción no es adecuada para aplicaciones web: esperamos que el enlace a la página interna se pueda marcar como favorito, y la actualización de la página no nos transferirá a la página principal del sitio.

Entonces, la primera opción: el código de la URL de la API de HATEOAS. El cliente conoce la estructura de las direcciones de recursos para las que se necesitan enlaces profundos y sabe cómo obtener el identificador de recursos para la búsqueda. Por ejemplo, el servidor devuelve la dirección https://api.domain.test/api/books/1 como referencia al recurso del libro. El cliente sabe que "1" es el identificador del libro y puede generar esta URL por sí solo al hacer clic en el enlace profundo. Esta es ciertamente una opción de trabajo, pero viola los principios de HATEOAS. La estructura de direcciones y el identificador de recursos ya no se pueden cambiar, de lo contrario, el cliente se interrumpirá, hay una conexión rígida. Esto no es HATEOAS, lo que significa que la opción no nos conviene.

La segunda opción es sustituir la URL de la API REST en la URL del cliente. Para un ejemplo con un libro, el enlace profundo se verá así: https://domain.test/books?url=https://api.domain.test/api/books/1 . Aquí, el cliente toma el enlace de recursos recibido del servidor y lo sustituye completamente en la dirección de la página. Esto es más como HATEOAS, el cliente no sabe acerca de los identificadores y la estructura de la dirección, recibe un enlace y lo usa como está. Al hacer clic en dicho enlace en profundidad, el cliente recibirá el recurso deseado a través del enlace REST API del parámetro url. Parece que la solución está funcionando, y bastante en el espíritu de HATEOAS. Pero si agrega dicho enlace a sus marcadores, en el futuro ya no podremos cambiar la dirección del recurso en la API (o siempre tendremos que redirigir a una nueva dirección). Una vez más, se pierde una de las ventajas de HATEOAS; esta opción tampoco es la ideal.

Por lo tanto, queremos tener enlaces permanentes, que, sin embargo, pueden cambiar. Dicha solución existe y se usa ampliamente en Internet: muchos sitios proporcionan enlaces cortos a páginas internas que se pueden compartir. Además de la brevedad, su ventaja es que el sitio puede cambiar la dirección real de la página, pero dichos enlaces no se romperán. Por ejemplo, Microsoft usa enlaces de Windows para ayudar a las páginas con el formato http://go.microsoft.com/fwlink/?LinkId=XXX . Con los años, los sitios de Microsoft se han rediseñado varias veces, pero los enlaces en versiones anteriores de Windows continúan funcionando.

Solo queda adaptar esta solución a HATEOAS. Y esta es la tercera opción: usar identificadores únicos de enlace profundo en la API REST. Ahora la dirección de la página del libro se verá así: https://domain.test/books?deepLinkId=3f0fd552-e564-42ed-86b6-a8e3055e2763 . Al hacer clic en un enlace tan profundo, el cliente debe preguntarle al servidor: ¿qué enlace de recursos corresponde a dicho identificador deepLinkId ? El servidor devolverá el enlace https://api.domain.test/api/books/1 (bueno, o inmediatamente un recurso, para no ir dos veces). Si la dirección del recurso en la API REST cambia, el servidor simplemente devolverá otro enlace. Se guarda un registro en la base de datos de que el identificador de referencia 3f0fd552-e564-42ed-86b6-a8e3055e2763 corresponde al identificador de entidad del libro 1.

Para esto, los recursos deben contener un campo deepLinkId con los identificadores de sus enlaces profundos, y el cliente debe sustituirlos en la dirección de la página. Esta dirección se puede marcar de forma segura y enviar a amigos. No es muy bueno que el cliente trabaje independientemente con ciertos identificadores, pero esto le permite guardar las ventajas de HATEOAS para la API en su conjunto.

Ejemplo


Este artículo no estaría completo sin un ejemplo de implementación. Para probar el concepto, considere un ejemplo de un hipotético sitio de catálogo de una tienda en línea con un backend en Spring Boot / Kotlin y una interfaz de SPA en Vue / JavaScript. La tienda vende libros y lápices, el sitio tiene dos secciones en las que puede ver la lista de productos y abrir sus páginas.

Sección "Libros":



Una página de libro:



Para el almacenamiento de bienes, se definen las entidades 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() } 

Para crear y almacenar DeepLink enlace profundo, se DeepLink entidad DeepLink , una instancia de la cual se crea con cada objeto de dominio. El identificador en sí se genera de acuerdo con el estándar UUID en el momento en que se creó la entidad. Su tabla contiene el identificador del enlace profundo, el identificador y el tipo de entidad a la que conduce el enlace.

La API REST del servidor está organizada de acuerdo con el concepto HATEOAS, el punto de entrada de la API contiene enlaces a colecciones de productos, así como un enlace #deepLink para formar enlaces profundos sustituyendo un 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 } } } 

El cliente, al abrir la sección "Libros", solicita una colección de recursos en el enlace #books en el punto 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" } } } ... 

SPA utiliza Vue Router, para el cual se define la ruta a la página del libro { path: '/books/:deepLinkId', name: 'book', component: Book, props: true } , y los enlaces en la lista de libros se ven así: <router-link :to="{name: 'book', params: {link: book._links.self.href, deepLinkId: book.deepLinkId}}">{{ book.name }}</router-link> .

Es decir, cuando abre la página de un libro específico, se llama al componente Book , que recibe dos parámetros: link (enlace al recurso del libro en la API REST, valor del campo href del enlace #self ) y deepLinkId del 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 }) }) } } 

Vue Router establece el valor de deepLinkId en la dirección de la página /books/:deepLinkId , y el componente solicita el recurso por enlace directo desde la propiedad de link . Al forzar una actualización de la página, Vue Router establece la propiedad del componente deepLinkId , obteniéndola de la dirección de la página. La propiedad del link permanece null . El componente verifica: si se obtiene un enlace directo de la colección, se solicita el recurso en él. Si solo está deepLinkId identificador deepLinkId , se sustituye en el enlace #deepLink desde el punto de entrada para recibir el recurso por el enlace profundo.

En el backend, el método del controlador para enlaces profundos se ve así:

 @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 es la esencia del enlace profundo. Dependiendo del tipo de entidad de aplicación, se forma un enlace al método del controlador, que devuelve su recurso por entityId . La solicitud se redirige a esta dirección. Por lo tanto, si en el futuro cambia el enlace al controlador de la entidad, será posible simplemente cambiar la lógica de la formación del enlace en el método deepLink .

El código fuente completo para el ejemplo está disponible en Github .

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


All Articles