REST web service versioning in practice

2019-03-26 06:51发布

问题:

I am creating a new web service and I have read some of the ebooks from APIgee where versioning the web service is a recommended. I understand there is some "battle" between keeping versioning info in the URL vs. the header. From what I have read and understand I want to use versioning in the header.

My question is; how does this looks like in practice? I am using Spring MVC 3.2. Do you just create a methods like this in the same controller which responds to different versions?

Version 1:

@RequestMapping(method = RequestMethod.GET, produces = "application/vnd.example-v1+json")

Version 2:

@RequestMapping(method = RequestMethod.GET, produces = "application/vnd.example-v2+json")

Or is this wrong? Or is it more usual to create different packages that holds different versions of the controller? Or are there other ways?

回答1:

The issue here is less about where the version information lives (URI vs header) and more about how you organize code for different versions.

I doubt there's a single standard approach. It just depends on how different the versions are.

Simple format change. Suppose for example that the only difference was that you moved from XML in V1 to JSON in V2. In that case you could use exactly the same code, but just configure the app to output JSON globally instead. No need for different packages or controllers. (For example you can use JAXB annotations to drive both XML and Jackson-generated JSON output.)

Modest schema changes. Say that V2 introduces a small number of breaking schema changes. In this case it probably wouldn't make sense to create new packages over it. You might just have simple conditional logic in your controller to process/serve the right representation for the version.

Major schema changes. If your schema changes are deep and far-ranging, you might need more than separate controllers. You might even need a different domain model (entities/services). In this case it may well make sense to have a parallel set of packages for controllers all the way down to the entities, repos and maybe even database tables.

Applying the ideas

Approach 1. Applying these ideas your @RequestMapping examples, you could do what you say there, but if the response is exactly the same between versions, then they should just delegate to a single shared method:

@RequestMapping(
    value = "/orders/{id}",
    method = RequestMethod.GET,
    produces = "application/vnd.example-v1")
@ResponseBody
public Order getOrderV1(@PathVariable("id") Long id) {
    return getOrder(id);
}

@RequestMapping(
    value = "/orders/{id}",
    method = RequestMethod.GET,
    produces = "application/vnd.example-v2")
@ResponseBody
public Order getOrderV2(@PathVariable("id") Long id) {
    return getOrder(id);
}

private Order getOrder(Long id) {
    return orderRepo.findOne(id);
}

Something like that would work. If the orders are different between versions then you can implement the differences right in the method.

Approach 2. Another thing you might try--and I haven't myself tried this--is each resource type (e.g., orders, products, customers, etc.) having its own base controller with method-level annotations for the HTTP method (just value and method defined, but not produces). Then use version-specific extensions that extend the base, where the extension controllers have the @RequestMapping(value = "/orders", produces = "application/vnd.example-v1") at the class level. Then override only deltas between the version and the baseline. I'm not sure whether this will work but if so it would be a pretty clean way to organize controllers. Here's what I mean:

// The baseline
public abstract class BaseOrderController {

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @ResponseBody
    public Order getOrder(@PathVariable("id") Long id) { ... }
}    

// V1 controller
@RequestMapping(value = "/orders", produces = "application/vnd.example-v1")
public class OrderControllerV1 extends BaseOrderController {

    ... no difference from baseline, so nothing to implement ...
}

// V2 controller
@RequestMapping(value = "/orders", produces = "application/vnd.example-v2")
public class OrderControllerV2 extends BaseOrderController {

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @ResponseBody
    @Override
    public Order getOrder(@PathVariable("id") Long id) {
        return orderRepoV2.findOne(id);
    }
}