如何用SpringMVC做api介面版本控制

本文所有程式碼基於 SpringBoot 2。1。9(spring-webmvc-5。1。10。RELEASE)版本進行編寫測試,若版本不一致呢可能會導致部分配置報錯。

如何用SpringMVC做api介面版本控制

目的: 可以將業務介面以版本分開管理,針對於每次版本的迭代更新,只需要新增新的版本介面,從而不會影響仍在使用舊版本介面的使用者體驗,即是請求介面的時候以版本向下匹配,匹配最新的介面,這樣使得系統升級比較平滑,對使用者體驗比較友好。

編寫版本註解

此註解@ApiVersion用於介面註解,標識這個介面是歸屬於哪個版本的。

package org。apiserver。spring。config;

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

import java。lang。annotation。*;

/**

* @author JTJ

*/

@Target({ ElementType。METHOD, ElementType。TYPE })

@Retention(RetentionPolicy。RUNTIME)

@Documented

@Mapping

public @interface ApiVersion {

/**

* @Author: JTJ

* @Date: 2020/09/15 上午 09:20

* @Description: 版本號

*/

int value() default 1;

}

構建一個自定義請求條件

這裡採用了 v[1-9] 這樣的版本請求方式的寫法

package org。apiserver。spring。config;

import lombok。extern。slf4j。Slf4j;

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

import javax。servlet。http。HttpServletRequest;

import java。util。regex。Matcher;

import java。util。regex。Pattern;

/**

* @author JTJ

*/

@Slf4j

public class ApiVersionCondition implements RequestCondition {

/**

* 路徑中版本的字首, 這裡用 /v[1-9]/的形式

*/

private final static Pattern VERSION_PREFIX_PATTERN = Pattern。compile(“v(\\d+)/”);

private int apiVersion;

public ApiVersionCondition(int apiVersion) {

this。apiVersion = apiVersion;

}

@Override

public ApiVersionCondition combine(ApiVersionCondition other) {

// 採用最後定義優先原則,則方法上的定義覆蓋類上面的定義

log。info(“combine方法({})”,other);

return new ApiVersionCondition(other。getApiVersion());

}

@Override

public int compareTo(ApiVersionCondition other, HttpServletRequest request) {

// 優先匹配最新的版本號

log。info(“compareTo方法({})”,other);

return other。getApiVersion() - this。apiVersion;

}

@Override

public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {

Matcher m = VERSION_PREFIX_PATTERN。matcher(request。getServletPath());

Integer version;

if (m。find()) {

version = Integer。valueOf(m。group(1));

if (version >= this。apiVersion) {

return this;

}

} else {

return this;

}

log。info(“compareTo方法({})”,version);

return null;

}

public int getApiVersion() {

return apiVersion;

}

}

構建自定義請求對映處理器

package org。apiserver。spring。config;

import lombok。extern。slf4j。Slf4j;

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;

/**

* @author JTJ

*/

@Slf4j

public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

@Override

protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {

return createCondition(AnnotationUtils。findAnnotation(handlerType,ApiVersion。class));

}

@Override

protected RequestCondition<?> getCustomMethodCondition(Method method) {

return createCondition(AnnotationUtils。findAnnotation(method,ApiVersion。class));

}

private RequestCondition createCondition(ApiVersion apiVersion) {

log。info(“開始構建請求條件({})”,apiVersion);

return apiVersion == null ? null : new ApiVersionCondition(apiVersion。value());

}

}

配置請求對映器

package org。apiserver。spring。config;

import org。springframework。context。annotation。Configuration;

import org。springframework。web。servlet。config。annotation。WebMvcConfigurationSupport;

import org。springframework。web。servlet。mvc。method。annotation。RequestMappingHandlerMapping;

/**

* @author JTJ

*/

@Configuration

public class WebConfig extends WebMvcConfigurationSupport {

@Override

public RequestMappingHandlerMapping requestMappingHandlerMapping() {

RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();

handlerMapping。setOrder(0);

handlerMapping。setInterceptors(getInterceptors());

return handlerMapping;

}

}

測試

編寫測試介面

這裡編寫v1、v2、v3版本介面

package org。apiserver。spring。v_1;

import lombok。extern。slf4j。Slf4j;

import org。apiserver。spring。config。ApiVersion;

import org。springframework。web。bind。annotation。GetMapping;

import org。springframework。web。bind。annotation。RequestMapping;

import org。springframework。web。bind。annotation。RestController;

/**

* @author JTJ

*/

@Slf4j

@RestController

@RequestMapping(“api/{version}”)

public class TestControllerV1 {

@ApiVersion()

@GetMapping(“test”)

public void v1(){

log。error(“v1 的 test方法!!”);

}

@ApiVersion(2)

@GetMapping(“test”)

public void v2(){

log。error(“v2 的 test方法!!”);

}

@ApiVersion(3)

@GetMapping(“test”)

public void v3(){

log。error(“v3 的 test方法!!”);

}

}

測試結果

http://localhost:8080/api/v1/test 響應 ‘v1 的 test方法!!’

http://localhost:8080/api/v2/test 響應 ‘v2 的 test方法!!’

http://localhost:8080/api/v3/test 響應 ‘v3 的 test方法!!’

http://localhost:8080/api/v4/test 響應 ‘v3 的 test方法!!’

可以分析出已經做到了請求版本匹配的規則了:若版本存在則匹配相應版本的介面,若不存在則匹配最新的介面,v[1-9]是版本取值範圍,若請求v0或v10等等,則會找不到對應的介面。