How to control REST API versions using spring?

I was looking for how to control REST API versions using Spring 3.2.x, but I did not find anything that is easy to maintain. First I will explain the problem, and then the solution ... but I really wonder if I am not inventing the wheel here.

I want to control the version based on the Accept header, and for example, if the request has the Accept header application/vnd.company.app-1.1+json , I want Spring MVC to forward this to the method that processes this version. And since not all methods in the API change in the same version, I do not want to access each of my controllers and change anything for a handler that has not changed between versions. I also do not want the logic to determine which version to use in the controller itself (using service locators), since Spring already detects which method to call.

So, taken API with versions 1.0, 1.8, where the handler was introduced in version 1.0 and changed in version 1.7, I would like to handle it as follows. Imagine that the code is inside the controller and that there is code that can extract the version from the header. (In Spring) is invalid

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

This is not possible in Spring since the 2 methods have the same RequestMapping annotation and Spring does not load. The idea is that the VersionRange annotation can define a range of open or closed versions. The first method is valid from versions 1.0 to 1.6, and the second for version 1.7 (including the latest version 1.8). I know that this approach breaks if someone decides to transfer version 99.99, but with this I agree to live.

Now, since the foregoing is impossible without a serious revision of Spring's work, I was thinking about working with handlers that correspond to requests, in particular, writing my own ProducesRequestCondition and having a range of versions there. for example

the code:

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

This way, I can have closed or open version ranges defined in the annotation creation part. I am currently working on this solution, and the problem is that I still had to replace some of the main Spring MVC classes ( RequestMappingInfoHandlerMapping , RequestMappingHandlerMapping and RequestMappingInfo ), which I don’t like because it means extra when I decide to upgrade spring.

I would appreciate any thoughts ... and especially, any suggestion to make this simpler, easier to maintain.




Edit

Adding generosity. To receive a reward, please answer the question above without offering this logic in the controller itself. Spring already has a lot of logic to choose which controller method to call, and I want to deal with it.




Edit 2

I shared the original POC (with some improvements) on github: https://github.com/augusto/restVersioning

+101
java spring rest spring-mvc versioning
Nov 25 '13 at 16:37
source share
9 answers

Regardless of whether it is possible to avoid version control by making backward compatible changes (which is not always possible when you are bound by some corporate rules or your API clients are implemented with errors and disrupt even if they should not), the abstracted requirement is interesting one:

How can I do a custom query mapping that does arbitrary evaluations of the header values ​​from the query without making an assessment in the body of the method?

As described in this SO answer, you can actually have the same @RequestMapping and use a different annotation to differentiate during real-time routing that happens at runtime. For this you need:

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

This does not require any hacker replacement of Spring components, but uses Spring configuration and extension mechanisms, so it should work even if you upgrade the Spring version (if the new version supports these mechanisms).

+56
Dec 03 '13 at 8:23
source share

I just created my own solution. I use the @ApiVersion annotation in combination with the @RequestMapping annotation inside the @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 abstract:

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

ApiVersionRequestMappingHandlerMapping.java (this is basically copying and pasting 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 in WebMvcConfigurationSupport:

 public class WebMvcConfig extends WebMvcConfigurationSupport { @Override public RequestMappingHandlerMapping requestMappingHandlerMapping() { return new ApiVersionRequestMappingHandlerMapping("v"); } } 
+46
Jan 17 '14 at 2:49
source share

I still recommend using URLs for version control, because @RequestMapping supports URL patterns and path parameters in URLs, the format of which can be specified using a regular expression.

And to handle client updates (which you mentioned in the comment) you can use aliases like "recent". Or has an unversioned version of the api that uses the latest version (yes).

Also, using path parameters, you can implement any complex versioning logic, and if you already want to have ranges, you might need something more soon.

Here are some 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 latter approach, you can actually implement something like what you want.

For example, you might have a controller containing only hits with version processing.

In this processing, you look (using reflection library / AOP / code libraries) in some spring service / component or in the same class for a method with the same name / signature and required @VersionRange and cause it to pass all parameters.

+16
Nov 29 '13 at 5:12
source share

I implemented a solution that handles the PERFECTLY versioning issue with version control.

In general, there are three main approaches to managing vacation versions:

  • A statement-based path in which the client defines the version in the URL:

     http://localhost:9001/api/v1/user http://localhost:9001/api/v2/user 
  • The Content-Type header , in which the client defines the version in the Accept header:

     http://localhost:9001/api/v1/user with Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json 
  • A custom header in which the client identifies the version in the custom header.

Problem with the first approach is that if you change the version, say, from v1 → v2, maybe you need to copy-paste resources v1 that haven’t been changed to path v2

Problem with the second approach, some tools like http://swagger.io/ cannot differ between operations with the same path, but with a different Content-Type (check issue https://github.com/ OAI / OpenAPI-Specification / issues / 146 )

Decision

Since I work a lot with recreational documentation tools, I prefer to use the first approach. My solution handles the problem using the first approach, so you don't need to copy the endpoint to the new version.

Let's say we have versions v1 and v2 for the user controller:

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

Demand is that if I request v1 for a user resource, I have to accept the answer "User V1" , otherwise if I request v2 , v3 , etc. I need to answer "Custom V2" .

enter image description here

To implement this in spring, we need to override the default behavior of 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 ; } 

}

The implementation reads the version in the URL and requests from spring to resolve the URL. In case this URL does not exist (for example, the client requested v3 ), try with v2 and so on until we find the latest version for the resource.

To see the benefits of this implementation, let's say we have two resources: User and Company:

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

Let's say we made changes to the "contract" of the company, which breaks the client. So, we implement http://localhost:9001/api/v2/company , and we ask the client to switch to v2 instead of v1.

So, new requests from the client:

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

instead:

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

the best part is that with this solution the client will get user information from v1 and company information from v2 without the need to create a new (same) user endpoint v2!

Leisure documentation As I said before, the reason I choose a URL-based approach is because some tools like swagger don't document endpoints with the same URL differently but with a different type of content. Both endpoints are displayed with this solution because they have different URLs:

enter image description here

Git

Implementation of the solution: https://github.com/mspapant/restVersioningExample/

+10
Aug 21 '16 at 16:00
source share

Annotation @RequestMapping supports the headers element, which allows you to narrow down the corresponding requests. In particular, here you can use the Accept header.

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

This is not exactly what you are describing, since it does not directly handle ranges, but the element supports the wildcard character * as well! =. Thus, at least you can leave using the template for cases where all versions support the specified endpoint or even all minor versions of this major version (for example, 1. *).

I don’t think I used this element before (if I don’t remember), so I just delete the documentation in

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html

+8
Dec 01 '13 at 8:25
source share

How about using inheritance to model versions? This is what I use in my project and does not require a special spring configuration and gets exactly what I want.

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

This setting allows you to slightly duplicate the code and the ability to overwrite methods in new versions of api with little work. It also eliminates the need to complicate source code with version-switching logic. If you do not encode the endpoint in a version, it will use the previous version by default.

Compared to what others do, it looks easier. Is there something I'm missing?

+3
Feb 05 '16 at 22:58
source share

In creations you can deny. Thus, for method1 they say produces="!...1.7" , and in method2 they say positive.

It also releases an array, so you can use method1, which you can say produces={"...1.6","!...1.7","...1.8"} , etc. (accept everything except 1.7)

Of course, not as perfect as the ranges that you have in mind, but I find that they are easier to maintain than other custom things if this is something unusual on your system. Good luck

+1
Nov 27 '13 at 23:07 on
source share

I have already tried to create a version of my API using URI versions, for example:

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

But there are some problems when trying to make this work: how to organize your code with different versions? How to manage two (or more) versions at the same time? How will the removal of any version affect?

The best alternative I found was not the version of the entire API, but version control at each endpoint . This template is called versioning using the Accept header or versioning through content negotiation :

This approach allows us to create versions of a single resource view instead of version control of the entire API, which gives us more granular version control. This also creates less space in the code base, since we do not need to disassemble the entire application when creating a new version. Another advantage of this approach is that it does not require the implementation of URI routing rules introduced by version control through the URI path.

Spring Implementation

First, you create a controller with a basic product attribute that will be applied by default for each endpoint within the class.

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

After that, create a possible scenario in which you have two versions of the endpoint to create an order:

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

Done! Just call each endpoint using the correct version of the Http header:

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

Or, to name version two:

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

About your worries:

And since not all methods in the API change in one release, I don’t want to go to each of my controllers and change anything for a handler that did not change between versions.

As explained, this strategy supports each controller and endpoint with its current version. You only change the endpoint, which has modifications and needs a new version.

And swagger?

Configuring Swagger with different versions is also very easy using this strategy. See this answer for more details.

+1
Apr 04 '19 at 20:09 on
source share

You can use AOP around interception

Consider the presence of a query mapping that gets all /**/public_api/* and does nothing in this method;

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

After

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

The only limitation is that everything must be in one controller.

For AOP configuration see http://www.mkyong.com/spring/spring-aop-examples-advice/

0
Dec 04 '13 at 15:56
source share



All Articles