SpringBoot 快速搭建、自动流程进阶、装配机制、功能扩展点详解
Spring Boot 启动
Spring Boot 是 Spring 旗下的一个子项目,其设计目的是简化 Spring 应用的初始搭建及开发过程,Spring Boot 可以快速启动和运行你的 Spring 应用服务。
Spring Boot 概述
Spring Boot 本质上是基于 Spring 内核的一个快速开发框架,是“约定优先于配置”理念下的最佳实践,通过解析 Spring Boot 的启动过程,可以帮助我们逐渐了解它的工作机制和其背后整合 Spring 快速开发的实现原理。
磨刀不误砍柴工
在开始讲解 Spring Boot 之前,首先让我们从整体架构上认识 Spring 家族,正所谓“不知全局者不足以谋一域”,如下图所示是 Spring Boot 与 Spring 生态的关系。
Spring Core:Spring Core 是 Spring 框架的核心模块,集成在 Spring 框架中,提供了我们熟知的控制反转机制。
Spring 的核心是管理轻量级的 JavaBean 组件,提供对组件的统一生命周期和配置组装服务管理,如下图所示。
Spring 框架:Spring 框架的核心就是,控制反转和面向切面(AOP)机制,同时它为开发者了提供众多组件,包括 Web 容器 组 件 ( Spring Web MVC ) 、 数 据 接 入 组 件 ( SpringDAO)、数据对象映射组件(Spring ORM)等。这些组件基于 Spring Core 的 IoC 容器开发,同时 Spring 框架可以配置管理所有轻量级的 JavaBean 组件和维护众多 JavaBean 组件之间的关系。简单地说,Spring 为开发者提供了一个一站式的轻量级开发框架平台。
● Spring Boot:Spring Boot 是一个微服务框架,以“Boot”命名,很好地说明这个框架的初衷——快速启动。Spring Boot 从代码结构上来说包含了 Spring 框架,或者说是在 Spring 框架基础上做的一个扩展。它在延续 Spring 框架的核心思想和设计理念的基础上,简化了应用的开发和组件的集成难度。
Spring Boot 是为了简化 Spring 应用的创建、运行、调试、部署等特性而出现的,使用 Spring Boot 脚手架可以让微服务开发者做到专注于业务领域的开发,无须过多地关注底层技术实现细节。
● Spring 中的 IoC 机制与 JavaConfig 的关系:我们知道,SpringIoC 机 制 是 Spring 框 架 的 核 心 , 通 过 控 制 反 转 机 制 实 现 JavaBean 组件和 JavaBean 组件依赖关系的管理。如果不使用 IoC 技术,开发者需要手动创建、查找、管理业务逻辑对象和依赖。
如果说“程序=算法+数据”,那么这里我们可以把这些 JavaBean 组件看作我们需要维护的数据,当数据(对象)规模膨胀时,将给我们的应用带来极大的耦合度和复杂度。而通过 Spring IoC 容器可以方便地管理我们的对象。
下图是 Spring IoC 容器给开发人员带来的编程模型的转变,它可以降低程序代码之间的耦合度,将耦合的对象依赖关系从代码中移除,通过将对象和依赖关系放在注解(或者 XML 配置文件)中,将程序对组件的控制权转交给 IoC 容器,进行统一管理。开发者只需要专注于业务的 JavaBean 组件的实现,查找逻辑和依赖逻辑全部由 Spring IoC 容器帮助打理。
在 Spring 3.0 之前,JavaBean 组件一直是通过 XML 配置文件来配置管理的,Spring 3.0 之后为我们提供了 Java 代码形式(JavaConfig)的配置功能。JavaConfig 功能从 Spring 3.0 以后已经包含在了 Spring 的 核 心 模 块 中 ( JavaConfig 并 非 Spring Boot 新 特 性 ) , 可 以 说 JaveConfig 就是 Spring IoC 容器的纯 Java 实现版本。在 Spring Boot 中,JavaConfig 已经完全代替 applicationContext.xml,实现了 XML 的零配置,如下所示是两种不同配置模式示例。
● 基于 XML 配置文件方式
基于 JavaConfig 方式
JavaConfig 可以被看成一个 XML 文件,只不过是用 Java 代码编写的。
JavaConfig 的优势
上面我们介绍了 Spring 实现的 JavaBean 管理模式,主要有 XML 配置文件和 JavaConfig 两种方式,Spring Boot 采用的 JavaConfig 主要有下面几个优点。
● 面向对象配置:由于配置被定义在 JavaConfig 中的类中,可以充分使用 Java 面向对象的功能,用户可以实现配置继承、配置重写等面向对象特性。
● 减少大量滥用 XML:由于 Spring 把所有逻辑业务类都以 XML 配置文件的形式来表达 Bean,造成 XML 文件充斥整个项目,带来了开发、维护的复杂性,开发人员需要频繁地在 XML 和 Java 语言之间来回切换。
● 类 型 安 全 和 重 构 支 持 : 因 为 注 释 在 类 源 代 码 中 , 所 以 JavaConfig 为应用提供了类型安全的方法来配置管理 Spring 容器,由于 Java 对泛型的支持,我们可以按照类型而不是名称检索 JavaBean,这带来了更大的灵活性和重构支持。
本节我们带大家简单地回顾了 Spring 框架的整体架构,这些都是“开胃菜”,下面让我们看看 Spring Boot 是如何创建一个独立运行、生产级别的 Spring 应用的。
Spring Boot 快速搭建
1.开发环境准备工作
在开始之前,我们需要搭建 IDE(集成开发环境),目前流行的 IDE 有 IntelliJ IDEA、Spring Tools、Visual Studio Code 和 Eclipse 等 。 Java Development Kit ( JDK ) 我 们 推 荐 使 用 OpenJDK 8 或 者 OpenJDK 11。
2.使用 Spring-Initializr 快速构建工程
我们可以通过 Spring 官方提供的 Spring Initializr 来构建 Spring Boot 项目,它不仅完美支持 IDEA 和 Eclipse,而且能自动生成启动类和单元测试代码,给开发人员带来了极大的便利。如下图所示是 Spring 官方的 Spring Boot 构建工程模板,你可以使用 Maven 或者 Gradle 进行初始化项目的构建。你需要填写 Group、Artifact 等工程元数据信息,最后单击 GENERATE 按钮生成 Spring Boot 模板工程。
3.静态工程模板
Spring Boot 静态工程目录模板示例如下:
4.增加启动代码,开始 Spring Boot 的启动流程
5.运行 Spring Boot 应用
@SpringBootApplication 注解详解
下面我们通过源码了解 Spring Boot 是如何工作的。首先我们看一下 @SpringBootApplication 注解,它是用来标注主程序的,表明这是一个 Spring Boot 应用,也是一个组合注解,主要由下面三个注解组成:
● @SpringBootConfiguration
● @EnableAutoConfiguration
● @ComponentScan
@SpringBootApplication 注解定义如下:
@SpringBootConfiguration 注解解析
@SpringBootConfiguration 注解代码如下:
@SpringBootConfiguration 来源于 @Configuration,二者的功能都是将当前类标注为配置类,并将类中以 @Bean 注解标记的方法的实例注入 Spring 容器。@Configuration 是 JavaConfig 形式的 Spring 容器的配置类所使用的。
@EnableAutoConfiguration 注解解析
@EnableAutoConfiguration 注解代码如下:
@EnableAutoConfiguration 可以说是 Spring Boot 启动注解的主角,其中最关键的注解是 @Import(AutoConfigurationImportSelector.class)。
借助 AutoConfigurationImportSelector 类,注解 @EnableAutoConfiguration 可以帮助 Spring Boot 应用将所有符合条件的 @Configuration 配置加载到当前 Spring Boot 创建并使用的 Spring 容器中。
注意:在 SpringBoot1.5 以后,EnableAutoConfigurationImportSelector 类已经被 AutoConfigurationImportSelector 类所取代。
下 面 我 们 看 一 下 AutoConfigurationImportSelector 类 中 的 selectImports 方法是如何实现自动加载配置类的,源码如下:
getCandidateConfigurations 方法如下:
getCandidateConfigurations 方法的主要逻辑是调用 Spring 框架提供的一个工具类 Spring-FactoriesLoader。SpringFactoriesLoader 是 Spring 内部提供的一种约定俗成的加载配置类的方式,使用 SpringFactoriesLoader 可 以 从 指 定 classpath 下 读 取 METAINF/spring.factories 文件的配置,并返回一个字符串数组。通过这个方法,所有自动配置类都会自动加载到 Spring 容器中。
如下图所示是一个 Spring Boot 应用启动过程的内存快照,可以看到,在“配置列表对象”中除了 Spring 自带的配置类,还有第三方的自动配置类。我们可以根据 SpringFactoriesLoader 规定的协议自定义配置类。
上面框线标注的配置类对应下面的 META-INF/spring.factories 配置文件,这个 Properties 格式的文件中主键(Key)可以是接口、注解、抽象类的全名,值(Value)是以“,”分割的实现类,如下所示:
总 结 一 下 , @EnableAutoConfiguration 的 作 用 及 SpringFactoriesLoader 启动加载配置类流程如下:
(1)从 classpath 中搜索所有 META-INF/spring.factories 配置文件,然后将其中 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的 Key 对应的配置项加载到 Spring 容器。
(2)@EnableAutoConfiguration 可以排除配置选项,排除方式有两 种 , 一 种 方 式 是 在 使 用 @SpringBootApplication 注 解 时 , 使 用 exclude 属性排除指定的类,代码如下:
另外一种方式是:单独使用 @EnableAutoConfiguration 注解,其内部的关键代码实现如下:
@ComponentScan 注解解析
@ComponentScan 注解代码如下:
@ComponentScan 注解本身是 Spring 框架加载 Bean 的主要组件,它并不是 Spring Boot 的新功能,这里不对 @ComponentScan 扫描和解析 Bean 的过程进行详细说明,感兴趣的读者可以自行查阅资料进行了解。
@ComponentScan 注解的作用总结一句话就是:定义扫描路径,默认会扫描该类所在的包下所有符合条件的组件和 Bean 定义,最终将这些 Bean 加载到 Spring 容器中。下面是我们总结的 @ComponentScan 的主要使用方式:
● @ComponentScan 注解默认会装配标识了 @Component 注解的类到 Spring 容器中。
● 通过 basepackage 可以指定扫描包的路径。
● 通过 includeFilters 将扫描路径下没有以上注解的类加入 Spring 容器。
● 通过 excludeFilters 过滤出不用加入 Spring 容器的类。
Spring Boot 启动流程进阶
每一个 Spring Boot 程序都有一个主入口,这个主入口就是 main 方法,而 main 方法中都会调用 SpringBootApplication.run 方法,一个快速了解 SpringBootApplication 启动过程的好方法就是在 run 方法中打一个断点,然后通过 Debug 的模式启动工程,逐步跟踪了解 SpringBoot 源码是如何完成环境准备和启动加载 Bean 的。
查看 SpringBootApplication.run 方法的源码就可以发现 SpringBoot 的启动流程主要分为两个大的阶段:初始化 SpringApplication 和运行 SpringApplication。而运行 SringApplication 的过程又可以细化为下面几个部分,后面我们会对启动的主要模块加以详解。
初始化 SpringApplication
步骤 1 进行 SpringApplication 的初始化,配置基本的环境变量、资 源 、 构 造 器 、 监 听 器 。 初 始 化 阶 段 的 主 要 作 用 是 为 运 行 SpringApplication 对象实例启动做环境变量准备以及进行必要资源构造器的初始化动作,代码如下:
SpringApplication 构造方法的核心是 this.initialize(sources)初始化方法,SpringApplication 通过调用该方法完成初始化工作。deduceWebEnvironment 方法用来判断当前应用的环境,该方法通过获取两个类来判断当前环境是否是 Web 环境。
而 getSpringFactoriesInstances 方法主要用来从 spring.factories 文件中找出 Key 为 ApplicationContextInitializer 的类并实例化,然后调用 setInitializers 方法设置到 SpringApplication 的 initializers 属性中,找到它所有应用的初始化器。接着调用 setListeners 方法设置应用监听器,这个过程可以找到所有应用程序的监听器,最后找到应用启动主类名称。
运行 SpringApplication
步骤 2 Spring Boot 正式地启动加载过程,包括启动流程监控模块、配置环境加载模块、ApplicationContext 容器上下文环境加载模块。refreshContext 方法刷新应用上下文并进行自动化配置模块加载,也就是上文提到的 SpringFactoriesLoader 根据指定 classpath 加载 META-INF/spring.factories 文件的配置,实现自动配置核心功能。
运行 SpringApplication 的主要代码如下:
1.SpringApplicationRunListeners 应用启动监控模块
应用启动监控模块对应上述步骤 2.1,它创建了应用的监听器 SpringApplicationRunListeners 并 开 始 监 听 , 监 听 模 块 通 过 调 用 getSpringFactoriesInstances 私有协议从 METAINF/spring.factories 文件中取得 SpringApplicationRunListeners 监听器实例。
当前的事件监听器 SpringApplicationRunListeners 中只有一个 EventPublishingRunListener 广播事件监听器,它的 Starting 方法会封装成 SpringApplicationEvent 事件广播出去,被 SpringApplication 中配置的 listeners 所监听。这一步骤执行完成后也会同时通知 SpringBoot 其他模块目前监听初始化已经完成,可以开始执行启动方案了。
2.ConfigurableEnvironment 配置环境模块和监听
对应上述步骤 2.2,下面是分解步骤说明。
(1)创建配置环境,对应上述步骤 2.2.1,创建应用程序的环境信息。如果是 Web 程序,创建 StandardServletEnvironment,否则创建 StandardEnvironment。
(2)加载属性配置文件,对应上述步骤 2.2.2,将配置环境加入监 听 器 对 象 中 (SpringApplicationRunListeners ) 。 通 过 configurePropertySources 方法设置 properties 配置文件,通过执行 configureProfiles 方法设置 profiles。
(3)配置监听,对应上述步骤 2.2.3,发布 environmentPrepared 事件,即调用 ApplicationListener 的 onApplicationEvent 事件,通知 Spring Boot 应用的 environment 已经准备完成。
3.ConfigurableApplicationContext 配置应用上下文
对应上述步骤 2.3,下面是分解步骤说明。
(1)配置 Spring 应用容器上下文对象,对应上述步骤 2.3.1,它的作用是创建 run 方法的返回对象 ConfigurableApplicationContext(应用配置上下文),此类主要继承 了 ApplicationContext 、 Lifecycle 、 Closeable 接 口 , 而 ApplicationContex 是 Spring 框架中负责 Bean 注入容器的主要载体,负责 Bean 加载、配置管理、维护 Bean 之间的依赖关系及 Bean 的生命周期管理。
(2)配置基本属性,对应上述步骤 2.3.2,prepareContext 方法将 listeners、environment、banner、applicationArguments 等重要组件与 Spring 容器上下文对象相关联。借助 SpringFactoriesLoader 查找可用的 ApplicationContextInitializer,它的 initialize 方法会对创 建 好 的 ApplicationContext 进 行 初 始 化 , 然 后 它 会 调 用 SpringApplicationRunListener 的 contextPrepared 方法,此时 SpringBoot 应用的 ApplcaionContext 已经准备就绪,为刷新应用上下文准备好容器。
( 3 ) 刷 新 应 用 上 下 文 , 对 应 上 述 的 步 骤 2.3.3 ,refreshContext(context)方法将通过工厂模式产生应用上下文环境中所需要的 Bean。实现 spring-boot-starter-*(mybatis、redis 等)自动化配置的关键,包括 spring.factories 的加载、Bean 的实例化等核心工作。最后 SpringApplicationRunListener 调用 finished 方法告诉 Spring Boot 应用程序容器已经完成 ApplicationContext 装载。
Spring Boot 自动装配机制
Spring Boot 的快速发展壮大,得益于“约定优于配置”的理念。
Spring Boot 自 动 装 配 流 程 中 最 核 心 的 注 解 是 @EnableAutoConfiguration,在上一节的启动流程中我们已经讲过,它 可 以 借 助 SpringFactoriesLoader“ 私 有 协 议 特 性 ” 将 标 注 了 @Configuration 的 JavaConfig 全部加载到 Spring 容器中,而如果是基于条件的装配及调整顺序的 Bean 装配,需要 Spring Boot 有额外的自动化装配机制。下面从 @EnableAutoConfiguration 开始进阶讲解,加深我们对 Spring Boot 自动装配机制的认识。
基于条件的自动装配
下面是 @EnableAutoConfiguration 注解,它同样是一个组合注解:
从源码可见,最关键的就是 @Import(AutoConfigurationImportSelector.class)注解的实现。借助 EnableAutoConfigurationImportSelector 模块,@EnableAutoConfiguration 可以帮助 Spring Boot 应用将所有符合条件的 @Configuration 配置都加载到当前的容器中。同时借助 Spring 框架原有的底层工具 SpringFactoriesLoader(服务发现机制)和根据特定条件装备 Bean 的 Conditionxxx 条件注解实现智能的自动化配置工作。
Bean 的加载过滤过程主要是通过下面的方法实现的。
源码解析如下:
(1)执行 AutoConfigurationMetadataLoader.loadMetadata ( this.beanClassLoader)会加载 META-INF/spring-autoconfiguremetadata.properties 下的所有配置信息。
(2)执行 getCandidateConfigurations(annotationMetadata,attributes)会加载所有包下 META-INF/spring.factories 的信息并组装成 Map,然后读取 Key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的数组,并将这个数组返回。
(3)执行 getExclusions(annotationMetadata,attributes)会获取限制候选配置的所有排除项(找到不希望被自动装配的配置类)。
( 4 ) 执 行 checkExcludedClasses ( configurations ,exclusions)会对参数 exclusions 进行验证并去除多余的类,它对应 @EnableAutoConfiguration 注解中的 exclusions 属性。
(5)执行 filter(configurations,autoConfigurationMetadata ) 会 根 据 项 目 中 配 置 的 AutoConfiguration-ImportFilter 类进行配置过滤。
通过查看源码,我们可以发现 AutoConfigurationImportFilter 是一个接口,OnClassCondition 才是它的实现类,而 OnClassCondition 就是 Spring Boot 的 Condition 实现类。@ConditionalOnClass 代码如下:
@ConditionalOnClass 是基于 @Conditional 的组合注解,在上述的第(5)步中,Spring Boot 可以通过这个注解实现按需加载,只有在 @Configuration 中符合条件的 Class 才会被加载进来。@Conditional 注解本身是一个元注解,用来标注其他注解,如下所示:
通过利用 @Conditional 元注解,可以构造满足自己条件的组合条件注解,Spring Boot 正是通过这样的方式实现了众多条件注解,实现了基于条件的 Bean 构造,还有 Bean 相互依赖情况下的顺序加载,它不需要再通过显性的基于 XML 文件的依赖文件进行构造。从上述讲解我们可以知道,Spring Boot 结合 Java 元注解概念、Spring 底层容器配置机制,以及使用类似 Java SPI(Service Provider Interface)机制实现的私有配置加载协议,最终实现了“约定优于配置”。
在 Spring Boot 的 Autoconfigure 模块中,还包含了一批这样的组合注解,这些条件的限制在 Spring Boot 中以注解的形式体现,通常这些条件注解使用 @Conditional 来配合 @Configuration 和 @Bean 等注解来干预 Bean 的生成,常见的条件注解如下。
● @ConditionalOnBean:Spring 容器中存在指定 Bean 时,实例化当前 Bean。
● @ConditionalOnClass:Spring 容器中存在指定 Class 时,实例化当前 Bean。
● @ConditionalOnExpression:使用 SpEL 表达式作为判断条件,满足条件时,实例化当前 Bean。
● @ConditionalOnJava:使用 JVM 版本作为判断条件来实例化当前 Bean。
● @ConditionalOnJndi:在 JNDI 存在时查找指定的位置,满足条件时,实例化当前 Bean。
● @ConditionalOnMissingBean:Spring 容器中不存在指定 Bean 时,实例化当前 Bean。
● @ConditionalOnMissingClass : Spring 容 器 中 不 存 在 指 定 Class 时,实例化当前 Bean。
● @ConditionalOnNotWebApplication:当前应用不是 Web 项目时,实例化当前 Bean。
● @ConditionalOnProperty:指定的属性是否有指定的值。
● @ConditionalOnResource:类路径是否有指定的值。
● @ConditionalOnSingleCandidate:指定 Bean 在 Spring 容器中只有一个。
● @ConditionalOnWebApplication:当前应用是 Web 项目时,则实例化当前 Bean。
有了组合注解,开发人员从大量的 XML 和 Properties 中得到了解放,可以抛弃 Spring 传统的外部配置,使用 Spring 自动配置,springboot-autoconfigure 依赖默认配置项,根据添加的依赖自动加载相关的配置属性并启动依赖。应用者只需要引入对应的 jar 包,SpringBoot 就可以自动扫描和加载依赖信息。调整自动配置顺序在 Spring Boot 的 Autoconfigure 模块中还可以通过注解对配置和组件的加载顺序做出调整,从而可以让这些存在依赖关系的配置和组件顺利地在 Spring 容器中被构造出来。
● @AutoConfigureAfter 是 spring-boot-autoconfigure 包下的注解,其作用是将一个配置类在另一个配置类之后加载。
● @AutoConfigureBefore 是 spring-boot-autoconfigure 包下的注解,其作用是将一个配置类在另一个配置类之前加载。
例如,在加载 ConfigurationB 之后加载 ConfigurationA:
○ 实现 ConfigurationA.class
○ 实现 ConfigurationB.class
○ 创建配置 META-INF/spring.factories 文件
通过上面的步骤,就可以实现自动调整 Bean 的加载顺序。另外,Spring 为我们提供了 @AutoConfigureOrder 注解,也可以修改配置文件的加载顺序,示例代码如下:
自动化配置流程
无论是应用初始化还是具体的执行过程,都要调用 Spring Boot 自动配置模块,下图有助于我们形象地理解自动配置流程。
例 如 ,mybatis-spring-boot-starter 、 spring-boot-starterweb 等组件的 META-INF 下均含有 spring.factories 文件,在自动配置模块中,SpringFactoriesLoader 收集到文件中的类全名并返回一个类全名的数组,返回的类全名通过反射被实例化,就形成了具体的工厂实例,最后工厂实例来生成组件所需要的 Bean。
Spring Boot 功能扩展点详解
在深入分析了 Spring Boot 的启动过程及其自动装配原理后,我们发现,Spring Boot 的启动过程中使用了“模板”模式和“策略”模式,并且利用 SpringFactoreisLoader 的“私有协议”可以完成很多功能的扩展,满足启动的定制化。
我们将这些主要的扩展点结合源码加以总结,如下图所示。
@EnableAutoConfiguration
从 功 能 扩 展 点 的 角 度 , @EnableAutoConfiguration 借 助 SpringFactoriesLoader 可 以 将 标 注 了 @Configuration 注 解 的 JavaConfig 类汇总并加载到最终的 ApplicationContext 中,使用条件注解可以在自动化配置过程中定制化 Bean 的加载过程:
ApplicationListener
ApplicationListener 属于 Spring 框架,它是对 Java 中监听模式的一种实现方式,如果需要为 Spring Boot 应用添加我们自定义的 ApplicationListener,那么有两种方式:
● 通 过 SpringApplication.addListeners ( … ) 或 者 SpringApplication.setListener(…)方法添加一个或者多个自定义的 ApplicationListener。
● 借助 SpringFactoriesLoader 机制,在 Spring Boot 项目自定义的 META-INF/spring.factories 文 件 中 添 加 配 置 , 以 下 是 Spring Boot 默认的 ApplicationListener 配置:
SpringApplicationRunListener
SpringApplicationRunListener 的作用是,在整个启动流程中,作为监听者接收不同执行点的事件通知。没有特殊情况一般不需自定义 的 SpringApplicationRunListener 。 SpringApplicationRunListener 源码如下:
ApplicationContextInitializer
ApplicationContextInitializer 也属于 Spring 框架,它的主要作用是,在 ConfigurableApplicationContext 类型(或者子类型)的 ApplicationContext 做刷新(refreshContext)之前,允许我们对 ConfiurableApplicationContext 的实例做进一步的设置和处理。
不 过 一 般 情 况 下 我 们 不 需 要 自 定 义 一 个 ApplicationContextInitializer,Spring Boot 框架默认也只有以下四个实现而已:
CommandLineRunner
CommandLineRunner 并不是 Spring 框架原有的概念,它属于 SpringBoot 应用特定的回调扩展接口,源码如下:
所有 CommandLineRunner 的执行时间点是在 Spring Boot 应用完全初始化之后(这里我们可以认为是 Spring Boot 应用启动类 main 方法执行 完 成 之 前 的 最 后 一 步 ) 。 当 前 Spring Boot 应 用 的 ApplicationContext 中的所有 CommandLineRunner 都会被加载并执行。
评论