SpringBoot Interface - How to provide multiple versions of the interface

Posted by eddedwards on Mon, 03 Jan 2022 01:48:40 +0100

When developing Restful interfaces with SpringBoot, different versions of parameter implementations are required for the same interface due to changes in business such as modules and systems (older interfaces also have modules or systems in use and cannot be changed directly, so different versions are required). How do you implement multiversion interfaces more elegantly?

Why do multiple versions of interfaces occur?

Why do multiple versions of interfaces occur?

In general, the Restful API interface is provided for use by other modules, systems, or other companies and cannot be changed freely and frequently. However, as demand and business change, interfaces and parameters change accordingly. If the original interface is modified directly, it will affect the normal operation of other systems on the line. This requires effective version control of the api interface.

What are the ways to control multiversions of interfaces?

  • Same URL, distinguished by different version parameters

    • api.pdai.tech/user?version=v1 represents the V1 version of the interface, leaving the original interface intact
    • api.pdai.tech/user?version=v2 represents the V2 version of the interface, updating the new interface
  • Differentiate between different interface domain names, different versions have different subdomains, and route to different instances:

    • v1.api.pdai.tech/user represents the v1 version of the interface, leaving the original interface intact and routing to instance1
    • v2.api.pdai.tech/user represents v2 version interface, updates new interface, routes to instance2
  • Gateway routes different subdirectories to different instances (different package s can also)

    • api.pdai.tech/v1/user represents the V1 version of the interface, leaving the original interface intact and routing to instance1
    • api.pdai.tech/v2/user represents the V2 version of the interface, updates the new interface, and routes to instance2
  • Same instance, separate different versions with annotations

    • api.pdai.tech/v1/user represents the V1 version of the interface, leaving the original interface intact, matching the handlerMapping of @ApiVersion("1")
    • api.pdai.tech/v2/user represents the V2 version of the interface, updates the new interface, and matches the handlerMapping of @ApiVersion("2")

This is the version that shows how elegant the control interface is in the fourth single instance.

Realization case

This example encapsulates the @ApiVersion annotated control interface version based on SpringBoot.

Customize the @ApiVersion comment

package tech.pdai.springboot.api.version.config.version;

import org.springframework.web.bind.annotation.Mapping;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    String value();
}

Define Version Matching RequestCondition

Version matching supports three-tier versions

  • v1.1.1 (large version, small version, patch version)
  • V1. 1 (equivalent to v1.1.0)
  • V1 (equivalent to v1.0.0)
package tech.pdai.springboot.api.version.config.version;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.condition.RequestCondition;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {

    /**
     * support v1.1.1, v1.1, v1; three levels .
     */
    private static final Pattern VERSION_PREFIX_PATTERN_1 = Pattern.compile("/v\\d\\.\\d\\.\\d/");
    private static final Pattern VERSION_PREFIX_PATTERN_2 = Pattern.compile("/v\\d\\.\\d/");
    private static final Pattern VERSION_PREFIX_PATTERN_3 = Pattern.compile("/v\\d/");
    private static final List<Pattern> VERSION_LIST = Collections.unmodifiableList(
            Arrays.asList(VERSION_PREFIX_PATTERN_1, VERSION_PREFIX_PATTERN_2, VERSION_PREFIX_PATTERN_3)
    );

    @Getter
    private final String apiVersion;

    public ApiVersionCondition(String apiVersion) {
        this.apiVersion = apiVersion;
    }

    /**
     * method priority is higher then class.
     *
     * @param other other
     * @return ApiVersionCondition
     */
    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        return new ApiVersionCondition(other.apiVersion);
    }

    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        for (int vIndex = 0; vIndex < VERSION_LIST.size(); vIndex++) {
            Matcher m = VERSION_LIST.get(vIndex).matcher(request.getRequestURI());
            if (m.find()) {
                String version = m.group(0).replace("/v", "").replace("/", "");
                if (vIndex == 1) {
                    version = version + ".0";
                } else if (vIndex == 2) {
                    version = version + ".0.0";
                }
                if (compareVersion(version, this.apiVersion) >= 0) {
                    log.info("version={}, apiVersion={}", version, this.apiVersion);
                    return this;
                }
            }
        }
        return null;
    }

    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        return compareVersion(other.getApiVersion(), this.apiVersion);
    }

    private int compareVersion(String version1, String version2) {
        if (version1 == null || version2 == null) {
            throw new RuntimeException("compareVersion error:illegal params.");
        }
        String[] versionArray1 = version1.split("\\.");
        String[] versionArray2 = version2.split("\\.");
        int idx = 0;
        int minLength = Math.min(versionArray1.length, versionArray2.length);
        int diff = 0;
        while (idx < minLength
                && (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0
                && (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {
            ++idx;
        }
        diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
        return diff;
    }
}

Define HandlerMapping

package tech.pdai.springboot.api.version.config.version;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;

public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    /**
     * add @ApiVersion to controller class.
     *
     * @param handlerType handlerType
     * @return RequestCondition
     */
    @Override
    protected RequestCondition<?> getCustomTypeCondition(@NonNull Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return null == apiVersion ? super.getCustomTypeCondition(handlerType) : new ApiVersionCondition(apiVersion.value());
    }

    /**
     * add @ApiVersion to controller method.
     *
     * @param method method
     * @return RequestCondition
     */
    @Override
    protected RequestCondition<?> getCustomMethodCondition(@NonNull Method method) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return null == apiVersion ? super.getCustomMethodCondition(method) : new ApiVersionCondition(apiVersion.value());
    }

}

Configure Register HandlerMapping

package tech.pdai.springboot.api.version.config.version;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

@Configuration
public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {

    @Override
    public RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping();
    }
}

Or implement the interface for WebMvcRegistrations

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
    //...

    @Override
    @NonNull
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping();
    }

}

test run

controller

package tech.pdai.springboot.api.version.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.pdai.springboot.api.version.config.version.ApiVersion;
import tech.pdai.springboot.api.version.entity.User;

/**
 * @author pdai
 */
@RestController
@RequestMapping("api/{v}/user")
public class UserController {

    @RequestMapping("get")
    public User getUser() {
        return User.builder().age(18).name("pdai, default").build();
    }

    @ApiVersion("1.0.0")
    @RequestMapping("get")
    public User getUserV1() {
        return User.builder().age(18).name("pdai, v1.0.0").build();
    }

    @ApiVersion("1.1.0")
    @RequestMapping("get")
    public User getUserV11() {
        return User.builder().age(19).name("pdai, v1.1.0").build();
    }

    @ApiVersion("1.1.2")
    @RequestMapping("get")
    public User getUserV112() {
        return User.builder().age(19).name("pdai2, v1.1.2").build();
    }
}

output

http://localhost:8080/api/v1/user/get
// {"name":"pdai, v1.0.0","age":18}

http://localhost:8080/api/v1.1/user/get
// {"name":"pdai, v1.1.0","age":19}

http://localhost:8080/api/v1.1.1/user/get
// {"name":"pdai, v1.1.0","age":19} matches the largest version number with a minimum ratio of 1.1.1

http://localhost:8080/api/v1.1.2/user/get
// {"name":"pdai2, v1.1.2","age":19}

http://localhost:8080/api/v1.2/user/get
// {name":"pdai2, v1.1.2","age":19} matches the largest version number, v1. 1.2

In this way, if we provide V1 version of the interface to another module, only one interface method has been changed in the new requirements, at this time we only need to add one interface to add version number v1.1 for v1. Version 1 accesses all interfaces.

In addition, this may result in the V3 version of the interface not being published, but it can be accessed through v3; In this case, you can add some logic to limit versions, such as maximum versions, version collections, and so on.

Sample Source

https://github.dev/realpdai/tech-pdai-spring-demos

Copyright Ownership https://pdai.tech All. Links: SpringBoot Interface - How to Provide Multiple Version Interfaces | Java Full Stack Knowledge System

Topics: Java Spring Spring Boot