The SpringBoot application implements backend interface version support

As a primary backend developer, one of the most annoying things to do in your daily work is to support versioning of the parameter validation and interface. For students on the client side, the historical burden of business will be much smaller. When incompatible business changes occur, it is better to develop new ones directly. However, the backend is not so simple, the historical interface has to support, the new business also have to support, and a new service interface, but the URL can not be the same as the previous, how to do? You can only add something like v1, v2…

So is there a way to support versioning in some other way without changing urls?

This article introduces an example case that uses the request header to pass the client version, finding the interface that best suits the version request in the same URL

The main knowledge points used are:

  • RequestCondition
  • RequestMappingHandlerMapping

I. Application scenarios

We want the same business to always use the same URL, even if the business is completely incompatible between different versions, and respond to the request by selecting the most appropriate back-end interface for the version in the request parameters

1. Agreed

To implement the case above, there are two conventions

  • Version parameters must be carried with each request
  • Each interface is defined with a supported version

Rules of 2.

With these two premises identified, the basic rules are in place

Version definition

According to the common three-stage version design, the version format is defined as follows

x.x.x
Copy the code
  • The first x: corresponds to the major version, which generally only changes when major changes are made
  • Where the second x: represents the normal business iteration version number, +1 for each regular app update released
  • The last x is for Bugfixes, for example, when an app is released and an exception occurs, an emergency fix is required, and another version needs to be released, the value can be +1

Interface to select

Usually web requests are based on URL matching rules to select the corresponding response interface, but here, a URL, there may be multiple different interfaces, how to choose?

  • First, get the version parameter version from the request
  • Find all interfaces that are less than or equal to version from all the same URL interfaces based on the version defined on the interface
  • Of the interfaces that meet the criteria above, select the interface with the largest version to respond to the request

II. Application implementation

Once the above application scenarios are clear, design and implement them

1. Interface definition

First we need a version-defined annotation to mark the version of the Web service interface, preferably 1.0.0 by default

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Api {

    /** * Version **@return* /
    String value(a) default "1.0.0";
}
Copy the code

Second, a version of the entity class is required. Note that in the implementation below, the default version is 1.0.0 and the Comparable interface is implemented to support comparisons between versions

@Data
public class ApiItem implements Comparable<ApiItem> {

    private int high = 1;

    private int mid = 0;

    private int low = 0;

    public ApiItem(a) {}@Override
    public int compareTo(ApiItem right) {
        if (this.getHigh() > right.getHigh()) {
            return 1;
        } else if (this.getHigh() < right.getHigh()) {
            return -1;
        }

        if (this.getMid() > right.getMid()) {
            return 1;
        } else if (this.getMid() < right.getMid()) {
            return -1;
        }

        if (this.getLow() > right.getLow()) {
            return 1;
        } else if (this.getLow() < right.getLow()) {
            return -1;
        }
        return 0; }}Copy the code

You need a conversion class that converts the string version to ApiItem and supports the default version 1.0.0

public class ApiConverter {
    public static ApiItem convert(String api) {
        ApiItem apiItem = new ApiItem();
        if (StringUtils.isBlank(api)) {
            return apiItem;
        }

        String[] cells = StringUtils.split(api, ".");
        apiItem.setHigh(Integer.parseInt(cells[0]));
        if (cells.length > 1) {
            apiItem.setMid(Integer.parseInt(cells[1]));
        }

        if (cells.length > 2) {
            apiItem.setLow(Integer.parseInt(cells[2]));
        }
        returnapiItem; }}Copy the code

2. Select the HandlerMapping interface

You need a URL that supports multiple request interfaces. Consider RequestCondition. Here are the implementation classes

public class ApiCondition implements RequestCondition<ApiCondition> {

    private ApiItem version;

    public ApiCondition(ApiItem version) {
        this.version = version;
    }

    @Override
    public ApiCondition combine(ApiCondition other) {
        // Select the interface with the largest version
        return version.compareTo(other.version) >= 0 ? new ApiCondition(version) : new ApiCondition(other.version);
    }

    @Override
    public ApiCondition getMatchingCondition(HttpServletRequest request) {
        String version = request.getHeader("x-api");
        ApiItem item = ApiConverter.convert(version);
        // Get all interfaces that are less than or equal to version
        if (item.compareTo(this.version) >= 0) {
            return this;
        }

        return null;
    }

    @Override
    public int compareTo(ApiCondition other, HttpServletRequest request) {
        // Get the interface corresponding to the maximum version
        return other.version.compareTo(this.version); }}Copy the code

Although the above implementation is relatively simple, it is important to note two pieces of logic

  • getMatchingConditionMethod controls that ApiCondition satisfies the rule only if its version is less than or equal to the version in the request parameter
  • compareToSpecifies when there are multipleApiCoonditionWhen this request is satisfied, select the largest version

Custom RequestMappingHandlerMapping ApiHandlerMapping implementation class

public class ApiHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protectedRequestCondition<? > getCustomTypeCondition(Class<? > handlerType) {return buildFrom(AnnotationUtils.findAnnotation(handlerType, Api.class));
    }

    @Override
    protectedRequestCondition<? > getCustomMethodCondition(Method method) {return buildFrom(AnnotationUtils.findAnnotation(method, Api.class));
    }

    private ApiCondition buildFrom(Api platform) {
        return platform == null ? new ApiCondition(new ApiItem()) :
                newApiCondition(ApiConverter.convert(platform.value())); }}Copy the code

registered

@Configuration
public class ApiAutoConfiguration implements WebMvcRegistrations {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping(a) {
        return newApiHandlerMapping(); }}Copy the code

Based on this, a micro-framework for interface versioning has been completed. Next comes the test

III. The test

Case1. Add the version to the case1

Design three interfaces, one without annotations, and the other two with different versions of annotations

@RestController
@RequestMapping(path = "v1")
public class V1Rest {

    @GetMapping(path = "show")
    public String show1(a) {
        return "V1 / show 1.0.0";
    }

    @Api(1.1.2 "")
    @GetMapping(path = "show")
    public String show2(a) {
        return "V1 / show 1.1.2." ";
    }

    @Api("1.1.0")
    @GetMapping(path = "show")
    public String show3(a) {
        return "V1 / show 1.1.0." "; }}Copy the code

When the request is initiated, the corresponding response is tested with the specified version and no version

  • As you can see from the screenshot above, when there is no version in the request header, one is given by default1.0.0The version of the
  • The largest version of the interface that responds is smaller than the requested version

Case2. Class version + method version

Adding a version to each method is a bit of a pain. In the annotation definition above, class annotations are supported. As you can see from the implementation, choose the largest version when both methods and classes have annotations

@Api("2.0.0")
@RestController
@RequestMapping(path = "v2")
public class V2Rest {

    @Api("1.1.0")
    @GetMapping(path = "show")
    public String show0(a) {
        return "V2 / show0 1.1.0." ";
    }

    @GetMapping(path = "show")
    public String show1(a) {
        return "V2 / show1 2.0.0." ";
    }

    @Api(2.1.1 "")
    @GetMapping(path = "show")
    public String show2(a) {
        return "V2 / show2 2.1.1";
    }

    @Api("2.2.0")
    @GetMapping(path = "show")
    public String show3(a) {
        return "V2 / show3 2.2.0." "; }}Copy the code

According to our implementation rules, both show0 and show1 will respond to a <2.1.1 version request.

  • From the screenshot above, you can see that a 404 error is reported for a request with version less than 2.0.0
  • Conflicting exceptions are reported if the request version is less than 2.1.1

IV. The other

0. Projects & Related posts

  • Project: github.com/liuyueyi/sp…
  • Source: github.com/liuyueyi/sp…

Related blog

  • RequestCondition custom request matching condition in SpringBoot tutorial Web

1. An ashy Blog

As far as the letter is not as good, the above content is purely one’s opinion, due to the limited personal ability, it is inevitable that there are omissions and mistakes, if you find bugs or have better suggestions, welcome criticism and correction, don’t hesitate to appreciate

Below a gray personal blog, record all the study and work of the blog, welcome everyone to go to stroll

  • A grey Blog Personal Blog blog.hhui.top
  • A Grey Blog-Spring feature Blog Spring.hhui.top