Spring注解驱动开发

视频学习来源:尚硅谷

博客参考:Spring注解驱动开发

学习时间:2021年12月28日

1 组件注册

在Spring容器的底层,最重要的功能就是IOC和DI,也就是控制反转和依赖注入。

image-20211228144025382

DI和IOC它俩之间的关系是DI不能单独存在,DI需要在IOC的基础上来完成。

在Spring内部,所有的组件都会放到IOC容器中,组件之间的关系通过IOC容器来自动装配,也就是我们所说的依赖注入。接下来,我们就使用注解的方式来完成容器中组件的注册、管理及依赖、注入等功能。

在介绍使用注解完成容器中组件的注册、管理及依赖、注入等功能之前,我们先来看看使用XML配置文件是如何注入bean的。

1.1 组件注册 @Configuration和@bean

1.1.1 xml方式注入

引入spring依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

实体类Person

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
private String name;
private Integer age;
}

工程结构

image-20211228144336656

beans.xml配置文件注入bean

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 注册组件 -->
<bean id="person" class="com.hongyi.bean.Person">
<property name="name" value="Mark"></property>
<property name="age" value="12"></property>
</bean>
</beans>

测试类

1
2
3
4
5
6
7
8
9
public class Test1 {
@Test
public void test1(){
// 配置xml的方式,获取IOC容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Person bean = (Person) context.getBean("person");
System.out.println(bean);
}
}

image-20211228144745959

从输出结果中,我们可以看出,Person类通过beans.xml文件的配置,已经注入到Spring的IOC容器中去了。

1.1.2 注解注入

配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 配置类 == beans.xml
// 告诉spring这是一个配置类
@Configuration
public class MainConfig {
// 给容器中注册一个Bean
// bean的类型为返回值的类型,bean的id默认为方法名
// @Bean("person1") 也可用这样手动指定bean的id
@Bean
public Person person() {
return new Person("Lisi", 20);
}
// 对比一下xml配置方式
/*<bean id="person" class="com.hongyi.bean.Person">
<property name="name" value="Mark"></property>
<property name="age" value="12"></property>
</bean>*/
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test2() {
// 注解形式的IOC容器获取
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
Person bean = context.getBean(Person.class);
System.out.println(bean);
// 获取bean在spring容器中的id
String[] namesForType = context.getBeanNamesForType(Person.class);
for (String name : namesForType) {
System.out.println(name);
}
}

image-20211228144959343

1.2 组件扫描 @ComponentScan

在实际项目中,我们更多的是使用Spring的包扫描功能对项目中的包进行扫描,凡是在指定的包或其子包中的类上标注了@Repository、@Service、@Controller、@Component注解的类都会被扫描到,并将这个类注入到Spring容器中。

Spring包扫描功能可以使用XML配置文件进行配置,也可以直接使用@ComponentScan注解进行设置,使用@ComponentScan注解进行设置比使用XML配置文件来配置要简单的多。

1.2.1 xml方式

我们可以在Spring的XML配置文件中配置包的扫描,在配置包扫描时,需要在Spring的XML配置文件中的beans节点中引入context标签

1
2
<!--包扫描 只要标注了@Controller @Service @Repository @Component-->
<context:component-scan base-package="com.hongyi"></context:component-scan>

这样配置以后,只要在com.hongyi包下,或者com.hongyi的子包下标注了@Repository、@Service、@Controller、@Component注解的类都会被扫描到,并自动注入到Spring容器中

此时,我们分别创建BookDao、BookService以及BookController这三个类,并在这三个类中分别添加@Repository、@Service、@Controller注解。

  • BookDao
1
2
3
4
@Repository
public class BookDao {

}
  • BookService
1
2
3
4
@Service
public class BookService {

}
  • BookController
1
2
3
4
@Controller
public class BookController {

}

我们就可以在IOCTest测试类中编写如下一个方法来进行测试了,即看一看IOC容器中现在有哪些bean。

1
2
3
4
5
6
7
8
9
@Test
public void test() {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
// 我们现在就来看一下IOC容器中有哪些bean,即容器中所有bean定义的名字
String[] definitionNames = applicationContext.getBeanDefinitionNames();
for (String name : definitionNames) {
System.out.println(name);
}
}
1
2
3
4
5
6
7
8
9
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
mainConfig
bookController
bookDao
bookService
person

可以看到,除了输出我们自己创建的bean的名称之外,也输出了Spring内部使用的一些重要的bean的名称。接下来,我们使用注解来完成这些功能。

1.2.2 注解方式

1) 包扫描

配置类

1
2
3
4
5
6
7
8
9
10
// 配置类 == beans.xml
@Configuration
// 包扫描
@ComponentScan(value = "com.hongyi")
public class MainConfig {
@Bean
public Person person() {
return new Person("Lisi", 20);
}
}

测试

1
2
3
4
5
6
7
8
9
10
@Test
public void test3() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
// 获取容器中所有组件的id
String[] names = context.getBeanDefinitionNames();
for (String name :
names) {
System.out.println(name);
}
}
2) 扫描过滤器
  • 需求:除了@Controller和@Service标注的组件之外,IOC容器中剩下的组件我都要,即相当于是我要排除@Controller和@Service这俩注解标注的组件。要想达到这样一个目的,我们可以在MainConfig类上通过@ComponentScan注解的excludeFilters()方法实现。例如,我们在MainConfig类上添加了如下的注解。
1
2
3
@ComponentScan(value = "com.hongyi", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class})
})

执行结果:

image-20211228151312963

  • 需求:扫描时只包含注解标注的类。我们也可以使用ComponentScan注解类中的includeFilters()方法来指定Spring在进行包扫描时,只包含哪些注解标注的类。
    • 这里需要注意的是,当我们使用includeFilters()方法来指定只包含哪些注解标注的类时,需要禁用掉默认的过滤规则
    • 还记得我们以前在XML配置文件中配置这个只包含的时候,应该怎么做吗?我们需要在XML配置文件中先配置好use-default-filters=”false”,也就是禁用掉默认的过滤规则,因为默认的过滤规则就是扫描所有的,只有我们禁用掉默认的过滤规则之后,只包含才能生效
1
2
3
@ComponentScan(value = "com.hongyi", includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class})
}, useDefaultFilters = false) // 注意最后的禁用

执行结果:

image-20211228151446840

1.2.3 自定义TypeFilter

Spring的强大之处不仅仅是提供了IOC容器,能够通过过滤规则指定排除和只包含哪些组件,它还能够通过自定义TypeFilter来指定过滤规则。如果Spring内置的过滤规则不能够满足我们的需求,那么我们便可以通过自定义TypeFilter来实现我们自己的过滤规则。

在使用@ComponentScan注解实现包扫描时,我们可以使用@Filter指定过滤规则,在@Filter中,通过type来指定过滤的类型。而@Filter注解中的type属性是一个FilterType枚举,其源码如下。

1
2
3
4
5
6
7
8
9
10
public enum FilterType {
ANNOTATION,
ASSIGNABLE_TYPE,
ASPECTJ,
REGEX,
CUSTOM;

private FilterType() {
}
}
1) FilterType.ANNOTATION

按照注解进行包含或者排除。例如,使用@ComponentScan注解进行包扫描时,如果要想按照注解只包含标注了@Controller注解的组件,那么就需要像下面这样写了。

1
2
3
4
5
6
7
8
@ComponentScan(value="com.hongyi", includeFilters={
/*
* type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
* classes:我们需要Spring在扫描时,只包含@Controller注解标注的类
*/
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
}, useDefaultFilters=false) // value指定要扫描的包

2) FilterType.ASSIGNABLE_TYPE

按照给定的类型进行包含或者排除。

1
2
3
4
@ComponentScan(value="com.hongyi", includeFilters={
// 只要是BookService这种类型的组件都会被加载到容器中,不管是它的子类还是什么它的实现类。记住,只要是BookService这种类型的
@Filter(type=FilterType.ASSIGNABLE_TYPE, classes={BookService.class})
}, useDefaultFilters=false)

此时,只要是BookService这种类型的组件,都会被加载到容器中。也就是说,当BookService是一个Java类时,该类及其子类都会被加载到Spring容器中;当BookService是一个接口时,其子接口或实现类都会被加载到Spring容器中。

3) FilterType.REGEX

按照REGEX(正则表达式)表达式进行包含或者排除。

4) FilterType.CUSTOM

按照自定义规则进行包含或者排除。如果实现自定义规则进行过滤时,自定义规则的类必须是org.springframework.core.type.filter.TypeFilter接口的实现类

要想按照自定义规则进行过滤,首先我们得创建org.springframework.core.type.filter.TypeFilter接口的一个实现类,例如MyTypeFilter,该实现类的代码一开始如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyTypeFilter implements TypeFilter {

/**
* 自定义包扫描过滤器
* @param metadataReader 读取到的当前正在扫描的类的信息
* @param metadataReaderFactory 可以获取到其他任何类信息的
* @return
* @throws IOException
*/
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
// 获取当前类注解的信息
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
// 获取当前正在扫描的类的信息
ClassMetadata classMetadata = metadataReader.getClassMetadata();
// 获取当前类资源:类的路径
Resource resource = metadataReader.getResource();
// 获取类名
String className = classMetadata.getClassName();
System.out.println("--->" + className);
if (className.contains("er")){
return true; // 类名中包含“er”的就加入扫描
}
return false; // 否则返回false
}
}

image-20211228194018800

使用:

1
2
3
@ComponentScan(value = "com.hongyi", includeFilters = {
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = {MyTypeFilter.class})
}, useDefaultFilters = false)

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--->com.hongyi.test.Test1
--->com.hongyi.bean.Person
--->com.hongyi.config.MyTypeFilter
--->com.hongyi.controller.BookController
--->com.hongyi.dao.BookDao
--->com.hongyi.service.BookService
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
mainConfig
person
myTypeFilter
bookController
bookService

1.3 组件作用域 @Scope

Spring容器中的组件默认是单例的,在Spring启动时就会实例化并初始化这些对象,并将其放到Spring容器中,之后,每次获取对象时,直接从Spring容器中获取,而不再创建对象。如果每次从Spring容器中获取对象时,都要创建一个新的实例对象,那么该如何处理呢?此时就需要使用@Scope注解来设置组件的作用域了。

1.3.1 概述

image-20211228201141478

从@Scope注解类的源码中可以看出,在@Scope注解中可以设置如下值:

  • ConfigurableBeanFactory#SCOPE_PROTOTYPE

  • ConfigurableBeanFactory#SCOPE_SINGLETON

  • org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST

  • org.springframework.web.context.WebApplicationContext#SCOPE_SESSION

很明显,在@Scope注解中可以设置的值包括ConfigurableBeanFactory接口中的SCOPE_PROTOTYPE和SCOPE_SINGLETON,以及WebApplicationContext类中的SCOPE_REQUEST和SCOPE_SESSION。

首先,我们查看一下ConfigurableBeanFactory接口的源码,发现在该接口中存在两个常量的定义,如下所示。

image-20211228201158279

没错,SCOPE_SINGLETON就是singleton,而SCOPE_PROTOTYPE就是prototype。

综上,在@Scope注解中的取值如下所示。

image-20211228201242320

1.3.2 单实例bean作用域

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test4() {
// 获取ioc容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig2.class);
// 获取容器中所有组件的id
String[] names = context.getBeanDefinitionNames();
for (String name :
names) {
System.out.println(name);
}
// 默认bean是单实例的
Object bean1 = context.getBean("person");
Object bean2 = context.getBean("person");
System.out.println(bean1 == bean2); // true
}

这也正好验证了我们的结论:对象在Spring容器中默认是单实例的,Spring容器在启动时就会将实例对象加载到Spring容器中,之后,每次从Spring容器中获取实例对象,都是直接将对象返回,而不必再创建新的实例对象了

1.3.3 多实例bean作用域

修改Spring容器中组件的作用域,我们需要借助于@Scope注解。此时,我们将MainConfig2配置类中Person对象的作用域修改成prototype,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class MainConfig2 {

// 默认是单实例的
/*
prototype:多实例,当获取对象时,ioc容器才创建此对象(懒汉式),多次获取,多次创建
singleton:单例(默认),ioc容器启动时,会调用方法创建对象并注册到容器中。以后每次获取,直接从容器中拿(饿汉式)。
request:同一次请求创建一个实例
session:同一个session创建一个实例
*/
@Scope("prototype")
@Bean("person")
public Person person() {
System.out.println("给容器中添加Person...");
return new Person("Hongyi", 24);
}
}

测试结果:

1
false

1.3.4 注意事项

单实例bean注意事项

单实例bean是整个应用所共享的,所以需要考虑到线程安全问题,之前在玩SpringMVC的时候,SpringMVC中的Controller默认是单例的,有些开发者在Controller中创建了一些变量,那么这些变量实际上就变成共享的了,Controller又可能会被很多线程同时访问,这些线程并发去修改Controller中的共享变量,此时很有可能会出现数据错乱的问题,所以使用的时候需要特别注意。

多实例bean注意事项

多实例bean每次获取的时候都会重新创建,如果这个bean比较复杂,创建时间比较长,那么就会影响系统的性能,因此这个地方需要注意点。

1.3.5 懒加载 @Lazy

懒加载:针对单实例bean:默认在容器启动时创建对象。对于懒加载,容器启动时不创建对象,第一次获取bean创建对象,并且初始化。

1
2
3
4
5
6
7
8
9
@Configuration
public class MainConfig2 {
@Bean("person")
@Lazy
public Person person() {
System.out.println("给容器中添加Person...");
return new Person("Hongyi", 24);
}
}

懒加载,也称延时加载仅针对单实例bean生效。 单实例bean是在Spring容器启动的时候加载的,添加@Lazy注解后就会延迟加载,在Spring容器启动的时候并不会加载,而是在第一次使用此bean的时候才会加载,但当你多次获取bean的时候并不会重复加载,只是在第一次获取的时候才会加载,这不是延迟加载的特性,而是单实例bean的特性。

1.4 条件注册 @Conditional

image-20211229101507511

按照一定条件进行判断,满足条件则给容器中注册bean。

需求:如果系统是Windows,则给容器注册(“bill”),如果系统是Linux,则给容器注册(“linus”)

image-20211229101551587

编写Condition接口实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 判断系统是否是linux系统
*/
public class LinuxCondition implements Condition {

/**
* 判断系统是否是linux系统
* @param conditionContext 判断条件能使用的上下文(环境)
* @param annotatedTypeMetadata 注释信息
* @return
*/
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
// 1.获取到ioc使用的beanfactory
ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
// 2.获取类加载器
ClassLoader classLoader = conditionContext.getClassLoader();
// 3.获取当前环境信息
Environment environment = conditionContext.getEnvironment();
String property = environment.getProperty("os.name");
if (property.contains("linux")) {
return true;
}
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 判断系统是否是Windows系统
*/
public class WindowsCondition implements Condition {
/**
* 判断系统是否是Windows系统
* @param conditionContext 判断条件能使用的上下文(环境)
* @param annotatedTypeMetadata 注释信息
* @return
*/
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment();
String property = environment.getProperty("os.name");
if (property.contains("Windows")) {
return true;
}
return false;
}
}

配置类MainConfig1.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 也可以声明在整个配置类上,将对所有的bean都生效
// @Conditional({WindowsCondition.class})
@Configuration
public class MainConfig2 {
// Conditional注解的用法:
@Conditional({WindowsCondition.class})
@Bean("bill")
public Person person01() {
return new Person("Bill Gates", 62);
}

@Conditional({LinuxCondition.class})
@Bean("linus")
public Person person02() {
return new Person("Linus", 48);
}
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test5() {
// 获取ioc容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig2.class);
// 获取环境变量
ConfigurableEnvironment environment = context.getEnvironment();
// 获取操作系统属性
String property = environment.getProperty("os.name");
System.out.println(property);
String[] namesForType = context.getBeanNamesForType(Person.class);
for (String name :
namesForType) {
System.out.println(name);
}

Map<String, Person> beansOfType = context.getBeansOfType(Person.class);
System.out.println(beansOfType);
}

执行结果:

image-20211229101752101

修改vm参数为linux环境

1
-Dos.name = linux

image-20211229102130030

执行结果:

image-20211229102141857

1.5 导入组件 @Import

1.5.1 注册组件的方式

向Spring容器中注册bean通常有以下几种方式:

  1. 包扫描+给组件标注注解(@Controller、@Servcie、@Repository、@Component),但这种方式比较有局限性,局限于我们自己写的类
  2. @Bean注解,通常用于导入第三方包中的组件
  3. @Import注解,快速向Spring容器中导入一个组件

1.5.2 @Import概述和使用

概述

Spring 3.0之前,创建bean可以通过XML配置文件与扫描特定包下面的类来将类注入到Spring IOC容器内。而在Spring 3.0之后提供了JavaConfig的方式,也就是将IOC容器里面bean的元信息以Java代码的方式进行描述,然后我们可以通过@Configuration与@Bean这两个注解配合使用来将原来配置在XML文件里面的bean通过Java代码的方式进行描述。

@Import注解提供了@Bean注解的功能,同时还有XML配置文件里面标签组织多个分散的XML文件的功能,当然在这里是组织多个分散的@Configuration,因为一个配置类就约等于一个XML配置文件。

注意:@Import注解只允许放到类上面,不允许放到方法上。

使用方式

@Import注解的三种用法主要包括:

  1. 直接填写class数组的方式

  2. ImportSelector接口的方式,即批量导入,这是重点

  3. ImportBeanDefinitionRegistrar接口方式,即手工注册bean到容器中

注意:我们先来看第一种方法,即直接填写class数组的方式:

MainConfig1.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@Import({Color.class, Animal.class})
// 快速导入组件,id默认为组件的全类名
public class MainConfig2 {

@Bean("person")
public Person person() {
return new Person("Hongyi", 24);
}

@Conditional({WindowsCondition.class})
@Bean("bill")
public Person person01() {
return new Person("Bill Gates", 62);
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 打印容器中所有的组件id
* @param context
*/
public void printBeans(AnnotationConfigApplicationContext context){
String[] names = context.getBeanDefinitionNames();
for (String name :
names) {
System.out.println(name);
}
}

@Test
public void test6() {
printBeans(context);
}

image-20211229103602794

1.5.3 @ImportSelector使用

ImportSelector接口是Spring中导入外部配置的核心接口,在Spring Boot的自动化配置和@EnableXXX(功能性注解)都有它的存在。它返回需要导入的组件的全类名数组(是一个字符串数组)。

image-20211231160645463

该接口文档上说的明明白白,其主要作用是收集需要导入的配置类,selectImports()方法的返回值就是我们向Spring容器中导入的类的全类名。如果该接口的实现类同时实现EnvironmentAware,BeanFactoryAware,BeanClassLoaderAware或者ResourceLoaderAware,那么在调用其selectImports()方法之前先调用上述接口中对应的方法,如果需要在所有的@Configuration处理完再导入时,那么可以实现DeferredImportSelector接口。

在ImportSelector接口的selectImports()方法中,存在一个AnnotationMetadata类型的参数,这个参数能够获取到当前标注@Import注解的类的所有注解信息,也就是说不仅能获取到@Import注解里面的信息,还能获取到其他注解的信息。

MyImportSelector.java

创建一个MyImportSelector类实现ImportSelector接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 自定义逻辑返回需要导入的组件
*/
public class MyImportSelector implements ImportSelector {

/**
*
* @param annotationMetadata 当前标注@Import注解的类的所有注解信息
* @return 导入到容器中的组件全类名的字符串数组
*/
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 组件全类名的字符串数组
return new String[]{"com.hongyi.bean.Blue", "com.hongyi.bean.Yellow"};
}

@Override
public Predicate<String> getExclusionFilter() {
return ImportSelector.super.getExclusionFilter();
}
}

配置类

1
@Import({Color.class, Animal.class, MyImportSelector.class})

测试类

1
2
3
4
5
6
@Test
public void test6() {
printBeans(context);
Blue blue = context.getBean(Blue.class);
System.out.println(blue);
}

image-20211231160911942

1.5.4 @ImportBeanDefinitionRegistrar使用

接口实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
*
* @param importingClassMetadata 当前类的注解信息
* @param registry BeanDefinition注册类
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
boolean yellow = registry.containsBeanDefinition("com.hongyi.bean.Yellow");
boolean blue = registry.containsBeanDefinition("com.hongyi.bean.Blue");
if (yellow && blue){
// 指定bean的定义信息(Bean的类型,作用域...)
RootBeanDefinition definition = new RootBeanDefinition(Rainbow.class);
registry.registerBeanDefinition("rainbow", definition);
}
}
}

配置类的使用

1
@Import({Color.class, Animal.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class})

测试结果

image-20211231162117354

1.5.5 FactoryBean使用

1) 概述

一般情况下,Spring是通过反射机制利用bean的class属性指定实现类来实例化bean的。在某些情况下,实例化bean过程比较复杂,如果按照传统的方式,那么则需要在标签中提供大量的配置信息,配置方式的灵活性是受限的,这时采用编码的方式可以得到一个更加简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化bean的逻辑。

FactoryBean接口对于Spring框架来说占有非常重要的地位,Spring自身就提供了70多个FactoryBean接口的实现。它们隐藏了实例化一些复杂bean的细节,给上层应用带来了便利。从Spring 3.0开始,FactoryBean开始支持泛型,即接口声明改为FactoryBean<T>的形式。

FactoryBean源码

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

@Nullable
T getObject() throws Exception;

@Nullable
Class<?> getObjectType();

default boolean isSingleton() {
return true;
}
}
  • T getObject():返回由FactoryBean创建的bean实例,如果isSingleton()返回true,那么该实例会放到Spring容器中单实例缓存池中

  • boolean isSingleton():返回由FactoryBean创建的bean实例的作用域是singleton还是prototype

  • Class getObjectType():返回FactoryBean创建的bean实例的类型

这里,需要注意的是:当配置文件中标签的class属性配置的实现类是FactoryBean时,通过 getBean()方法返回的不是FactoryBean本身,而是FactoryBean#getObject()方法所返回的对象,相当于FactoryBean#getObject()代理了getBean()方法。

2) 使用
  • ColorFactoryBean.java接口实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 创建一个Spring定义的FactoryBean
public class ColorFactoryBean implements FactoryBean<Color> {

/**
* 返回一个Color对象,这个对象会被添加进容器中
* @return 返回一个Color对象,这个对象会被添加进容器中
* @throws Exception 抛出异常
*/
@Override
public Color getObject() throws Exception {
System.out.println("ColorFactoryBean...getObject");
return new Color();
}

@Override
public Class<?> getObjectType() {
return Color.class;
}

/**
* 控制是否是单例
* @return true则是单实例,在容器中保存一个
*/
@Override
public boolean isSingleton() {
return true;
}
}
  • 然后,我们在MainConfig2配置类中加入ColorFactoryBean的声明,如下所示。
1
2
3
4
@Bean
public ColorFactoryBean colorFactoryBean() {
return new ColorFactoryBean();
}

这里需要注意的是:这里使用@Bean注解向Spring容器中注册的是ColorFactoryBean对象。

那现在我们就来看看Spring容器中到底都有哪些bean。我们所要做的事情就是,运行IOCTest类中的testImport()方法,此时,输出的结果信息如下所示。

1
2
3
4
5
6
7
@Test
public void test6() {
printBeans(context);
// 工厂bean获取到的是调用getObject创建的对象
Object colorFactoryBean = context.getBean("colorFactoryBean");
System.out.println(colorFactoryBean.getClass());
}

image-20211231163605067

可以看到,虽然在代码中使用@Bean注解注入的是ColorFactoryBean对象,但是实际上从Spring容器中获取到的bean对象却是调用ColorFactoryBean类中的getObject()方法获取到的Color对象

获取FactoryBean对象本身

只需要在获取工厂Bean本身时,在id前面加上&符号即可,例如&colorFactoryBean。

1
2
3
4
5
6
7
@Test
public void test6() {
printBeans(context);
// 工厂bean获取到的是调用getObject创建的对象
Object colorFactoryBean = context.getBean("&colorFactoryBean");
System.out.println(colorFactoryBean.getClass());
}

image-20211231163802404

2 生命周期

bean的生命周期:创建—初始化—销毁

通常意义上讲的bean的生命周期,指的是bean从创建到初始化,经过一系列的流程,最终销毁的过程。只不过,在Spring中,bean的生命周期是由Spring容器来管理的。在Spring中,我们可以自己来指定bean的初始化和销毁的方法。我们指定了bean的初始化和销毁方法之后,当容器在bean进行到当前生命周期的阶段时,会自动调用我们自定义的初始化和销毁方法。

容器管理bean的生命周期:我们可以自定义初始化和销毁方法,容器在bean进行到当前生命周期的时候来调用我们自定义的初始化和销毁。

2.1 通过@Bean指定

首先,创建一个名称为Car的类,这个类的实现比较简单,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Car {
public Car() {
System.out.println("car constructor...");
}

// 自定义初始化方法
public void init() {
System.out.println("car init...");
}

// 自定义销毁方法
public void destroy() {
System.out.println("car destroy...");
}
}

然后,我们将Car类对象通过注解的方式注册到Spring容器中,具体的做法就是新建一个MainConfigOfLifeCycle类作为Spring的配置类,将Car类对象通过MainConfigOfLifeCycle类注册到Spring容器中,MainConfigOfLifeCycle类的代码如下所示。

1
2
3
4
5
6
7
8
@Configuration
public class MainConfigOfLifeCycle {
// 指定自定义init和destroy方法
@Bean(initMethod = "init", destroyMethod = "destroy")
public Car car() {
return new Car();
}
}

构造(对象创建)

  1. 单实例:在容器启动时创建对象
  2. 多实例:在每次获取对象的时候创建对象

2.1.1 初始化

初始化:对象创建完成,并赋值好,调用初始化方法

  1. 单实例:在容器启动时创建对象,并调用初始化方法
  • 测试类:
1
2
3
4
5
6
@Test
public void test7() {
// 创建ioc容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
System.out.println("容器创建完成...");
}

image-20220102105815714

  1. 多实例:在每次获取对象的时候创建对象,并调用初始化方法
  • 配置类:
1
2
3
4
5
6
7
8
9
@Configuration
public class MainConfigOfLifeCycle {
// 配置多实例
@Scope("prototype")
@Bean(initMethod = "init", destroyMethod = "destroy")
public Car car() {
return new Car();
}
}
  • 测试类:
1
2
3
4
5
6
7
8
@Test
public void test7() {
// 创建ioc容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
System.out.println("容器创建完成...");
// 获取对象
Object car = context.getBean("car");
}

image-20220102105955765

注意与单实例的顺序

2.1.2 销毁

  1. 单实例:容器关闭的时候,调用销毁方法
  • 测试类
1
2
3
4
5
6
7
8
@Test
public void test7() {
// 创建ioc容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
System.out.println("容器创建完成...");
// 关闭容器
context.close();
}

image-20220102110301518

  1. 多实例:容器不会管理这个bean,不会调用销毁方法
  • 测试类:
1
2
3
4
5
6
7
8
9
10
@Test
public void test7() {
// 创建ioc容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
System.out.println("容器创建完成...");
// 获取对象
Object car = context.getBean("car");
// 关闭容器
context.close();
}

image-20220102110355484

可见ioc容器没有调用car的销毁方法

2.2 InitializingBean和DisposableBean接口

通过让Bean实现InitializingBean(定义初始化逻辑),DisposableBean(定义销毁逻辑)

Cat类

首先创建一个Cat类,实现其接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 作为一个组件,让配置类扫描添加进容器即可
@Component
public class Cat implements InitializingBean, DisposableBean {
public Cat() {
System.out.println("cat constructor...");
}


@Override
public void afterPropertiesSet() throws Exception {
System.out.println("cat init...");
}

@Override
public void destroy() throws Exception {
System.out.println("cat destroy...");
}
}

配置类

1
2
3
4
5
@ComponentScan("com.hongyi.bean")
@Configuration
public class MainConfigOfLifeCycle {

}

测试类

1
2
3
4
5
6
7
8
@Test
public void test7() {
// 创建ioc容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
System.out.println("容器创建完成...");
// 关闭容器
context.close();
}

image-20220102111240029

2.3 @PostConstructor和@PreDestroy

  • @PostConstructor:在bean创建完成并且属性赋值完成,来执行初始化方法
  • @PreDestroy:在容器销毁bean之前,通知我们进行清理工作

首先添加依赖(这个api在java9之后被移除):

1
2
3
4
5
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>

然后创建一个Dog类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class Dog {
public Dog() {
System.out.println("Dog Constructor...");
}

@PostConstruct
public void init() {
System.out.println("Dog init...");
}

@PreDestroy
public void destroy() {
System.out.println("dog destroy...");
}
}

测试类

1
2
3
4
5
6
7
8
@Test
public void test7() {
// 创建ioc容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
System.out.println("容器创建完成...");
// 关闭容器
context.close();
}

image-20220102112527982

2.4 BeanPostProcessor后置处理器接口

2.4.1 使用

在bean初始化前后进行一些处理工作。

  • postProcessBeforeInitialization:在初始化之前工作
  • postProcessAfterInitialization:在初始化之后工作
  1. 首先创建MyBeanPostProcessor类,实现BeanPostProcessor接口,并添加进容器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 后置处理器,初始化前后进行处理
* 将后置处理器加入到ioc容器中
*/
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessBeforeInitialization..." + beanName + "=>" + bean);
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessAfterInitialization..." + beanName + "=>" + bean);
return bean;
}
}
  1. 测试类:
1
2
3
4
5
6
7
8
@Test
public void test7() {
// 创建ioc容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class);
System.out.println("容器创建完成...");
// 关闭容器
context.close();
}

image-20220102113914536

  • 对象构造后(Dog Constructor),调用初始化之前(Dog init),postProcessBeforeInitialization方法被调用;
  • 对象初始化之后(Dog init),postProcessAfterInitialization方法被调用。

2.4.2 原理

暂略

2.4.3 在spring底层的使用

暂略

3 属性赋值

3.1 @Value赋值

3.1.1 源码

Spring中的@Value注解可以为bean中的属性赋值。我们先来看看@Value注解的源码,如下所示。

1
2
3
4
5
6
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {
String value();
}

从@Value注解的源码中我们可以看出,@Value注解可以标注在字段、方法、参数以及注解上,而且在程序运行期间生效。

3.1.2 使用

通过@Value注解将外部的值动态注入到bean的属性中,一般有如下这几种情况:

  1. 注入普通字符串
1
2
@Value("Mark")
private String name; // 注入普通字符串
  1. 注入操作系统属性
1
2
@Value("#{systemProperties['os.name']}")
private String systemPropertiesName; // 注入操作系统属性
  1. 注入SpEL表达式结果
1
2
@Value("#{ T(java.lang.Math).random() * 100.0 }")
private double randomNumber; //注入SpEL表达式结果
  1. 注入其他bean中属性的值
1
2
@Value("${person.name}")
private String username; // 注入其他bean中属性的值,即注入person对象的name属性中的值
  1. 注入文件资源
1
2
@Value("classpath:/config.properties")
private Resource resourceFile; // 注入文件资源
  1. 注入URL资源
1
2
@Value("http://www.baidu.com")
private Resource url; // 注入URL资源

示例

  • 新建一个配置类:
1
2
3
4
5
6
7
@Configuration
public class MainConfigOfPropertyValues {
@Bean
public Person person() {
return new Person();
}
}
  • 在Person类的属性上添加@Value注解
1
2
3
4
5
6
7
8
9
10
11
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person {
@Value("Mark")
private String name;

@Value("#{20-2}") // SpEL表达式
private Integer age;
}
  • 测试类
1
2
3
4
5
6
7
8
@Test
public void test1() {
printBeans(context);
System.out.println("------------------");
Person person = (Person) context.getBean("person");
System.out.println(person);
context.close();
}

image-20220107162814632

可见属性已被成功赋值

3.2 @PropertySource赋值

@PropertySource注解是Spring 3.1开始引入的配置类注解。通过@PropertySource注解可以将properties配置文件中的key/value存储到Spring的Environment中,Environment接口提供了方法去读取配置文件中的值,参数是properties配置文件中定义的key值。当然,也可以使用@Value注解用${}占位符为bean的属性注入值。

3.2.1 使用

  1. 首先创建properties文件:person.properties
1
person.nickName = Jayden
  1. 在Person类中添加nickName属性,并使用@Value注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person {
@Value("Mark")
private String name;

@Value("#{20-2}") // 注意符号为#
private Integer age;

@Value("${person.nickName}") // 注意符号为$
private String nickName;
}
  1. 在配置类中使用@PropertySource注解,并导入properties文件
1
2
3
4
5
6
7
8
9
// 读取外部配置文件中的键值对,并保存到运行环境变量中
@PropertySource(value = {"classpath:/person.properties"})
@Configuration
public class MainConfigOfPropertyValues {
@Bean
public Person person() {
return new Person();
}
}
  1. 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test1() {
printBeans(context);
System.out.println("------------------");
Person person = (Person) context.getBean("person");
System.out.println(person);
// 获取运行时环境
ConfigurableEnvironment environment = context.getEnvironment();
// 通过k获取v
String property = environment.getProperty("person.nickName");
// 打印v
System.out.println(property);
context.close();
}

image-20220107164608721

3.2.2 #{ }和${ }的区别

1) ${ }的用法

{ }里面的内容必须符合SpEL表达式,通过@Value(“${spelDefault.value}”)我们可以获取属性文件中对应的值,但是如果属性文件中没有这个属性,那么就会报错。不过,我们可以通过赋予默认值来解决这个问题,如下所示。

1
2
@Value("${author.name:meimeixia}")
private String name;

上述代码的含义是表示向bean的属性中注入属性文件中的author.name属性所对应的值,如果属性文件中没有author.name这个属性,那么便向bean的属性中注入默认值meimeixia

2) #{ }的用法

{ }里面的内容同样也是必须符合SpEL表达式。例如,

1
2
3
4
5
6
7
// SpEL:调用字符串Hello World的concat方法
@Value("#{'Hello World'.concat('!')}")
private String helloWorld;

// SpEL:调用字符串的getBytes方法,然后再调用其length属性
@Value("#{'Hello World'.bytes.length}")
private String helloWorldBytes;
3) 混合使用
1
2
3
// SpEL:传入一个字符串,根据","切分后插入列表中, #{}和${}配合使用时,注意不能反过来${}在外面,而#{}在里面
@Value("#{'${server.name}'.split(',')}")
private List<String> severs;

那么反过来是否可以呢?也就是说能否让${}在外面,#{}在里面? 失败!

因为Spring执行${}的时机要早于#{},当Spring执行外层的${}时,内部的#{}为空,所以会执行失败!

4) 小结
  • #{···}:用于执行SpEl表达式,并将内容赋值给属性
  • ${···}:主要用于加载外部属性文件中的值
  • ${···}#{···}可以混合使用,但是必须#{}在外面,${}在里面

4 自动装配

Spring组件的自动装配就是Spring利用依赖注入,也就是我们通常所说的DI,完成对IOC容器中各个组件的依赖关系赋值。

4.1 Spring规范注解

4.1.1 @Autowired

@Autowired注解可以对类成员变量、方法和构造函数进行标注,完成自动装配的工作。

@Autowired注解可以放在类、接口以及方法上。

在使用@Autowired注解之前,我们对一个bean配置属性时,是用如下XML配置文件的形式进行配置的(set方法注入)。

1
<property name="属性名" value=" 属性值"/>

@Autowired源码

1
2
3
4
5
6
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
  1. @Autowired注解默认是优先按照类型去容器中找对应的组件,相当于是调用了如下这个方法:
1
applicationContext.getBean(类名.class);

若找到则就赋值。

  1. 如果找到多个相同类型的组件,那么是将变量名称作为组件的id,再用这个id到IOC容器中进行查找,这时就相当于是调用了如下这个方法:
1
applicationContext.getBean("组件的id");

4.1.2 @Qualifier

@Autowired是根据类型进行自动装配的,如果需要按名称进行装配,那么就需要配合@Qualifier注解来使用了。

源码

1
2
3
4
5
6
7
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
String value() default ""; // 指定按照哪个名称的bean进行注入
}

4.1.3 @Primary

在Spring中使用注解时,常常会使用到@Autowired这个注解,它默认是根据类型Type来自动注入的。但有些特殊情况,对同一个接口而言,可能会有几种不同的实现类,而在默认只会采取其中一种实现的情况下,就可以使用@Primary注解来标注优先使用哪一个实现类。

源码

1
2
3
4
5
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Primary {
}

4.1.4 注解使用

1) @Autowired

这里,我们以之前项目中创建的BookDao、BookService和BookController为例进行说明。

BookDao、BookService和BookController的初始代码分别如下所示。

  • BookDao
1
2
3
4
5
// id默认是类名首字母小写
@Repository
public class BookDao {

}
  • BookService
1
2
3
4
5
6
7
8
9
10
11
12
@Service
@ToString
public class BookService {

@Autowired
private BookDao bookDao;

public void print() {
System.out.println(bookDao);
}

}
  • BookController
1
2
3
4
5
6
7
8
@Controller
public class BookController {

@Autowired
private BookService bookService;

}

可以看到,我们在BookService中使用@Autowired注解注入了BookDao,在BookController中使用@Autowired注解注入了BookService。在IOC容器中,有bookDao(类型为BookDao),bookService(类型为BookService),bookController(类型为BookController)的组件bean。

  • 创建一个配置类:
1
2
3
4
5
6
7
8
9
/**
* 自动装配
*/
@Configuration
// 扫描,并添加到IOC容器中
@ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"})
public class MainConfigOfAutowired {

}
  • 测试类
1
2
3
4
5
6
7
8
9
10
11
@Test
public void test2() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAutowired.class);
// 按类型获取组件
BookService bookService = context.getBean(BookService.class);
// 按类型获取组件
BookDao bookDao = context.getBean(BookDao.class);
System.out.println(bookService);
System.out.println(bookDao);
context.close();
}

image-20220109213823922

可以看到,我们在BookService类中使用@Autowired注解注入的BookDao对象和直接从IOC容器中获取的BookDao对象是同一个对象

如果在Spring容器中存在对多个BookDao对象,那么这时又该如何处理呢?

首先,为了更加直观的看到我们使用@Autowired注解装配的是哪个BookDao对象,我们得对BookDao类进行改造,为其加上一个lable字段,并为其赋一个默认值,如下所示。

1
2
3
4
5
6
7
@Repository
@Getter
@Setter
@ToString
public class BookDao {
private String label = "1";
}

然后,我们就在MainConfigOfAutowired配置类中手动注入一个BookDao对象,并且显示指定该对象在IOC容器中的bean的id为bookDao2,并还为该对象的lable字段赋值为2,如下所示。

1
2
3
4
5
6
7
8
9
10
@Configuration
@ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"})
public class MainConfigOfAutowired {
@Bean("bookDao2")
public BookDao bookDao() {
BookDao bookDao = new BookDao();
bookDao.setLabel("2");
return bookDao;
}
}

目前,在我们的IOC容器中就会注入两个BookDao对象。那此时,@Autowired注解到底装配的是哪个BookDao对象呢?

发现输出的结果信息如下所示(这是我的结果,报错了)。

1
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hongyi.dao.BookDao' available: expected single matching bean but found 2: bookDao,bookDao2

下面是参考博客的结果和说明:

image-20220109214202720

可以看到,结果信息输出了lable=1,这说明,@Autowired注解默认是优先按照类型去容器中找对应的组件,找到就赋值;如果找到多个相同类型的组件,那么再将属性变量的名称作为组件的id,到IOC容器中进行查找。

那我们如何让@Autowired注解装配bookDao2呢? 这个问题问的好,其实很简单,我们只须将BookService类中的bookDao属性的名称全部修改为bookDao2即可,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@ToString
public class BookService {

@Autowired
private BookDao bookDao2;

public void print() {
System.out.println(bookDao2);
}

}

此时在IOC容器中有两个相同类型(BookDao)的组件bookDao(BookDao扫描后添加,label=1)和bookDao2(配置类手动声明添加,label=2),自动装配时,Spring在容器中查找到两个,但是要按照@Autowired下面的变量名称(bookDao2)进行注入,于是得到label=2的bookDao2。

这是我的结果,还是报错了:

1
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hongyi.dao.BookDao' available: expected single matching bean but found 2: bookDao,bookDao2

下面是参考博客的结果和结论:

image-20220109214439604

2) @Qulifier

从测试@Autowired注解的结果来看,@Autowired注解默认是优先按照类型去容器中找对应的组件,找到就赋值;如果找到多个相同类型的组件,那么再将属性变量的名称作为组件的id,到IOC容器中进行查找。

如果IOC容器中存在多个相同类型的组件时,那么我们可不可以显示指定@Autowired注解装配哪个组件呢?有些小伙伴肯定会说:废话!你都这么问了,那肯定可以啊!没错,确实是可以的!此时,@Qualifier注解就派上用场了!

在之前的测试案例中,Eclipse控制台中输出了BookDao [lable=2],这说明@Autowired注解装配了bookDao2,那我们如何显示的让@Autowired注解装配bookDao呢?

比较简单,我们只需要在BookService类里面的bookDao2字段上添加@Qualifier注解,显示指定@Autowired注解装配bookDao即可,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@ToString
public class BookService {
// 强制要求注入id为bookDao的BookDao对象
@Qualifier("bookDao")
@Autowired
private BookDao bookDao2;

public void print() {
System.out.println(bookDao2);
}
}

此时,我们再次运行IOCTest_Autowired类中的test01()方法,输出的结果信息如下所示。

image-20220109214935197

可以看到,此时尽管字段的名称为bookDao2,但是我们使用了@Qualifier注解显示指定了@Autowired注解装配bookDao对象,所以,最终的结果中输出了bookDao对象的信息。

3) 容器中无组件的情况

如果IOC容器中无相应的组件,那么会发生什么情况呢?这时我们可以做这样一件事情,先注释掉BookDao类上的@Repository注解,然后再注释掉MainConfigOfAutowired配置类中的bookDao()方法上的@Bean注解。

此时IOC容器中不再有任何BookDao对象了。

接着,我们再次运行IOCTest_Autowired类中的test01()方法,发现Eclipse控制台报了一个错误,截图如下。

1
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'bookService': Unsatisfied dependency expressed through field 'bookDao2'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.meimeixia.dao.BookDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=bookDao), @org.springframework.beans.factory.annotation.Autowired(required=true)}

解决方案就是在BookService类的@Autowired注解里面添加一个属性required=false,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@ToString
public class BookService {

@Qualifier("bookDao")
@Autowired(required = false)
private BookDao bookDao2;

public void print() {
System.out.println(bookDao2);
}
}

加上required=false的意思就是说找到就装配,找不到就拉到,就别装配了。

执行结果:

image-20220109215242865

可以看到,当为@Autowired注解添加属性required=false后,即使IOC容器中没有对应的对象,Spring也不会抛出异常了。不过,此时装配的对象就为null了

4) @Primary

在Spring中,对同一个接口而言,可能会有几种不同的实现类,而默认只会采取其中一种实现的情况下,就可以使用@Primary注解来标注优先使用哪一个实现类。

如果IOC容器中相同类型的组件有多个,那么我们不可避免地就要来回用@Qualifier注解来指定要装配哪个组件,这还是比较麻烦的,Spring正是帮我们考虑到了这样一种情况,就提供了这样一个比较强大的注解,即@Primary。我们可以利用这个注解让Spring进行自动装配的时候,默认使用首选的bean。

首先,我们在MainConfigOfAutowired配置类的bookDao()方法上添加上@Primary注解,如下所示。

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"})
public class MainConfigOfAutowired {
@Primary
@Bean("bookDao2")
public BookDao bookDao() {
BookDao bookDao = new BookDao();
bookDao.setLabel("2");
return bookDao;
}
}

注意:此时,我们需要注释掉BookService类中bookDao字段上的@Qualifier注解,这是因为@Qualifier注解为显示指定装配哪个组件,如果使用了@Qualifier注解,无论是否使用了@Primary注解,都会装配@Qualifier注解标注的对象。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@ToString
public class BookService {

// @Qualifier("bookDao")
@Autowired(required = false)
private BookDao bookDao2;

public void print() {
System.out.println(bookDao2);
}
}

image-20220109215533010

此时在IOC容器中有两个相同类型(BookDao)的组件bookDao(BookDao扫描后添加,label=1)和bookDao2(配置类手动声明添加,label=2),自动装配时,Spring在容器中查找到两个,但是要按照@Primary下面的变量名称(bookDao2)进行注入,于是得到label=2的bookDao2。

4.2 Java规范注解

这两种是Java规范的注解,第一节是Spring规范的注解。

4.2.1 @Resource

@Resource注解是Java规范里面的,也可以说它是JSR250规范里面定义的一个注解。该注解默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,那么默认取字段名将其作为组件的名称在IOC容器中进行查找,如果注解写在setter方法上,那么默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的一点是,如果name属性一旦指定,那么就只会按照名称进行装配。

它与@Autowired注解不同,与@Primary和@Qualifier搭配时,后者并不生效。

使用

1
2
3
4
5
6
7
8
9
10
11
@Service
@ToString
public class BookService {

@Resource
private BookDao bookDao;

public void print() {
System.out.println(bookDao);
}
}
1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"})
public class MainConfigOfAutowired {
@Primary
@Bean("bookDao2")
public BookDao bookDao() {
BookDao bookDao = new BookDao();
bookDao.setLabel("2");
return bookDao;
}
}

image-20220110204038535

可见@Primary注解并不生效

4.2.2 @Inject

@Inject注解也是Java规范里面的,也可以说它是JSR330规范里面定义的一个注解。该注解默认是根据参数名去寻找bean注入,支持Spring的@Primary注解优先注入,@Inject注解还可以增加@Named注解指定要注入的bean。

作用等同于@Autowired,只不过默认按照参数名注入。

要想使用@Inject注解,需要在项目的pom.xml文件中添加如下依赖,即导入javax.inject这个包。

1
2
3
4
5
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>

使用

1
2
3
4
5
6
7
8
9
10
11
@Service
@ToString
public class BookService {

@Inject
private BookDao bookDao;

public void print() {
System.out.println(bookDao);
}
}
1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"})
public class MainConfigOfAutowired {
@Primary
@Bean("bookDao2")
public BookDao bookDao() {
BookDao bookDao = new BookDao();
bookDao.setLabel("2");
return bookDao;
}
}

image-20220110204324060

可见@Primary已将@Inject作用覆盖。

4.2.3 总结

  • @Autowired是Spring中的专有注解,而@Resource是Java中JSR250规范里面定义的一个注解,@Inject是Java中JSR330规范里面定义的一个注解
  • @Autowired支持参数required=false,而@Resource和@Inject都不支持
  • @Autowired和@Inject支持@Primary注解优先注入,而@Resource不支持
  • @Autowired通过@Qualifier指定注入特定bean,@Resource可以通过参数name指定注入bean,而@Inject需要通过@Named注解指定注入bean

4.3 其他位置的自动装配

@Autowired注解不仅可以标注在字段上,而且还可以标注在构造方法、实例方法以及参数上。

4.3.1 案例准备

首先,我们在项目中新建一个Boss类,在Boss类中有一个Car类的引用,并且我们使用@Component注解将其加载到IOC容器中,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 默认加在IOC容器中的组件,容器启动会调用无参构造器创建对象,然后再进行初始化、赋值等操作
@Component
public class Boss {

private Car car;

public Car getCar() {
return car;
}

@Override
public String toString() {
return "Boss{" +
"car=" + car +
'}';
}
}
1
2
3
4
@Component
public class Car {

}

注意,Car类上也要标注@Component注解,即它也要被加载到IOC容器中。

新建好以上Boss类之后,我们还需要在MainConfigOfAutowired配置类的@ComponentScan注解中进行配置,使其能够扫描com.hongyi.bean包下的类。

4.3.2 标注在实例方法

我们可以将@Autowired注解标注在setter方法上,如下所示。

1
2
3
4
@Autowired
public void setCar(Car car) {
this.car = car;
}
1
2
3
4
// 形式2
public void setCar(@Autowired Car car) {
this.car = car;
}

标注在方法上:Spring容器创建该对象(boss),就会调用该方法,完成赋值。方法使用的参数和自定义类型的值都会从ioc中获取。接下来验证:

  • 测试类
1
2
3
4
5
6
7
8
@Test
public void test3() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAutowired.class);
Boss boss = context.getBean(Boss.class);
Car car = context.getBean(Car.class);
System.out.println(boss);
System.out.println(car);
}

image-20220110211124143

可见是同一个对象。

4.3.3 标注在构造方法

spring会默认将类加载进IOC容器中,IOC容器启动的时候默认会调用bean的无参构造器创建对象,然后再进行初始化、赋值等操作。

接下来,我们为Boss类添加一个有参构造方法,将@Autowired注解标注在有参构造方法上,并在构造方法中打印一条信息,如下所示。

1
2
3
4
5
@Autowired
public Boss(Car car) {
this.car = car;
System.out.println("Boss...有参构造器");
}
1
2
3
4
5
// 形式2
public Boss(@Autowired Car car) {
this.car = car;
System.out.println("Boss...有参构造器");
}

构造器要用到的参数,也是从ioc中获取。

如果组件中只有一个有参构造器,这个有参构造器的@Autowired可以省略:

1
2
3
4
public Boss(Car car) {
this.car = car;
System.out.println("Boss...有参构造器");
}

运行结果:

image-20220110211801547

4.3.4 标注在参数

见2,3小节的形式2。无论@Autowired注解是标注在字段上、实例方法上、构造方法上还是参数上,参数位置的组件都是从IOC容器中获取。

4.3.5 标注在方法位置

@Autowired注解可以标注在某个方法的位置上。这里,为了更好的演示效果,我们新建一个Color类,在Color类中有一个Car类型的成员变量,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Color {
private Car car;

public Car getCar() {
return car;
}

public void setCar(Car car) {
this.car = car;
}

@Override
public String toString() {
return "Color{" +
"car=" + car +
'}';
}
}

然后,我们在MainConfigOfAutowired配置类中实例化Color类,如下所示。

1
2
3
4
5
6
7
8
9
// @Bean标注的方法创建对象的时候,方法参数car从容器中获取
// 默认不写@Autowired
// Autowired
@Bean
public Color color(Car car) {
Color color = new Color();
color.setCar(car);
return color;
}

测试方法:

1
2
3
4
5
6
7
8
9
10
@Test
public void test3() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAutowired.class);
Boss boss = context.getBean(Boss.class);
Car car = context.getBean(Car.class);
Color color = context.getBean(Color.class);
System.out.println(boss);
System.out.println(car);
System.out.println(color);
}

执行结果:

image-20220110212056596

发现三者用到的car都是同一个对象

综上,我们用到最多的还是把@Autowired注解标注在方法位置,即使用@Bean注解+方法参数这种形式,此时,该方法参数的值从IOC容器中获取,并且还可以默认不写@Autowired注解,因为效果都是一样的,都能实现自动装配!

4.4 获取Spring底层组件

4.4.1 概述

如果我们现在自定义的组件中需要用到Spring底层的一些组件,比如ApplicationContext(IOC容器)、底层的BeanFactory等等,那么该怎么办呢?先说说自定义的组件中能不能用Spring底层的一些组件吧?既然都这样说了,那么肯定是能够的。

回到主题,自定义的组件要想使用Spring容器底层的一些组件,比如ApplicationContext(IOC容器)、底层的BeanFactory等等,那么只需要让自定义组件实现XxxAware接口即可。此时,Spring在创建对象的时候,会调用XxxAware接口中定义的方法注入相关的组件。

本质上,Spring中形如XxxAware这样的接口都继承了Aware接口。接下来,我们看看都有哪些接口继承了Aware接口,如下所示。

image-20220111212040281

4.4.2 使用

接下来,我们就挑选几个常用的XxxAware接口来简单的说明一下。

ApplicationContextAware接口使用的比较多,我们先来说说这个接口,通过ApplicationContextAware接口我们可以获取到IOC容器

首先,我们创建一个Red类,它得实现ApplicationContextAware接口,并在实现的setApplicationContext()方法中将ApplicationContext输出,我们也可以让Red类同时实现几个XxxAware接口,例如,使Red类再实现一个BeanNameAware接口,我们可以通过BeanNameAware接口获取到当前bean在Spring容器中的名称,当然了,我们可以再让Red类实现一个EmbeddedValueResolverAware接口,我们通过EmbeddedValueResolverAware接口能够获取到String值解析器,如下所示。

  • ApplicationContextAware
  • BeanNameAware
  • EmbeddedValueResolverAware
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
public class Red implements ApplicationContextAware, BeanNameAware, EmbeddedValueResolverAware {

private ApplicationContext context;

// 这里的参数即为spring传入的applicationContext,即创建的ioc容器
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("传入的ioc: " + applicationContext);
this.context = applicationContext;
}

// 参数name:IOC容器创建当前对象时,为这个对象起的名字
@Override
public void setBeanName(String s) {
System.out.println("当前bean的名字: " + s);
}

// spring为我们创建好的字符解析器
@Override
public void setEmbeddedValueResolver(StringValueResolver stringValueResolver) {
String s = stringValueResolver.resolveStringValue("你好${os.name} 我是#{20 * 18}");
System.out.println("解析的字符串: " + s);
}
}

IOC容器启动时会自动地将String值的解析器(即StringValueResolver)传递过来给我们用,咱们可以用它来解析一些字符串,解析哪些字符串呢?比如包含#{}这样的字符串。

测试类

1
2
3
4
5
6
@Test
public void test3() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAutowired.class);
// 打印容器
System.out.println(context);
}

image-20220111212653441

可见返回的是同一个ioc容器。

4.4.3 原理

XxxAware接口的底层原理是由XxxAwareProcessor实现类实现的,也就是说每一个XxxAware接口都有它自己对应的XxxAwareProcessor实现类。例如,我们这里以ApplicationContextAware接口为例,ApplicationContextAware接口的底层原理就是由ApplicationContextAwareProcessor类实现的。从ApplicationContextAwareProcessor类的源码可以看出,其实现了BeanPostProcessor接口,本质上是一个后置处理器

image-20220111212937414

接下来,我们就以分析ApplicationContextAware接口的原理为例,看看Spring是怎么将ApplicationContext对象注入到Red类中的。

首先,我们在Red类的setApplicationContext()方法上打一个断点,如下所示。

image-20220111213139984

然后,我们以debug的方式来运行IOCTest_Autowired类中的test02()方法。

image-20220111213220867

这里,我们可以看到,实际上ApplicationContext对象已经注入到Red类的setApplicationContext()方法中了。

接着,我们在Eclipse的方法调用栈中找到postProcessBeforeInitialization()方法并鼠标单击它,如下所示,此时,自动定位到了postProcessBeforeInitialization()方法中。

其实,postProcessBeforeInitialization()方法所在的类就是ApplicationContextAwareProcessor。postProcessBeforeInitialization()方法的逻辑还算比较简单。

4.5 根据环境注册组件

在实际的企业开发环境中,往往都会将环境分为开发环境、测试环境和生产环境,并且每个环境基本上都是互相隔离的,也就是说,开发环境、测试环境和生产环境它们之间是互不相通的。在以前的开发过程中,如果开发人员完成相应的功能模块并通过单元测试后,那么他会通过手动修改配置文件的形式,将项目的配置修改成测试环境,发布到测试环境中进行测试。测试通过后,再将配置修改为生产环境,发布到生产环境中。这样手动修改配置的方式,不仅增加了开发和运维的工作量,而且总是手工修改各项配置文件会很容易出问题。那么,有没有什么方式可以解决这些问题呢?答案是:有!通过@Profile注解就可以完全做到这点。

4.5.1 @Profile概述

在容器中如果存在同一类型的多个组件,那么可以使用@Profile注解标识要获取的是哪一个bean。也可以说@Profile注解是Spring为我们提供的可以根据当前环境,动态地激活和切换一系列组件的功能。这个功能在不同的环境使用不同的变量的情景下特别有用,例如,开发环境、测试环境、生产环境使用不同的数据源,在不改变代码的情况下,可以使用这个注解来动态地切换要连接的数据库。

1
2
3
4
5
6
7
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
String[] value();
}

从其源码中我们可以得出如下三点结论:

  1. @Profile注解不仅可以标注在方法上,也可以标注在配置类上。

  2. 如果@Profile注解标注在配置类上,那么只有是在指定的环境的时候,整个配置类里面的所有配置才会生效。

  3. 如果一个bean上没有使用@Profile注解进行标注,那么这个bean在任何环境下都会被注册到IOC容器中,当然了,前提是在整个配置类生效的情况下。

4.5.2 注解使用

接下来,我们就一起来看一个案例,即使用@Profile注解实现开发、测试和生产环境的配置和切换。这里,我们以开发过程中要用到的数据源为例(数据源也是一种组件哟😊)。

我们希望在开发环境中,数据源是连向A数据库的;在测试环境中,数据源是连向B数据库的,而且在这一过程中,测试人员压根就不需要改动任何代码;最终项目上线之后,数据源连向C数据库,而且最重要的一点是在整个过程中,我们不希望改动大量的代码,而实现数据源的切换。

1) 环境搭建
  1. 首先,我们需要在pom.xml文件中添加c3p0数据源和MySQL驱动的依赖,如下所示。
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
  1. 添加完以上依赖之后,我们还得在项目中新建一个配置类,例如MainConfigOfProfile,并在该配置类中模拟开发、测试、生产环境的数据源,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Configuration
// 读取配置文件
@PropertySource("classpath:/dbconfig.properties")
public class MainConfigOfProfile implements EmbeddedValueResolverAware {

// 第一种属性赋值的形式
@Value("${db.user}")
private String user;

// 第二种属性赋值的形式:用到了上一节讲解过的知识
private StringValueResolver resolver;

private String driverClass;

@Bean("testDataSource")
// 第三种属性赋值的形式:写在参数列表中
public DataSource dataSourceTest(@Value("${db.password}") String pwd) throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser(user);
dataSource.setPassword(pwd);
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
dataSource.setDriverClass(driverClass);
return dataSource;
}

@Bean("devDataSource")
public DataSource dataSourceDev(@Value("${db.password}") String pwd) throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser(user);
dataSource.setPassword(pwd);
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/dev");
dataSource.setDriverClass(driverClass);
return dataSource;
}

@Bean("prodDataSource")
public DataSource dataSourceProd(@Value("${db.password}") String pwd) throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser(user);
dataSource.setPassword(pwd);
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/prod");
dataSource.setDriverClass(driverClass);
return dataSource;
}

@Override
public void setEmbeddedValueResolver(StringValueResolver stringValueResolver) {
this.resolver = stringValueResolver;
driverClass = resolver.resolveStringValue("${db.driverClass}");
}
}

其中配置文件dbconfig.properties

1
2
3
db.user = root
db.password = 12345678
db.driverClass = com.mysql.jdbc.Driver
  1. 测试类:
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test4() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(MainConfigOfProfile.class);
// 根据组件的类型获取组件的id名称
String[] names = context.getBeanNamesForType(DataSource.class);
for (String name:
names) {
System.out.println(name);
}
context.close();
}

image-20220111203732145

可见,三个数据源都被注册到容器当中。

2) @Profile使用

指定组件在哪一个环境的情况下才能被注册到容器中,不指定则任何环境下都能注册这个组件

  • 加了环境标识的bean,只有在这个环境被激活的时候才能注册到容器中。@Profile默认的值是default
  • 写在配置类上时,只有是在指定的环境的时候,整个配置类里面所有的配置才能生效
  • 没有标注环境标识的bean,任何环境下都是加载的。

指定运行环境

  1. 使用命令行动态参数指定激活环境:在虚拟机参数中添加:
1
-Dspring.profiles.active=test

image-20220111204423682

运行结果:

image-20220111204530443

  1. 代码方式指定激活环境:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test4() {
// 1.创建context对象:要使用无参构造器
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext();
// 2.设置需要激活的环境,可设置多个
context.getEnvironment().setActiveProfiles("test", "dev");
// 3.注册主配置类
context.register(MainConfigOfProfile.class);
// 4.启动,刷新容器
context.refresh();

// 根据组件的类型获取组件的id名称
String[] names = context.getBeanNamesForType(DataSource.class);
for (String name:
names) {
System.out.println(name);
}
context.close();
}

image-20220111205009833

  1. 写在配置类上时:
1
2
3
4
5
6
7
@Configuration
// 只有环境是prod时,配置才起作用
@Profile("prod")
@PropertySource("classpath:/dbconfig.properties")
public class MainConfigOfProfile implements EmbeddedValueResolverAware {
// code...
}

测试类1:

1
2
3
4
5
6
@Test
public void test4() {
// 设置为生产环境
context.getEnvironment().setActiveProfiles("prod");
// ...
}

image-20220111205640255

测试类2:

1
2
3
4
5
6
@Test
public void test4() {
// 设置为测试环境
context.getEnvironment().setActiveProfiles("test");
// ...
}

image-20220111205719021

可见没有数据源被注册。

  1. 没有环境标识的bean存在时:
1
2
3
4
5
6
7
8
9
10
@Configuration
@PropertySource("classpath:/dbconfig.properties")
public class MainConfigOfProfile implements EmbeddedValueResolverAware {
// 未指定环境
@Bean
public Car car() {
return new Car();
}
// ...
}

测试类1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test4() {
// 1.创建context对象:要使用无参构造器
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext();
// 2.设置需要激活的环境
context.getEnvironment().setActiveProfiles("test");
// 3.注册主配置类
context.register(MainConfigOfProfile.class);
// 4.启动,刷新容器
context.refresh();

// 获取容器中所有组件的名称
String[] names = context.getBeanDefinitionNames();
for (String name:
names) {
System.out.println(name);
}
context.close();
}

image-20220111210523194

测试类2:

1
2
3
4
5
@Test
public void test4() {
// ...
context.getEnvironment().setActiveProfiles("dev");
// ...

image-20220111210625444

可见,不管何种环境,car都被注册到容器中了。


5 面向切面编程

AOP:指在程序运行期间动态地将某段代码切入到指定方法的指定位置进行运行的编程方式。

AOP(Aspect Orient Programming),直译过来就是面向切面编程。AOP是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。

在《Spring实战(第4版)》中有如下一张图描述了AOP的大体模型。

image-20220113172125374

从这张图中,我们可以看出:所谓切面,其实就相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。

总之一句话:AOP是指在程序的运行期间动态地将某段代码切入到指定方法、指定位置进行运行的编程方式。AOP的底层是使用动态代理实现的。

5.1 AOP功能测试

5.1.1 步骤

  1. 导入AOP模块依赖
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.9</version>
</dependency>
  1. 定义一个业务逻辑类,在业务逻辑运行的时候,将日志进行打印(方法之前,方法结束运行,方法出现异常等)
1
2
3
4
5
6
public class MathCalculator {
public int div(int i, int j) {
System.out.println("div方法被调用了...");
return i / j;
}
}
  1. 定义一个日志切面类:切面类里面的方法需要动态感知业务逻辑类运行到哪里,然后执行相应方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LogAspects {
public void logStart() {
System.out.println("除法运行...参数列表是: {}");
}

public void logEnd() {
System.out.println("除法结束...");
}

public void logReturn() {
System.out.println("除法正常返回...运行结果: {}");
}

public void logException() {
System.out.println("除法异常...异常信息: {}");
}
}
  • 通知方法:
    • 前置通知@Before:logStart:在目标方法(div)运行之前运行
    • 后置通知@After:logEnd:在目标方法运行结束之后运行,无论是正常结束还是异常结束
    • 返回通知@AfterReturning:logReturn:在目标方法正常返回之后运行
    • 异常通知@AfterThrowing:logException:在目标方法出现异常后运行
    • 环绕通知@Around:动态代理,手动推进目标方法运行(joinPoint.proceed
  1. 给切面类的目标方法标注何时何地运行(利用通知注解)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 切面类
*/
public class LogAspects {

// 抽取公共的切入点表达式
// 1.本类引用
// 2.其他的切面引用
@Pointcut("execution(public int com.hongyi.aop.MathCalculator.*(..))")
public void pointCut() {

}

// @Before在目标方法之前切入:切入点表达式(指定在哪个方法切入)
// *表示该类下的所有方法
// ..表示带两个参数
// 第一种写法:@Before("public int com.hongyi.aop.MathCalculator.*(..)") 不常用
// 第二种写法:本类引用
@Before("pointCut()")
public void logStart() {
System.out.println("@Before除法运行...参数列表是: {}");
}

// 第三种写法:外部切面类引用
@After("com.hongyi.aop.LogAspects.pointCut()")
public void logEnd() {
System.out.println("@After除法结束...");
}

@AfterReturning("pointCut()")
public void logReturn() {
System.out.println("@AfterReturning除法正常返回...运行结果: {}");
}

@AfterThrowing("pointCut()")
public void logException() {
System.out.println("@AfterThrowing除法异常...异常信息: {}");
}
}
  1. 将切面类和业务逻辑类都加入到容器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MainConfigOfAOP {
// 业务逻辑类
@Bean
public MathCalculator calculator() {
return new MathCalculator();
}

// 切面类
@Bean
public LogAspects logAspects() {
return new LogAspects();
}
}
  1. 告诉Spring哪个是切面类:给切面类加一个注解@Aspect
1
2
3
4
5
6
7
/**
* 切面类
*/
@Aspect
public class LogAspects {
// ....
}
  1. 给配置类加上注解@EnableAspectJAutoProxy:开启基于注解的AOP模式
1
2
3
4
5
@EnableAspectJAutoProxy
@Configuration
public class MainConfigOfAOP {
// code...
}

测试方法1

1
2
3
4
5
6
7
@Test
public void test5() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAOP.class);
MathCalculator calculator = context.getBean(MathCalculator.class);
calculator.div(1, 1);
context.close();
}

image-20220113170921838

  1. 获取业务方法的参数列表和异常等信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 切面类
*/
@Aspect
public class LogAspects {

@Pointcut("execution(public int com.hongyi.aop.MathCalculator.*(..))")
public void pointCut() {

}

@Before("pointCut()")
public void logStart(JoinPoint joinPoint) {
// 获取方法参数列表
Object[] args = joinPoint.getArgs();
// joinPoint.getSignature().getName():获取执行方法的名称
System.out.println(joinPoint.getSignature().getName() + "@Before除法运行...参数列表是: {"+ Arrays.asList(args) +"}");
}

@After("com.hongyi.aop.LogAspects.pointCut()")
public void logEnd(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature().getName() + "@After除法结束...");
}

// 注意参数列表中的JoinPoint joinPoint一定要放在第一个位置
@AfterReturning(value = "pointCut()", returning = "result")
public void logReturn(JoinPoint joinPoint, Object result) {
System.out.println(joinPoint.getSignature().getName() + "@AfterReturning除法正常返回...运行结果: {"+ result +"}");
}

@AfterThrowing(value = "pointCut()", throwing = "exception")
public void logException(JoinPoint joinPoint, Exception exception) {
System.out.println(joinPoint.getSignature().getName() + "@AfterThrowing除法异常...异常信息: {"+ exception +"}");
}
}

测试方法2

1
2
3
4
5
6
7
@Test
public void test5() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAOP.class);
MathCalculator calculator = context.getBean(MathCalculator.class);
calculator.div(1, 1);
context.close();
}

image-20220113171734546

测试方法3

1
2
3
4
5
6
7
@Test
public void test5() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAOP.class);
MathCalculator calculator = context.getBean(MathCalculator.class);
calculator.div(1, 0);
context.close();
}

image-20220113171808355

5.1.2 总结

三步走战略

  1. 将业务逻辑组件和切面类都加入到容器中,并告诉Spring哪一个是切面类(@Aspect)
  2. 在切面类上的每一个通知方法上标注通知注解,告诉Spring何时何地运行(利用切入点表达式)
  3. 开启基于注解的aop模式,@EnableXXX

5.2 AOP原理

5.2.1 @EnableAspectJAutoProxy