无论是注解方式还是配置文件方式,它们的底层工作流程都是将配置解析成BeanDefinition转换为Bean实例对象,并将其注册到Spring容器中。区别在于BeanDefinition的来源不同,注解方式是通过注解信息生成BeanDefinition,配置文件方式是通过配置文件解析器。
样例
@Configuration
public class PersonConfiguration {
@Bean(name = "person")
public Person getPerson() {
return new Person(20, "法外狂徒");
}
}
record Person(int id, String name) {
}
class Client {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(PersonConfiguration.class);
applicationContext.refresh();
Person person = (Person) applicationContext.getBean("person");
System.out.println(person);
}
}
复制代码
依旧是曾经的那个工厂(BeanFactory)
AnnotationConfigApplicationContext
- AnnotationConfigApplicationContext有两个成员变量
- 注解型就不需要像配置型一样声明Reader了,毕竟配置文件的类型千奇百怪,当然要单独告诉Spring是用的xml,还Groovy了。换句话说,注解就是注解,没有其他内容了,所以该类就直接将Reader封装了。
- AnnotatedBeanDefinitionReader用于解析指定类中的注解,而ClassPathBeanDefinitionScanner用于扫描类路径下的所有类并解析指定的注解。
一个是扫描特定类,一个扫描特定路径,如果有重叠的地方,放心,Spring必然有做过滤,毕竟这reader和scanner共用同一个registry。
- 这意味着AnnotatedBeanDefinitionReader更适合在单个类上使用,而ClassPathBeanDefinitionScanner更适合在整个项目中使用。
- 此外,ClassPathBeanDefinitionScanner可以扫描并注册包含在jar文件中的类,而AnnotatedBeanDefinitionReader只能解析已加载到JVM的类。这也是它们之间的区别之一。
- 需要注意的是,这两个类生成的BeanDefinition本质上是相同的,因为它们都是通过解析注解来创建的。因此,在使用上,它们没有本质上的区别。
- 实例化AnnotationConfigApplicationContext时会导致必要的Spring内置组件被解析成BeanDefinition。简单说就是一些准备工作。
register(Class<?>... componentClasses)
applicationContext.register(PersonConfiguration.class);
复制代码
注册Bean
- doRegisterBean,注册BeanDefinition,之所以说是registry是工厂是因为其子类是工厂。
- BeanDefinitionHolder 是一个封装了 BeanDefinition 和其对应的名称的类。它的作用是在创建 BeanDefinition 的同时,也为其分配一个名称,并在 BeanFactory 中保存。这个名称可以是在配置文件中指定的,也可以是自动生成的。
- 在 Spring 的 IoC 容器中,每一个 Bean 都有一个唯一的名称,这个名称通常是由该 Bean 在配置文件中的 id 或 name 属性指定的。而一个 BeanDefinition 本身并没有包含这个名称信息,因此需要将 BeanDefinition 和其对应的名称一起封装起来,才能在 IoC 容器中正确地处理该 Bean。因此,为了将 BeanDefinition 与其名称一起保存在 BeanFactory 中,就需要使用 BeanDefinitionHolder 这个封装类。
- 还是和配置文件型一样,注册Bean不是实例化Bean。
核心方法refresh()
refresh()
方法是 Spring 框架中ApplicationContext
接口的核心方法,用于刷新ApplicationContext
,也就是将ApplicationContext
容器初始化和刷新,使其能够响应外部请求。
研究一下Spring是什么时候解析注解@Bean
简述一下refresh()的大致流程
BeanFactoryPostProcessor
是一个在容器实例化所有BeanDefinition
对象之后、在它们进行实例化之前运行的后置处理器。因此,它被称为“后置处理器”。在这个阶段,BeanFactory
对象已经被创建,但是还没有被用来创建单例Bean
实例。BeanFactoryPostProcessor
提供了一种方式,可以对已经加载到BeanFactory
中的BeanDefinition
进行修改或添加属性。这种方式允许应用程序在运行时更改其行为。例如,一个BeanFactoryPostProcessor
可以动态地向BeanDefinition
添加属性,也可以用默认值覆盖属性。- 要知道,
BeanDefinition
只是Bean的描述,简单说只是配置信息,并不是class对象,生成BeanDefinition
时,某些类可能都还没被加载。
详细讲解@Bean解析时机,以及实例化Bean
- ConfigurationClassPostProcessor类,会将配置类(@Configuration)解析成Spring中的ConfigurationClass。
-
解析@Configuration的关键片段
-
之后的流程就是将@Bean方法,也解析成BeanDefinition
-
最后就是在refresh()方法中的finishBeanFactoryInitialization(beanFactory)方法中调用喜闻乐见的doGetBean()方法,其中@Configuration被Spring视为代理类,@Bean视为工厂方法,最后通过反射调用工厂方法生成Bean实例。如果是单例就会被放入Spring的缓存中。
-
@Configuration会被先解析,最后再解析@Bean,Spring会成为每个Bean生成BeanName,按一定顺序放入beanDefinitionNames,然后实例化顺序就确定了。
小结
基于注解与基于XML配置的Spring Bean在创建时机上存在的不同之处
- 基于XML配置的方式,Bean对象的创建是在程序首次从工厂中获取该对象时才创建的。
- 基于注解配置的方式,Bean对象的创建是在注解处理器解析相应的@Bean注解时调用了该注解所修饰的方法,当该方法执行后,相应的对象自然就已经被创建出来了,这时,Spring就会将该对象纳入到工厂的管理范围之内,当我们首次尝试从工厂中获取到该Bean对象时,这时,该Bean对象实际上已经完成了创建并已被纳入到了工厂的管理范围之内。
- 简单说,配置文件型,在需要Bean的时候Spring才会去实例化Bean。(延迟加载)
- 而注解型,Spring会将Bean先实例化。
注解型为什么不延迟加载
@Component
public class MyService {
@Autowired
private MyRepository myRepository;
// ...
}
@Component
public class MyRepository {
// ...
}
复制代码
- 如果我们采用延迟加载的方式,当容器加载
MyService
类时,由于不知道MyRepository
的具体信息,Spring 框架也就无法为MyService
生成对应的 Bean 实例。这就会导致MyService
中myRepository
属性的自动注入失败,无法使用MyRepository
对象。 - Java语言算是动态的,类之间的关系算是强绑定的,在启动时可能没错,但在运行期间可能会出错,为了保证应用程序的正确性,注解配置默认是不延迟加载的。
- 而配置文件中的Bean定义是通过XML或者其他格式的文件进行描述的,这些文件是静态的,关系在启动时方便检查,所以延迟加载安全性高一些。