注意
: 本文 SpringBoot 版本为 2.5.2; JDK 版本 为 jdk 11.
前言:
写这篇文章的原因是在于昨天一个学 Go 语言的后端小伙伴,问了我一个问题。
问题大致如下:
为什么浏览器向后端发起请求时,就知道要找的是哪一个接口?采用了什么样的匹配规则呢?
SpringBoot 后端是如何存储 API 接口信息的?又是拿什么数据结构存储的呢?
@ResponseBody@GetMapping("/test")publicStringtest(){return"test";}
说实话,听他问完,我感觉我又不够卷了,简直灵魂拷问,我一个答不出来。我们一起去看看吧。
一、注解派生概念
算是一点点前提概念吧
在java体系中,类是可以被继承,接口可以被实现。但是注解没有这些概念,而是有一个派生的概念。举例,注解A。被标记了在注解B头上,那么我们可以说注解B就是注解A的派生。
如:
就像 注解 @GetMapping
上就还有一个 @RequestMapping(method = RequestMethod.GET)
,所以我们本质上也是使用了 @RequestMapping
注解。
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}
还有 @Controller 和 @RestController 也是如此。
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Controller@ResponseBodypublic@interfaceRestController{}
废话不多说,直接肝啦。
二、启动流程
更前面的不去做探究了,我们直接到这个入口处。
做了一个大致的分析流程图给大家做参考,也是我个人探究的路线。
2.1、AbstractHandlerMethodMapping
/**HandlerMapping实现的抽象基类,定义了请求和HandlerMethod之间的映射。对于每个注册的处理程序方法,一个唯一的映射由定义映射类型<T>细节的子类维护*/publicabstractclassAbstractHandlerMethodMapping<T>extendsAbstractHandlerMappingimplementsInitializingBean{//.../**在初始化时检测处理程序方法。可以说是入口处啦*/@OverridepublicvoidafterPropertiesSet(){initHandlerMethods();}/**扫描ApplicationContext中的bean,检测和注册处理程序方法。*/protectedvoidinitHandlerMethods(){//getCandidateBeanNames():确定应用程序上下文中候选bean的名称。for(StringbeanName:getCandidateBeanNames()){if(!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)){//确定指定候选bean的类型,如果标识为处理程序类型,则调用detectHandlerMethods//这里的处理程序就为我们在controller中书写的那些接口方法processCandidateBean(beanName);}}//这里的逻辑不做讨论啦handlerMethodsInitialized(getHandlerMethods());}//...}
只有当扫描到 是由@RestController 或@RequestMapping 注解修饰时,进入 processCandidateBean
方法,这个时候才是我们要找的东西。其他的bean我们不是我们讨论的点,不做讨论。
我们来接着看看 processCandidateBean
的处理逻辑,它做了一些什么事情。
/**确定指定候选bean的类型,如果标识为处理程序类型,则调用detectHandlerMethods。*/protectedvoidprocessCandidateBean(StringbeanName){Class<?>beanType=null;try{//确定注入的bean类型beanType=obtainApplicationContext().getType(beanName);}catch(Throwableex){//无法解析的beanif(logger.isTraceEnabled()){logger.trace("Couldnotresolvetypeforbean'"+beanName+"'",ex);}}//isHandler方法判断是否是web资源类。if(beanType!=null&&isHandler(beanType)){//算是这条线路上重点啦detectHandlerMethods(beanName);}}
isHandler 方法判断是否是web资源类。当一个类被标记了 @Controller 或者@RequestMapping。 注意 @RestController 是@Controller的派生类。所以这里只用判断 @Controller 或者@RequestMapping就行了。
另外 isHandler 定义在 AbstractHandlerMethodMapping< T > ,实现在 RequestMappingHandlerMapping
/**给定类型是否是具有处理程序方法的处理程序。处理程序就是我们写的Controller类中的接口方法期望处理程序具有类型级别的Controller注释或类型级别的RequestMapping注释。*/@OverrideprotectedbooleanisHandler(Class<?>beanType){return(AnnotatedElementUtils.hasAnnotation(beanType,Controller.class)||AnnotatedElementUtils.hasAnnotation(beanType,RequestMapping.class));}
继续往下:
2.2、detectHandlerMethods() 方法
这个方法detectHandlerMethods(beanName);
它是做什么的呢?
它的方法注释为:在指定的处理程序 bean 中查找处理程序方法。
其实 detectHandlerMethods
方法就是真正开始解析Method的逻辑。通过解析Method上的@RequestMapping
或者其他派生的注解。生成请求信息。
/**在指定的处理程序bean中查找处理程序方法。*/protectedvoiddetectHandlerMethods(Objecthandler){Class<?>handlerType=(handlerinstanceofString?obtainApplicationContext().getType((String)handler):handler.getClass());if(handlerType!=null){//返回给定类的用户定义类:通常只是给定的类,但如果是CGLIB生成的子类,则返回原始类。Class<?>userType=ClassUtils.getUserClass(handlerType);//selectMethods://根据相关元数据的查找,选择给定目标类型的方法。//调用者通过MethodIntrospector.MetadataLookup参数定义感兴趣的方法,允许将关联的元数据收集到结果映射中//简单理解:解析RequestMapping信息Map<Method,T>methods=MethodIntrospector.selectMethods(userType,(MethodIntrospector.MetadataLookup<T>)method->{try{//为处理程序方法提供映射。不能为其提供映射的方法不是处理程序方法returngetMappingForMethod(method,userType);}catch(Throwableex){thrownewIllegalStateException("Invalidmappingonhandlerclass["+userType.getName()+"]:"+method,ex);}});if(logger.isTraceEnabled()){logger.trace(formatMappings(userType,methods));}elseif(mappingsLogger.isDebugEnabled()){mappingsLogger.debug(formatMappings(userType,methods));}//这里将解析的信息,循环进行注册methods.forEach((method,mapping)->{MethodinvocableMethod=AopUtils.selectInvocableMethod(method,userType);registerHandlerMethod(handler,invocableMethod,mapping);});}}
2.3、getMappingForMethod
getMappingForMethod
定义在 AbstractHandlerMethodMapping< T >
,实现在 RequestMappingHandlerMapping 类下
这里简单说就是 将类层次的RequestMapping和方法级别的RequestMapping结合 (createRequestMappingInfo
)
/**使用方法和类型级别的RequestMapping注解来创建RequestMappingInfo。*/@Override@NullableprotectedRequestMappingInfogetMappingForMethod(Methodmethod,Class<?>handlerType){RequestMappingInfoinfo=createRequestMappingInfo(method);if(info!=null){RequestMappingInfotypeInfo=createRequestMappingInfo(handlerType);if(typeInfo!=null){info=typeInfo.combine(info);}//获取类上Stringprefix=getPathPrefix(handlerType);if(prefix!=null){info=RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);}}returninfo;}
createRequestMappingInfo:
/**委托createRequestMappingInfo(RequestMapping,RequestCondition),根据提供的annotatedElement是类还是方法提供适当的自定义RequestCondition。*/@NullableprivateRequestMappingInfocreateRequestMappingInfo(AnnotatedElementelement){//主要是解析Method上的@RequestMapping信息RequestMappingrequestMapping=AnnotatedElementUtils.findMergedAnnotation(element,RequestMapping.class);RequestCondition<?>condition=(elementinstanceofClass?getCustomTypeCondition((Class<?>)element):getCustomMethodCondition((Method)element));return(requestMapping!=null?createRequestMappingInfo(requestMapping,condition):null);}
2.4、MethodIntrospector.selectMethods()方法
根据相关元数据的查找,选择给定目标类型的方法。
很多杂七杂八的东西在里面,很难说清楚,这里只简单说了一下。
publicstatic<T>Map<Method,T>selectMethods(Class<?>targetType,finalMetadataLookup<T>metadataLookup){finalMap<Method,T>methodMap=newLinkedHashMap<>();Set<Class<?>>handlerTypes=newLinkedHashSet<>();Class<?>specificHandlerType=null;if(!Proxy.isProxyClass(targetType)){specificHandlerType=ClassUtils.getUserClass(targetType);handlerTypes.add(specificHandlerType);}handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType));for(Class<?>currentHandlerType:handlerTypes){finalClass<?>targetClass=(specificHandlerType!=null?specificHandlerType:currentHandlerType);//对给定类和超类(或给定接口和超接口)的所有匹配方法执行给定的回调操作。ReflectionUtils.doWithMethods(currentHandlerType,method->{MethodspecificMethod=ClassUtils.getMostSpecificMethod(method,targetClass);Tresult=metadataLookup.inspect(specificMethod);if(result!=null){//BridgeMethodResolver:给定一个合成bridgeMethod返回被桥接的Method。//当扩展其方法具有参数化参数的参数化类型时,编译器可能会创建桥接方法。在运行时调用期间,可以通过反射调用和/或使用桥接Method//findBridgedMethod:找到提供的bridgeMethod的原始方法。MethodbridgedMethod=BridgeMethodResolver.findBridgedMethod(specificMethod);if(bridgedMethod==specificMethod||metadataLookup.inspect(bridgedMethod)==null){methodMap.put(specificMethod,result);}}},ReflectionUtils.USER_DECLARED_METHODS);}returnmethodMap;}
方法上的doc注释:
根据相关元数据的查找,选择给定目标类型的方法。 调用者通过MethodIntrospector.MetadataLookup参数定义感兴趣的方法,允许将关联的元数据收集到结果映射中
一眼两言说不清楚,直接贴一张debug 的图片给大家看一下。
2.5、registerHandlerMethod 方法
这一段代码其本质就是 这里将解析出来的信息,循环进行注册
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}0
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}1
这里的 this.mappingRegistry
是 AbstractHandlerMethodMapping<T>
的一个内部类。
MappingRegistry
: doc注释:一个注册表,它维护到处理程序方法的所有映射,公开执行查找的方法并提供并发访问。
对于它的结构,在这里不做探讨啦。感兴趣,可以点进去继续看看。
我们继续探究我们 register
方法做了什么
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}2
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}3
这里的 this.registry
的定义如下:private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
不同的方法走到这,其实差别不是很大
其实看完这个启动流程,对于我们刚开始的三个问题,我们大概率可以找到其中两个答案了。
2.6、小结
你们 SpringBoot 后端框架是如何存储API接口的信息的?是拿什么数据结构存储的呢?
第一个答案:大致就是和MappingRegistry
这个注册表类相关.
第二个答案:我们之前看到存储信息时,都是 HashMap
相关的类来存储的,那么我们可以知道它底层的数据结构就是 数组+链表+红黑树
注意
: 本文 SpringBoot 版本为 2.5.2;JDK 版本 为 jdk 11.
并未针对多个版本进行比较,但是推测下来,多半都是如此.
那么我们的下一步就是去查看 SpringBoot 请求时,是如何找到 对应的 接口的。哪里才又是我们的一个重点。
三、小结流程
扫描所有注册的Bean
遍历这些Bean,依次判断是否是处理器,并检测其HandlerMethod
遍历Handler中的所有方法,找出其中被@RequestMapping注解标记的方法。
获取方法method上的@RequestMapping实例。
检查方法所属的类有没有@RequestMapping注解
将类层次的RequestMapping和方法级别的RequestMapping结合 (createRequestMappingInfo)
循环注册进去,请求的时候会再用到
四、请求流程
其他的不看了,我们就直接从DispatcherServlet
处入手了.
我们只看我们关注的,不是我们关注的,我们就不做多讨论了.
这边同样也画了一个流程图给大家参考:
4.1、DispatcherServlet
我们都熟悉SpringMVC 处理请求的模式,就不多讨论了.直接肝了.0
1)doService
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}4
2)doDispatch
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}5
3)getHandler
返回此请求的 HandlerExecutionChain。 按顺序尝试所有处理程序映射。
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}6
4.2、HandlerMapping
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}7
4.3、AbstractHandlerMapping
AbstractHandlerMapping
:HandlerMapping 实现的抽象基类。 支持排序、默认处理程序、处理程序拦截器,包括由路径模式映射的处理程序拦截器。
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}8
getHandlerInternal
方法定义在AbstractHandlerMapping
,但它是个抽象方法,我们往下看它实现,才知晓它做了什么。
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method=RequestMethod.GET)public@interfaceGetMapping{}9
我们往下看他的实现:
4.4、AbstractHandlerMethodMapping< T >
4.4.1、getHandlerInternal
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Controller@ResponseBodypublic@interfaceRestController{}0
4.4.2、lookupHandlerMethod (匹配接口代码)
需要注意的是匹配方法时,是根据@RequestMapping
里面的value路径来匹配的,如果匹配到的有多个,如你配置了通配符,也配置了精确配置,他都会匹配到放在一个集合中,根据规则排序,然后取集合的第一个元素。有兴趣的可以看看这个排序的规则,理论上肯定是路径越精确的会优先,具体代码实现如下:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Controller@ResponseBodypublic@interfaceRestController{}1
第二句中的this.mappingRegistry
,它就是一个private final MappingRegistry mappingRegistry = new MappingRegistry();
它的方法getMappingsByDirectPath(lookupPath)
方法,真实调用如下:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Controller@ResponseBodypublic@interfaceRestController{}2
hxdm,看到这个this.mappingRegistry
和this.pathLookup
有没有一股子熟悉感啊,它就是我们启动时存储信息的类和数据结构啊,xd。
那这结果就非常明了了啊。
我们获取到的List<T> directPathMatches
的这个 list 就是我们启动时扫描到的所有接口,之后再经过排序,取第一个,找到最匹配的。
xdm,我们完事了啊。
4.4.3、addMatchingMappings
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Controller@ResponseBodypublic@interfaceRestController{}3
这么说还是不太好说清楚,我们直接去方法调用处,看它改变了什么了吧。
简单说就是将信息存储到 matches 变量中了。还有就是将匹配HandlerMethod的实例取出来了。
五、小结
扫描所有注册的Bean
遍历这些Bean,依次判断是否是处理器,并检测其HandlerMethod
遍历Handler中的所有方法,找出其中被@RequestMapping注解标记的方法。
获取方法method上的@RequestMapping实例。
检查方法所属的类有没有@RequestMapping注解
将类层次的RequestMapping和方法级别的RequestMapping结合 (createRequestMappingInfo)
当请求到达时,去urlMap中需找匹配的url,以及获取对应mapping实例,然后去handlerMethods中获取匹配HandlerMethod实例。
后续就是SpringMVC 执行流程了。
将RequestMappingInfo实例以及处理器方法注册到缓存中。
写到这里基本可以回答完文前所说的三个问题了。
他问的是为什么浏览器在向后端发起请求的时候,就知道要找的是哪一个API 接口,你们 SpringBoot 后端框架是如何存储API接口的信息的?是拿什么数据结构存储的呢?
第一个答案:将所有接口信息存进一个HashMap,请求时,取出相关联的接口,排序之后,匹配出最佳的 接口。
第二个答案:大致就是和MappingRegistry
这个注册表类相关了。
第三个答案:我们之前看到存储信息时,底层是用HashMap
来存储的,那么我们可以知道它底层的数据结构就是数组+链表+红黑树
六、后语
另外就只能说是在此提供一份个人见解。因文字功底不足、知识缺乏,写不出十分术语化的文章,望见谅。
如果觉得本文让你有所收获,希望能够点个赞,给予一份鼓励。
也希望大家能够积极交流。如有不足之处,请大家及时批正,在此郑重感谢大家。
作者:宁在春