Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Spring Boot 源码学习】自动装配流程源码解析(下)

【Spring Boot 源码学习】自动装配流程源码解析(下)

原创
作者头像
huazie
发布于 2024-05-05 09:14:09
发布于 2024-05-05 09:14:09
3210
举报

引言

上篇博文,笔者带大家了解了自动装配流程中有关自动配置加载的流程;

本篇将介绍自动装配流程剩余的内容,包含了自动配置组件的排除和过滤、触发自动配置事件。

主要内容

书接上篇,本篇继续从源码分析自动装配流程:

1. 排除指定自动配置组件

如果我们在实际使用时,并不需要其中的某些组件,那就可以通过 @EnableAutoConfiguration 注解的 excludeexcludeName 属性来进行有针对性的排除 或者 在Spring Boot 的配置文件进行排除。

下面我们来分析一下排除逻辑的源码:

代码语言:java
AI代码解释
复制
Set<String> exclusions = getExclusions(annotationMetadata, attributes);

protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    Set<String> excluded = new LinkedHashSet<>();
    // 获取 exclude 属性 配置的 待排除的自动配置组件
    excluded.addAll(asList(attributes, "exclude"));
    // 获取 excludeName 属性 配置的 待排除的自动配置组件
    excluded.addAll(asList(attributes, "excludeName"));
    // 获取 Spring Boot 配置文件中 配置的 待排除的自动配置组件
    excluded.addAll(getExcludeAutoConfigurationsProperty());
    return excluded;
}

protected List<String> getExcludeAutoConfigurationsProperty() {
    Environment environment = getEnvironment();
    if (environment == null) {
        return Collections.emptyList();
    }
    if (environment instanceof ConfigurableEnvironment) {
        Binder binder = Binder.get(environment);
        return binder.bind(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class)
            .map(Arrays::asList)
            .orElse(Collections.emptyList());
    }
    String[] excludes = environment.getProperty(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class);
    return (excludes != null) ? Arrays.asList(excludes) : Collections.emptyList();
}

上面的代码也挺好理解,分别从注解属性 exclude 、 excludeName 以及配置文件中获取待排除的自动配置组件。

下面我们来演示一下该如何配置,从而排除我们不需要的自动配置组件:

  • 添加注解属性 exclude 和 excludeName
  • 添加配置文件属性
  • 我们启动先前建的 Spring Boot 项目的应用类,分别查看到如下的信息:

当上面获取了被排除的自动配置组件之后,需要对待排除的类进行检查,如下所示:

代码语言:java
AI代码解释
复制
checkExcludedClasses(configurations, exclusions);

private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {
    List<String> invalidExcludes = new ArrayList<>(exclusions.size());
    for (String exclusion : exclusions) {
        // 如果待排除的自动配置类存在且可以加载
        // 并且已去重过的自动配置组件中不存在该待排除的自动配置类
        if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) {
            // 添加到非法的排除列表中
            invalidExcludes.add(exclusion);
        }
    }
    // 如果存在非法的排除项,则抛出相应的异常信息
    if (!invalidExcludes.isEmpty()) {
        handleInvalidExcludes(invalidExcludes);
    }
}

protected void handleInvalidExcludes(List<String> invalidExcludes) {
    StringBuilder message = new StringBuilder();
    for (String exclude : invalidExcludes) {
        message.append("\t- ").append(exclude).append(String.format("%n"));
    }
    throw new IllegalStateException(String.format(
            "The following classes could not be excluded because they are not auto-configuration classes:%n%s",
            message));
}

上述代码中对于待排除类的检查逻辑也好理解,如果待排除的自动配置类存在且可以加载【即存在于当前的ClassLoader中】,并且已去重过的自动配置组件中不存在该待排除的自动配置类,则认为待排除的自动配置类是非法的,抛出相关异常。

我们下面通过示例来验证一下:

  • 在我们的示例项目中添加一个自动配置类【注意这里只做演示,无其他意义】
  • 配置文件添加项目中的一个自动配置类
  • 我们启动先前建的 Spring Boot 项目的应用类,可以看到如下的启动异常报错:

如果上述检查通过,则说明待排除的自动配置类都符合要求,则调用如下代码从自动配置集合中移除上面获取的待排除的自动配置类信息。

代码语言:java
AI代码解释
复制
configurations.removeAll(exclusions);

2. 过滤自动配置组件

经过上面的自动配置组件排除逻辑之后,接下来就要过滤自动配置组件了,而过滤逻辑主要是通过检查配置类的注解是否符合 spring.factories 文件中 AutoConfigurationImportFilter 指定的注解检查条件,来决定该过滤哪些自动配置组件。

下面开始分析相关代码,如下所示【Spring Boot 2.7.9】:

代码语言:java
AI代码解释
复制
configurations = getConfigurationClassFilter().filter(configurations);

进入 getConfigurationClassFilter 方法,如下所示:

代码语言:java
AI代码解释
复制
private ConfigurationClassFilter getConfigurationClassFilter() {
    if (this.configurationClassFilter == null) {
        List<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();
        for (AutoConfigurationImportFilter filter : filters) {
            invokeAwareMethods(filter);
        }
        this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
    }
    return this.configurationClassFilter;
}

getConfigurationClassFilter 方法返回一个 ConfigurationClassFilter 实例,用来过滤掉不必要的配置类。

继续看 getAutoConfigurationImportFilters 方法,如下所示:

代码语言:java
AI代码解释
复制
protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
    return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader);
}

它通过 SpringFactoriesLoader 类的 loadFactories 方法来获取 META-INF/spring.factories 中配置 keyAutoConfigurationImportFilterFilters 列表;

我们可以查看相关配置了解一下,如下所示:

代码语言:java
AI代码解释
复制
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

如上所示,在 spring-boot-autoconfigure 中默认配置了三个筛选条件:OnBeanConditionOnClassConditionOnWebApplicationCondition,它们均实现了 AutoConfigurationImportFilter 接口。

相关类图如下所示:

我们继续往下看 invokeAwareMethods,如下所示:

代码语言:java
AI代码解释
复制
private void invokeAwareMethods(Object instance) {
    if (instance instanceof Aware) {
        if (instance instanceof BeanClassLoaderAware) {
            ((BeanClassLoaderAware) instance).setBeanClassLoader(this.beanClassLoader);
        }
        if (instance instanceof BeanFactoryAware) {
            ((BeanFactoryAware) instance).setBeanFactory(this.beanFactory);
        }
        if (instance instanceof EnvironmentAware) {
            ((EnvironmentAware) instance).setEnvironment(this.environment);
        }
        if (instance instanceof ResourceLoaderAware) {
            ((ResourceLoaderAware) instance).setResourceLoader(this.resourceLoader);
        }
    }
}

这里先判断传入的 instance 对象是否是 Aware 接口?

如果是 Aware 接口,则判断是否是它的 BeanClassLoaderAwareBeanFactoryAwareEnvironmentAwareResourceLoaderAware 这 4 个子接口实现?

如果是,则调用对应的回调方法设置相应参数。

Aware 接口是一个一个标记超接口,它表示一个 bean 有资格通过回调方式从 Spring 容器中接收特定框架对象的通知。具体的方法签名由各个子接口确定,但通常应该只包括一个接受单个参数并返回 void 的方法。

继续往下翻看源码,在 getConfigurationClassFilter 方法最后,我们可以看到它返回了一个内部类 ConfigurationClassFilter 的实例对象。

有了内部类 ConfigurationClassFilter ,接下来就可以开始自动配置组件的过滤操作,主要是通过内部类 ConfigurationClassFilterfilter 方法来实现过滤自动配置组件的功能。

不过在分析 filter 方法之前,我们先了解下内部类 ConfigurationClassFilter 中两个成员变量 :

  • List<AutoConfigurationImportFilter> filters : 上面已介绍,它是 META-INF/spring.factories 中配置的 keyAutoConfigurationImportFilterFilters 列表
  • AutoConfigurationMetadata autoConfigurationMetadata :元数据文件 META-INF/ spring-autoconfigure-metadata.properties 中配置对应实体类,详细分析请看下面。

AutoConfigurationMetadata 自动配置元数据,这个前面没有涉及到,从内部类 ConfigurationClassFilter 的构造函数中,我们可以看到如下:

代码语言:java
AI代码解释
复制
this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(classLoader);

详细代码,由于篇幅受限,这里就不贴了,大家可以自行查看相关源码,从如下的截图中,我们也可以直观了解下。

好了,现在我们进入 filter 方法中,最关键的就是下面 的双层 for 循环处理:

代码语言:java
AI代码解释
复制
List<String> filter(List<String> configurations) {
    long startTime = System.nanoTime();
    String[] candidates = StringUtils.toStringArray(configurations);
    boolean skipped = false;
    // 具体的过滤匹配操作
    for (AutoConfigurationImportFilter filter : this.filters) {
        boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
        for (int i = 0; i < match.length; i++) {
            if (!match[i]) {
                // 不符合过滤匹配要求,则清空当前的自动配置组件
                candidates[i] = null;
                skipped = true;
            }
        }
    }
    // 如果匹配完了,都无需跳过,直接返回当前配置即可
    if (!skipped) {
        return configurations;
    }
    // 有一个不满足过滤匹配要求,都重新处理并返回符合要求的自动配置组件
    List<String> result = new ArrayList<>(candidates.length);
    for (String candidate : candidates) {
        // 如果当前自动配置组件不满足过滤匹配要求,则上面会被清空
        // 因此这里只需判断即可获取符合要求的自动配置组件
        if (candidate != null) {
            result.add(candidate);
        }
    }
    if (logger.isTraceEnabled()) {
        int numberFiltered = configurations.size() - result.size();
        logger.trace("Filtered " + numberFiltered + " auto configuration class in "
                + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
    }
    return result;
}

翻看上面的 filter 方法源码,我们可以很明显地看到,Spring Boot 就是通过如下的代码来实现具体的过滤匹配操作。

代码语言:java
AI代码解释
复制
boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);

在介绍如何实现具体的过滤匹配操作之前,先来看一下 AutoConfigurationImportFilter 接口的源码:

代码语言:java
AI代码解释
复制
@FunctionalInterface
public interface AutoConfigurationImportFilter {
    boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata);
}

上面的 match 方法就是实现具体的过滤匹配操作;

参数:

  • String[] autoConfigurationClasses :待过滤的自动配置类数组
  • AutoConfigurationMetadata autoConfigurationMetadata :自动配置的元数据信息

返回值:

过滤匹配后的结果布尔数组,数组的大小与 autoConfigurationClasses 一致,如果自动配置组件需过滤掉,则设置布尔数组对应值为 false

结合上面的关联类图,我们可以看到 AutoConfigurationImportFilter 接口实际上是由抽象类 FilteringSpringBootCondition 来实现的,另外该抽象类还定义了一个抽象方法 getOutcomes ,然后 OnBeanConditionOnClassConditionOnWebApplicationCondition 继承该抽象类,实现 getOutcomes 方法,完成实际的过滤匹配操作。

抽象类 FilteringSpringBootCondition 的相关源码如下【Spring Boot 2.7.9】:

代码语言:java
AI代码解释
复制
abstract class FilteringSpringBootCondition extends SpringBootCondition
        implements AutoConfigurationImportFilter, BeanFactoryAware, BeanClassLoaderAware {

    // 其他代码省略

    @Override
    public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
        ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory);
        // 调用 由子类实现的 getOutcomes 方法,完成实际的过滤匹配操作
        ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata);
        boolean[] match = new boolean[outcomes.length];
        // 将 getOutcomes 方法返回结果转换成布尔数组
        for (int i = 0; i < outcomes.length; i++) {
            match[i] = (outcomes[i] == null || outcomes[i].isMatch());
            if (!match[i] && outcomes[i] != null) {
                logOutcome(autoConfigurationClasses[i], outcomes[i]);
                if (report != null) {
                    report.recordConditionEvaluation(autoConfigurationClasses[i], this, outcomes[i]);
                }
            }
        }
        return match;
    }

    protected abstract ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
            AutoConfigurationMetadata autoConfigurationMetadata);

    // 其他代码省略
}

通过上面源码可以看出,抽象类 FilteringSpringBootConditionmatch 方法主要是调用 getOutcomes 方法,并将其返回的结果转换成布尔数组。而这个 getOutcomes 方法是过滤匹配的核心功能,由抽象类 FilteringSpringBootCondition 的子类来实现它。

有关 OnBeanConditionOnClassConditionOnWebApplicationCondition 的内容由于篇幅受限,后续 Huazie 会再通过一篇博文详细讲解。

3. 触发自动配置事件

经过上面的排除和过滤之后,我们需要的自动配置类集合已经可以返回了。不过在返回之前,还需要再进行最后一步,触发自动配置导入事件,用来通知所有注册的自动配置监听器进行相关处理。

代码语言:java
AI代码解释
复制
fireAutoConfigurationImportEvents(configurations, exclusions);

进入 fireAutoConfigurationImportEvents 方法,可以看到如下源码:

代码语言:java
AI代码解释
复制
private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) {
    List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
    if (!listeners.isEmpty()) {
        AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions);
        for (AutoConfigurationImportListener listener : listeners) {
            invokeAwareMethods(listener);
            listener.onAutoConfigurationImportEvent(event);
        }
    }
}

接着,我们进入 getAutoConfigurationImportListeners 方法里,它是通过SpringFactoriesLoader 类提供的 loadFactories 方法将 spring.factories 中配置的接口 AutoConfigurationImportListener 的实现类加载出来。

代码语言:java
AI代码解释
复制
protected List<AutoConfigurationImportListener> getAutoConfigurationImportListeners() {
    return SpringFactoriesLoader.loadFactories(AutoConfigurationImportListener.class, this.beanClassLoader);
}

spring.factories 中配置的自动配置监听器,如下所示:

代码语言:properties
AI代码解释
复制
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

然后,将过滤出的自动配置类集合和被排除的自动配置类集合作为入参创建一个 AutoConfigurationImportEvent 事件对象;

其中 invokeAwareMethods(listener); 类似上面的 invokeAwareMethods(filter); 这里不再赘述了。

最后,调用上述自动配置监听器的 onAutoConfigurationImportEvent 方法,并传入上述获取的 AutoConfigurationImportEvent 事件对象,来通知所有注册的监听器进行相应的处理。

那这样做有什么好处呢?

通过触发 AutoConfigurationImportEvent 事件,来通知所有注册的监听器进行相应的处理,我们就可以在导入自动配置类之后,执行一些附加的自定义逻辑或修改自动配置行为。

总结

本篇 Huazie 带大家通读了 Spring Boot 自动装配逻辑的源码,详细分析了自动装配的后续流程,主要包含 自动配置的排除 和 过滤。超过万字,能够看到这的小伙伴,Huazie 在这感谢各位的支持。后续我将持续输出有关 Spring Boot 源码学习系列的博文,想要及时了解更新的朋友,关注这里即可

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
http 策略之 Referrer-Policy
说道referer ,大家想必知道的清楚一些。referer是用来防止 CORS(跨站请求伪造)的一种最常见及有效的方式。对于自身服务器,通过客户端发来的请求中带有的referer信息,可以判断该请求是否来源于本网站。这样就可以一定程度上避免其他网站盗取自身服务器信息,或者可以通过referer来实现广告流量引流,说白了,referer是一种客户端带到服务器的客户端信息,而Referrer-Policy则是客户端对这个带信息策略的配置。
码农编程进阶笔记
2021/12/29
1.7K0
Referrer Policy那些事
今天还是像往常一样看自己的博客。突然发现我的网站统计显示某文章访问次数有1W+,心里不由得惊喜一番。结果发现每篇文章都是1W+。访问次数统计用的是不蒜子提供的免费服务(https://busuanzi.ibruce.info/)。难道出问题了?不由得打开f12看了下,发现请求的referrer为https://huai.pub/而不是某文章的具体页面。
yumusb
2020/10/26
2.9K0
Chrome 新的默认 Referrer-Policy : strict-origin-when-cross-origin
如果你的站点有使用 Referer 标头收集网页的访问来源信息,则此策略变化可能对你的程序造成影响,请仔细阅读。
ConardLi
2020/11/19
105.8K0
Chrome 新的默认 Referrer-Policy : strict-origin-when-cross-origin
【网络安全】前端程序员务必掌握的图片防盗链
在 http 协议请求中 header 里会带个 Referer 字段。通过图片服务器检查 Referer 是否来自规定的域名(白名单),而进行防盗链。 在浏览器中输入防盗链图片地址是能直接访问的。
andyhu
2022/12/14
9570
浅析HTTP请求中的referrer和Referrer-Policy
本文将介绍一个涉及安全和隐私的http请求头中的字段—referrer,以及如何通过Referrer Policy去修改referrer的值或者是显示与否。
码农编程进阶笔记
2021/12/27
2.9K0
浅析HTTP请求中的referrer和Referrer-Policy
<meta name=“referrer“ content=“never“>简介
注意:此方法会破坏浏览器原有的 referer 策略,所有从包含了上面标签的页面中发起的请求将不会携带referer。慎用!
无刺鱼
2022/03/29
1.9K0
【html】referrer值的设置小记
当html页面中引入跨域的资源时(image,js,css等),可在html的header中加上
一朵灼灼华
2022/08/05
6.4K0
HTTP系列之Referer和Referrer policy简介
referrer policy是unsafe url的,ok,下面介绍一下Referer和referrer policy
SmileNicky
2019/11/04
4.2K0
利用 leancloud 记录 referrer 访问来源
最近 leancloud 后台记录了部分页面访问是从 pages 目录来访的,这个目录是之前还没使用二级域名 blog 前使用的,后面加了 blog 之后就不再用 pages 目录来访问页面了,现在想要了解访问 pages 目录的源头都在哪里(一般在谷歌)一般来说,通过前端统计信息比较有限,我们想要拿到一部分访问者的信息需要一些特定的 api ,比如访问 ip 地段,用户基本访问源等等,这里主要聊下如何将获取到的信息储存到 leancloud 数组内。
2Broear
2024/03/12
3470
利用 leancloud 记录 referrer 访问来源
一个拼写错误让整个互联网一起犯错
在 Web 开发的世界里,有这样一个字段——它每天默默地工作着,记录着用户的来源,保护着网站的安全,却因为一个历史性的拼写错误而成为了程序员们茶余饭后的谈资。它就是 HTTP 头部中的 Referer 字段。
wayn
2025/06/19
2510
一个拼写错误让整个互联网一起犯错
Web应用服务器安全:攻击、防护与检测
点击劫持,clickjacking 是一种在网页中将恶意代码等隐藏在看似无害的内容(如按钮)之下,并诱使用户点击的手段,又被称为界面伪装(UI redressing)。例如用户收到一封包含一段视频的电子邮件,但其中的“播放”按钮并不会真正播放视频,而是被诱骗进入一个购物网站。
RiboseYim
2018/01/20
4.3K0
学习 HTTP Referer
HTTP 中 Referer 字段在工作中或许并不会吸引你的注意,隐藏在 Network 的请求之下,但是却有着非常重要的作用。平常你一定会遇到一些问题需要去排查,假如这个问题在你排查完全部代码后,依然没有解决,这个时候你会怎么办?此时我们就需要将排查问题的角度转换一下,切换到 HTTP 协议上。
每周聚焦
2022/08/10
2K0
HTTP Referer 教程
互联网也是一样,你不会无缘无故访问一个网页,总是有人告诉你,可以去那里看看。服务器也想知道,你的"引荐人"是谁?
ruanyf
2020/01/22
2.9K0
H5 Http请求403 - Referrer Policy
    表示资源不可用。服务器理解客户的请求,但拒绝处理它,通常由于服务器上文件或目录 的权限设置导致的WEB访问错误。
White feathe
2021/12/08
1.5K0
H5 Http请求403 - Referrer Policy
WEB安全防护相关响应头(下)
前篇“WEB安全防护相关响应头(上)”中,我们分享了 X-Frame-Options、X-Content-Type-Options、HTTP Strict Transport Security (HSTS) 等安全响应头的内容。下文中,我们则侧重介绍一些和跨站安全相关的响应头——
天存信息
2021/06/07
3.2K0
WEB安全防护相关响应头(下)
解决 strict-origin-when-cross-origin 问题
使用90版本之后的谷歌浏览器, 在部署前端项目后, 调用后端接口出现 strict-origin-when-cross-origin, 并且静态资源被拦截的情况
时间静止不是简史
2023/02/23
16K0
解决 strict-origin-when-cross-origin 问题
Geekpwn 2020云端挑战赛 Noxss & umsg
前两天看了今年Geekpwn 2020 云端挑战赛,web题目涉及到了几个新时代前端特殊技巧,可能在实战中利用起来难度比较大,但是从原理上又很符合真实世界的逻辑,这里我们主要以解释题目为主,但是也探索一下在真实场景下的利用。
LoRexxar
2023/02/21
6170
Geekpwn 2020云端挑战赛 Noxss & umsg
国产github崩了?是防盗链啦~
右键复制这张图片的地址,放到一个第三方的在线编辑器中,发现图片变成gitee的logo了
用户9899350
2022/07/29
7590
国产github崩了?是防盗链啦~
meta 标签知多少
meta 标签提供该页面的一些信息,比如针对搜索引擎和更新频度的描述和关键词,它还可以控制页面缓冲、响应式窗口等,定义 meta 标签有利于网站 SEO(有利于搜索引擎访问),对于响应式窗口也起着作用,因此 meta 标签是 HTML 中很重要的一个标签。在生成默认的 HTML 文档结构时,通常会有两个 meta 标签:
多云转晴
2020/02/19
1.2K0
meta 标签知多少
防盗链
网站资源都有域的概念,浏览器加载一个站点时,首先加载这个站点的首页,一般是index.html或者index.php等。页面加载,如果仅仅是加载一个index.html页面,那么该页面里面只有文本,最终浏览器只能呈现一个文本页面。丰富的多媒体信息无法在站点上面展现。
Tom2Code
2022/04/15
1.9K0
防盗链
相关推荐
http 策略之 Referrer-Policy
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档