1. Bean管理
1.1. 什么是Bean?
Bean可以说是Spring当中最为重要的概念之一了。
简单来说,Bean就是一个对象
,只不过这个对象是由Spring容器来初始化,装配,管理的,因此也可以叫做Spring Bean
。
当Spring程序启动的时候,程序会自动扫描需要被管理的Bean,将其放入容器,当需要使用的时候,能够随时取出来Bean对象使用。
需要格外注意的是,请分清Bean对象和@Bean注解的区别。Bean对象指的是被Spring容器管理的对象,而@Bean注解只是创建Bean对象的一种方式。不要混淆概念。
1.2. 一般的bean管理
此处的“一般”,指的是被管理的bean对象的类是我们自己写的。
对于一个我们自己编写的类,如果想要放到spring容器中管理,常见的方式有以下几种:
-
@Component 及其衍生注解,需要放在类上,将该类的对象作为bean放入spring容器
包含
@Component、@Controller、@Service、@Configration
等 -
@Mapper注解,mybatis-spring借助spring的IOC技术完成对象管理,并交给spring容器。
-
使用xml配置文件注册bean。由于现在spring已经转入注解开发,这种方式已经被替代了。
另外,如果想要Bean被Spring管理,则需要Bean对象类本身能够被spring扫描到,大体有两个方法
- Bean对象的类在启动类(@SpringBootApplication注解的类)所在的包下,则会自动被扫描
- 在任意能够被扫描到的配置类上,添加@ComponentScan注解
- 如果@ComponentScan没有写任何参数,则默认扫描当前配置类所在的包及包下
- 如果@ComponentScan写了value参数,则扫描value值对应的包及包下
1.3. 第三方类的bean管理
对于第三方的类,如果我们想把这些类的对象放入Spring容器中管理,但是由于我们无法直接修改这些类的代码,不能直接在代码上加@Component等注解。
这时候,就需要使用@Bean作为工厂方法
@Bean注解一般放在方法上,且该方法所在的类也需要被Spring管理,所以@Bean一般配合@Configuration注解使用。
被@Bean注解的方法,其返回值会被Spring容器管理,Bean对象的名字为方法名。(内部的核心原理为CGLIB动态代理)
示例:管理Druid数据源
需求:从配置文件中读取数据库连接的相关信息,并用这些信息初始化一个Druid数据源,生成一个数据源对象交由Spring容器管理
- 导入依赖
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--德鲁伊数据连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.9</version>
</dependency>
- 在配置文件中写入数据库连接的相关配置信息
datasource:
driverName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///test_db
username: root
password: root123
- 通过@ConfigurationProperties注解,读取配置文件中对应的信息,并给类注入属性(DI)
(在此例中,额外指定了该类的Bean对象的名字为“datasource”)
@Component
@ConfigurationProperties(prefix = "datasource")
@Data
public class DruidParam {
private String driverName;
private String url;
private String username;
private String password;
}
- 通过@Bean对象,创建一个类型为DataSource的数据源对象,实际的类型为DruidDataSource。
(注入DruidParam对象的时候,使用了@Qualifier注解,指定了需要注入名字为“druidParam”的Bean对象)
@Configuration
public class DruidConfig {
@Bean
public DataSource dataSource(@Qualifier("druidParam") DruidParam druidParam){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(druidParam.getDriverName());
druidDataSource.setUrl(druidParam.getUrl());
druidDataSource.setPassword(druidParam.getPassword());
druidDataSource.setUsername(druidParam.getUsername());
return druidDataSource;
}
}
2. 相关注解
在了解自动装配之前,我们还需要了解两个重要的注解
2.1. @Import注解
作用:
① 导入Bean
② 导入配置类
③ 导入 ImportSelector 实现类。当导入 ImportSelector 实现类时,会触发其中的钩子函数
,执行selectImports方法
。该方法一般用于加载配置文件中的配置类的全限定名,然后根据这个全限定名生成Bean对象。
④ 导入 ImportBeanDefinitionRegistrar 实现类。
1. 导入配置类演示:
第三方包中有一个配置类,其中有一个bean
@Configuration
public class JacksonConfig {
@Bean
// @ConditionalOnProperty(name = "shen", havingValue = "ikun")
// @ConditionalOnMissingBean
public ObjectMapper objectMapper(){
return new ObjectMapper();
}
}
我们自己的代码中可以直接导入
@SpringBootApplication
@Import({JacksonConfig.class})
public class MyUse3DApp {
public static void main(String[] args) {
SpringApplication.run(MyUse3DApp.class, args);
}
}
2. 导入 ImportSelector 实现类演示:
第三方包中声明一个ImportSelector:
public class ImportJacksonBeanSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"org.itcast.jackson.bean.JacksonConfig"};
}
}
我们自己的代码中可以直接导入
@SpringBootApplication
@Import({ImportJacksonBeanSelector.class})
public class MyUse3DApp {
public static void main(String[] args) {
SpringApplication.run(MyUse3DApp.class, args);
}
}
2.2. @EnableXxx注解
此处我们指的是,在SpringBoot中,以Enable开头的注解。
@EnableXxx注解的定义中,一般就是包含一个@Import注解,@Import注解中包含一个参数,为具体需要引入的内容。
所以,@EnableXxx类的注解,本质上就是一个@Import注解,只不过不用再去写冗长的参数,对编码和阅读都更加友好。
例:第三方包中声明一个EnableJK注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ImportJacksonBeanSelector.class)
public @interface EnableJK {
}
我们自己的代码中可以直接使用
@SpringBootApplication
@EnableJK
public class MyUse3DApp {
public static void main(String[] args) {
SpringApplication.run(MyUse3DApp.class, args);
}
}
3. 自动装配原理
3.1. 什么是自动装配?
在说自动装配之前,我们先提一下“手动装配”,毕竟“自动”就是区别于“手动”的。
所谓的Bean的手动装配,指的是在早先的Spring版本中,存在着一个xml配置文件,所有的bean配置都要手动写在xml文件中才会生效。
而自动装配,指的就是我们不再需要手动配置,在Spring容器在启动的时候,Spring会自动将我们自己写的,和引入依赖中的,需要被Spring容器管理的Bean对象放入Spring容器中,并把这些Bean对象自动注入到需要它们的地方。
3.2 SpringBoot 是如何实现自动装配的?
1)启动类
在启动类中,我们关注一个重要的注解 @SpringBootApplication
。这个注解是我们实现自动装配的关键。我们接下来进入这个注解中,看看这个注解中包含哪些其他注解。
@SpringBootApplication
public class ReadApp {
public static void main(String[] args) {
// run 做spring容器管理 bean自动创建
SpringApplication.run(ReadApp.class, args);
}
}
2)@SpringBootApplication注解定义
在@SpringBootApplication注解的定义中,抛开四个元注解(@Target、@Retention、@Documented、@Inherited)不谈,元注解都是用来描述注解本身的属性的。
剩下的三个注解分别为 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan
,这三个注解非常重要
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {}
在这三个注解中,@SpringBootConfiguration
内部其中就是一个@Configuration注解,声明当前类是一个配置类,实现配置类的相关功能。
@ComponentScan
将自动扫描当前类(启动类)的包下的所有文件,查看是否有需要被管理的Bean对象。
而@EnableAutoConfiguration
的作用就是引入其他依赖。接下来我们继续进入这个注解的源码查看。
3)@EnableAutoConfiguration注解定义
抛开元注解,我们看到@EnableAutoConfiguration
主要有两个注解,分别是@AutoConfigurationPackage
和@Import({AutoConfigurationImportSelector.class})
其中,我们可以通过@AutoConfigurationPackage的源码看到,这个注解的本质也是一个@Import
,引入了一个AutoConfigurationPackages.Registrar的组件,功能是将当前类中所有的组件批量注册进容器中。
我们之前提到过,Enable开头的注解,其核心一般是一个@Import注解。而此处的@Import({AutoConfigurationImportSelector.class})
就是我们找到外部依赖中Bean对象的核心。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {}
4)@Import(AutoConfigurationImportSelector.class)
我们在这里分析一下@Import注解和AutoConfigurationImportSelector类。
在上面我们提到了,当@Import引入一个ImportSelector 实现类的时候,会执行内部的钩子函数
- selectImports
方法。这个方法会返回一个字符串数组,数组中的每个字符串都是某个类的全限定名,而Spring容器会将数组中的全限定名对应的类对象加入到Bean容器中。
在钩子函数中,获取自动配置类的核心代码就是
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
我们再进入这个方法看一下。
public class AutoConfigurationImportSelector implements DeferredImportSelector,
BeanClassLoaderAware,ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
//String[] 返回值 放着要被自动管理bean对象的 全限定类名字符串
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 判断SpringBoot是否开启了启动配置(在配置文件中可以更改设置)
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 获取自动配置类
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
5)getAutoConfigurationEntry(annotationMetadata);
在这个类中,我们可以看到,核心在于
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
我们再进去看一下
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 判断是否开启自动配置
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取@EnableAutoConfiguration注解的属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 从META-INF/spring.factories文件和META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中,获取配置类的全限定名数组
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 去重
configurations = removeDuplicates(configurations);
// 获取注解中exclude或excludeName排除的类集合
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 检查被排除类是否可以实例化,是否被自动配置所使用,否则抛出异常
checkExcludedClasses(configurations, exclusions);
// 去除被排除的类
configurations.removeAll(exclusions);
// 使用上文提到的两个文件中配置的过滤器对自动配置类进行过滤
configurations = getConfigurationClassFilter().filter(configurations);
// 抛出事件
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
6)getCandidateConfigurations(annotationMetadata, attributes);
在这个方法中,通过SpringFactoriesLoader
的loadFactoryNames()
方法,查看每个依赖包中的META-INF/spring.factories
文件和META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,获取其中需要配置的bean全限定名列表。
【注意】在SpringBoot2.7之前,自动装配的配置写在 META-INF/spring.factories 文件中。在2.7版本之后,配置应该放在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中,且官方文档中已经标注,spring.factories的方式在2.7版本已经过期,在3.0版本后彻底无法使用。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = new ArrayList<>(
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
7)spring容器拿到这些字符串基于IOC管理Bean
Spring拿到这些字符串列表以后,基于OC底层源码,使用工厂模式和反射,创造出对应的Bean对象。
3.3 条件判断
问题:
Spring容器获取到这些全限定列表以后,难道都要全部生成这些配置bean和里面的bean?
不是的,其实创建Bean的时候需要@ConditionalXxx进行判断,只有判断通过的才创建bean对象,并放到容器中。这就依赖于SpringBoot中的条件注解
-
@ConditionalOnClass:判断环境中有对应字节码文件,才注册bean到IOC容器。
-
@ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器。
-
@ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。
Comments NOTHING