写点什么

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

作者:宁在春
  • 2022 年 7 月 31 日
  • 本文字数:7157 字

    阅读完需:约 23 分钟

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

纸上得来终觉浅,绝知此事要躬行


注意: 本文 SpringBoot 版本为 2.5.2; JDK 版本 为 jdk 11.


前言:

在写文章的时候,我都会习惯性的记录下,是什么因素促使我去写的这篇文章。并竟对于感兴趣的东西,写起来也上心,也更得心应手,文章质量相应也更高。当然更多的是想和更多人分享自己的看法,与更多的人一起交流。“三人行,必有我师焉” ,欢迎大家留言评论交流。


写这篇文章的原因是在于昨天一个学 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 请求时,是如何找到 对应的 接口的。哪里才又是我们的一个重点。

三、小结流程

  1. 扫描所有注册的 Bean

  2. 遍历这些 Bean,依次判断是否是处理器,并检测其 HandlerMethod

  3. 遍历 Handler 中的所有方法,找出其中被 @RequestMapping 注解标记的方法。

  4. 获取方法 method 上的 @RequestMapping 实例。

  5. 检查方法所属的类有没有 @RequestMapping 注解

  6. 将类层次的 RequestMapping 和方法级别的 RequestMapping 结合 (createRequestMappingInfo)

  7. 循环注册进去,请求的时候会再用到

四、后续


个人所谈

阅读源码的过程中,其实真的是充满有趣和枯燥的。

读懂了一些关键东西,就开心的不得了;而像“又忘记 debug 到哪了,思路又凉了",就会开始满心抱怨(我常常想骂上一两句)。然后就继续苦逼的去看。

大家好,我是博主 宁在春

一名喜欢文艺却踏上编程这条道路的小青年。

希望:我们,待别日相见时,都已有所成


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


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


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

发布于: 刚刚阅读数: 4
用户头像

宁在春

关注

一个喜欢文艺风的程序员 2022.07.01 加入

他日凌云,万事胜意

评论

发布
暂无评论
你了解SpringBoot启动时API相关信息是用什么数据结构存储的吗?_springboot_宁在春_InfoQ写作社区