我们将在这里分享来自指令集小伙伴们的行业经验、技术讨论,构建一个高品质的交流平台。在这里,你可以开启物联网奥秘的大门;在这里,可以点燃技术起飞的引信。
is 百脑汇
今日主题:
从API版本控制说起,
实现SpringBoot 一种版本控制的方式(下篇)
标签
Spring、SpringBoot、版本控制、RequestMappingHandlerMapping
涉及知识点
· springboot 注解原理;
· spring 请求mapping的注册与匹配;
· springboot 常用的mapping注解。
前文讲了API接口版本的意义及Springboot实现请求头定义接口版本实现的一种方式,但是存在一个小问题,即定义同样接口版本信息的时候,在应用启动时并不报错,在调用时,才会出现异常,本文接下来说下,如何实现自定义mapping时,重复定义接口时,启动应用即能检查出来。
要解决这个问题,一般思路,是从Springboot启动mapping注册时做这个重复检查,是否可以从VersionRequestMappingHandlerMapping这里入手解决注册时的检查呢?
查看VersionRequestMappingHandlerMapping的父类源码org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,找不到对应register开头的方法可供实现,继续查看父接口或类:
public class RequestMappingHandlerMapping extendsRequestMappingInfoHandlerMapping
implements MatchableHandlerMapping, EmbeddedValueResolverAware {
包括RequestMappingInfoHandlerMapping、MatchableHandlerMapping、EmbeddedValueResolverAware。
从mapping定义来说,得看RequestMappingInfoHandlerMapping,发现也没有注册的接口或方法,接着看他的父类AbstractHandlerMethodMapping,发现正如我们的思路,有注册mapping的方法:
AbstractHandlerMethodMapping:
/**
* Register the given mapping.
* This method may be invoked at runtime after initialization has completed.
* @param mapping the mapping for the handler method
* @param handler the handler
* @param method the method
*/
public void registerMapping(T mapping, Object handler, Method method) {
this.mappingRegistry.register(mapping, handler, method);
}
我们试着实现这个方法看看是否OK,在VersionRequestMappingHandlerMapping里覆写此方法:
/**
* 版本号匹配器
* @author 凌封
*/
public class VersionRequestMappingHandlerMapping extendsRequestMappingHandlerMapping {
/**
自定义类型注解匹配,即Controller接口类匹配
**/
@Override protected RequestConditiongetCustomTypeCondition(Class> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType,ApiVersion.class);
return createCondition(apiVersion);
}
/**
自定义方法注解匹配,即具体方法级别的注解匹配
**/
@Override protected RequestConditiongetCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method,ApiVersion.class);
return createCondition(apiVersion);
}
private RequestConditioncreateCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : newApiVersionCondition(apiVersion.value());
}
/**
* 自定义方法注册,先打印出来参数有些什么信息
*/
@Override
public void registerMapping(RequestMappingInfo mapping, Object handler,Method method) {
System.out.println("lingfeng mapping:"+JsonMapper.toAlwaysJson(mapping));
System.out.println("lingfeng handler:"+JsonMapper.toAlwaysJson(handler));
System.out.println("lingfeng method:"+JsonMapper.toAlwaysJson(method));
super.registerMapping(mapping, handler, method);
}
}
启动应用,发现并没有打印出“lingfeng...”等类似信息,可以确定,注册过程,并不会调用自定义mapping里面对registerMapping方法的实现。OK,然后不调用,想在这里做拦载检查是否重复走不通。得了解Springboot怎么实现应用启动注册mapping的过程,再看在哪一步做。前文已经提到过,不使用自定义注解mapping时,定义同样的接口启动会报错:
这里面只是Springboot容器抛出的异常,没有相关mapping注册的信息,按异常信息往下找,现有这样一段异常:
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map'com.isyscore.common.sms.api.test.TestController2' method
public java.lang.Stringcom.isyscore.common.sms.api.test.TestController2.send1()
to {[/api/list/item],methods=[GET]}: There is already'com.isyscore.common.sms.api.test.TestController2' bean method
public java.lang.Stringcom.isyscore.common.sms.api.test.TestController2.item() mapped.
atorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry.assertUniqueMethodMapping(AbstractHandlerMethodMapping.java:582)
atorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry.register(AbstractHandlerMethodMapping.java:546)
atorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping.registerHandlerMethod(AbstractHandlerMethodMapping.java:267)
atorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lambda$detectHandlerMethods$1(AbstractHandlerMethodMapping.java:252)
atorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping$$Lambda$351/1990722999.accept(Unknown Source)
at java.util.LinkedHashMap.forEach(LinkedHashMap.java:676)
atorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping.detectHandlerMethods(AbstractHandlerMethodMapping.java:250)
atorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping.initHandlerMethods(AbstractHandlerMethodMapping.java:219)
atorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping.afterPropertiesSet(AbstractHandlerMethodMapping.java:189)
atorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.afterPropertiesSet(RequestMappingHandlerMapping.java:136)
atorg.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1753)
atorg.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1690)
... 127 common frames omitted
这里我们看到,异常是由org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry.assertUniqueMethodMapping方法抛出来的,而这个是在bean初始化完成后进行的:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.afterPropertiesSet(AbstractHandlerMethodMapping.java:189)
查看AbstractHandlerMethodMapping.java源代码,方法入口为:afterPropertiesSet,可以知道注册mapping的过程:
/**
* Detects handler methods at initialization.
* @see #initHandlerMethods
*/
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
/**
* Scan beans in the ApplicationContext, detect and register handler methods.
* @see #isHandler
* @see #detectHandlerMethods
* @see #handlerMethodsInitialized
*/
protected void initHandlerMethods() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: " +getApplicationContext());
}
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
obtainApplicationContext().getBeanNamesForType(Object.class));
for (String beanName : beanNames) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
Class> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve target class for bean with name '" +beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
}
handlerMethodsInitialized(getHandlerMethods());
}
initHandlerMethods方法里面,主要是拿到容器里面的bean,判断bean isHandler,然后去注册handler(detectHandlerMethods方法)。我们再来看detectHandlerMethods方法实现:
/**
* Look for handler methods in the specified handler bean.
* @param handler either a bean name or an actual handler instance
* @see #getMappingForMethod
*/
protected void detectHandlerMethods(Object handler) {
Class> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class> userType = ClassUtils.getUserClass(handlerType);
Map methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isDebugEnabled()) {
logger.debug(methods.size() + " request handler methods found on " +userType + ": " + methods);
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method,userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
通过handler类型获取实现方法Method再去注册真正的接口方法:registerHandlerMethod,该方法调用mappingRegistry的register方法:
/**
* Register a handler method and its unique mapping. Invoked at startup for
* each detected handler method.
* @param handler the bean name of the handler or the handler instance
* @param method the method to register
* @param mapping the mapping conditions associated with the handler method
* @throws IllegalStateException if another method was already registered
* under the same mapping
*/
protected void registerHandlerMethod(Object handler, Method method, Tmapping) {
this.mappingRegistry.register(mapping, handler, method);
}
mappingRegistry的register方法就清楚了,创建一个HandlerMethod,然后检查是否存在重复的handlerMethod,不重复的话,再放入mappingLookup、urlLookup、corsLookup等mapping容器中,其中关键的地方就是检查唯一性方法assertUniqueMethodMapping:
private void assertUniqueMethodMapping(HandlerMethodnewHandlerMethod, T mapping) {
HandlerMethod handlerMethod = this.mappingLookup.get(mapping);
if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) {
throw new IllegalStateException(
"Ambiguous mapping. Cannot map '" +newHandlerMethod.getBean() + "' method \n" +
newHandlerMethod + "\nto " + mapping + ": There is already '" +
handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped.");
}
}
这个方法是用当前mapping对像作为key,从mappingLookup这个LinkedHashMap里面查找是否已存在HandlerMethod,如果已存在,判断和当前传入的是同个时,就抛出异常。 到这里,我们基本清楚了mapping注册的整个过程,接下来我们看怎么把自定义的mapping也加入这里面检查。
如果存在两个相同的带有自定义的ApiVersion注解值的mapping,理论上第一个注册会正常通过assertUniqueMethodMapping检查,第二个会不通过,这时候传入的mapping值两次注册一样的,debug发现两次并不一样的:
起先,以为这个地方传入两个不同的mapping拿不到同样的值有问题,应该传入相对的mapping,才会拿到一样的handlerMethod,符合重复定义HandlerMethod,会检查异常不通过。然而一想,两个不同的方法,面像对像思想,理论上也会创建出不同的方法注解的ApiCondition对像。 但是这不就与mappping定义唯一矛盾了么?继续debug进入assertUniqueMethodMapping方法看看:
private void assertUniqueMethodMapping(HandlerMethodnewHandlerMethod, T mapping) {
HandlerMethod handlerMethod = this.mappingLookup.get(mapping);
if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) {
throw new IllegalStateException(
"Ambiguous mapping. Cannot map '" +newHandlerMethod.getBean() + "' method \n" +
newHandlerMethod + "\nto " + mapping + ": There is already '" +
handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped.");
}
}
这里看到查找HandlerMethod是从一个mappingLookup容器列表里拿的,这个前面定义是一个LinkedHashMap:
private final Map mappingLookup = newLinkedHashMap<>();
里面取value,传入对像的话,是用对像的hasCode作为key获取:
public V get(Object key) {
Node e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
如果没有使用自定义的ApiCondtion时,这个hashCode获取方法是什么样的呢?debug进入“T mapping”,发现实例是RequestMappingInfo,查看其hashCode方法:
@Override
public int hashCode() {
return (this.patternsCondition.hashCode() * 31 + // primary differentiation
this.methodsCondition.hashCode() + this.paramsCondition.hashCode() +
this.headersCondition.hashCode() + this.consumesCondition.hashCode() +
this.producesCondition.hashCode() +this.customConditionHolder.hashCode());
}
增加了ApiCondtion定义时,其他没改变,改变的就是最后一个this.customConditionHolder.hashCode())。OK,查看CustomConditionHolder的hasCode方法:
@Override
public int hashCode() {
return getContent().hashCode();
}
getContent()又是什么鬼?继续追踪,它在AbstractRequestCondition是一个抽像接口,其实现在RequestConditionHolder实现是:
@Override
protected Collection> getContent() {
return (this.condition != null ? Collections.singleton(this.condition) :Collections.emptyList());
}
返回的是一个继续AbstractSet的SingletonSet集合,这个集合的hashCode是里面元素的hash值之合:
public int hashCode() {
int h = 0;
Iterator i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
}
return h;
}
看到这里,关键之处就是这个集合里面的元素hashCode值,debug发现RequestConditionHolder里面放的content正是ApiCondition对象,终于理清过程了,那么注册时,这个ApiCondition对象是怎么设置进去的呢?
进一步查看RequestMappingInfo构造器,并查看其引用地方,是在创建RequestMappingHandlerMapping时,创建的,传入的是自定义的ApiCondtion:
protected RequestMappingInfo createRequestMappingInfo(
RequestMapping requestMapping, @Nullable RequestCondition>customCondition) {
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name());
if (customCondition != null) {
builder.customCondition(customCondition);
}
return builder.options(this.config).build();
}
至此,判断是否存在重复的mapping时,只要判断自定义的ApiCondition hashCode是否相同,解决也很简单,覆写hashCode方法,同样值的api入参,判断为相同hasCode,ApiVersionCondition.java增加方法:
/**
* 用于对比条件是否已存在这样的条件,Spring容器存放requestMapping里的mapping用到方法各注解的条件hash值合作为mapping key
*/
public int hashCode() {
return this.apiVersion.hashCode();
}
另外,还有一点,在AbstractHandlerMethodMapping的assertUniqueMethodMapping方法里,通过mappingLookup.get(mapping)取出HandlerMethod时,还要进一步判断是否和传入的HandlerMethod变量是否相同,才抛出异常:
if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) {
throw new IllegalStateException(
"Ambiguous mapping. Cannot map '" +newHandlerMethod.getBean() + "' method \n" +
newHandlerMethod + "\nto " + mapping + ": There is already '" +
handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped.");
}
同样的道理,也要在ApiCondition里覆写equals方法。完整的ApiCondition如下:
/**
* 版本号匹配筛选器
* @author 凌封
*
*/
public class ApiVersionCondition implementsRequestCondition {
/**
* 指令集OAS0.6标准协议指定 请求header头需带上版本号isc-api-version
*/
private static final String HEADER_VERSION = "isc-api-version";
private static final double DEFAULT_VERSION = 1.0;
private Double apiVersion;
public ApiVersionCondition(double apiVersion) {
this.apiVersion = apiVersion;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
if(this.apiVersion==other.getApiVersion())return this;
return other;
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequestrequest) {
String v = request.getHeader(HEADER_VERSION);
Double version = DEFAULT_VERSION;
if(StringUtil.isNotBlank(v)) {
version = Double.valueOf(v);
}
// 如果请求的版本号等于配置版本号, 则满足
if(version==this.apiVersion.doubleValue())return this;
return null;
}
/**
* 如果匹配到两个都符合版本需求的(理论上不应该有),如果有,调用期会异常,还没解决启动期检查
*/
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequestrequest) {
// 优先匹配最新的版本号
return 0;
//return Double.compare(other.getApiVersion(), this.apiVersion);
}
public double getApiVersion() {
return apiVersion;
}
/**
* 用于对比条件是否已存在这样的条件,Spring容器存放requestMapping里的mapping用到方法各注解的条件hash值合作为mapping key
*/
@Override
public int hashCode() {
return this.apiVersion.hashCode();
}
/**
* 同hasCode用处
*/
@Override
public boolean equals(Object obj) {
if(obj==null || !(obj instanceof ApiVersionCondition))return false;
ApiVersionCondition avc = (ApiVersionCondition)obj;
return this.apiVersion.doubleValue() ==avc.getApiVersion();
}
}
完成后,编写测试类,使用同样的方法注解:
@RestController
@RequestMapping(value = "/api/list")
public class TestController2{
@GetMapping(value = "/item")
@ApiVersion(1.0)
public String item() {
return "1.0";
}
@GetMapping(value = "/item")
@ApiVersion(1.0)
public String send1() {
return "1.0";
}
}
启动应用,达到预期目标,自检报告重复定义异常:
Caused by: org.springframework.beans.factory.BeanCreationException: Errorcreating bean with name 'requestMappingHandlerMapping' defined in classpath resource[org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed;nested exception is java.lang.IllegalStateException: Ambiguous mapping.Cannot map 'com.isyscore.common.sms.api.test.TestController2' method
public java.lang.Stringcom.isyscore.common.sms.api.test.TestController2.send1()
to {[/api/list/item],methods=[GET],custom=[com.isyscore.boot.web.request.version.ApiVersionCondition@3ff00000]}:There is already 'com.isyscore.common.sms.api.test.TestController2' beanmethod
public java.lang.Stringcom.isyscore.common.sms.api.test.TestController2.item() mapped.
atorg.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1694)
atorg.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:573)
...
修改为不同版本,启动正常。
总结:本文从API版本规范定义要求开始,讲解了常见的几种版本定义方式,实现了Springboot实现header头自定义版本的方式,并且能在应用启动时自检重复定义的异常。
扫二维码|关注指令集技术站
更多来自指令集小伙伴的
技术分享 | 一手掌握
扫二维码|关注指令集招聘
更多公司福利与员工活动
招聘信息 | 一手掌握
领航物联网智能操作系统,指令集完成过亿元A轮融资
引领新物联 共创新未来|指令集与中科网威签署战略合作协议
中国第一代程序员潘爱民的程序人生
基层管理需求旺盛,指令集智慧社区应用获多方认可
物联网“智慧”摊位上线,不了解一下么?