目录
3.5.2 自动装配源码分析例子——SpringBoot中自动配置类的Bean加载
码字不易,喜欢就点个关注❤,持续更新技术内容。相关资料请私信。
相关内容:
Servlet原理和简单的案例编写_Maxlec的博客-CSDN博客
第一篇:SpringBoot项目的创建和开发_Maxlec的博客-CSDN博客
1 Spring中常见的Bean注解
Spring中常用的控制反转的注解:
IOC注解 | 说明 | 位置 |
---|---|---|
@Component | 声明bean的基础注解,bean的名称默认使用类名或方法名首字母小写,也可以自己指定。 | 不属于以下三类时使用此注解 |
@Controller | 封装了@Component,相当于衍生注解 | 修饰控制器 |
@RestController | 包含@Controller和@ResponseBody | 修饰控制器 |
@Service | 封装了@Component,相当于衍生注解 | 修饰业务层 |
以及@Repository、@RequestBody、@Index、@Import等。
其中:
-
@Index提升@ComponentScan的效率。
-
@Import是import标签的替换,在SpringBoot的自动装配中非常重要,也是EnableXXX的前置条件。
-
@Repository标注在由Mybatis整合的数据访问类上。
Bean组件扫描:
-
前面声明bean的注解要生效好需要被@ComponentScan扫描到。
-
@ComponentScan注解已经在启动类声明注解@SpringBootApplication中隐式配置,默认扫描范围时启动类所在的包及其子包。所以尽量按照这样的规范进行编写。
2 依赖注入注解
@Autowired和@Resource的区别。@Resource的作用相当于@Autowired,只不过@Autowired按照byType方式进行装配注入。
主要体现在以下 5 点:
-
来源不同;
-
依赖查找的顺序不同;
-
支持的参数不同;
-
依赖注入的用法不同;
-
编译器 IDEA 的提示不同。
@Resource和@Autowired都是做bean注入时使用的,首先它们的来源不同,@Autowired是Spring的注解。而@Resource是java中的注解。
-
相同点:两者都能修饰字段和setter方法上,两者都写在字段上,那么就不需要再写setter方法了。
-
不同点:@Autowired按照byType方式匹配进行装配注入,@Resource可以按照byName和byType的方式匹配注入。当然如果@Autowired想按照byName方式匹配注入,Spring提供了@Qualifier注解指定bean的name。
2.1 @Autowired
@Autowired为Spring提供的注解,需要从springframework导入包。@Autowired注解是按照类型(byType)装配依赖对象,默认情况下要求依赖对象必须存在,如果允许为null需要设置它的required为false。
public class TestServiceImpl {
// 下面两种方式只需使用一种即可
@Autowired // 用于字段上
private UserMapper userMapper;
@Autowired // 用于方法属性上
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
}
因为@Autowired按照类型匹配进行装配注入,所以如果在注入一个类似于IUserService接口时,该接口有多个实现类,那按照类型匹配的话,注入的是哪一个bean呢?这时就会报自动装配失败的错误信息。
这时如果想按照名称(byName)来装配,可以结合@Qualififier注解一起使用:
public class TestServiceImpl {
@Autowired
@Qualifier("userMapper")
private UserMapper userMapper;
}
或者在想要注入的类上再加上@Primary注解设置优先级,优先装配注入该bean。或者使用@Resource注解。
2.2 @Resource
@Resource默认按照byName自动匹配注入,由J2EE提供,需要从java导入包。@Resource有两个属性:name和type,而Spring将@Resource注解的name属性解析为bean的名称,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName自动匹配注入,而使用type属性则使用byType自动匹配注入。都不定义的话,Spring会通过反射机制使用byName自动匹配注入。
public class TestServiceImpl {
// 下面两种方式只需使用一种即可
@Resource(name="userMapper") // 用于字段上
private UserMapper userMapper;
@Resource(name="userMapper") // 用于方法属性上
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
}
@Resource的装配注入顺序:
-
如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配注入,找不到则抛出异常。
-
如果指定了type,则从上下文中查找类似匹配的唯一bean进行装配注入,找不到或者找到多个都会抛出异常。
-
如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配注入,找不到则抛出异常。
-
如果都没指定,则自动按照byName方式进行装配注入,找不到则回退为一个原始类型进行匹配。
3 Bean初步原理分析
3.1 项目配置文件
SpringBoot中支持三种格式的配置文件:.properties、.yml、.yaml。
优先级:.properties > .yml > .yaml。
在项目开发时,推荐使用统一格式的配置文件。主流是使用.yml。
除了配置文件的方式进行端口配置,还可以在项目配置中通过java系统属性和命令行参数的方式进行属性配置。
-
java系统属性配置:-Dserver.port=xxxx。
-
命令行参数:--server.port=xxxx。
如果项目打包后,还需要修改配置,可以通过执行java指令运行jar包进行配置:
java -Dserver.port=xxxx -jar xxx.jar --server.port=xxxx
3.2 Bean管理
3.2.1 获取bean
在Spring项目启动时,会把bean都创建好放在IOC容器中,如果想要主动获取这些bean,可以通过如下方式:
-
根据name获取bean:Object getBean(String name)
-
根据名称和类型获取bean:<T> T getBean(String name, Class<T> requiredType)
-
根据类型获取bean:<T> T getBean(Class<T> requiredType)
@SpringBootTest
class SpringBootApplicationTest {
// 首先获取IOC容器
@Autowired
private ApplicationContext applicationContext;
@Test
public void GetBeanTest() {
// 根据bean名称获取bean对象
UserController bean1 = (UserController) applicationContext.getBean("userController");
System.out.println("bean1"+bean1);
// 根据bean类型获取bean对象
UserController bean2 = applicationContext.getBean(UserController.class);
System.out.println("bean2"+bean2);
// 根据bean名称和类型获取bean对象
UserController bean3 = applicationContext.getBean("userController", UserController.class);
System.out.println("bean3"+bean3);
}
}
可以看到三次获取到的bean对象的地址都是一样的,说明是同一个bean,bean对象的创建方式是单例的(分为饿汉和懒汉单例),简称单例bean。获取对象的时候,单例bean在项目启动时在内部提前创建好了,直接返回。如果我们需要每次获取的都是全新的bean,那么需要设置bean的作用域。
3.2.2 bean创建模式
Spring支持五种bean的创建方式,后三种在Web环境中生效:
作用域 | 说明 |
---|---|
singleton | 容器内同名称的bean只有一个实例。(默认饿汉单例创建方式) |
prototype | 每次获取该bean的时都创建新的实例。 |
request | 每个请求对应创建一个新的实例。 |
session | 每个会话对应创建一个新的实例。 |
application | 每个应用对应创建一个新的实例。 |
Spring中默认使用singleton模式创建实例,singleton分为饿汉单例和懒汉单例。顾名思义,前者饿汉先创建好,直接获取时直接返回。后者懒汉在获取时才进行创建再在返回:
public class SingleInstanceDemo01 {
public static void main(String[] args) {
Singleton01 s1 = Singleton01.getInstance();
Singleton01 s2 = Singleton01.getInstance();
System.out.println(s1 == s2);
}
}
// 饿汉单例设计模式
class Singleton01{
// b.定义一个静态变量存储一个对象( 在用类获取对象的时候,对象已经提前为你创建好了。)
private static final Singleton01 INSTANCE = new Singleton01();
// a.定义一个类,把构造器私有。
private Singleton01(){
}
// c.提供一个返回单例对象的方法。
public static Singleton01 getInstance(){
return INSTANCE;
}
}
public class SingleInstanceDemo02 {
public static void main(String[] args) {
Singleton02 s1 = Singleton02.getInstance();
Singleton02 s2 = Singleton02.getInstance();
System.out.println(s1 == s2);
}
}
// 懒汉单例设计模式
class Singleton02{
// b.定义一个静态变量存储一个对象(这里不能创建对象,需要的时候才创建,这里只是一个变量用于存储对象!)
public static Singleton02 instance ;
// a.定义一个类,把构造器私有。
private Singleton02(){
}
// c.提供一个返回单例对象的方法。
public static Singleton02 getInstance(){
if(instance == null){
// 第一次来拿单例对象!需要创建一次对象,以后直接返回!!
instance = new Singleton02();
}
return instance;
}
}
在Spring中想要实现懒汉单例的话只需要在类上加上@Lazy注解即可。其他非单例模式可以通过注解@Scope指示bean的创建模式。
@Scope("prototype")
在实际开发中,绝大部分的Bean是单例的,所以说大部分Bean不需要配置scope属性。
3.2.3 第三方依赖的Bean配置管理
我们自己定义Bean只需要在类上加上@Component、@RestController、@Service注解即可。而我们引入的第三方依赖中的类怎么定义为Bean呢。
如果要管理的bean对象来自于第三方(非自己定义),那么无法使用@Component注解定义Bean,需要使用到@Bean注解。
这些第三方Bean都是进行集中管理、分类配置的,可以通过@Configuration注解声明一个配置类。如在配置类中管理各种第三Bean:
@Configuration
public class CommonCofig {
@Bean
public Xyy xyy(){
return new Xyy();
}
@Bean
...
}
通过@Bean注解还可以声明bean的名称,不指定默认bean的名称默认是类名或方法名首字母小写。
如果第三方Bean需要依赖其他Bean,直接在Bean定义方法中依赖的形参即可,容器会将其一并定义为Bean。
3.3 SpringBoot起步依赖和自动装配
Spring是目前最流行的Java框架,即Spring Framework。直接基于Spring Framework开发会很繁琐,比如依赖配置以及大量的项目配置文件。SpringBoot就是基于Spring这个基础框架开发的,它简化开发过程,比如起步依赖配置,自动装配等等,这些让项目开发更加简单、快捷。
3.4 起步依赖-starter
我们通过SpringMVC搭建一个Web应用,需要做以下工作:
-
配置pom.xml添加Spring 、SpringMVC框架的依赖,同时还需要考虑这些不同的框架的不同版本是否存在不兼容的问题。
-
配置Web.xml,加载Spring、SpringMVC。
-
配置Spring。
-
配置Spring MVC。
-
编写业务逻辑代码。
而使用SpringBoot搭建的话,只需要做以下工作:
-
配置pom.xml继承SpringBoot的pom.xml,添加 Web 起步依赖。
-
创建启动引导类。
-
编写业务逻辑代码。
使用SpringBoot的最大优点就是简化了配置的工作,并不是说使用SpringBoot就不需要这些配置过程了。而是SpringBoot帮我们把这些起步配置工作给做了。SpringBoot为我们提供了起步依赖。
这些起步依赖就是starter,在各种starter中,定义了完成该功能需要的坐标合集。大部分版本信息来自于父工程,只要我们的工程继承starter-parent,通过依赖传递,就可以简单方便获得需要的jar包,并且不会存在版本冲突等问题。
比如引入spring-boot-starter-web,那么完成Web开发所需的依赖集合都引入了,利用的就是Maven的依赖传递。
在spring-boot-starter-web中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。
3.5 第三方Bean的加载
3.5.1 Bean加载的整体过程
SpringBoot的自动装配就是当spring容器启动后,一些配置类、bean对象就在容器中自动被创建,不需要我们手动去声明Bean,从而简化了开发。同样的,并不是不需要创建bean了,而是我们通过简单注解配置去让Spring帮助我们自动的创建。
如以下EurekaApplication项目启动后自动加载了很多的bean对象。可以直接使用完成相应的功能。
这就是引入了eureka的starter依赖后,在该starter中,定义了完成注册与发现服务功能需要的坐标合集。我们只需要通过简单的注解配置,Spring就能自动将依赖jar中的bean加载到IOC容器中,然后就能使用其功能了。
实现过程:
在引入依赖之后,因为SpringBoot项目启动后只能扫描启动类所在的包以及其所在包的子包,扫描不到引入的依赖中的包。
那Spring是如何扫描到依赖中定义的Bean,然后加载到IOC容器中的呢?就是简单的注解配置。
-
在项目启动类上添加@ComponentScan("...", "..."),指定扫描引入的依赖的包名。
-
在启动类上添加@Import({...})注解,其中可以导入普通类,Bean配置管理类,ImportSelector接口实现类。其中ImportSelector接口的实现类会读取每个依赖的自动配置类,最后达到扫描加载装配的目的。
-
在启动类上直接添加@EnableXXXX,该注解中封装了@Import注解,
如@EnableEurekaServer:
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
进入该注解,可以看到里面封装了@Import,里面定义好了导入某个Bean的配置管理类。
3.5.2 自动装配源码分析例子——SpringBoot中自动配置类的Bean加载
SpringBoot项目启动后需要自动加载org.springframework.boot包下的基础Bean,我们以此为例介绍Bean自动装配的原理。
项目的启动由项目启动类开始,我们进行源码跟踪时也可以从启动类开始。
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
点击进入SpringBoot项目的核心注解@SpringBootApplication,可以看到主要封装了以下三个注解:
第一个注解就是声明启动类也是一个配置类,第三个是用来进行组件扫描的注解,就是扫描有没有定义的Bean,然后加载到IOC容器中。我们主要来看第二个注解,开启自动配置功能的注解@EnableAutoConfiguration,在讲解第三方bean的加载时提到的注解,其中封装了@Import注解。
其中封装的@Import注解导入的就是ImportSelector接口实现类AutoConfigurationImportSelector。
@Import({AutoConfigurationImportSelector.class})
该类中的selectImports方法中会以字符串数组返回jar包中所有配置类的全类名,然后由@Import注解导入这些Bean的配置类,将这些类实例化后放入Spring容器中,然后Spring容器扫描其中定义的Bean,最后达到加载装配bean的目的。
这些配置类的全类名就是从依赖jar包的文件中读取来的。我们接着看,该方法中关键的对象就是autoConfigurationEntry。
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
我们进入getAutoConfigurationEntry方法看它是怎么得到该对象的。在该方法中返回的对象包含了configurations,我们可以看到configurations是一个List集合,我们继续进入返回configurations对象的方法中。
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
在getCandidateConfigurations方法中,可以看到,该方法主要是对configurations集合进行非空判断的,还要继续进入loadFactoryNames方法。不过根据非空判断的提示信息也可以判断出要做什么。"在META-INF/spring.factories中找不到自动配置类。如果您正在使用自定义打包,请确保该文件是正确的。",所以说,loadFactoryNames方法是去META-INF文件下读取配置类的全类名的。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
如在Web开发的起步依赖中也可看到这么一个jar包,spring-boot-autoconfigure,该jar包下就包含存储Bean的配置类的全类名的META-INF文件以及Bean的配置管理类。
可以看到,该包下的META文件就包含了 spring.factories 这个文件。
spring.factories这个文件中就包含了很多自动配置类以及Bean的配置管理类的全类名。SpringBoot3 版本是将Bean的配置管理类分开放入了org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件下。spring.factories文件只剩下了配置项。目的是为了优化springboot的启动速度。
loadFactoryName 方法的作用就是读取 classpath 下的 META-INF/spring.factories 文件的配置项,将key为 org.springframework.boot.autoconfigure.AutoConfiguration.imports 对应Bean的配置类读取出来,然后通过@Import注解导入配置类,最后通过反射机制将配置类实例化交给Spring容器,完成配置类中bean的加载。
我们在spring.factories文件中点击进入其中一个Bean的配置管理类,可以看到该配置类中定义的Bean。
然后在定义Bean的方法上还有一个注解@ConditionalXXXX,该注解是按条件加载Bean,就是当满足该注解条件时才会加载该Bean。接下来有讲解按条件加载Bean。
3.5.3 按条件加载Bean
扫描到所有 Bean 并不是都需要加载的。会根据满足的条件进行加载,避免占用内存。
@ConditionalOnXxx
作用:按照一定条件进行判断,满足条件后才会加载Bean到IOC容器中。
位置:方法,类。加载类上对整个配置类中定义的Bean都有效。
@Conditional本身是一个父注解,它派生出了以下部分子注解:
条件注解 | 说明 |
---|---|
@ConditionalOnClass("...") | 判断是否存在指定的字节码文件,存在才加载Bean到容器中。不指定时默认是方法名。 |
@ConditionalOnMissingBean("...") | 判断是否存在指定的bean,不存在才会加载bean到容器中。不指定时默认是方法名。 |
@ConditionalOnProperty(xx="...", xx="...") | 判断配置文件中是否存在对应属性和值,存在才加载Bean到容器中。 |
@Bean
@ConditionalOnSingleCandidate(RabbitTemplate.class)
public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
return new RabbitMessagingTemplate(rabbitTemplate);
}
如以上Bean的定义,@ConditionalOnSingleCandidate注解用来判断RabbitMessagingTemplate类在 BeanFactory 中是否只有一个实例:
-
如果在 BeanFactory 中存在多个实例,则匹配失败;
-
如果在 BeanFactory 中仅仅存在一个实例,则匹配成功。
举一个因为配置问题而出现bean创建失败的例子:(一些 Bean 的加载条件是必须配置了某配置项信息)
我们在eureka-server项目的配置文件中将项目服务端口删除,然后运行eureka服务。
不出所料出异常了,在最下面的报错中可以看到是因为bean创建失败,bean的名称是eurekaInstanceConfigBean。
我们点击进入查看,该Bean是在EurekaClientAutoConfiguration配置类中定义的,该Bean的定义方法上有一个@ConditionalOnMissingBean注解,虽然项目中没有eurekaInstanceConfigBean这个bean对象,但是还是因为项目启动后没有找到服务端口的配置而创建失败。
所以 isSecurePortEnabled 为 false,在该方法内进行判断时出错了。所以此Bean不能被创建。
最后,我们将项目运行的服务端口加上,打开Actuator查看,可以看到EurekaClientAutoConfiguration配置下定义的EurekaInstanceConfigBean已经注册到了IOC称为eurekaInstanceConfigBean的bean对象。
打脸了,这个例子可能不太合适,因为不由于@ConditionalOnXXX而创建失败的,不过从中可以学到相关bean的创建。
4 总结
第三方Bean的加载装配:
第三方依赖的配置类中的Bean的加载可以简单总结为以下:
引入了eureka依赖后,通过以下的注解配置,将扫描到Bean配置类实例化交给容器后,自动将其中定义的Bean加载到IOC容器中。
-
在项目启动类上添加@ComponentScan("...", "..."),指定扫描引入的依赖的包名。
-
在启动类上添加@Import({...})注解,其中可以导入普通类,Bean配置管理类,ImportSelector接口实现类。其中ImportSelector接口的实现类会读取存储全类名文件,然后以字符串数组返回jar包中所有的类的全类名,最后达到扫描加载装配的目的。
-
在启动类上直接添加@EnableXXXX,该注解中封装了@Import注解,
在Bean加载的源码分析中,我们以SpringBoot中Bean的自动装配为例。
从核心注解@SpringBootApplication开始,其中封装配置类注解,自动配置注解以及自定义组件扫描注解。核心就是自动配置注解,其中封装了@Import注解,就是导入了ImportSelector接口的实现类,该实现类重要的方法就是selectImports,目的还是读取jar文件下所有Bean配置类的全类名,最后交给@Import注解导入配置类,通过反射机制将配置类实例化交给Spring容器,完成配置类中bean的加载。
@Import({AutoConfigurationImportSelector.class})
可以说自动装配的核心就是@Import注解。
按条件加载Bean:
因为有了按条件加载Bean,所以说,SpringBoot项目一启动,并不是所有导入的第三方依赖的配置类都会实例化交给Spring扫描,扫描的配置类中所有定义的Bean也并不是全部注册到IOC容器中。这样就不用白白浪费更多的资源。