Saltar al contenido

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

Solución:

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 de 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 el mismo @RequestMapping y use 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 VersionRange Los valores proporcionan una mejor coincidencia para la solicitud actual.
  3. Implementar un VersionRangeRequestMappingHandlerMapping según la condición de anotación y solicitud (como se describe en la publicación Cómo implementar las propiedades personalizadas de @RequestMapping).
  4. Configure Spring para evaluar su VersionRangeRequestMappingHandlerMapping antes 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 usa 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).

Acabo de crear una solución personalizada. Estoy usando el @ApiVersion anotación en combinación con @RequestMapping anotación dentro @Controller clases.

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:

ApiVersion.java anotación:

@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");
    }
}

Todavía 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 mencionaste en el comentario) puedes 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 contenga solo apuñalamientos de métodos con manejo de versiones.

En ese manejo, busca (usando la 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.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *