How to Implement Interface Versioning in SpringBoot

  • 2021-11-24 01:44:50
  • OfStack

Directory SpringBoot Interface Version Control Custom 1 Version Number Annotation Interface ApiVersion. java Version Number Filter ApiVersionCondition Version Number Matching Interceptor Configuration WebMvcRegistrationsConfigSpringBoot 2. x Interface Multiversion 1. Custom Interface Version Annotation ApiVersion2. Request Mapping Condition ApiVersionCondition3. Create Custom Match Processor ApiVersionRequestMappingHandlerMapping4. Use ApiVersionConfig Configuration to Decide whether to Turn on Multiversion 5. Configure WebMvcRegistrations, Enable Custom Routing Mapping

SpringBoot Interface Versioning

A system will be iteratively updated after it goes online, Requirements will also change constantly, and it is possible that the parameters of the interface will also change. If the original parameters are directly modified, it may affect the normal operation of the existing project. At this time, we need to set different versions, so that even if the parameters change, the old version will not affect the operation of the online system.

Here, we choose to use a floating-point number with one decimal place as the version number, and put the version number at the end of the request address. The approximate address is http://api/test/1. 0, where 1.0 represents the version number. Please see the code for specific methods

Customize the annotation interface ApiVersion. java with 1 version number


import org.springframework.web.bind.annotation.Mapping;
import java.lang.annotation.*;
/**
 *  Version control 
 * @author Zac
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    /**
     *  Identify version number 
     * @return
     */
    double value();
}

Version number filter ApiVersionCondition


import org.springframework.web.servlet.mvc.condition.RequestCondition;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern; 
/**
 *  Version number matching filter 
 * @author Zac
 */
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
 
    /**
     *  The regular expression of the version in the path matches,   Used here  /1.0 Form of 
     */
    private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("^\\S+/([1-9][.][0-9])$");
    private double apiVersion;
     public ApiVersionCondition(double apiVersion) {
        this.apiVersion = apiVersion;
    }
 
    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        //  If the last definition precedence principle is adopted, the definition on the method overrides the definition above the class 
        return new ApiVersionCondition(other.getApiVersion());
    }
 
    @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
         Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
        if (m.find()) {
            Double version = Double.valueOf(m.group(1));
            //  If the requested version number is greater than the configured version number,   Is satisfied 
            if (version >= this.apiVersion) {
                return this;
            }
        }
        return null;
    }
 
    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        //  Preferably match the latest version number 
        return Double.compare(other.getApiVersion(), this.apiVersion);
    } 
    public double getApiVersion() {
        return apiVersion; 
    }
}

Version number matching interceptor


import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
 
/**
 *  Version number matching interceptor 
 * @author Zac
 */
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
     @Override protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return createCondition(apiVersion);
    }
 
    @Override protected RequestCondition<ApiVersionCondition> getCustomMethodCondition(Method method) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return createCondition(apiVersion);
    } 
    private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) {
        return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
    }  
}

Configuring WebMvcRegistrationsConfig


import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcRegistrationsAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
 
@SpringBootConfiguration
public class WebMvcRegistrationsConfig extends WebMvcRegistrationsAdapter {
 
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new CustomRequestMappingHandlerMapping();
    }  
}
controller Layer implementation 
/**
     * version
     * @return
     */
    @GetMapping("/api/test/{version}")
    @ApiVersion(2.0)
    public String searchTargetImage() {
        return "Hello! Welcome to Version2";
    }  
/**
     *  Multi-target type search and associated picture query interface 
     *
     * @param attribute
     * @return
     */
    @GetMapping("/api/test/{version}")
    @ApiVersion(1.0)
    public AppResult searchTargetImage() {
        return "Hello! Welcome to Version1";
    }

SpringBoot 2. x Interface Multiversion

Prepare to add version management to the existing interface to be compatible with previous versions. Online 1 research found that there are many examples, but there are still the following two problems.

1. Most use Integer as the version number, but the usual version number is v 1.0. 0,

2. The version number is carried in header, and the docking call is unclear.

In view of the above two problems, do the following transformation.

1. Custom interface version annotation ApiVersion

Later conditional mappings use equals matching, and whether to change String to String [] here addresses the problem of multiple versions using the same 1 code.


package com.yugioh.api.common.core.version;
import org.springframework.web.bind.annotation.Mapping;
import java.lang.annotation.*;
/**
 *  Interface version 
 *
 * @author lieber
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {  
    String value() default "1.0.0";
}

2. Request mapping condition ApiVersionCondition


package com.yugioh.api.common.core.version;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 *  Version control 
 *
 * @author lieber
 */
@AllArgsConstructor
@Getter
@Slf4j
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
    private String version;
    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+(.\\d+){0,2}).*");
    public final static String API_VERSION_CONDITION_NULL_KEY = "API_VERSION_CONDITION_NULL_KEY";
    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        //  Annotations on methods are better than annotations on classes 
        return new ApiVersionCondition(other.getVersion());
    }
    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
        if (m.find()) {
            String version = m.group(1);
            if (this.compareTo(version)) {
                return this;
            }
        }
        //  Put the error in the request You can explicitly prompt on the error page, where you can reconfigure to throw a runtime exception 
        request.setAttribute(API_VERSION_CONDITION_NULL_KEY, true);
        return null;
    }
    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        return this.compareTo(other.getVersion()) ? 1 : -1;
    }
    private boolean compareTo(String version) {
        return Objects.equals(version, this.version);
    }
}

3. Create a custom match processor ApiVersionRequestMappingHandlerMapping

Most of the network only rewrites getCustomTypeCondition and getCustomMethodCondition methods. In order to solve the problem of route mapping, the method of getMappingForMethod is rewritten and the prefix {version} is added to the route. After adding the prefix, the route becomes/api/{version}/xxx


package com.yugioh.api.common.core.version;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
/**
 *  Custom matching processor 
 *
 * @author lieber
 */
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    private final static String VERSION_PREFIX = "{version}";
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return createCondition(apiVersion);
    }
    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return createCondition(apiVersion);
    }
    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo requestMappingInfo = super.getMappingForMethod(method, handlerType);
        if (requestMappingInfo == null) {
            return null;
        }
        return createCustomRequestMappingInfo(method, handlerType, requestMappingInfo);
    }
    private RequestMappingInfo createCustomRequestMappingInfo(Method method, Class<?> handlerType, RequestMappingInfo requestMappingInfo) {
        ApiVersion methodApi = AnnotatedElementUtils.findMergedAnnotation(method, ApiVersion.class);
        ApiVersion handlerApi = AnnotatedElementUtils.findMergedAnnotation(handlerType, ApiVersion.class);
        if (methodApi != null || handlerApi != null) {
            return RequestMappingInfo.paths(VERSION_PREFIX).options(this.config).build().combine(requestMappingInfo);
        }
        return requestMappingInfo;
    }
    private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
    private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) {
        return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
    }
}

4. Use ApiVersionConfig configuration to decide whether to turn on multiple versions


package com.yugioh.api.common.core.version;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
 *  Version Manager Configuration 
 *
 * @author lieber
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "api.config.version", ignoreInvalidFields = true)
public class ApiVersionConfig {
    /**
     *  Whether to turn it on 
     */
    private boolean enable;
}

5. Configure WebMvcRegistrations and enable custom routing Mapping


package com.yugioh.api.common.core.version;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
 * @author lieber
 */
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ApiWebMvcRegistrations implements WebMvcRegistrations {
    private final ApiVersionConfig apiVersionConfig;
    @Autowired
    public ApiWebMvcRegistrations(ApiVersionConfig apiVersionConfig) {
        this.apiVersionConfig = apiVersionConfig;
    }
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        if (apiVersionConfig.isEnable()) {
            return new ApiVersionRequestMappingHandlerMapping();
        }
        return null;
    }
}

At this point, we can happily use @ APIVersion to specify the version in the project, but now this integration with swagger will have problems, and we will continue to study …


Related articles: