从API版本控制说起,实现SpringBoot 一种版本控制的方式(下篇)

从API版本控制说起,实现SpringBoot 一种版本控制的方式(下篇)_第1张图片

我们将在这里分享来自指令集小伙伴们的行业经验、技术讨论,构建一个高品质的交流平台。在这里,你可以开启物联网奥秘的大门;在这里,可以点燃技术起飞的引信。

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时,定义同样的接口启动会报错:

从API版本控制说起,实现SpringBoot 一种版本控制的方式(下篇)_第2张图片

这里面只是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 RequestConditioncustomCondition) {

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头自定义版本的方式,并且能在应用启动时自检重复定义的异常。


从API版本控制说起,实现SpringBoot 一种版本控制的方式(下篇)_第3张图片

扫二维码|关注指令集技术站

更多来自指令集小伙伴的

技术分享 | 一手掌握

从API版本控制说起,实现SpringBoot 一种版本控制的方式(下篇)_第4张图片

扫二维码|关注指令集招聘

更多公司福利与员工活动

招聘信息 | 一手掌握

  • 领航物联网智能操作系统,指令集完成过亿元A轮融资

  • 引领新物联 共创新未来|指令集与中科网威签署战略合作协议

  • 中国第一代程序员潘爱民的程序人生

  • 基层管理需求旺盛,指令集智慧社区应用获多方认可

  • 物联网“智慧”摊位上线,不了解一下么?

从API版本控制说起,实现SpringBoot 一种版本控制的方式(下篇)_第5张图片

你可能感兴趣的:(spring,java,css,spring,boot,redis)