首页>>后端>>SpringBoot->你了解SpringBoot启动时API相关信息是用什么数据结构存储的吗?(上篇)

你了解SpringBoot启动时API相关信息是用什么数据结构存储的吗?(上篇)

时间:2023-11-30 本站 点击:0

注意: 本文 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.mappingRegistryAbstractHandlerMethodMapping<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.mappingRegistrythis.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来存储的,那么我们可以知道它底层的数据结构就是数组+链表+红黑树

六、后语

另外就只能说是在此提供一份个人见解。因文字功底不足、知识缺乏,写不出十分术语化的文章,望见谅。

如果觉得本文让你有所收获,希望能够点个赞,给予一份鼓励。

也希望大家能够积极交流。如有不足之处,请大家及时批正,在此郑重感谢大家。

作者:宁在春


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/SpringBoot/4363.html