在API系统设计,特别是有android或者ios移动客户端的系统设计过程中,当业务发生比较大的变动的时候,就会出现一个问题:我们为了使客户端的新旧版本(客户端有的用户可能不会主动升级版本)能准确的访问api接口并得到准确的数据,我们就不得不在该api接口实现中写代码做兼容。这样的话,随着业务的不断调整,整个api接口实现将变得臃肿不堪,同时bug不断,导致不能适用各个版本客户端的请求。
因此,对api接口做版本迭代,让接口实现变得简单、易于维护、减少bug就显得十分必要了。

通常,restful-api的版本迭代实现方式主要又两种:

  • 在url中显示设置,如:https://api.example.com/v1/
  • 在http请求头中添加,如:

    设置请求头:
    Content-Version: 1
    
    请求:
    https://api.example.com
    

首先,建立spring-boot-web项目:

在请求头中设置

1.创建注解类ApiVersion
在controller中添加注解标志api版本

package com.ymu.demo.springboot2apiversion.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 {
    int value();
}

2.创建处理类ApiVersionCondition
继承RequestCondition,每次url请求都会首先进入该方法。

package com.ymu.demo.springboot2apiversion.version;

import org.springframework.web.servlet.mvc.condition.RequestCondition;

import javax.servlet.http.HttpServletRequest;
import java.util.regex.Pattern;

public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {

    private int apiVersion;

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

    public ApiVersionCondition combine(ApiVersionCondition other) {
        // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
        return new ApiVersionCondition(other.getApiVersion());
    }

    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        String path = request.getServletPath();
        if (path == null) {
            return null;
        }
        String contentVersion = request.getHeader("Content-Version"); //在http请求头中定义api版本,而不是在url中
        if (null == contentVersion || "".equals(contentVersion)) {
            throw new IllegalArgumentException("Content-Version非null非空");
        }
        if (!isInteger(contentVersion)) {
            throw new IllegalArgumentException("Content-Version必须为整数");
        }

        int version = Integer.valueOf(contentVersion).intValue();
        if(version >= this.apiVersion) { // 如果请求的版本号大于配置版本号, 则满足
            return this;
        }
        return null;
    }

    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        // 优先匹配最新的版本号
        return other.getApiVersion() - this.apiVersion;
    }

    public int getApiVersion() {
        return apiVersion;
    }

    /**
     * 判断字符串是否为整数。
     * @param str
     * @return
     */
    private boolean isInteger(String str) {
        Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
        return pattern.matcher(str).matches();
    }

}

3.自定义url注册回调类CustomRequestMappingHandlerMapping
url注解回调句柄类。继承RequestMappingHandlerMapping。

package com.ymu.demo.springboot2apiversion.version;

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;

public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    /**
     * 类。
     * @param handlerType
     * @return
     */
    @Override
    protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class); 
        return createCondition(apiVersion);
    }

    /**
     * 方法
     * @param method
     * @return
     */
    @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());
    }
}

4.创建web配置类并编辑内容:WebConfig
配置自定义类RequestMappingHandlerMapping。

package com.ymu.demo.springboot2apiversion;

import com.ymu.demo.springboot2apiversion.version.CustomRequestMappingHandlerMapping;
import org.springframework.context.annotation.Bean;
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 WebConfig extends WebMvcConfigurationSupport{

    @Override
    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
        handlerMapping.setOrder(0);
        handlerMapping.setInterceptors(getInterceptors()); 
        return handlerMapping;
    }

}

5.创建演示类HelloController

package com.ymu.demo.springboot2apiversion;

import com.ymu.demo.springboot2apiversion.version.ApiVersion;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping
public class HelloController {

    //---------------- api版本管理 demo start ------------------//

    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    public String hello0(HttpServletRequest request){
        print(request);
        return "hello";
    }

    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    @ApiVersion(1)
    public String hello1(HttpServletRequest request){
        print(request);
        return "hello:v1";
    }


    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    @ApiVersion(5)
    public String hello5(HttpServletRequest request){
        print(request);
        return "hello:v5";
    }

    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    @ApiVersion(2)
    public String hello2(HttpServletRequest request){
        print(request);
        return "hello:v2";
    }

    private void print(HttpServletRequest request) {
        System.out.println("version:" + request.getHeader("Content-Version"));
    }

}    

6.演示:


在url中显示设置

在url中显示设置基本和上面过程一样。只不过是要稍微调整下注册路径,在注册路径中添加版本号。

1.修改CustomRequestMappingHandlerMapping类。
只需要重写方法:

/**
 * 为所有注册路径添加"/{version}"匹配规则。目的,做api版本管理。
 * 不用在每个类或方法的@RequestMapping中加。
 * @param method
 * @param handlerType
 * @return
 */
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
    RequestMappingInfo requestMappingInfo = super.getMappingForMethod(method, handlerType);
    if (requestMappingInfo != null) {
        PatternsRequestCondition pcOri = requestMappingInfo.getPatternsCondition();
        Set<String> s = pcOri.getPatterns();
        StringBuilder pathNew = new StringBuilder("");
        if (s != null && !s.isEmpty()) {
            for (String str: s ) {
                if (!"/error".equals(str)) {
                    pathNew.append("/{version}");
                    pathNew.append(str);
                } else {
                    pathNew.append(str);
                }
            }
        }

        PatternsRequestCondition pcnNew = new PatternsRequestCondition(pathNew.toString());

        RequestMappingInfo requestMappingInfoNew = new RequestMappingInfo(requestMappingInfo.getName(),pcnNew,requestMappingInfo.getMethodsCondition(),requestMappingInfo.getParamsCondition(),requestMappingInfo.getHeadersCondition(),requestMappingInfo.getConsumesCondition(),requestMappingInfo.getProducesCondition(),requestMappingInfo.getCustomCondition());

        return requestMappingInfoNew;

    }
    return requestMappingInfo;
}

-----------------------------------------------------

完整代码如下:    

package com.ymu.framework.spring.mvc.api;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
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;
import java.util.Set;

public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    /**
     * 为所有注册路径添加"/{version}"匹配规则。目的,做api版本管理。
     * 不用在每个类或方法的@RequestMapping中加。
     * @param method
     * @param handlerType
     * @return
     */
    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo requestMappingInfo = super.getMappingForMethod(method, handlerType);
        if (requestMappingInfo != null) {
            PatternsRequestCondition pcOri = requestMappingInfo.getPatternsCondition();
            Set<String> s = pcOri.getPatterns();
            StringBuilder pathNew = new StringBuilder("");
            if (s != null && !s.isEmpty()) {
                for (String str: s ) {
                    if (!"/error".equals(str)) {
                        pathNew.append("/{version}");
                        pathNew.append(str);
                    } else {
                        pathNew.append(str);
                    }
                }
            }

            PatternsRequestCondition pcnNew = new PatternsRequestCondition(pathNew.toString());

            RequestMappingInfo requestMappingInfoNew = new RequestMappingInfo(requestMappingInfo.getName(),pcnNew,requestMappingInfo.getMethodsCondition(),requestMappingInfo.getParamsCondition(),requestMappingInfo.getHeadersCondition(),requestMappingInfo.getConsumesCondition(),requestMappingInfo.getProducesCondition(),requestMappingInfo.getCustomCondition());

            return requestMappingInfoNew;

        }
        return requestMappingInfo;
    }

    @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());
    }
}

2.修改类ApiVersionCondition。
主要修改方法:getMatchingCondition

public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
//        String pathInfo = request.getPathInfo();//这个方法获取是null,报错。
    String path = request.getServletPath(); 
    if (path == null) {
        return null;
    }
    Matcher m = VERSION_PREFIX_PATTERN.matcher(path);//匹配路径
    if(m.find()){
        Integer version = Integer.valueOf(m.group(1));
        if(version >= this.apiVersion) // 如果请求的版本号大于配置版本号, 则满足
            return this;
    }
    return null;
}

-----------------------------------------------------------

完整代码如下:

package com.ymu.framework.spring.mvc.api;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.servlet.mvc.condition.RequestCondition;

public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {

    // 路径中版本的前缀, 这里用 /v[1-9]/的形式
    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");

    private int apiVersion;

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

    public ApiVersionCondition combine(ApiVersionCondition other) {
        // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
        return new ApiVersionCondition(other.getApiVersion());
    }

    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
//        String pathInfo = request.getPathInfo();//这个方法获取是null,报错。
        String path = request.getServletPath(); 
        if (path == null) {
            return null;
        }
        Matcher m = VERSION_PREFIX_PATTERN.matcher(path);//匹配路径
        if(m.find()){
            Integer version = Integer.valueOf(m.group(1));
            if(version >= this.apiVersion) // 如果请求的版本号大于配置版本号, 则满足
                return this;
        }
        return null;
    }

    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        // 优先匹配最新的版本号
        return other.getApiVersion() - this.apiVersion;
    }

    public int getApiVersion() {
        return apiVersion;
    }

}

3.演示: