封面画师:T5-茨舞(微博)     封面ID:67342794

本文以 SpringBoot 3.1.x 为例

1. 对象创建相关注解

1.1 @Component

作用:用于把当前 类对象 存入(注册到) Spring 容器中。

主要属性:

属性名 作用
value 指定 bean 的 id 。 若未指定,则默认为当前类名首字母小写。

如果 Spring 配置文件中使用 <bean> 标签配置的 id、class 与 @Component 注解指定的 id、class 一样时,配置文件的配置将覆盖注解的配置。

@Component 有以下几个衍生注解,它们与 @Component 的作用是一样的,但在实际开发中它们所处的位置不一样:

1、@Controller:用于表现层

2、@Service:用于业务层

3、@Repository:用于持久层

Spring 注解使用注意

常规 SSM 项目中,使用 Spring 注解开发时,记得在 Spring 配置文件中告知 Spring 在创建容器时需要扫描的包。如:

1
<context:component-scan base-package="xxx.xxx"/>

1.2 @Scope

作用:用于指定 bean 的作用范围(控制简单对象的创建次数)。未使用时,默认 bean 单例。

主要属性:

属性名 作用
value 指定范围的取值。 常用的 value 属性值有:singletonprototype

1.3 @Lazy

作用:延时创建单例 bean。未使用时,Spring 工厂创建时就会创建 单例 bean;使用后,在使用 bean 时才创建 bean。

1.4 @PostConstruct

此注解并不是由 Spring 提供的,而是 JSR-250 规范中指定的。

此注解只能在 非静态无返回值 方法上使用,被此注解标记了的方法,作为 Spring 中 bean 的 初始化 方法。

在整个 bean 的初始化过程中,最先执行构造方法,再进行依赖注入,然后再执行被此注解标记的方法。

注意: 此注解在 JDK9 中已被废弃,在 JDK11 中已被移除。要想在高版本 JDK 中使用此注解,需要添加依赖 jakarta.annotation-api

1.5 @PreDestroy

此注解并不是由 Spring 提供的,而是 JSR-250 规范中指定的。

此注解只能在 非静态无返回值 方法上使用,被此注解标记了的方法,作为 Spring 中 bean 的 销毁 方法。

注意: 此注解在 JDK9 中已被废弃,在 JDK11 中已被移除。要想在高版本 JDK 中使用此注解,需要添加依赖 jakarta.annotation-api

1.6 @Lookup

Lookup,意为“查找、查阅”,在 Spring 中也有一个 @Lookup 注解,该注解只能作用在 方法 上。

当使用 ApplicationContext#getBean() 方法按类型获取 Spring 容器中的 Bean 时,如果传入的类型是一个抽象类,那么会尝试在容器中查找该抽象类的实现类类型的 Bean,如果不存在这样的 Bean,则会抛出 NoSuchBeanDefinitionException 异常。

生成抽象类的代理类类型的 Bean

可以在抽象类中任意定义一个方法并使用 @Lookup 标记:

1
2
3
4
5
6
7
@Component
public abstract class AbstractBean {

@Lookup
public void method() {
}
}

然后再尝试获取该类型的 Bean 时,将得到由 CGLib 生成的代理对象:

1
2
3
4
5
6
7
@Test
public void testLookup() {
AbstractBean abstractBean = context.getBean(AbstractBean.class);
assertThat(abstractBean).isNotNull()
.extracting(i -> i.getClass().getName())
.asString().contains("CGLIB$$");
}

在单例对象中注入多例对象

向单例 Bean 中注入多例 Bean 时,尽管注入的是多例 Bean,但每次获取注入的 Bean 时都将是同一个。此时可以声明一个 无参、返回值类型是想要注入的多例 Bean 对应的类型的方法(这个方法甚至可以是抽象方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeBean {
}

@Component
public class SingletonBean {
@Autowired
private PrototypeBean prototypeBean;

public PrototypeBean getBean() {
return this.prototypeBean;
}

@Lookup
public PrototypeBean getPrototypeBean() {
return this.prototypeBean;
}

@Lookup
public PrototypeBean returnNull() {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testLookup() {
assertThat(context.getBean(PrototypeBean.class))
.isNotSameAs(context.getBean(PrototypeBean.class));

SingletonBean singletonBean = context.getBean(SingletonBean.class);
// @Autowired 注入的是单例对象
assertThat(singletonBean.getBean()).isSameAs(singletonBean.getBean());
// 使用 @Lookup 后每次获取的对象都不同
assertThat(singletonBean.getPrototypeBean()).isNotSameAs(singletonBean.getPrototypeBean());
// 使用 @Lookup 会生成代理方法,尽管原方法返回的是 null,但代理方法返回的并不是
assertThat(singletonBean.returnNull()).isNotNull()
.isOfAnyClassIn(PrototypeBean.class);
}

2. 注入相关注解

2.1 @Autowried

作用:自动按照 类型 注入。只要容器中有唯一的一个 bean 对象类型(子类)和要注解的对象类型匹配时,就可以注入成功。

位置:set 方法、构造方法、成员变量。作用在成员变量上时,Spring 使用反射对其进行注入,并且 set 方法变得不再必须。

注入方式

先按照类型注入:

1、如果有唯一的 bean 对象类型在 IoC 容器中,则注入成功。

2、如果 IoC 容器中一个匹配的 bean 对象类型都没有,则报错。

3、如果 IoC 容器中有多个匹配的 bean 对象类型,则再按照变量名进行注入,在圈出的多个匹配的 bean 对象类型中寻找匹配的变量名:

  • 如果存在,则注入成功;反之,则注入失败。有多个匹配的 bean 对象时,常与 @Qualifier 搭配使用。

高端玩法

如果一个泛型为自定义类型的集合对象被 @Autowried 标记,那么 Spring 会自动把相同的类型(包括子类)的自定义对象收集到集合中。比如:

1
2
3
4
5
6
@Autowried
private List<CustomObject> list;
@Autowried
private Set<CustomObject> set;
@Autowried
private Map<String, CustomObject> map;

如果 @Autowried 作用在数组或集合上,数组或集合中的 bean 的顺序是根据 Spring 创建的顺序。如果想指定里面排序的优先级,可以使用 @Order 或者 @Priority 指定优先级,值越小,优先级就越高。

如果作用在 map 集合上,其 key 类型只能为 String,表示 bean 的 id。

@Order 注解(或者实现 Ordered 接口、又或者实现 PriorityOrdered接口),不会影响 Bean 的实例化顺序和执行顺序,更不会影响 Spring 的 Bean 的扫描顺序,它影响着 Spring 将扫描的多个 Bean 放入数组、集合(Map)时的排序。

2.2 @Qualifier

作用:在按照类注入的基础上再按照名称注入。这个注解在给成员变量注入时,不能单独使用(与 @Autowried 一起搭配使用),但是在给方法参数注入时可以单独使用。

主要属性:

属性名 作用
value 用于指定注入 bean 的 id。

注意: 当我们使用 @Bean 给一个配置类中的方法进行注解时,会判断这个方法是否有参数,Spring 框架会去容器中查找有无可用的 bean 对象,查找方式和 @Autowried 注解一样。如果没找到,就会报错。如果我们在 IoC 容器中添加了两个相同的对象,恰逢这个对象又出现在使用了 @Bean 注解的方法的参数中,这个时候可以使用 @Qualifier 指定具体是哪个对象(给方法参数注入,单独使用)。

2.3 @Primary

作用:当一个接口有多个实现类时,指定 某一个 实现类作为依赖注入的默认优先选择。

注意: 默认优先选择的实现类 只能有 一个。

2.4 @Resource

作用:默认按照 bean 的 id 进行注入,可以单独使用。

主要属性:

属性名 作用
name 指定 bean 的 id。
type 指定 bean 的类型。

属性值使用规则

1、如果同时指定了 name 和 type ,则从 Spring 上下文中找到唯一匹配的 bean 进行装配,找不到则抛出异常(异常信息:找不到 “bean” 的声明);

2、如果只指定了 name ,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常;

3、如果只指定了 type ,则从上下文中找到类型匹配的唯一 bean 进行装配,找不到或者找到多个,都会抛出异常;

4、如果既没有指定 name ,又没有指定 type ,则自动按照 byName 方式进行装配(见2)。如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配,否则抛出异常。

使用建议

推荐使用 @Resource 注解,这个注解是属于 JDK 的,可以减少与 Spring 的耦合(但人们依旧习惯使用 @Autowried )。

@Autowried@Qualifier @Resource 都只能注入其他 bean 类型的数据,而基本数据类型和 String 类型是无法使用这三个注解的。另外,集合类型的注入只能通过 XML 来实现。

2.5 @Value

作用:常用于注入基本类型和 String 类型的数据,可以作用在字段、方法、参数和注解上。

主要属性:

属性名 作用
value 用于指定数据的值(可以使用 Spring 中的 EL 表达式,即 SPEL )。

SPEL 的写法:${表达式},为了避免无法匹配到值,可以设置默认值,比如 @Value("${name:张三}")

@Value 与 EL 表达式

如果要 @Value 注解与 EL 表达式结合使用,需要先编写一个 xxx.properties 文件:

1
2
name = "mofan"
age = 20

然后在 Spring 配置文件中指定这个 properties 文件的位置(location 属性值支持使用 classpath):

1
<context:property-placeholder location="classpath:/xxx.properties"/>

<context:property-placeholder> 标签的命名空间为:

1
<beans xmlns:context="http://www.springframework.org/schema/context"></beans>

然后在成员变量上使用 @Value 注解即可:

1
2
3
4
5
6
7
8
@Component
public class Person{
@Value("${name}")
private String name;
@Value("${age}")
private Integer age;
// ...
}

使用细节

  1. @Value 注解不能应用在 静态变量 上,否则会注入失败,但也有变通之道,可以将 @Value 标记在静态变量的 Setter 上或构造方法的参数上,在方法内部设置静态变量的值,比如:

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

    private static String str1;

    private static String str2;

    @Value("${prop.str1}")
    public void setStr1(String str1) {
    str1 = str1;
    }

    MyProperties(@Value("${prop.str2}") String str2){
    str2 = str2;
    }
    }
  2. @Value 注解不能应用在被 final 修饰的变量上,final 变量 必须 在构造方法中进行初始化,并且被赋值后不能再次更改,而 @Value 注解是在 Bean 实例化后才进行注入的,无法在构造方法中初始化 final 变量;

  3. @Value 注解不能注入集合类型的数据;

  4. @Value 注解只能作用在被 Spring 管理的类中,也就是说该注解必须在 Spring Bean 的生命周期中才能生效。

3. 注解扫描详解

在扫描注解时,需要在 Spring 配置文件中指定扫描路径,即:

1
<context:component-scan base-package="xxx.xxx"/>

但这种方式并不能满足绝大多数场景,比如排除无需扫描的类。这时可以增加一个子标签 <context:exclude-filter>

<context:exclude-filter> 标签中 type 属性表示排除策略,expression 属性用于指定排除策略对应的排除方式。比如下述配置就是排除对 User 类的扫描:

1
2
3
<context:component-scan base-package="indi.mofan">
<context:exclude-filter type="assignable" expression="indi.mofan.domain.User"/>
</context:component-scan>

type 属性值有以下五种,其名称与含义如下:

名称 含义
assignable 排除特定的类型
annotation 排除特定的注解
aspectj 使用包切入点或类切入点表达式进行排除
regex 使用正则表达式进行排除
custom 自定义排除策略

排除策略是可以叠加使用的。

<context:component-scan> 标签中还有名为 <context:include-filter> 的子标签,这个标签用于设置注解扫描包含方式,其使用方式与 <context:exclude-filter> 标签是一样的。

使用 <context:include-filter> 标签时需要将 <context:component-scan> 标签的 use-default-filters 属性值设置为 false,表示不使用 Spring 默认扫描规则。

4. 配置相关注解

小知识

在 Spring 中,解析 Spring 的配置文件(以 applicationContext.xml 为例),使用:

1
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

解析配置类使用(以MyConfiguration.java为例),使用:

1
ApplicationContext ac1 = new AnnotationConfigApplicationContext(MyConfiguration.class);

分析一下 AnnotationConfigApplicationContext()方法,我们可以点击进入查看源码:

1
2
3
4
5
6
7
8
9
10
11
public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
this();
register(annotatedClasses);
refresh();
}

public AnnotationConfigApplicationContext(String... basePackages) {
this();
this.scan(basePackages);
this.refresh();
}

这个构造方法的参数是可变长度的参数,可以传入配置类(被 @Configuration 标记的类)的 Class 对象,也可以传入配置类所在路径。

4.1 @Configuration

作用:指定当前类是配置类。其本质也是 @Component 注解的衍生注解。

它相当于 xml 文件的 <beans> 标签,这个类里面需要注册 bean ,相当于在 <beans> 中注册 bean 。

细节: 当配置类作为 AnnotationConfigApplicationContext 对象创建的参数时,可以 不用@Configuration 注解在配置类上。

bean 的注册方法

bean 有两种方式:

1、使用 @Bean 标记一个返回 JavaBean 的方法,返回的 JavaBean 作为 bean 注册到 beans 中。

2、使用@Component + @ComponentScan 的方式:@Component 标记在要注册 bean 的类上,@ComponentScan 标记在配置类上用于扫描组件。

最后,将配置类注册到上下文中来初始化容器。

父配置类加载子配置类的问题

如果有多个配置类,且这些配置类存在父子关系、 AnnotationConfigApplicationContext 对象创建的参数中只有父配置类,要想在父配置类中加载子配置类,需要在父配置类上使用 @ComponentScan 扫描子配置类,而且这个子配置类必须是配置类,即:子配置类上必须有 @Configuration

当然你也可以在父配置类上使用 @Import 注解导入子配置类,这个时候子配置类可以不用有 @Configuration

4.2 @ComponentScan

作用:通过注解指定 Spring 在创建容器时需要扫描的包。

主要属性:

属性名 作用
value、basePackage 这俩的作用是一样的,都是用于指定创建容器时要扫描的包。
basePackageClasses 定义扫描的类。

在没有定义@ComponentScan属性的情况下,它只会扫描当前包和其子包的路径。

细节: 在父配置类上使用该注解扫描需要被装配的类时,这个类必须被 @Configuration@Component 所注解。

使用方式

1
2
3
4
5
@Configuration
@ComponentScan("indi.mofan")
public class MyConfiguration(){
// ...
}

以上代码相当于:

1
2
3
<beans>
<context:component-scan base-package="indi.mofan"/>
</beans>

扫描排除

<context:component-scan> 一样,@ComponentScan 注解也可以配置扫描排除策略。如果在 Spring 配置文件中有这样一段配置:

1
2
3
4
<context:component-scan base-package="indi.mofan">
<context:exclude-filter type="assignable" expression="indi.mofan.domain.User"/>
<context:exclude-filter type="aspectj" expression="*..User"/>
</context:component-scan>

那么等价于:

1
2
3
4
5
6
@ComponentScan(basePackages = "indi.mofan",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = User.class),
@ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "*..User")
}
)

其他排除策略使用方式与 <context:exclude-filter> 是一样的,这些排除策略也是可以叠加的。

@ComponentScan 也支持扫描包含,举一反三即可。

4.3 @Bean

作用:把当前 public 方法的返回值作为 bean 对象存入到 Spring 的 IoC 容器中。常与 @Configuration 配合使用。

主要属性:

属性名 作用
name 用于指定 bean 的 id 。不写时,默认值为当前方法名称。

@Bean 注解了的方法的名字,就相当于 <bean> 标签中的 id 属性;这个方法的返回值,就相当于 <bean> 标签中的 class 属性。

@Bean 注解默认作用域为单例 singleton 作用域,可通过 @Scope("prototype") 设置为原型作用域。

细节分析

当我们使用注解配置方法时,如果方法有参数,Spring 框架会去容器中查找有无可用的 bean 对象,查找方式和 @Autowried 注解一样。如果找到了就会进行注入,否则会报错。

在被 @Configuration 标记的配置类中,一个 @Bean 标记的方法可以调用当前类中另一个被 @Bean 注解标记的方法,并将后者作为依赖注入到前者创建的 bean 中。虽然 @Configuration@Component 的衍生注解,但是使用 @Component 是无法完成上述场景的。

4.4 @Import

作用:用于导入其他的配置类或 bean。

主要属性:

属性名 作用
value 用于指定其他配置类的 Class 对象(可以指定多个)

当我们使用 @Import 注解后,有 @Import 注解的类就是父配置类,而导入的都是子配置类。

细节: 使用 @Import 注解导入的子配置类上可以不用写 @Configuration 注解。

配置优先级

Spring 配置优先级如下:

Spring 配置文件 <bean> 标签的优先级 大于 @Bean 注解 大于 @Component 及其衍生注解

优先级高的配置覆盖优先级低的配置,但在进行配置覆盖值,bean 的 id 值需要保持一致。

4.5 @ConfigurationProperties

SpringBoot 提供了多种配置文件方便用户对配置的管理,如果需要读取配置文件中的信息,可以使用 @Value 注解。

在 SpringBoot 的配置文件中,针对同一种配置往往有相同的前缀,为简化 @Value 注解的使用,SpringBoot 提供了 @ConfigurationProperties 注解。

使用这个注解可以指定相同的前缀,SpringBoot 将根据指定的前缀和属性名称在配置文件中寻找对应的信息并完成注入。

假设有这样的配置信息:

1
2
3
properties:
name: "mofan"
age: 20

使用 @Value 读取

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author mofan
* @date 2022/10/27 10:24
*/
@Getter
@Component
public class MyProperties {
@Value("${properties.name}")
private String name;
@Value("${properties.age}")
private Integer age;
}

测试一下:

1
2
3
4
5
6
7
8
@Autowired
private ApplicationContext context;
@Test
public void testEnableConfigurationProperties() {
MyProperties properties = context.getBean(MyProperties.class);
Assertions.assertEquals("mofan", properties.getName());
Assertions.assertEquals(20, properties.getAge());
}

使用 @Value 注解进行读取时,无需 Setter 方法。

使用 @ConfigurationProperties 读取

1
2
3
4
5
6
7
8
@Setter
@Getter
@Component
@ConfigurationProperties("properties")
public class MyProperties {
private String name;
private Integer age;
}

使用 @ConfigurationProperties 注解进行读取时,必须要有 Setter 方法, 否则会报错。

Spring Boot Configuration Annotation Processor not configured
在 IDEA 中使用 @ConfigurationProperties 注解时,顶部可能会出现以下警告:

Spring Boot Configuration Annotation Processor not configured

警告的意思是:没有配置 SpringBoot 配置注解执行器。该警告不影响代码的正常执行,如果需要消除警告,添加以下依赖即可:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

添加 SpringBoot 配置注解执行器后,IDEA 能够给出更准确的提示信息。

@ConfigurationProperties@Value 的混合使用
有时一个 Bean 可能会牵扯到很多信息,单用 @ConfigurationProperties 无法完成所有配置的注入,可以使用 @Value 注入剩下的配置信息:

1
2
3
4
5
properties:
name: "mofan"
age: 20
person:
gender: "man"
1
2
3
4
5
6
7
8
9
10
@Setter
@Getter
@Component
@ConfigurationProperties("properties")
public class MyProperties {
private String name;
private Integer age;
@Value("${person.gender}")
private String gender;
}

@ConfigurationProperties 的其他配置

ignoreUnknownFields 属性值默认为 true,如果修改为 false,SpringBoot 在启动时会查找所有配置文件中的内容进行匹配,当存在没有匹配的信息时就会报错。

比如:

1
2
3
4
properties:
name: "mofan"
age: 20
height: 178
1
2
3
4
5
6
7
8
@Setter
@Getter
@Component
@ConfigurationProperties(value = "properties", ignoreUnknownFields = false)
public class MyProperties {
private String name;
private Integer age;
}

配置文件中的 height 未在 MyProperties 中绑定,启动时出现以下错误:

org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException: The elements [properties.height] were left unbound.

ignoreInvalidFields 默认为 false,如果修改为 true,SpringBoot 启动时会忽略转换错误的值,但这可能会对后续维护带来不必要的麻烦,因此需要慎用。比如:

1
2
3
4
properties:
name: "mofan"
age: 20
bool: "invalid"
1
2
3
4
5
6
7
8
9
10
@Setter
@Getter
@Component
@ConfigurationProperties(value = "properties", ignoreInvalidFields = true)
public class MyProperties {
private String name;
private Integer age;

private Boolean bool;
}

期望 bool 的类型是 Boolean,但是配置文件中的类型是 String,如果配置了 ignoreInvalidFields = true,那么会忽略这种转换错误的值,否则 启动时出现以下错误:

org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Boolean] for value 'invalid'; nested exception is java.lang.IllegalArgumentException: Invalid boolean value 'invalid'
java.lang.IllegalArgumentException: Invalid boolean value 'invalid'

校验注入的值

类加上 @Validated 注解,字段上加上 @NotEmpty 等注解,在注入值的时候会自动校验,校验不通过将报错。比如:

1
2
3
4
5
6
7
8
9
10
@Setter
@Getter
@Validated
@Component
@ConfigurationProperties("properties")
public class MyProperties {
@NotEmpty
private String name;
private Integer age;
}

@Validated 是 Spring 中自带的,使用 @NotEmpty 等校验注解则需要导入以下依赖:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>

<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>

加载多层嵌套配置

某些复杂的配置往往不是简单的一层,可能会嵌套多层,@ConfigurationProperties 注解也支持加载多层拥有相同前缀的配置。

修改原有的配置信息:

1
2
3
4
5
6
7
properties:
my-name: "mofan"
age: 20
innerProperties:
integer: 212
person:
gender: "man"

properties 下的 name 变成了 my-name,同时增加一层名为 innerProperties 的配置,此时需要对 MyProperties 配置类进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Setter
@Getter
@ConfigurationProperties("properties")
public class MyProperties {

private String myName;

private Integer age;

@Value("${person.gender}")
private String gender;

private InnerProperties innerProperties = new InnerProperties();
}
1
2
3
4
5
6
@Getter
@Setter
public static class InnerProperties {
private Integer integer;
private Boolean bool = Boolean.FALSE;
}

重新测试下:

1
2
3
4
5
6
7
8
9
10
@Test
public void testEnableConfigurationProperties() {
MyProperties properties = context.getBean(MyProperties.class);
assertThat(properties)
.extracting(
MyProperties::getMyName, MyProperties::getAge,
MyProperties::getGender, i -> i.getInnerProperties().getInteger(),
i -> i.getInnerProperties().getBool()
).containsExactly("mofan", 20, "man", 212, false);
}

Java 中类里的字段通常采用小驼峰命名,对使用 @ConfigurationProperties 注解加载的配置信息,配置类中采用小驼峰命名的字段可以对应配置文件中相同的配置项,也可以对应配置文件中中划线命名的配置项。

4.6 @EnableConfigurationProperties

如果一个类仅仅配置 @ConfigurationProperties 注解,也没有被添加到 Spring 容器中,那么是不能完成配置文件与 Bean 的数据绑定。

假设 A 类上使用了 @ConfigurationProperties 注解,且未被添加到 Spring 容器中。此时可以使用 @EnableConfigurationProperties({A.class}) 指定 A 与配置文件进行绑定,同时将 A 添加到 Spring 容器中。

1
2
3
properties:
name: "mofan"
age: 20
1
2
3
4
5
6
7
8
9
10
11
/**
* @author mofan
* @date 2022/10/27 10:24
*/
@Setter
@Getter
@ConfigurationProperties("properties")
public class MyProperties {
private String name;
private Integer age;
}

在配置类上使用 @EnableConfigurationProperties 指定 MyProperties.class,使其与配置文件绑定并交由 Spring 管理:

1
2
3
4
5
6
7
8
/**
* @author mofan
* @date 2022/10/27 10:51
*/
@Configuration
@EnableConfigurationProperties({MyProperties.class})
public class MyConfig {
}

注意: 使用 @EnableConfigurationProperties 注解时,所在类必须被添加到 Spring 容器中,否则无效。

4.7 @PropertySource

作用:与 Spring 配置文件的 <context:property-placeholder> 标签作用一样。用于指定 properties 文件的位置。

使用示例:

1
2
3
4
5
6
7
8
9
@Component
@PropertySource("classpath:/xxx.properties")
public class Person{
@Value("${name}")
private String name;
@Value("${age}")
private Integer age;
// ...
}

如果需要定义多个配置文件,可以使用 @PropertySources 注解,在其内部声明多个 @PropertySource 注解即可。

如果想要定义配置文件的编码,可以使用 @PropertySource 提供的 encoding 属性。

绑定 yaml 配置文件

@PropertySource 只能用于指定 properties 文件,如果想要解析 yaml 文件,需要自定义配置工厂,然后使用 factory 属性指定自定义的配置工厂,比如:

1
2
3
4
5
6
7
8
9
10
public class YamlPropertySourceFactory implements PropertySourceFactory {

@Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject();
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
}
}

使用时:

1
@PropertySource(value = "classpath:xxx.yaml", encoding = "utf-8", factory = YamlPropertySourceFactory.class)

4.8 @EnbleAspectJAutoProxy

使用该注解后,表示开启 Spring AOP 动态代理的自动配置。

@EnbleAspectJAutoProxy 内有两个属性:

  • proxyTargetClass:使用 CGLib 动态代理还是使用 JDK 动态代理。设置为 true 时表示使用 CGLib 动态代理,设置为 false 时表示 尽可能 使用 JDK 动态代理。如果被代理的类未实现接口,就算是 false 也会使用 CGLib 实现动态代理。
  • exposeProxy:是否暴露代理对象,设置为 true 时,可以使用 AopContext.currentProxy() 获取到代理对象。

使用示例

导入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

SpringBoot 的启动类上添加 @EnableAspectJAutoProxy(exposeProxy = true) 注解,表示通过 AOP 框架暴露代理对象,可以使用 AopContext.currentProxy() 获取到代理对象:

1
2
3
4
5
6
7
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(BlogClientApplication.class, args);
}
}

使用:

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

public void methodA() {
// 创建代理对象
XServiceImpl xService = (XServiceImpl) AopContext.currentProxy();
xService.methodB();
}

public void methodB() {
// ...
}
}

4.9 @ConstructorBinding

在使用 @ConfigurationProperties 注解时,需要提供 Setter 方法,但如果要求对象不能被修改,就不会再提供 Setter 方法,而是只提供构造方法,此时应该怎么实现配置文件信息绑定呢?

可以使用 @ConstructorBinding 注解,该注解可以作用在构造方法、注解上:

1
2
3
4
5
@Target({ ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConstructorBinding {
}

该注解也需要与 @ConfigurationProperties 搭配使用,通过构造方法将配置文件中的信息绑定到属性上。

使用示例

配置文件信息:

1
2
3
4
5
6
7
another-properties:
myName: "mofan212"
age: 21
innerProperties:
integer: 123
person:
gender: "man"

使用 @ConstructorBinding 将配置文件中的信息绑定到属性上:

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
/**
* @author mofan
* @date 2023/6/25 10:36
*/
@Getter
@ConfigurationProperties("another-properties")
public class MyAnotherProperties {
private final String myName;

private final Integer age;

@Value("${person.gender}")
private String gender;

private final InnerProperties innerProperties;

@ConstructorBinding
public MyAnotherProperties(String myName, Integer age, InnerProperties innerProperties) {
this.myName = myName;
this.age = age;
// 不能直接 new 一个对象,必须通过构造方法传入,否则无法绑定配置文件中的信息
// this.innerProperties = new InnerProperties();
this.innerProperties = innerProperties;
}
}

简单测试下:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testConstructorBinding() {
MyAnotherProperties properties = context.getBean(MyAnotherProperties.class);
assertThat(properties)
.extracting(MyAnotherProperties::getMyName,
MyAnotherProperties::getAge,
MyAnotherProperties::getGender,
i -> i.getInnerProperties().getInteger(),
i -> i.getInnerProperties().getBool())
.containsExactly("mofan212", 21, "man", 123, false);
}

注意: 使用 @ConstructorBinding 注解时,不能与 @Configuration@Component 注解及其衍生注解搭配使用。Spring 使用构造方法来创建 Bean 时,要求 Spring 容器中存在构造方法的参数类型的 Bean,而常见的 String 类型显然是不存在的,应用启动时就会抛出 NoSuchBeanDefinitionException 异常。在 IDEA 中出现搭配使用的场景时,会提示以下信息:

Annotated with @ConstructorBinding but defined as Spring component

4.10 @DurationUnit

Spring 支持直接从配置文件中读取 Duration 类型的配置信息。

在配置文件中未显式指定单位时,默认单位为毫秒,支持配置带单位(比如:nsusmssmhd 等)的文本,也可以使用 @DurationUnit 注解来显式指定单位,因此 @DurationUnit 常与 @ConfigurationProperties 搭配使用。

使用示例

配置文件信息:

1
2
3
4
properties:
default-duration: 10
duration-with-unit: 7d
duration: 60

配置信息绑定类:

1
2
3
4
5
6
7
8
9
10
11
@Setter
@Getter
@ConfigurationProperties("properties")
public class MyProperties {
private Duration defaultDuration;

private Duration durationWithUnit;

@DurationUnit(ChronoUnit.SECONDS)
private Duration duration;
}

简单测试下:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testEnableConfigurationProperties() {
// --snip--

// duration
assertThat(properties).extracting(
MyProperties::getDefaultDuration,
MyProperties::getDurationWithUnit,
MyProperties::getDuration
).containsExactly(Duration.ofMillis(10), Duration.ofDays(7), Duration.ofSeconds(60));
}

相似注解

@DurationUnit 外,还有两个类似的注解:

  • @PeriodUnit:用于指定 Period 类型配置信息的单位,未显式指定单位时,默认单位为天
  • @DataSizeUnit:用于指定 DataSize 类型配置信息的单位,DataSize 是 Spring 提供的用于描述文件大小的类,未显式指定单位时,默认单位是 bytes

4.11 @ConfigurationPropertiesBinding

Spring 内置了一些将配置文件中的信息转换成常见 Java 对象的转换器,如果要转换成自定义对象,则需要自定义转换器。

比如有配置信息如下:

1
2
3
properties:
# 加引号才识别为字符串,否则识别成 Double,导致找不到对应的 Convertor
weight: "1000.1"

且存在自定义类 Weight

1
2
3
4
5
6
@Getter
@AllArgsConstructor
public class Weight {
private BigDecimal value;
private boolean overWight;
}

现在需要读取配置文件中的 weight 信息,将其绑定到 Weight 对象上:

1
2
3
4
5
6
@Setter
@Getter
@ConfigurationProperties("properties")
public class MyProperties {
private Weight weight;
}

自定义转换器 WeightConvertor

1
2
3
4
5
6
7
8
9
public class WeightConvertor implements Converter<String, Weight> {
private static final BigDecimal MAX_WEIGHT = new BigDecimal(1000);

@Override
public Weight convert(String source) {
BigDecimal value = new BigDecimal(source);
return new Weight(value, MAX_WEIGHT.compareTo(value) < 0);
}
}

使用 @Bean 注解将自定义转换器添加到 Spring 容器中,并使用 @ConfigurationPropertiesBinding 指定该 Bean 用于读取配置文件信息的数据转换:

1
2
3
4
5
6
7
8
@Configuration
public class PropertiesConfig {
@Bean
@ConfigurationPropertiesBinding
public WeightConvertor weightConvertor() {
return new WeightConvertor();
}
}

简单测试下:

1
2
3
4
5
6
7
8
@Test
public void testEnableConfigurationProperties() {
// --snip--

// custom convertor
assertThat(properties).extracting("weight.value", "weight.overWight")
.containsExactly(new BigDecimal("1000.1"), true);
}