¿Cómo administrar el control de versiones de la API REST con Spring?

118

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 RequestMappinganotación y Spring no se carga. La idea es que la VersionRangeanotació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, RequestMappingHandlerMappingy 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

Augusto
fuente
1
@flup No entiendo tu comentario. Eso solo dice que puede usar encabezados y, como dije, lo que Spring proporciona fuera de la caja no es suficiente para admitir API que se actualizan constantemente. Peor aún, el enlace de esa respuesta usa la versión en la URL.
Augusto
Quizás no sea exactamente lo que está buscando, pero Spring 3.2 admite un parámetro "produce" en RequestMapping. La única advertencia es que la lista de versiones debe ser explícita. Por ejemplo produces={"application/json-1.0", "application/json-1.1"}
,,
1
Necesitamos admitir varias versiones de nuestras API, estas diferencias suelen ser cambios menores que harían incompatibles algunas llamadas de algunos clientes (no sería extraño si necesitamos admitir 4 versiones menores, en las que algunos endpoints son incompatibles). Agradezco la sugerencia de ponerlo en la URL, pero sabemos que es un paso en la dirección equivocada, ya que tenemos un par de aplicaciones con la versión en la URL y hay mucho trabajo involucrado cada vez que necesitamos subir el versión.
Augusto
1
@Augusto, en realidad tú tampoco. Simplemente diseñe sus cambios de API de una manera que no rompa la compatibilidad con versiones anteriores. Solo dame un ejemplo de los cambios que rompen la compatibilidad y te mostraré cómo hacer estos cambios de manera ininterrumpida.
Alexey Andreev

Respuestas:

62

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 @RequestMappingy usar una anotación diferente para diferenciar durante el enrutamiento real que ocurre durante el tiempo de ejecución. Para hacerlo, deberá:

  1. Crea una nueva anotación VersionRange.
  2. Implementar un RequestCondition<VersionRange>. Dado que tendrá algo así como un algoritmo de mejor coincidencia, tendrá que verificar si los métodos anotados con otros VersionRangevalores proporcionan una mejor coincidencia para la solicitud actual.
  3. Implemente una VersionRangeRequestMappingHandlerMappingcondición basada en la anotación y la solicitud (como se describe en la publicación Cómo implementar las propiedades personalizadas de @RequestMapping ).
  4. Configure spring para evaluar su VersionRangeRequestMappingHandlerMappingantes de usar el predeterminado RequestMappingHandlerMapping(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).

xwoker
fuente
Gracias por agregar tu comentario como respuesta xwoker. Hasta ahora es el mejor. Implementé la solución basada en los enlaces que mencionaste y no está tan mal. El mayor problema se manifestará al actualizar a una nueva versión de Spring, ya que requerirá verificar cualquier cambio en la lógica subyacente mvc:annotation-driven. Es de esperar que Spring proporcione una versión mvc:annotation-drivenen la que se puedan definir condiciones personalizadas.
Augusto
@Augusto, medio año después, ¿cómo te ha ido esto? Además, tengo curiosidad, ¿realmente estás versionando por método? En este punto, me pregunto si no sería más claro la versión en un nivel de granularidad por clase / por controlador.
Sander Verhagen
1
@SanderVerhagen está funcionando, pero hacemos la versión de toda la API, no por método o controlador (la API es bastante pequeña ya que se centra en un aspecto del negocio). Tenemos un proyecto considerablemente más grande en el que eligieron usar una versión diferente por recurso y especificar eso en la URL (para que pueda tener un punto final en / v1 / sesiones y otro recurso en una versión completamente diferente, por ejemplo, / v4 / orders) ... es un poco más flexible, pero ejerce más presión sobre los clientes para saber a qué versión llamar de cada punto final.
Augusto
1
Desafortunadamente, esto no funciona bien con Swagger, ya que gran parte de la configuración automática se desactiva al extender WebMvcConfigurationSupport.
Rick
Probé esta solución pero en realidad no funciona con 2.3.2.RELEASE. ¿Tiene algún proyecto de ejemplo para mostrar?
Patrick
54

Acabo de crear una solución personalizada. Estoy usando la @ApiVersionanotación en combinación con la @RequestMappinganotación dentro de las @Controllerclases.

Ejemplo:

@Controller
@RequestMapping("x")
@ApiVersion(1)
class MyController {

    @RequestMapping("a")
    void a() {}         // maps to /v1/x/a

    @RequestMapping("b")
    @ApiVersion(2)
    void b() {}         // maps to /v2/x/b

    @RequestMapping("c")
    @ApiVersion({1,3})
    void c() {}         // maps to /v1/x/c
                        //  and to /v3/x/c

}

Implementación:

Anotación ApiVersion.java :

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    int[] value();
}

ApiVersionRequestMappingHandlerMapping.java (esto es principalmente copiar y pegar desde RequestMappingHandlerMapping):

public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    private final String prefix;

    public ApiVersionRequestMappingHandlerMapping(String prefix) {
        this.prefix = prefix;
    }

    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
        if(info == null) return null;

        ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        if(methodAnnotation != null) {
            RequestCondition<?> methodCondition = getCustomMethodCondition(method);
            // Concatenate our ApiVersion with the usual request mapping
            info = createApiVersionInfo(methodAnnotation, methodCondition).combine(info);
        } else {
            ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
            if(typeAnnotation != null) {
                RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
                // Concatenate our ApiVersion with the usual request mapping
                info = createApiVersionInfo(typeAnnotation, typeCondition).combine(info);
            }
        }

        return info;
    }

    private RequestMappingInfo createApiVersionInfo(ApiVersion annotation, RequestCondition<?> customCondition) {
        int[] values = annotation.value();
        String[] patterns = new String[values.length];
        for(int i=0; i<values.length; i++) {
            // Build the URL prefix
            patterns[i] = prefix+values[i]; 
        }

        return new RequestMappingInfo(
                new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch(), useTrailingSlashMatch(), getFileExtensions()),
                new RequestMethodsRequestCondition(),
                new ParamsRequestCondition(),
                new HeadersRequestCondition(),
                new ConsumesRequestCondition(),
                new ProducesRequestCondition(),
                customCondition);
    }

}

Inyección en WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}
Benjamín M
fuente
4
Cambié el int [] a String [] para permitir versiones como "1.2", y así puedo manejar palabras clave como "más reciente"
Maelig
3
Sí, eso es bastante razonable. Para proyectos futuros, tomaría un camino diferente por algunas razones: 1. Las URL representan recursos. /v1/aResourcey /v2/aResourceparecen 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.
Benjamín M
5
Si desea inyectar el suyo RequestMappingHandlerMappingen su WebMvcConfiguration, debe sobrescribir en createRequestMappingHandlerMappinglugar de requestMappingHandlerMapping! De lo contrario, encontrará problemas extraños (de repente tuve problemas con la inicialización perezosa de Hibernates debido a una sesión cerrada)
stuXnet
1
El enfoque se ve bien, pero de alguna manera parece que no funciona con casos de prueba junti (SpringRunner). Cualquier posibilidad de que tenga el enfoque que funciona con casos de prueba
JDev
1
Hay una manera de hacer que esto funcione, no extender WebMvcConfigurationSupport sino extender DelegatingWebMvcConfiguration. Esto funcionó para mí (consulte stackoverflow.com/questions/22267191/… )
SeB.Fr
17

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:

@RequestMapping({
    "/**/public_api/1.1/method",
    "/**/public_api/1.2/method",
})
public void method1(){
}

@RequestMapping({
    "/**/public_api/1.3/method"
    "/**/public_api/latest/method"
    "/**/public_api/method" 
})
public void method2(){
}

@RequestMapping({
    "/**/public_api/1.4/method"
    "/**/public_api/beta/method"
})
public void method2(){
}

//handles all 1.* requests
@RequestMapping({
    "/**/public_api/{version:1\\.\\d+}/method"
})
public void methodManual1(@PathVariable("version") String version){
}

//handles 1.0-1.6 range, but somewhat ugly
@RequestMapping({
    "/**/public_api/{version:1\\.[0123456]?}/method"
})
public void methodManual1(@PathVariable("version") String version){
}

//fully manual version handling
@RequestMapping({
    "/**/public_api/{version}/method"
})
public void methodManual2(@PathVariable("version") String version){
    int[] versionParts = getVersionParts(version);
    //manual handling of versions
}

public int[] getVersionParts(String version){
    try{
        String[] versionParts = version.split("\\.");
        int[] result = new int[versionParts.length];
        for(int i=0;i<versionParts.length;i++){
            result[i] = Integer.parseInt(versionParts[i]);
        }
        return result;
    }catch (Exception ex) {
        return null;
    }
}

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.

codigo elusivo
fuente
14

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:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
    
  • Encabezado Content-Type , en el que el cliente define la versión en el encabezado Accept :

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
    
  • 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:

package com.mspapant.example.restVersion.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * The user controller.
 *
 * @author : Manos Papantonakos on 19/8/2016.
 */
@Controller
@Api(value = "user", description = "Operations about users")
public class UserController {

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v1/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV1() {
         return "User V1";
    }

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v2/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV2() {
         return "User V2";
    }
 }

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

ingrese la descripción de la imagen aquí

Para implementar esto en primavera, debemos anular el comportamiento predeterminado RequestMappingHandlerMapping :

package com.mspapant.example.restVersion.conf.mapping;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class VersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    @Value("${server.apiContext}")
    private String apiContext;

    @Value("${server.versionContext}")
    private String versionContext;

    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        HandlerMethod method = super.lookupHandlerMethod(lookupPath, request);
        if (method == null && lookupPath.contains(getApiAndVersionContext())) {
            String afterAPIURL = lookupPath.substring(lookupPath.indexOf(getApiAndVersionContext()) + getApiAndVersionContext().length());
            String version = afterAPIURL.substring(0, afterAPIURL.indexOf("/"));
            String path = afterAPIURL.substring(version.length() + 1);

            int previousVersion = getPreviousVersion(version);
            if (previousVersion != 0) {
                lookupPath = getApiAndVersionContext() + previousVersion + "/" + path;
                final String lookupFinal = lookupPath;
                return lookupHandlerMethod(lookupPath, new HttpServletRequestWrapper(request) {
                    @Override
                    public String getRequestURI() {
                        return lookupFinal;
                    }

                    @Override
                    public String getServletPath() {
                        return lookupFinal;
                    }});
            }
        }
        return method;
    }

    private String getApiAndVersionContext() {
        return "/" + apiContext + "/" + versionContext;
    }

    private int getPreviousVersion(final String version) {
        return new Integer(version) - 1 ;
    }

}

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:

http://localhost:9001/api/v{version}/user
http://localhost:9001/api/v{version}/company

Digamos que hicimos un cambio en el "contrato" de la empresa que rompe al cliente. Así que implementamos http://localhost:9001/api/v2/companyy le pedimos al cliente que cambie a v2 en lugar de v1.

Entonces, las nuevas solicitudes del cliente son:

http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company

en vez de:

http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company

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:

ingrese la descripción de la imagen aquí

GIT

Implementación de la solución en: https://github.com/mspapant/restVersioningExample/

mspapant
fuente
9

La @RequestMappinganotación admite un headerselemento que le permite limitar las solicitudes coincidentes. En particular, puede utilizar el Acceptencabezado aquí.

@RequestMapping(headers = {
    "Accept=application/vnd.company.app-1.0+json",
    "Accept=application/vnd.company.app-1.1+json"
})

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
2
Lo sé, pero como notó, en cada versión necesitaría ir a todos mis controladores y agregar una versión, incluso si no han cambiado. El rango que mencionó solo funciona en el tipo completo, por ejemplo, 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 primavera MediaTypeobras
Augusto
@Augusto, no es necesario que hagas esto. Con este enfoque, no está versionando la "API" sino el "Endpoint". Cada endpoint puede tener una versión diferente. Para mí, es la forma más fácil de versionar la API en comparación con la versión de API . Swagger también es más sencillo de configurar . Esta estrategia se denomina control de versiones mediante negociación de contenido.
Dherik
3

¿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.

@RestController
@RequestMapping(value = "/test/1")
@Deprecated
public class Test1 {
...Fields Getters Setters...
    @RequestMapping(method = RequestMethod.GET)
    @Deprecated
    public Test getTest(Long id) {
        return serviceClass.getTestById(id);
    }
    @RequestMapping(method = RequestMethod.PUT)
    public Test getTest(Test test) {
        return serviceClass.updateTest(test);
    }

}

@RestController
@RequestMapping(value = "/test/2")
public class Test2 extends Test1 {
...Fields Getters Setters...
    @Override
    @RequestMapping(method = RequestMethod.GET)
    public Test getTest(Long id) {
        return serviceClass.getAUpdated(id);
    }

    @RequestMapping(method = RequestMethod.DELETE)
    public Test deleteTest(Long id) {
        return serviceClass.deleteTestById(id);
    }
}

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?

Ceekay
fuente
1
+1 por compartir el código. Sin embargo, la herencia lo une estrechamente. En lugar. los controladores (Test1 y Test2) deben ser solo un paso a través ... sin implementación lógica. Toda la lógica debe estar en la clase de servicio, someService. En ese caso, solo use una composición simple y nunca herede del otro controlador
Dan Hunex
1
@ dan-hunex Parece que Ceekay usa la herencia para administrar las diferentes versiones de la api. Si elimina la herencia, ¿cuál es la solución? ¿Y por qué la pareja estrecha es un problema en este ejemplo? Desde mi punto de vista, Test2 extiende Test1 porque es una mejora (con el mismo rol y las mismas responsabilidades), ¿no es así?
jeremieca
2

Ya intenté versionar mi API usando el control de versiones de URI , como:

/api/v1/orders
/api/v2/orders

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 :

Este enfoque nos permite versionar una representación de recurso único en lugar de versionar toda la API, lo que nos da un control más granular sobre el versionado. También crea una huella más pequeña en la base del código, ya que no tenemos que bifurcar toda la aplicación al crear una nueva versión. Otra ventaja de este enfoque es que no requiere la implementación de reglas de enrutamiento de URI introducidas mediante el control de versiones a través de la ruta de URI.

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.

@RestController
@RequestMapping(value = "/api/orders/", produces = "application/vnd.company.etc.v1+json")
public class OrderController {

}

Después de eso, cree un posible escenario en el que tenga dos versiones de un punto final para crear un pedido:

@Deprecated
@PostMapping
public ResponseEntity<OrderResponse> createV1(
        @RequestBody OrderRequest orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

@PostMapping(
        produces = "application/vnd.company.etc.v2+json",
        consumes = "application/vnd.company.etc.v2+json")
public ResponseEntity<OrderResponseV2> createV2(
        @RequestBody OrderRequestV2 orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

¡Hecho! Simplemente llame a cada punto final utilizando la versión de encabezado Http deseada :

Content-Type: application/vnd.company.etc.v1+json

O, para llamar a la versión dos:

Content-Type: application/vnd.company.etc.v2+json

Sobre tus preocupaciones:

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.

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.

Dherik
fuente
1

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!

codesalsa
fuente
Gracias codesalsa, estoy tratando de encontrar una forma que sea fácil de mantener y que no requiera actualizar cada punto final cada vez que necesitemos mejorar la versión.
Augusto
0

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;

@RequestMapping({
    "/**/public_api/*"
})
public void method2(Model model){
}

Después

@Override
public void around(Method method, Object[] args, Object target)
    throws Throwable {
       // look for the requested version from model parameter, call it desired range
       // check the target object for @VersionRange annotation with reflection and acquire version ranges, call the function if it is in the desired range


}

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/

hevi
fuente
Gracias hevi, estoy buscando una forma más amigable de hacer esto, ya que Spring ya selecciona qué método llamar sin usar AOP. En mi opinión, AOP agrega un nuevo nivel de complejidad de código que me gustaría evitar.
Augusto
@Augusto, Spring tiene un gran apoyo AOP. Deberías probarlo. :)
Konstantin Yovkov