纸上得来终觉浅,绝知此事要躬行
留神
: 本文 SpringBoot 版本为 2.5.2; JDK 版本 为 jdk 11.
后续文章 从浏览器发送申请给SpringBoot后端时,是如何精确找到哪个接口的?(下篇)
前言:
在写文章的时候,我都会习惯性的记录下,是什么因素促使我去写的这篇文章。并竟对于感兴趣的货色,写起来也上心,也更得心应手,文章品质相应也更高。当然更多的是想和更多人分享本人的认识,与更多的人一起交换。“三人行,必有我师焉” ,欢送大家留言评论交换。
写这篇文章的起因是在于昨天一个学 Go 语言的后端小伙伴,问了我一个问题。
问题大抵如下:
为什么浏览器向后端发动申请时,就晓得要找的是哪一个接口?采纳了什么样的匹配规定呢?
SpringBoot 后端是如何存储 API 接口信息的?又是拿什么数据结构存储的呢?
@ResponseBody@GetMapping("/test")public String test(){ return "test";}
说实话,听他问完,我感觉我又不够卷了,几乎灵魂拷问,我一个答不进去。咱们一起去看看吧。
我对于SpringBoot的框架源码浏览教训可能就一篇SpringBoot主动拆卸原理算是吧,所以在肯定水平上我集体对于SpringBoot 框架了解的还是十分通俗的。
如果文章中有不足之处,请你肯定要及时批正!在此郑重感激。
一、注解派生概念
算是一点点前提概念吧
在java体系中,类是能够被继承,接口能够被实现。然而注解没有这些概念,而是有一个派生的概念。举例,注解A。被标记了在注解B头上,那么咱们能够说注解B就是注解A的派生。
如:
就像 注解 @GetMapping
上就还有一个 @RequestMapping(method = RequestMethod.GET)
,所以咱们实质上也是应用了 @RequestMapping
注解。
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@RequestMapping(method = RequestMethod.GET)public @interface GetMapping {}
还有 @Controller 和 @RestController 也是如此。
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Controller@ResponseBodypublic @interface RestController {}
废话不多说,间接肝啦。
二、启动流程
更后面的不去做探索了,咱们间接到这个入口处。
做了一个大抵的剖析流程图给大家做参考,也是我集体探索的路线。
2.1、AbstractHandlerMethodMapping
/** HandlerMapping实现的形象基类,定义了申请和HandlerMethod之间的映射。对于每个注册的处理程序办法,一个惟一的映射由定义映射类型<T>细节的子类保护 */public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean { // ... /**在初始化时检测处理程序办法。 能够说是入口处啦*/ @Override public void afterPropertiesSet() { initHandlerMethods(); } /**扫描 ApplicationContext 中的 bean,检测和注册处理程序办法。 */ protected void initHandlerMethods() { //getCandidateBeanNames() :确定应用程序上下文中候选 bean 的名称。 for (String beanName : getCandidateBeanNames()) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { //确定指定候选 bean 的类型,如果标识为处理程序类型,则调用detectHandlerMethods // 这里的处理程序 就为咱们在controller 中书写的那些接口办法 processCandidateBean(beanName); } } // 这里的逻辑不做探讨啦 handlerMethodsInitialized(getHandlerMethods()); } // ...}
只有当扫描到 是由@RestController 或@RequestMapping 注解润饰时,进入 processCandidateBean
办法,这个时候才是咱们要找的货色。其余的bean咱们不是咱们探讨的点,不做探讨。
咱们来接着看看 processCandidateBean
的解决逻辑,它做了一些什么事件。
/** 确定指定候选 bean 的类型,如果标识为处理程序类型,则调用detectHandlerMethods 。 */protected void processCandidateBean(String beanName) { Class<?> beanType = null; try { // 确定注入的bean 类型 beanType = obtainApplicationContext().getType(beanName); } catch (Throwable ex) { // 无奈解析的bean if (logger.isTraceEnabled()) { logger.trace("Could not resolve type for bean '" + 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正文。*/@Overrideprotected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));}
持续往下:
2.2、detectHandlerMethods() 办法
这个办法detectHandlerMethods(beanName);
它是做什么的呢?
它的办法正文为:在指定的处理程序 bean 中查找处理程序办法。
其实 detectHandlerMethods
办法就是真正开始解析Method的逻辑。通过解析Method上的 @RequestMapping
或者其余派生的注解。生成申请信息。
/** 在指定的处理程序 bean 中查找处理程序办法。*/ protected void detectHandlerMethods(Object handler) { Class<?> handlerType = (handler instanceof String ? 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 { //为处理程序办法提供映射。 不能为其提供映射的办法不是处理程序办法 return getMappingForMethod(method, userType); } catch (Throwable ex) { throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex); } }); if (logger.isTraceEnabled()) { logger.trace(formatMappings(userType, methods)); } else if (mappingsLogger.isDebugEnabled()) { mappingsLogger.debug(formatMappings(userType, methods)); } // 这里将解析的信息,循环进行注册 methods.forEach((method, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); registerHandlerMethod(handler, invocableMethod, mapping); }); } }
2.3、getMappingForMethod
getMappingForMethod
定义在 AbstractHandlerMethodMapping< T >
,实现在 RequestMappingHandlerMapping 类下
这里简略说就是 将类档次的RequestMapping和办法级别的RequestMapping联合 (createRequestMappingInfo
)
/** 应用办法和类型级别的RequestMapping注解来创立RequestMappingInfo。 */@Override@Nullableprotected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { RequestMappingInfo info = createRequestMappingInfo(method); if (info != null) { RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); if (typeInfo != null) { info = typeInfo.combine(info); } //获取类上 String prefix = getPathPrefix(handlerType); if (prefix != null) { info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info); } } return info;}
createRequestMappingInfo:
/**委托createRequestMappingInfo(RequestMapping, RequestCondition) ,依据提供的annotatedElement是类还是办法提供适当的自定义RequestCondition 。*/@Nullableprivate RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { //次要是 解析 Method 上的 @RequestMapping 信息 RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);}
2.4、MethodIntrospector.selectMethods()办法
依据相干元数据的查找,抉择给定指标类型的办法。
很多杂七杂八的货色在外面,很难说分明,这里只简略说了一下。
public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) { final Map<Method, T> methodMap = new LinkedHashMap<>(); Set<Class<?>> handlerTypes = new LinkedHashSet<>(); Class<?> specificHandlerType = null; if (!Proxy.isProxyClass(targetType)) { specificHandlerType = ClassUtils.getUserClass(targetType); handlerTypes.add(specificHandlerType); } handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType)); for (Class<?> currentHandlerType : handlerTypes) { final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType); //对给定类和超类(或给定接口和超接口)的所有匹配办法执行给定的回调操作。 ReflectionUtils.doWithMethods(currentHandlerType, method -> { Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); T result = metadataLookup.inspect(specificMethod); if (result != null) { // BridgeMethodResolver :给定一个合成bridge Method返回被桥接的Method 。 //当扩大其办法具备参数化参数的参数化类型时,编译器可能会创立桥接办法。 在运行时调用期间,能够通过反射调用和/或应用桥接Method //findBridgedMethod : 找到提供的bridge Method的原始办法。 Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) { methodMap.put(specificMethod, result); } } }, ReflectionUtils.USER_DECLARED_METHODS); } return methodMap;}
办法上的doc正文:
依据相干元数据的查找,抉择给定指标类型的办法。
调用者通过MethodIntrospector.MetadataLookup参数定义感兴趣的办法,容许将关联的元数据收集到后果映射中
一眼两言说不清楚,间接贴一张debug 的图片给大家看一下。
2.5、registerHandlerMethod 办法
这一段代码其本质就是 这里将解析进去的信息,循环进行注册
methods.forEach((method, mapping) -> { //抉择指标类型上的可调用办法:如果理论公开在指标类型上,则给定办法自身,或者指标类型的接口之一或指标类型自身上的相应办法。 // 简略了解返回个办法吧 Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); registerHandlerMethod(handler, invocableMethod, mapping);});
protected void registerHandlerMethod(Object handler, Method method, T mapping) { this.mappingRegistry.register(mapping, handler, method);}
这里的 this.mappingRegistry
是 AbstractHandlerMethodMapping<T>
的一个外部类。
MappingRegistry
: doc正文:一个注册表,它保护到处理程序办法的所有映射,公开执行查找的办法并提供并发拜访。
对于它的构造,在这里不做探讨啦。感兴趣,能够点进去持续看看。
咱们持续探索咱们 register
办法做了什么
public void register(T mapping, Object handler, Method method) { this.readWriteLock.writeLock().lock(); try { //创立 HandlerMethod 实例。 HandlerMethod handlerMethod = createHandlerMethod(handler, method); //验证办法映射 validateMethodMapping(handlerMethod, mapping); //这里就是间接获取门路 mapping 的值是 GET[/login] // 获取进去后 就是 /login Set<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping); for (String path : directPaths) { //this.pathLookup 它的定义如下: // private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>(); // 其实new 就是一个 new LinkedHashMap<>(); // 这里就是将 path 作为key ,mapping作为value 存起来 this.pathLookup.add(path, mapping); } String name = null; // 这里的意思能够演绎为: if (getNamingStrategy() != null) { ///确定给定 HandlerMethod 和映射的名称。 name = getNamingStrategy().getName(handlerMethod, mapping); addMappingName(name, handlerMethod); } // 上面几行是解决跨域问题的,不是咱们本章探讨的。大家感兴趣能够去看看。 CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { corsConfig.validateAllowCredentials(); this.corsLookup.put(handlerMethod, corsConfig); } this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null)); } finally { this.readWriteLock.writeLock().unlock(); }}
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));
这里的 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)
- 循环注册进去,申请的时候会再用到
四、后续
后续文章从浏览器发送申请给SpringBoot后端时,是如何精确找到哪个接口的?(下篇)
集体所谈
:浏览源码的过程中,其实真的是充斥乏味和干燥的。
读懂了一些要害货色,就开心的不得了;而像“又遗记debug到哪了,思路又凉了",就会开始满心埋怨(我经常想骂上一两句)。而后就持续苦逼的去看。
大家好,我是博主
宁在春
:主页一名喜爱文艺却踏上编程这条路线的小青年。
心愿:
咱们,待别日相见时,都已有所成
。
另外就只能说是在此提供一份个人见解。因文字功底有余、常识不足,写不出非常术语化的文章。望见谅
如果感觉本文让你有所播种,心愿可能点个赞,给予一份激励。
也心愿大家可能踊跃交换。如有不足之处,请大家及时批正,在此感激大家。
知乎 | 宁在春
简书 | 宁在春
CSDN | 宁在春
掘金 | 宁在春
博客园 | 宁在春