【重写SpringFramework】组件加载与AnnotationConfigApplicationContext(chapter 3-4)
1. 前言
上一节介绍了 Spring 是如何将资源读取到内存中的,这里的资源是指广泛存在于各种介质中的数据。对于字节码文件来说,只拿到输入流或字节数组还不够,还需要知道类的信息。一是 JVM 类加载机制,二是通过 ASM 框架,Spring 对这两种解析方式进行了统一的抽象,相关的 API 定义在 Spring 核心包中。
在加载 class 文件、解析类的信息这两步之后,最后一步注册 BeanDefinition
的操作是现成的,调用 BeanDefinitionRegistry
接口的 registerBeanDefinition
方法即可。现在的问题是,之前我们一直使用 RootBeanDefinition
来创建实例,但这只是方便测试的权宜之计,一个大型应用可能有成百上千个组件,我们不可能一个个地手动创建。鉴于此,Spring 提供了强大易用的组件,不仅可以批量地处理字节码文件,还可以将以上三个步骤封装起来,提供一站式的解决方案。
2. AnnotatedBeanDefinition
在 BeanDefinition
的继承体系中,AnnotatedBeanDefinition
接口的作用是以注解声明的方式获取 BeanDefinition
。由于一部分实现类在 context 模块,当时没有展开来讲。AnnotatedBeanDefinition
接口定义了两个方法,简单介绍如下:
getMetadata
方法:获取类的元数据getFactoryMethodMetadata
方法:获取工厂方法的元数据(部分子类不支持工厂方法,直接返回 null)
public interface AnnotatedBeanDefinition extends BeanDefinition {
AnnotationMetadata getMetadata();
MethodMetadata getFactoryMethodMetadata();
}
AnnotatedBeanDefinition
接口有三个实现类,它们的结构和功能基本类似,主要是从用途和解析方式两个方面来分析。其特点如下:
AnnotatedGenericBeanDefinition
:加载指定的类或元数据,支持反射和 ASM 解析(存在于 beans 模块中)ScannedGenericBeanDefinition
:通过扫描的方式加载BeanDefinition
,仅支持 ASM 解析ConfigurationClassBeanDefinition
:仅限配置类内部使用,负责处理 BeanMethod,支持反射和 ASM 解析(本节不涉及)
3. AnnotatedBeanDefinitionReader
3.1 概述
AnnotatedBeanDefinitionReader
组件的作用是加载指定的 Bean,从功能上看与手动创建 BeanDefinition
实例差不多,但实际上有两点区别:
- 自动注册了常用的组件,比如处理依赖注入和生命周期的
BeanPostProcessor
组件 - 支持对
@Role
、@Primary
等辅助性注解进行解析,并设置成BeanDefinition
对应的role
、primary
等属性
AnnotatedBeanDefinitionReader
持有一个 BeanDefinitionRegistry
实例,实际上是 Spring 容器,用于注册 BeanDefinition
。在构造方法中,通过 AnnotationConfigUtils
工具类注册了常用的 BeanPostProcessor
组件。
public class AnnotatedBeanDefinitionReader {
private final BeanDefinitionRegistry registry;
public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
this.registry = registry;
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
}
AnnotationConfigUtils
是一个工具类,提供了与注解配置相关的方法。registerAnnotationConfigProcessors
方法的作用是注册相关的 BeanPostProcessor
,比如支持依赖注入以及生命周期相关的组件。
public class AnnotationConfigUtils {
public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) {
//注册支持@Autowired和@Value注解的组件
if(!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)){
RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME);
}
//注册支持@PostConstruct和@PreDestroy注解的组件
if (!registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(InitDestroyAnnotationBeanPostProcessor.class);
registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME);
}
}
}
3.2 register方法
register
方法用于注册指定的类,参数是 Class
类型,说明是通过反射的方式来加载类。
public void register(Class<?>... annotatedClasses) {
for (Class<?> annotatedClass : annotatedClasses) {
registerBean(annotatedClass, null);
}
}
registerBean
方法完成了实际的加载逻辑,整个过程分为三步,如下所示:
-
根据条件判断是否加载,有关条件判定的内容将在后续介绍,先略过。
-
调用工具类
AnnotationConfigUtils
的processCommonDefinitionAnnotations
方法,根据@Role
、@Primary
等注解来设置相关属性。 -
生成 Bean 的名称,将
BeanDefinition
注册到 Spring 容器中。
private void registerBean(Class<?> annotatedClass, String name) {
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);
//1. 根据条件判定Bean是否加载(TODO 先略过,详见条件判定一节)
//2. 根据@Role、@Primary等注解的信息来设置BeanDefinition的相关属性
AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
//3. 注册BeanDefinition
String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd));
registry.registerBeanDefinition(beanName, abd);
}
4. ClassPathBeanDefinitionScanner
4.1 概述
ClassPathBeanDefinitionScanner
的作用是扫描指定目录及其子目录下的文件,对符合条件的类进行过滤,并注册到 Spring 容器中。扫描组件是一个批量操作,Spring 提供了一组注解来标识哪些类应该被加载。@Component
是一个元注解,可以声明在子注解上,因此 @Component
代表的是一组注解。cn.stimd.spring.context.stereotype
目录中定义了一部分子注解,如下所示:
-
@Controller
表示该组件是一个处理网络请求的接口类 -
@Service
表示组件是一个业务类 -
@Repository
表示组件与持久化有关,比如数据库相关的增删改查等操作
这些注解的共同特点是携带「语义化」,所谓语义化是指组件在系统内充当了某一角色。与之对比,@Component
没有具体的语义,泛指所有可以被 Spring 容器管理的组件。理论上,我们可以使用 @Component
注解完成所有的工作,但对一部分有着特殊用途的组件进行分类是良好的编程风格。
4.2 基本结构
ClassPathBeanDefinitionScanner
是 ClassPathScanningCandidateComponentProvider
的子类,两者的分工不同,父类只负责扫描类文件并转换成 BeanDefinition
集合,子类则将所有流程整合在一起。父类 ClassPathScanningCandidateComponentProvider
的主要属性如下所示:
includeFilters
:包含规则,允许哪些类被加载,比如声明了@Component
等注解excludeFilters
:排除规则,不允许哪些类被加载,这个需要自定义的指定resourcePatternResolver
:用于路径解析和资源加载metadataReaderFactory
:创建基于 ASM 元数据解析的工厂
一个目录下可能有多个类,那么哪些类应该加载,哪些类不应该加载,需要一个判断的依据。Spring 提供了 TypeFilter
来对类型进行过滤,includeFilters
属性是允许加载的过滤器集合,excludeFilters
是禁止加载的过滤器集合。为了方便称呼,将它们称为「包含规则」和「排除规则」。registerDefaultFilters
是一个重要的方法,作用是向包含规则中添加了一个过滤器,允许加载声明了 @Component
注解的类。
public class ClassPathScanningCandidateComponentProvider implements ResourceLoaderAware {
private final List<TypeFilter> includeFilters = new LinkedList<>();
private final List<TypeFilter> excludeFilters = new LinkedList<>();
private ResourcePatternResolver resourcePatternResolver;
private MetadataReaderFactory metadataReaderFactory;
private Environment environment;
//注册默认的包含过滤器
protected void registerDefaultFilters(){
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
}
}
子类 ClassPathBeanDefinitionScanner
持有一个 BeanDefinitionRegistry
实例,作用是将已加载 BeanDefinition
集合注册到 Spring 容器中。该类在构造方法中调用了父类的 registerDefaultFilters
方法,支持注册声明了 @Component
注解的组件。
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
private final BeanDefinitionRegistry registry;
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, Environment environment) {
this.registry = registry;
registerDefaultFilters(); //注册默认的TypeFilter,允许加载@Component声明的类
setResourceLoader((registry instanceof ResourceLoader ? (ResourceLoader) registry : null));
setEnvironment(environment);
}
}
4.3 scan 方法
子类 ClassPathBeanDefinitionScanner
的 scan
方法是入口方法,实际的处理逻辑是由 doScan
方法完成的,然后使用工具类 AnnotationConfigUtils
注册相关的后处理器。
public void scan(String... basePackages){
doScan(basePackages);
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
doScan
方法具体的处理过程分为三步,其中,后两步与 AnnotatedBeanDefinitionReader
基本相同,我们重点关心第一步是如何实现的。具体步骤如下:
- 扫描指定目录下,加载文件并转换成
BeanDefinition
- 生成 Bean 的名称,并根据
@Role
、@Primary
等注解来设置相关属性 - 将
BeanDefinition
注册到 Spring 容器中
protected Set<BeanDefinitionHolder> doScan(String[] basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = new HashSet<>();
for(String basePackage : basePackages){
//1. 扫描指定目录,加载class文件,具体逻辑由父类完成
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
//2. 解析@Lazy、@Primary、@DependsOn、@Role、@Description等注解,为BeanDefinition添加相应属性
String beanName = this.beanNameGenerator.generateBeanName(candidate); //生成Bean的名称
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
//3. 检查是否与已存在的Bean冲突,如果没有则将BeanDefinition添加到容器中
if(!this.registry.containsBeanDefinition(beanName)){
registry.registerBeanDefinition(beanName, candidate);
BeanDefinitionHolder holder = new BeanDefinitionHolder(candidate, beanName);
beanDefinitions.add(holder);
}
}
}
return beanDefinitions;
}
4.4 加载和解析组件
findCandidateComponents
方法是由父类 ClassPathScanningCandidateComponentProvider
实现的,可以分为三步:
-
加载指定目录的资源。
packageSearchPath
表示需要扫描的路径,格式为classpath*:/xx/**/*.class
,表示任意类路径下的、指定包路径下的、任意层级目录下的、任意名称的 class 文件。 -
使用
MetadataReaderFactory
以 ASM 的方式获取注解元数据。 -
过滤出符合条件的类。这里有两层检查,首先是特殊规则的检查,然后是通用的检查。当候选项通过所有的检查,就可以作为
BeanDefinition
稍后注册到 Spring 容器中。
//扫描指定目录,加载候选的组件
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
//1. 加载指定目录下的资源
basePackage = basePackage.replace('.', '/'); //将包名的.分隔符换成/
//格式为classpath*:/xx/**/*.class,表示任意类路径下的、指定包路径下的、任意层级目录下的、任意名称的class文件
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + basePackage + '/' + DEFAULT_RESOURCE_PATTERN;
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
for (Resource resource : resources) {
//2. 将加载的字节流以ASM的方式转换成Class元数据
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
//3. 过滤出符合条件的类,创建BeanDefinition
//3.1 根据TypeFilter和Condition过滤元数据
if(isCandidateComponent(metadataReader)){
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
//3.2 检查是否为实现类或静态内部类
if(isCandidateComponent(sbd)){
candidates.add(sbd);
}
}
}
return candidates;
}
我们需要对加载的元数据进行过滤,首先排除掉符合 excludeFilters
的候选项,然后让符合 includeFilters
的候选项通过。由此可见,排除规则的优先级高于包含规则。包含规则有一条默认的检查,之前已经提到过,就是检查类是否声明了 @Component
注解。此外,还需要进行条件判定,这部分内容先略过。
第一个 isCandidateComponent
方法是自定义检查,通过定义包含和排除规则,再配合条件判定,非常灵活。第二个 isCandidateComponent
方法是通用的检查,必须确保通过扫描加载的是一个普通类或静态内部类。
//3.1 根据排除或包含规则、以及条件判定,过滤出符合条件的候选项
private boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
//排除需要过滤掉的项(exclude规则的优先级高于include规则)
for(TypeFilter tf: this.excludeFilters){
if(tf.match(metadataReader, this.metadataReaderFactory)){
return false;
}
}
//加载指定规则的项,比如使用@Component注解声明的类
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return true;
//进一步检查Condition(TODO 详见条件判定一节)
}
}
return false;
}
//3.2 通用检查,确保Bean是普通类或静态内部类
//1) 必须是独立的,即普通类或静态内部类
//2) 必须是具体的,即非抽象类或接口
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
return metadata.isIndependent() && metadata.isConcrete();
}
最后,回顾一下组件扫描的主要流程,如图所示。
5. ApplicationContext 实现
AnnotationConfigApplicationContext
是通过注解声明的方式进行配置,这是本章最重要的 ApplicationContext
实现类。该类继承自 GenericApplicationContext
,最大的特点是持有 AnnotatedBeanDefinitionReader
和 ClassPathBeanDefinitionScanner
这两个组件,拥有了多种方式来加载 Bean 的能力。
public class AnnotationConfigApplicationContext extends GenericApplicationContext {
private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;
public AnnotationConfigApplicationContext(){
this.scanner = new ClassPathBeanDefinitionScanner(this);
this.reader = new AnnotatedBeanDefinitionReader(this);
}
}
6. 测试
6.1 加载指定的类
首先是准备工作。UserService
类使用 @Service
注解声明,表示一个业务类。UserController
使用 @Controller
注解声明,表示一个接口类。UserController
还通过依赖注入的方式持有 UserService
的实例。
//测试类,模拟业务层
@Service
public class UserService {
public User getUser(){
return new User();
}
}
//测试类,模拟控制层
@Controller
public class UserController {
@Autowired
private UserService userService;
public User getUser(){
return this.userService.getUser();
}
}
测试方法比较简单,首先创建 AnnotationConfigApplicationContext
实例,然后调用 register
方法完成了 UserService
的注册,再也不需要直接创建 RootBeanDefinition
实例。
//测试方法
@Test
public void testAnnotatedReader() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(UserService.class);
context.refresh();
UserService userService = context.getBean("userService", UserService.class);
System.out.println("annotatedReader测试: " + userService.getUser());
}
从测试结果可以看到,UserService
被注册到了 Spring 容器中。
annotatedReader测试: context.basic.User@59e84876
6.2 类路径扫描
创建 AnnotationConfigApplicationContext
实例,调用 scan
方法扫描指定的目录,加载所有声明了 @Component
注解(及其子注解)的类,包括 UserController
和 UserService
。由于 AnnotationConfigUtils
工具类自动注册了 AutowiredAnnotationBeanPostProcessor
,在获取 Bean 的流程中完成了依赖注入的功能。
//测试方法
@Test
public void testClassPathScanner(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.scan("context.basic");
context.refresh();
UserController userController = (UserController) context.getBean("userController");
System.out.println("ClasspathScan测试: " + userController.getUser());
}
从测试结果可以看到,Spring 容器自动加载了 UserController
和 UserService
两个单例,并完成了依赖注入的操作。
ClasspathScan测试: context.basic.User@26be92ad
7. 总结
我们知道,加载 Bean 的流程分为三步,资源加载、资源解析和注册 BeanDefinition
,上一节实现了前两步,而第三步是现成的,本节的目标是将这三步整合起来。Spring 将这三个步骤封装到 AnnotatedBeanDefinitionReader
和 ClassPathBeanDefinitionScanner
中,AnnotationConfigApplicationContext
作为最重要的实现类之一,持有这两个组件,从而拥有了通过注解声明的方式来启动应用的能力。
此外,Spring 使用注解元数据来描述类的信息,RootBeanDefinition
并不能体现出注解元数据的特性,而是通过 AnnotatedBeanDefinition
接口的实现类完成的。具体来说,AnnotatedGenericBeanDefinition
类专门用于 AnnotatedBeanDefinitionReader
组件,ScannedGenericBeanDefinition
类专门用于 ClassPathBeanDefinitionScanner
组件。
8. 项目结构
新增修改一览,新增(15),修改(1)。
context
└─ src
├─ main
│ └─ java
│ └─ cn.stimd.spring.context
│ ├─ annotation
│ │ ├─ AnnotatedBeanDefinitionReader.java (+)
│ │ ├─ AnnotationBeanNameGenerator.java (+)
│ │ ├─ AnnotationConfigApplicationContext.java (+)
│ │ ├─ AnnotationConfigUtils.java (+)
│ │ ├─ ClassPathBeanDefinitionScanner.java (+)
│ │ ├─ ClassPathScanningCandidateComponentProvider.java (+)
│ │ ├─ Primary.java (+)
│ │ ├─ Role.java (+)
│ │ └─ ScannedGenericBeanDefinition.java (+)
│ └─ stereotype
│ ├─ Component.java (+)
│ ├─ Controller.java (+)
│ ├─ Repository.java (+)
│ └─ Service.java (+)
└─ test
└─ java
└─ context
└─ basic
├─ BasicTest.java (*)
├─ UserController.java (+)
└─ UserService.java (+)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。
转载自:https://juejin.cn/post/7402060970426368009