He estado buscando cómo administrar las versiones de una API REST usando Spring 3.2.x, pero no he encontrado nada que sea fácil de mantener. Explicaré primero el problema que tengo y luego una solución ... pero me pregunto si estoy reinventando la rueda aquí.
Quiero administrar la versión según el encabezado Accept y, por ejemplo, si una solicitud tiene el encabezado Accept application/vnd.company.app-1.1+json
, quiero que Spring MVC reenvíe esto al método que maneja esta versión. Y dado que no todos los métodos en una API cambian en la misma versión, no quiero ir a cada uno de mis controladores y cambiar nada por un controlador que no ha cambiado entre versiones. Tampoco quiero tener la lógica para averiguar qué versión usar en el controlador (usando localizadores de servicio) ya que Spring ya está descubriendo qué método llamar.
Entonces, tomando una API con las versiones 1.0, a 1.8, donde se introdujo un controlador en la versión 1.0 y se modificó en la v1.7, me gustaría manejar esto de la siguiente manera. Imagina que el código está dentro de un controlador y que hay algún código que puede extraer la versión del encabezado. (Lo siguiente no es válido en Spring)
@RequestMapping(...)
@VersionRange(1.0,1.6)
@ResponseBody
public Object method1() {
// so something
return object;
}
@RequestMapping(...) //same Request mapping annotation
@VersionRange(1.7)
@ResponseBody
public Object method2() {
// so something
return object;
}
Esto no es posible en Spring ya que los 2 métodos tienen la misma RequestMapping
anotación y Spring no se carga. La idea es que la VersionRange
anotación pueda definir un rango de versiones abierto o cerrado. El primer método es válido desde las versiones 1.0 a 1.6, mientras que el segundo para la versión 1.7 en adelante (incluida la última versión 1.8). Sé que este enfoque se rompe si alguien decide pasar la versión 99.99, pero eso es algo con lo que estoy bien para vivir.
Ahora, dado que lo anterior no es posible sin una reelaboración seria de cómo funciona la primavera, estaba pensando en modificar la forma en que los controladores coincidían con las solicitudes, en particular para escribir la mía propia ProducesRequestCondition
, y tener el rango de versiones allí. Por ejemplo
Código:
@RequestMapping(..., produces = "application/vnd.company.app-[1.0-1.6]+json)
@ResponseBody
public Object method1() {
// so something
return object;
}
@RequestMapping(..., produces = "application/vnd.company.app-[1.7-]+json)
@ResponseBody
public Object method2() {
// so something
return object;
}
De esta manera, puedo tener rangos de versiones cerrados o abiertos definidos en la parte de producción de la anotación. Estoy trabajando en esta solución ahora, con el problema de que todavía tenía que reemplazar algunas clases principales de Spring MVC ( RequestMappingInfoHandlerMapping
, RequestMappingHandlerMapping
y RequestMappingInfo
), que no me gustan, porque significa trabajo adicional cada vez que decido actualizar a una versión más nueva de primavera.
Agradecería cualquier comentario ... y especialmente, cualquier sugerencia para hacer esto de una manera más simple y fácil de mantener.
Editar
Añadiendo una recompensa. Para obtener la recompensa, responda la pregunta anterior sin sugerir tener esta lógica en el controlador. Spring ya tiene mucha lógica para seleccionar a qué método de controlador llamar, y quiero aprovechar eso.
Editar 2
Compartí el POC original (con algunas mejoras) en github: https://github.com/augusto/restVersioning
fuente
produces={"application/json-1.0", "application/json-1.1"}
Respuestas:
Independientemente de si se puede evitar el control de versiones haciendo cambios compatibles con versiones anteriores (lo que puede no siempre ser posible cuando está sujeto a algunas pautas corporativas o sus clientes API se implementan con errores y se romperían incluso si no lo hicieran), el requisito abstraído es interesante uno:
¿Cómo puedo hacer una asignación de solicitud personalizada que realice evaluaciones arbitrarias de los valores de encabezado de la solicitud sin realizar la evaluación en el cuerpo del método?
Como se describe en esta respuesta SO , en realidad puede tener lo mismo
@RequestMapping
y usar una anotación diferente para diferenciar durante el enrutamiento real que ocurre durante el tiempo de ejecución. Para hacerlo, deberá:VersionRange
.RequestCondition<VersionRange>
. Dado que tendrá algo así como un algoritmo de mejor coincidencia, tendrá que verificar si los métodos anotados con otrosVersionRange
valores proporcionan una mejor coincidencia para la solicitud actual.VersionRangeRequestMappingHandlerMapping
condición basada en la anotación y la solicitud (como se describe en la publicación Cómo implementar las propiedades personalizadas de @RequestMapping ).VersionRangeRequestMappingHandlerMapping
antes de usar el predeterminadoRequestMappingHandlerMapping
(por ejemplo, estableciendo su orden en 0).Esto no requeriría ningún reemplazo hacky de los componentes de Spring, pero utiliza la configuración de Spring y los mecanismos de extensión, por lo que debería funcionar incluso si actualiza su versión de Spring (siempre que la nueva versión admita estos mecanismos).
fuente
mvc:annotation-driven
. Es de esperar que Spring proporcione una versiónmvc:annotation-driven
en la que se puedan definir condiciones personalizadas.Acabo de crear una solución personalizada. Estoy usando la
@ApiVersion
anotación en combinación con la@RequestMapping
anotación dentro de las@Controller
clases.Ejemplo:
Implementación:
Anotación ApiVersion.java :
ApiVersionRequestMappingHandlerMapping.java (esto es principalmente copiar y pegar desde
RequestMappingHandlerMapping
):Inyección en WebMvcConfigurationSupport:
fuente
/v1/aResource
y/v2/aResource
parecen recursos diferentes, ¡ pero es solo una representación diferente del mismo recurso! 2. El uso de encabezados HTTP se ve mejor, pero no puede darle a nadie una URL, porque la URL no contiene el encabezado. 3. Usar un parámetro de URL, es decir/aResource?v=2.1
(por cierto: esa es la forma en que Google hace el control de versiones)....
Todavía no estoy seguro de optar por la opción 2 o 3 , pero nunca volveré a usar 1 por las razones mencionadas anteriormente.RequestMappingHandlerMapping
en suWebMvcConfiguration
, debe sobrescribir encreateRequestMappingHandlerMapping
lugar derequestMappingHandlerMapping
! De lo contrario, encontrará problemas extraños (de repente tuve problemas con la inicialización perezosa de Hibernates debido a una sesión cerrada)WebMvcConfigurationSupport
sino extenderDelegatingWebMvcConfiguration
. Esto funcionó para mí (consulte stackoverflow.com/questions/22267191/… )Aún así, recomendaría usar URL para el control de versiones porque en las URL @RequestMapping admite patrones y parámetros de ruta, cuyo formato podría especificarse con regexp.
Y para manejar las actualizaciones del cliente (que mencionó en el comentario) puede usar alias como 'más reciente'. O tener una versión no versionada de api que usa la última versión (sí).
Además, utilizando parámetros de ruta, puede implementar cualquier lógica compleja de manejo de versiones, y si ya desea tener rangos, es muy posible que desee algo más pronto.
Aquí hay un par de ejemplos:
Según el último enfoque, puede implementar algo como lo que desee.
Por ejemplo, puede tener un controlador que solo contenga métodos con control de versiones.
En ese manejo, busca (usando reflexión / AOP / bibliotecas de generación de código) en algún servicio / componente de Spring o en la misma clase para el método con el mismo nombre / firma y requerido @VersionRange y lo invoca pasando todos los parámetros.
fuente
He implementado una solución que maneja PERFECTAMENTE el problema con el versionado del resto.
Hablando en general, hay 3 enfoques principales para el control de versiones de resto:
Ruta de acceso basados en approch, en el que el cliente define la versión en la URL:
Encabezado Content-Type , en el que el cliente define la versión en el encabezado Accept :
Encabezado personalizado , en el que el cliente define la versión en un encabezado personalizado.
El problema con el primer enfoque es que si cambia la versión, digamos de v1 -> v2, probablemente necesite copiar y pegar los recursos v1 que no han cambiado a la ruta v2
El problema con el segundo enfoque es que algunas herramientas como http://swagger.io/ no pueden distinguir entre operaciones con la misma ruta pero diferente tipo de contenido (verifique el problema https://github.com/OAI/OpenAPI-Specification/issues/ 146 )
La solución
Dado que estoy trabajando mucho con herramientas de documentación de resto, prefiero usar el primer enfoque. Mi solución maneja el problema con el primer enfoque, por lo que no es necesario copiar y pegar el punto final en la nueva versión.
Digamos que tenemos versiones v1 y v2 para el controlador de usuario:
El requisito es que si solicito la v1 para el recurso de usuario, tengo que tomar la respuesta de "Usuario V1" , de lo contrario, si solicito la v2 , v3 y así sucesivamente, debo tomar la respuesta de "Usuario V2" .
Para implementar esto en primavera, debemos anular el comportamiento predeterminado RequestMappingHandlerMapping :
}
La implementación lee la versión en la URL y le pide a Spring que resuelva la URL. En caso de que esta URL no exista (por ejemplo, el cliente solicitó v3 ), probamos con v2 y así uno hasta que encontremos la versión más reciente para el recurso. .
Para ver los beneficios de esta implementación, digamos que tenemos dos recursos: Usuario y Empresa:
Digamos que hicimos un cambio en el "contrato" de la empresa que rompe al cliente. Así que implementamos
http://localhost:9001/api/v2/company
y le pedimos al cliente que cambie a v2 en lugar de v1.Entonces, las nuevas solicitudes del cliente son:
en vez de:
La mejor parte aquí es que con esta solución, el cliente obtendrá la información del usuario de v1 y la información de la empresa de v2 sin la necesidad de crear un nuevo (mismo) punto final desde el usuario v2.
Resto de documentación Como dije antes, la razón por la que selecciono el enfoque de control de versiones basado en URL es que algunas herramientas como swagger no documentan de manera diferente los puntos finales con la misma URL pero con un tipo de contenido diferente. Con esta solución, ambos puntos finales se muestran ya que tienen una URL diferente:
GIT
Implementación de la solución en: https://github.com/mspapant/restVersioningExample/
fuente
La
@RequestMapping
anotación admite unheaders
elemento que le permite limitar las solicitudes coincidentes. En particular, puede utilizar elAccept
encabezado aquí.Esto no es exactamente lo que está describiendo, ya que no maneja rangos directamente, pero el elemento admite el comodín * y también! =. Entonces, al menos podría salirse con la suya usando un comodín para los casos en que todas las versiones admitan el punto final en cuestión, o incluso todas las versiones menores de una versión principal determinada (por ejemplo, 1. *).
No creo que haya usado este elemento antes (si lo hice, no lo recuerdo), así que simplemente salgo de la documentación en
http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html
fuente
application/*
y no en partes del tipo. Por ejemplo, lo siguiente no es válido en Spring"Accept=application/vnd.company.app-1.*+json"
. Esto se relaciona con cómo la clase de primaveraMediaType
obras¿Qué hay de usar la herencia para modelar versiones? Eso es lo que estoy usando en mi proyecto y no requiere una configuración especial de resorte y me da exactamente lo que quiero.
Esta configuración permite una pequeña duplicación de código y la capacidad de sobrescribir métodos en nuevas versiones de la API con poco trabajo. También evita la necesidad de complicar su código fuente con la lógica de cambio de versión. Si no codifica un extremo en una versión, tomará la versión anterior de forma predeterminada.
Comparado con lo que otros están haciendo, esto parece mucho más fácil. ¿Se me escapa algo?
fuente
Ya intenté versionar mi API usando el control de versiones de URI , como:
Pero existen algunos desafíos al intentar que esto funcione: ¿cómo organizar su código con diferentes versiones? ¿Cómo gestionar dos (o más) versiones al mismo tiempo? ¿Cuál es el impacto al eliminar alguna versión?
La mejor alternativa que encontré no fue la versión de la API completa, sino controlar la versión en cada punto final . Este patrón se denomina Control de versiones usando el encabezado Aceptar o Control de versiones a través de la negociación de contenido :
Implementación en primavera
Primero, crea un controlador con un atributo de producción básico, que se aplicará de forma predeterminada para cada punto final dentro de la clase.
Después de eso, cree un posible escenario en el que tenga dos versiones de un punto final para crear un pedido:
¡Hecho! Simplemente llame a cada punto final utilizando la versión de encabezado Http deseada :
O, para llamar a la versión dos:
Sobre tus preocupaciones:
Como se explicó, esta estrategia mantiene cada controlador y punto final con su versión real. Solo modifica el punto final que tiene modificaciones y necesita una nueva versión.
¿Y el Swagger?
Configurar el Swagger con diferentes versiones también es muy fácil con esta estrategia. Consulte esta respuesta para obtener más detalles.
fuente
En produce puedes tener negación. Entonces, para método1 decir
produces="!...1.7"
y en método2 tener el positivo.El produce también es una matriz, por lo que para el método1 puede decir,
produces={"...1.6","!...1.7","...1.8"}
etc. (acepte todos excepto 1.7)Por supuesto, no es tan ideal como los rangos que tiene en mente, pero creo que es más fácil de mantener que otras cosas personalizadas si esto es algo poco común en su sistema. ¡Buena suerte!
fuente
Puede usar AOP, alrededor de la interceptación
Considere tener un mapeo de solicitudes que reciba todo el
/**/public_api/*
y en este método no haga nada;Después
La única restricción es que todos deben estar en el mismo controlador.
Para la configuración de AOP, eche un vistazo a http://www.mkyong.com/spring/spring-aop-examples-advice/
fuente