How to manage REST API versioning with spring? How to manage REST API versioning with spring? java java

How to manage REST API versioning with spring?


Regardless whether versioning can be avoided by doing backwards compatible changes (which might not always possible when you are bound by some corporate guidelines or your API clients are implemented in a buggy way and would break even if they should not) the abstracted requirement is an interesting one:

How can I do a custom request mapping that does arbitrary evaluations of header values from the request without doing the evaluation in the method body?

As described in this SO answer you actually can have the same @RequestMapping and use a different annotation to differentiate during the actual routing that happens during runtime. To do so, you will have to:

  1. Create a new annotation VersionRange.
  2. Implement a RequestCondition<VersionRange>. Since you will have something like a best-match algorithm you will have to check whether methods annotated with other VersionRange values provide a better match for the current request.
  3. Implement a VersionRangeRequestMappingHandlerMapping based on the annotation and request condition (as described in the post How to implement @RequestMapping custom properties).
  4. Configure spring to evaluate your VersionRangeRequestMappingHandlerMapping before using the default RequestMappingHandlerMapping (e.g. by setting its order to 0).

This wouldn't require any hacky replacements of Spring components but uses the Spring configuration and extension mechanisms so it should work even if you update your Spring version (as long as the new version supports these mechanisms).


I just created a custom solution. I'm using the @ApiVersion annotation in combination with @RequestMapping annotation inside @Controller classes.

Example:

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

Implementation:

ApiVersion.java annotation:

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

ApiVersionRequestMappingHandlerMapping.java (this is mostly copy and paste from 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);    }}

Injection into WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {    @Override    public RequestMappingHandlerMapping requestMappingHandlerMapping() {        return new ApiVersionRequestMappingHandlerMapping("v");    }}


I would still recommend using URL's for versioning because in URLs @RequestMapping supports patterns and path parameters, which format could be specified with regexp.

And to handle client upgrades (which you mentioned in comment) you can use aliases like 'latest'. Or have unversioned version of api which uses latest version (yeah).

Also using path parameters you can implement any complex version handling logic, and if you already want to have ranges, you very well might want something more soon enough.

Here is a couple of examples:

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

Based on the last approach you can actually implement something like what you want.

For example you can have a controller that contains only method stabs with version handling.

In that handling you look (using reflection/AOP/code generation libraries) in some spring service/component or in the same class for method with the same name/signature and required @VersionRange and invoke it passing all parameters.