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

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

注意: 本文 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
@ResponseBody
public @interface RestController {
}

废话不多说,直接肝啦。

zhimg

二、启动流程

更前面的不去做探究了,我们直接到这个入口处。

做了一个大致的分析流程图给大家做参考,也是我个人探究的路线。

image-20211020211811200

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());
    }
    
    // ...
}
image-20211020145352560

只有当扫描到 是由@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注释。
*/
@Override
protected 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
@Nullable
protected 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 。
*/
@Nullable
private 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 的图片给大家看一下。

image-20211020164946806

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.mappingRegistryAbstractHandlerMethodMapping<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<>();

image-20211020172320552

不同的方法走到这,其实差别不是很大

image-20211020172513503

其实看完这个启动流程,对于我们刚开始的三个问题,我们大概率可以找到其中两个答案了。

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. 循环注册进去,请求的时候会再用到

四、后续

后续文章👉从浏览器发送请求给SpringBoot后端时,是如何准确找到哪个接口的?

若不是小伙伴提起那三问,我想我也不会有如此兴致,去一步一步Debug阅读相关源码,此文多半可能会胎死腹中了。

在此非常感谢 @小宇。不瞒大家,他又邀请我一起去读 ORM 框架源码了。不过得好好等上一段时间了。

个人所谈

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

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

大家好,我是博主宁在春主页

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

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

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

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

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

简书 | 宁在春

CSDN | 宁在春

掘金 | 宁在春

知乎 | 宁在春

博客园 | 宁在春

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349