之前有文章講述在Spring MVC 中擴展 RequestMappingHandlerMapping 實現(xiàn)對版本的控制。
但是在真正使用過程中不是很理想化,因為其需要替換掉WebMvcConfigurationSupport,替換后后,會將其提供的一系列默認組件全部移除。如我們注冊攔截器使用的(RequestMappingHandlerAdapter)、全局異常攔截(ExceptionHandlerExceptionResolver)等。
本文以Spring Boot 2.x為例,解決這個問題。
配置WebMvcRegistrationsConfig
@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiRequestMappingHandlerMapping();
}
}
創(chuàng)建ApiRequestMappingHandlerMapping
public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private static final String VERSION_FLAG = "{version}";
private static RequestCondition<ApiVersionCondition> createCondition(Class<?> clazz) {
RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
if (classRequestMapping == null) {
return null;
}
StringBuilder mappingUrlBuilder = new StringBuilder();
if (classRequestMapping.value().length > 0) {
mappingUrlBuilder.append(classRequestMapping.value()[0]);
}
String mappingUrl = mappingUrlBuilder.toString();
if (!mappingUrl.contains(VERSION_FLAG)) {
return null;
}
ApiVersion apiVersion = clazz.getAnnotation(ApiVersion.class);
return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value());
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return createCondition(method.getClass());
}
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
return createCondition(handlerType);
}
}
@ApiVersion自定義注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
/**
* @return 版本號
*/
int value() default 1;
}
ApiVersionCondition請求映射條件
RequestCondition相當針對請求與映射之間創(chuàng)建了判斷條件。以此判斷某個請求應(yīng)落在哪個controller上。
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+).*");
private int apiVersion;
ApiVersionCondition(int apiVersion) {
this.apiVersion = apiVersion;
}
private int getApiVersion() {
return apiVersion;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
return new ApiVersionCondition(apiVersionCondition.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
if (m.find()) {
Integer version = Integer.valueOf(m.group(1));
if (version >= this.apiVersion) {
return this;
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
return apiVersionCondition.getApiVersion() - this.apiVersion;
}
}
使用
V1Controller
@RequestMapping("/{version}/version")
@RestController
public class V1Controller {
@GetMapping
public String test() {
return "version1";
}
@GetMapping("/extend")
public String extendTest() {
return "extend";
}
}
V2Controller
@RequestMapping("/{version}/version")
@RestController
@ApiVersion(2)
public class V2Controller {
@GetMapping
public String test() {
return "version2";
}
}
版本測試
當請求http://localhost:8080/v1/version時會返回version1
當請求http://localhost:8080/v2/version時返回version2
實現(xiàn)了版本控制效果
版本繼承
當請求http://localhost:8080/v2/version/extend時,會返回V1Controller中的extend。
實現(xiàn)版本繼承效果。
版本分包
當引入版本控制后,為了不讓版本號污染我們的類名。
我們一般不希望命名出現(xiàn)類似V1、V2的字眼。使用包的形式進行區(qū)分會更直觀一些。如com.test.v1.UserController、com.test.v2.UserController。
但此時spring以及擴展spring的一些組件(如mybatis)會拋出bean名稱重復(fù)的錯誤。
此時我們需要覆蓋或手動指定spring的默認名稱生成器AnnotationBeanNameGenerator,該類默認使用class的名稱而非全類名作為bean的名稱,故會出現(xiàn)名稱沖突。拋出ConflictingBeanDefinitionException。
Bean名稱重復(fù)ConflictingBeanDefinitionException沖突解決
Bean名稱重復(fù)ConflictingBeanDefinitionException沖突解決
Spring Boot 1.x
Spring Boot 1.x中由于使用jdk1.8以下版本,沒有default方法,故無法直接實現(xiàn)WebMvcRegistrations,其提供了WebMvcRegistrationsAdapter供我們繼承。
@Configuration
public class WebMvcRegistrationsConfig extends WebMvcRegistrationsAdapter {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiRequestMappingHandlerMapping();
}
}