How to manage REST API versioning with spring?
Asked Answered
A

10

150

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

Aggregate answered 25/11, 2013 at 16:37 Comment(13)
#11716374Ediva
@Ediva I don't understand your comment. That just says that you can use headers and, as I said, what spring provides out of the box is not sufficient to support APIs that are updated constantly. Even worse the link on that answer uses the version in the URL.Aggregate
Maybe not exactly what you are looking for, but Spring 3.2 supports a "produces" parameter on RequestMapping. The one caveat is that the version list has to be explicit. E.g., produces={"application/json-1.0", "application/json-1.1"}, etcBillowy
@Billowy yes I know about that parameter. and you made me realise that I made a mistake in the question. Since produces doesn't accept a range of versions, I'm writing my own ProducesRequestCondition, which can understand versions. As I mentioned above, in order to write this class, I had to also rewrite RequestMappingInfoHandlerMapping, RequestMappingHandlerMapping and RequestMappingInfo, because RequestMappingInfo is final :(.Aggregate
The question/problem looks like a duplicate to me, even if you prefer a different solution direction. The accepted answer there says you can use headers, but other answers using content type would answer the question too.Ediva
It looks to me like you don't have 9 versions of this resource, you only have 2. I would say that if you want to version the API (it appears you do), then put the version in the URI. If you want to version the resource (or its representation) that this endpoint refers to, put it in the accept header. I wouldn't recommend doing both, or it will get messy (as you've discovered).Drape
We need to support several versions of our APIs, these differences are usually minor changes that would make some calls from some clients incompatible (it wont be strange if we need to support 4 minor versions, in which some endpoints are incompatible). I appreciate the suggestion to put it in the url, but we know that it's a step in the wrong direction, as we have a couple of apps with the version in the URL and there's a lot of work involved every time we need to bump the version.Aggregate
@Augusto, you actually you haven't too. Just design your API changes a way that doesn't break backward compatibility. Just give me example of changes that break compatibility and I show you how to make these changes in non-breaking fashion.Howey
Did you have a look at https://mcmap.net/q/160405/-how-to-implement-requestmapping-custom-properties which seems to imply that your statement "This is not possible in spring as the 2 methods have the same RequestMapping annotation and Spring fails to load." is not totally correct?Inorganic
@xworker I haven't seen that. It's a close solution to what I'm doing at the moment (as I need my own custom condition), but it definitely looks way tidier than my implementationAggregate
xwoker vs. xworker :-)Inorganic
I'm with Alexey on this one. Follow semantic versioning rules and minimize major version changes, and you won't have to micromanage version ranges. If you're required to make a series of backwards-incompatible changes, you aren't managing your system very well. I know that's not the answer you're looking for, but sometimes the answer is to not to build a ladder to scale the 20-foot wall at the end of the dark alley, but to turn around and go back to Main Street. :)Wendall
github.com/augusto/restVersioning is too difficult to debug, if error happened.Nugent
I
85

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

Inorganic answered 3/12, 2013 at 8:23 Comment(6)
Thanks for adding your comment as an answer xwoker. Up to now is the best one. I have implemented the solution based on the links you mentioned and it's not that bad. The biggest problem will manifest when upgrading to a new version of Spring as it will require to check any changes to the logic behind mvc:annotation-driven. Hopefully Spring will provide a version of mvc:annotation-driven in which one can define custom conditions.Aggregate
@Augusto, half a year later, how has this been working out for you? Also, I'm curious, are you really versioning on a per-method basis? At this point I'm wondering if it wouldn't be clearer to version on a per-class/per-controller level granularity?Suit
@SanderVerhagen it's working, but we do version the whole API, not per method or controller (the API is quite small as it's focused on one aspect of the business). We do have a considerably bigger project where they chose to use a different version per resource and specify that on the URL (so you can have an endpoint on /v1/sessions and another resource on a completely different version, e.g. /v4/orders)... it's a bit more flexible, but it puts more pressure on clients to know which version to call of each endpoint.Aggregate
Unfortunately, this doesn't play nice with Swagger, as a lot of auto configuration is turned off when extending WebMvcConfigurationSupport.Adorne
I tried this solution but its actually not working with 2.3.2.RELEASE. Do you have some example project to show?Retrocede
Sorry, no. I haven't looked into it for a looong time.Inorganic
A
71

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");
    }
}
Administrator answered 17/1, 2014 at 2:49 Comment(9)
I changed the int[] into String[] to allow versions like "1.2", and so I can handle keywords like "latest"Huei
Yes, that's pretty reasonable. For future projects, I'd go a different way for some reasons: 1. URLs represent resources. /v1/aResource and /v2/aResource look like different resources, but it's just a different representation of the same resource! 2. Using HTTP headers looks better, but you can't give someone a URL, because the URL doesn't contain the header. 3. Using a URL parameter, i.e. /aResource?v=2.1 (btw: that's the way Google does versioning). ... I'm still not sure if I'd go with option 2 or 3, but I will never use 1 again for reasons mentioned above.Administrator
If you want to inject your own RequestMappingHandlerMapping into your WebMvcConfiguration, you should overwrite createRequestMappingHandlerMapping instead of requestMappingHandlerMapping! Otherwise you will encounter weird problems (I suddenly had problems with Hibernates lazy initialization because of a closed session)Axolotl
The approach looks good but somehow seems it doesn't work with junti test cases(SpringRunner). Any chance that you have got the approach working with test casesAmathiste
There is a way to make this work, do not extend WebMvcConfigurationSupport but extend DelegatingWebMvcConfiguration. This worked for me (see #22267691)Clamber
Overriding requestMappingHandlerMapping of WebMvcConfigurationSupport disables spring boot's auto configuration, hence things get really messy. DO NOT DO IT! You can override WebMvcRegistrationsAdapter: stackoverflow.com/a/45340695Inhibition
NOTE : THIS VERSION WILL NOT BE SHOWN IN SWAGGER FILE.Coltson
This no longer works, when trying to define two methods with the same RequestMapping URI, an Caused by: java.lang.IllegalStateException: Ambiguous mapping. exception is thrownMidland
OK that might explain my surprise reading the code above, because actually in the version of org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping I found in spring-webmvc-5.1.9.RELEASE.jar both methods getCustomMethodCondition() and getCustomTypeCondition() return null, so I wonder what's the purpose of calling them...?!Koehler
D
22

I have implemented a solution which handles PERFECTLY the problem with rest versioning.

General Speaking there are 3 major approaches for rest versioning:

  • Path-based approch, in which the client defines the version in URL:

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

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

The problem with the first approach is that if you change the version let's say from v1 -> v2, probably you need to copy-paste the v1 resources that haven't changed to v2 path

The problem with the second approach is that some tools like http://swagger.io/ cannot distinct between operations with same path but different Content-Type (check issue https://github.com/OAI/OpenAPI-Specification/issues/146)

The solution

Since i am working a lot with rest documentation tools, i prefer to use the first approach. My solution handles the problem with the first approach, so you don't need to copy-paste the endpoint to the new version.

Let's say we have v1 and v2 versions 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";
    }
 }

The requirement is if i request the v1 for the user resource i have to take the "User V1" repsonse, otherwise if i request the v2, v3 and so on i have to take the "User V2" response.

enter image description here

In order to implement this in spring, we need to override the default RequestMappingHandlerMapping behavior:

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 asks from spring to resolve the URL .In case this URL does not exists (for example the client requested v3) then we try with v2 and so one until we find the most recent version for the resource.

In order to see the benefits from 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 a change in company "contract" that breaks the client. So we implement the http://localhost:9001/api/v2/company and we ask from client to change to v2 instead on v1.

So the new requests from client are:

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

instead of:

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

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

Rest Documentation As i said before the reason i select the URL-based versioning approach is that some tools like swagger do not document differently the endpoints with the same URL but different content type. With this solution, both endpoints are displayed since have different URL:

enter image description here

GIT

Solution implementation at: https://github.com/mspapant/restVersioningExample/

Derose answered 21/8, 2016 at 16:0 Comment(4)
I'd love feedback from people that have tried this solution. :)Planchet
Watch out: If you send a request with /v0 or /v-1 a infinite loop happens. To fix it you need to set if (previousVersion != 0) { to >0 and you need to catch the exception in getPreviousVersion() and return -1Smolensk
With the latest Spring version 2.4.2 you need to adjust creating the new request to:gist.github.com/eisenreich/6ab40616a9e694bc2220c68ec3a01455Smolensk
github.com/OAI/OpenAPI-Specification/issues/146 is solved in the OpenAPI 3.x specFinancial
P
18

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.

Pallid answered 29/11, 2013 at 5:12 Comment(0)
F
9

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

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

Fizgig answered 1/12, 2013 at 8:25 Comment(2)
I know about that, but as you noted, on each version I would need to go to all my controllers and add a version, even if they haven't changed. The range that you mentioned only works on the full type, for example application/* and not parts of the type. For example the following is invalid in Spring "Accept=application/vnd.company.app-1.*+json". This is related to how the spring class MediaType worksAggregate
@Aggregate you don't necessarily need to do this. With this approach, you are not versioning the "API" but "Endpoint". Each endpoint can have a different version. For me it's the easiest way to version the API compared with API version. Swagger is also more simple to setup. This strategy is called Versioning through content negotiation.Leroy
L
7

I already tried to version my API using the URI Versioning, like:

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

But there are some challenges when trying to make this work: how organize your code with different versions? How manage two (or more) versions at the same time? What's the impact when removing some version?

The best alternative that I found was not version the entire API, but control the version on each endpoint. This pattern is called Versioning using Accept header or Versioning through content negotiation:

This approach allows us to version a single resource representation instead of versioning the entire API which gives us a more granular control over versioning. It also creates a smaller footprint in the code base as we don’t have to fork the entire application when creating a new version. Another advantage of this approach is that it doesn’t require implementing URI routing rules introduced by versioning through the URI path.

Implementation on Spring

First, you create a Controller with a produces attribute, that will applied by default on each endpoint inside the same class.

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

}

After that, we can imagine a possible scenario where you have two versions (v1 and v2) of an endpoint for "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 desired Http Header version:

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

Or, to call the v2:

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

About your worries:

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

As explained, this strategy maintains each Controller and endpoint with his actual version. You only modify the endpoint that have modifications and needs a new version.

And the Swagger?

Setup the Swagger with different versions is also very easy using this strategy. See this answer to more details.

Leroy answered 4/4, 2019 at 20:9 Comment(0)
O
4

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?

Oviduct answered 5/2, 2016 at 22:58 Comment(2)
+1 for sharing the code. However, inheritance tightly couples it. Instead. the Controllers(Test1 and Test2) should be just a pass through ...no logic implemenation. Everything logic should be in service class , someService. In that case, just use simple composition and never inherit from the other controllerSelsyn
@dan-hunex It seems Ceekay use the inheritance to manage the different versions of the api. If you remove inheritance, what is the solution ? And why tightly couple is a problem in this example ? From my point of view, Test2 extends Test1 because it's an improvement of it (with same role and same responsibilities), isn't it ?Glutenous
T
3

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!

Tim answered 27/11, 2013 at 23:7 Comment(1)
Thanks codesalsa, I'm trying to find a way that is easy to maintain and doesn't require some to update each endpoint every time we need to bump the version.Aggregate
S
0

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/

Spectroheliograph answered 4/12, 2013 at 15:56 Comment(2)
Thanks hevi, I'm looking for a more "spring" friendly way of doing this, as Spring already selects which method to call without using AOP. I'm my view AOP adds a new level of code complexity that I would like to avoid.Aggregate
@Augusto, Spring has a great AOP support. You should try it out. :)Theomorphic
F
0

Just wanted to share my Kotlin / Spring 5.3.x implementation of URL versioning, in case someone finds it useful.

I have an Annotation class defining start and end version. If the end version is not set, it's only valid for a single version.

const val VERSION_PREFIX = "v"

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class VersionedResource(
    val version: Int,
    val toVersion: Int = 0
)

fun VersionedResource.validate() {
    /* Do whatever validation you need */
}

fun VersionedResource.getRange(): IntRange {
    validate()
    return if (toVersion == 0) version..version
    else version..toVersion
}

This is then utilized by a custom RequestMappingHandlerMapping like this:

class VersionedRequestHandlerMapping : RequestMappingHandlerMapping() {

companion object {
    val logger: Logger = LoggerFactory.getLogger(this::class.java)
}

override fun getMappingForMethod(method: Method, handlerType: Class<*>): RequestMappingInfo? {
    val mappingInfo = super.getMappingForMethod(method, handlerType) ?: return null
    val versions = getAndValidateAnnotatedVersions(method, handlerType) ?: return mappingInfo

    val versionedPatterns = mutableSetOf<String>()
    mappingInfo.patternsCondition?.apply {
        patterns.forEach { path ->
            versions.forEach { version ->
                "/$VERSION_PREFIX$version$path"
                    .apply { versionedPatterns.add(this) }
                    .also { logger.debug("Generated versioned request-mapping: '$it'") }
            }
        }
    } ?: throw IllegalStateException(
        "Cannot create versioned request mapping patterns when there are no patterns from before."
    )

    return buildRequestMappingWithNewPaths(versionedPatterns, mappingInfo)
}

private fun getAndValidateAnnotatedVersions(
    method: Method,
    handlerType: Class<*>
): IntRange? {
    return (AnnotationUtils.findAnnotation(method, VersionedResource::class.java) // Prioritizes method before class
        ?: AnnotationUtils.findAnnotation(handlerType, VersionedResource::class.java))
        ?.run { getRange() }
}

private fun buildRequestMappingWithNewPaths(
    versionedPatterns: Set<String>,
    mappingInfo: RequestMappingInfo
): RequestMappingInfo {
    return RequestMappingInfo
        .paths(*versionedPatterns.toTypedArray())
        .methods(*mappingInfo.methodsCondition.methods.toTypedArray())
        .params(*mappingInfo.paramsCondition.expressions.map { it.toString() }.toTypedArray())
        .headers(*mappingInfo.headersCondition.expressions.map { it.toString() }.toTypedArray())
        .consumes(*mappingInfo.consumesCondition.expressions.map { it.toString() }.toTypedArray())
        .produces(*mappingInfo.producesCondition.expressions.map { it.toString() }.toTypedArray())
        .apply { mappingInfo.name?.let { mappingName(it) } }
        .build()
}
}

Config looks like this.

@Configuration
class VersionedResourceConfig : DelegatingWebMvcConfiguration() {

    @Autowired
    private lateinit var context: ApplicationContext

    @Autowired
    private lateinit var jacksonObjectMapper: ObjectMapper

    override fun createRequestMappingHandlerMapping(): RequestMappingHandlerMapping {
        return VersionedRequestHandlerMapping().apply {
            applicationContext = context
            setRemoveSemicolonContent(false)
            setDetectHandlerMethodsInAncestorContexts(true)
        }
    }

    // For some reason I needed to add this, since it was being overridden 
    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>?>) {
        converters.add(MappingJackson2HttpMessageConverter(jacksonObjectMapper))
        super.configureMessageConverters(converters)
    }
}

Now I'm able to define a versioned resource like this:

@RestController
@VersionedResource(version = 1, toVersion =  2)
@RequestMapping(path = ["/some/resource"]) // Should handle multiple paths as well
class SomeResource() {

    @GetMapping
    fun getSomething() {
        // Will be mapped to /v1/some/resource and /v2/some/resource
    }

    @VersionedResource(2)
    @GetMapping("stuff")
    fun getSomethingElse() {
        // Will be mapped only to /v2/some/resource/stuff (overrides class spec)
    }
}
Frenzy answered 28/3, 2023 at 16:43 Comment(1)
Just to share that you can drop Spring with Kotlin. There's no need of all the magic it adds. For kotlin there are far better alternatives that were created with testing at the core. Please check http4k. In a more related topic, over time I found that adding the version to the URL was a lot simpler for developers to understand.Aggregate

© 2022 - 2024 — McMap. All rights reserved.