How to manage REST API versioning with spring?
How to manage REST API versioning with spring?
I've been searching how to manage a REST API versions using Spring 3.2.x, but I haven't find anything that is easy to maintain. I'll explain first the problem I have, and then a solution... but I do wonder if I'm re-inventing the wheel here.
I want to manage the version based on the Accept header, and for example if a request has the Accept header application/vnd.company.app-1.1+json
, I want spring MVC to forward this to the method that handles this version. And since not all methods in an API change in the same release, I don't want to go to each of my controllers and change anything for a handler that hasn't changed between versions. I also don't want to have the logic to figure out which version to use in the controller themselves (using service locators) as Spring is already discovering which method to call.
So taken an API with versions 1.0, to 1.8 where a handler was introduced in version 1.0 and modified in v1.7, I would like handle this in the following way. Imagine that the code is inside a controller, and that there's some code that is able to extract the version from the header. (The following is invalid in 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; }
This is not possible in spring as the 2 methods have the same RequestMapping
annotation and Spring fails to load. The idea is that the VersionRange
annotation can define an open or closed version range. The first method is valid from versions 1.0 to 1.6, while the second for version 1.7 onwards (including the latest version 1.8). I know that this approach breaks if someone decides to pass version 99.99, but that's something I'm OK to live with.
Now, since the above is not possible without a serious rework of how spring works, I was thinking of tinkering with the way handlers matched to requests, in particular to write my own ProducesRequestCondition
, and have the version range in there. For example
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; }
In this way, I can have closed or open version ranges defined in the produces part of the annotation. I'm working on this solution now, with the problem that I still had to replace some core Spring MVC classes (RequestMappingInfoHandlerMapping
, RequestMappingHandlerMapping
and RequestMappingInfo
), which I don't like, because it means extra work whenever I decide to upgrade to a newer version of spring.
I would appreciate any thoughts... and especially, any suggestion to do this in a simpler, easier to maintain way.
Edit
Adding a bounty. To get the bounty, please answer the question above without suggesting to have this logic in the controller themselves. Spring already has a lot of logic to select which controller method to call, and I want to piggyback on that.
Edit 2
I've shared the original POC (with some improvements) in github: https://github.com/augusto/restVersioning
Answer by codesalsa for How to manage REST API versioning with spring?
In produces you can have negation. So for method1 say produces="!...1.7"
and in method2 have the positive.
The produces is also an array so you for method1 you can say produces={"...1.6","!...1.7","...1.8"}
etc (accept all except 1.7)
Ofcourse not as ideal as ranges that you have in mind but I think easier to maintain than other custom stuff if this is something uncommon in your system. Good luck!
Answer by elusive-code for How to manage REST API versioning with spring?
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
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.
Answer by Willie Wheeler for How to manage REST API versioning with spring?
The @RequestMapping
annotation supports a headers
element that allows you to narrow the matching requests. In particular you can use the Accept
header here.
@RequestMapping(headers = { "Accept=application/vnd.company.app-1.0+json", "Accept=application/vnd.company.app-1.1+json" })
This isn't exactly what you're describing, since it doesn't directly handle ranges, but the element does support the * wildcard as well as !=. So at least you could get away with using a wildcard for cases where all versions support the endpoint in question, or even all minor versions of a given major version (e.g. 1.*).
I don't think I've actually used this element before (if I have I don't remember), so I'm just going off the documentation at
Answer by xwoker for 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:
- Create a new annotation
VersionRange
. - Implement a
RequestCondition
. 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. - Implement a
VersionRangeRequestMappingHandlerMapping
based on the annotation and request condition (as described in the linked post). - 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 sot it should work even if you update your Spring version (as long as the new version supports these mechanisms.
Answer by hevi for How to manage REST API versioning with spring?
You can use AOP, around interception
Consider having a request mapping which receives all the /**/public_api/*
and in this method do nothing;
@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 constraint is that all has to be in the same controller.
For AOP configuration have a look at http://www.mkyong.com/spring/spring-aop-examples-advice/
Answer by Benjamin M for How to manage REST API versioning with spring?
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); 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
Injection into WebMvcConfigurationSupport:
public class WebMvcConfig extends WebMvcConfigurationSupport { @Override public RequestMappingHandlerMapping requestMappingHandlerMapping() { return new ApiVersionRequestMappingHandlerMapping("v"); } }
Answer by Ceekay for How to manage REST API versioning with spring?
What about just using inheritance to model versioning? That is what I'm using in my project and it requires no special spring configuration and gets me 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 set up allows for little duplication of code and the ability to overwrite methods into new versions of the api with little work. It also saves the need to complicate your source code with version switching logic. If you don't code an endpoint in a version it will grab the previous version by default.
Compared to what others are doing this seems way easier. Is there something I'm missing?
Fatal error: Call to a member function getElementsByTagName() on a non-object in D:\XAMPP INSTALLASTION\xampp\htdocs\endunpratama9i\www-stackoverflow-info-proses.php on line 72
0 comments:
Post a Comment