我们在前面的篇章基本上已经熟悉了怎么使用SpringBoot,但是呢。对于SpringBoot的一些高级的特性 或者 底层原理还是不是很清楚。
例如SpringBoot是如何根据启动坐标去创建需要的 bean。
有时候我们可能需要通过一些条件,再去创建 bean。对于这种场景,我们该怎么做呢?
这个时候我们就要介绍以下 Condition 的使用了。
Condition是Spring4.0后引入的条件化配置接口,通过实现Condition接口可以完成有条件的加载相应的Bean
@Conditional要配和Condition的实现类(ClassCondition)进行使用
在Spring的 IOC 容器中创建一个User的bean,现要求:
初始化的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- springboot父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- 项目坐标 -->
<groupId>com.lijw</groupId>
<artifactId>springboot-condition</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-condition</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<!-- 项目依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
好了,现在我们搭建好了项目,也配置了 redis 的启动依赖,我们下面来逐步实验一下。
首先我们要知道 SpringBoot IOC 容器怎么获取,如下:
package com.lijw.springbootcondition;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import javax.lang.model.element.VariableElement;
@SpringBootApplication
public class SpringbootConditionApplication {
public static void main(String[] args) {
// 启动SpringBoot应用就可以获取 IOC 容器
ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args);
// 通过IOC容器获取Bean
Object redisTemplate = context.getBean("redisTemplate");
System.out.println(redisTemplate);
}
}
在上面,我们通过SpringBoot的应用启动获取到了 IOC 容器,并通过 IOC 容器获取到了 redisTemplate 的 Bean。那么这个 Bean 能否控制产生或者不产生的呢?
这时候我们就要来介绍 Condition 接口,通过这个接口 我们将可以控制一个 Bean 是否能够自动生成。
我们来回顾一下需求:
“在Spring的 IOC 容器中创建一个User的bean,现要求:
”
下面我们首先来写一个 User 类,然后再写要给 UserConfig 类来控制 User 类是否创建 Bean
package com.lijw.springbootcondition.domain;
public class User {
}
尝试获取 Bean 如下:
package com.lijw.springbootcondition;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import javax.lang.model.element.VariableElement;
@SpringBootApplication
public class SpringbootConditionApplication {
public static void main(String[] args) {
// 启动SpringBoot应用就可以获取 IOC 容器
ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args);
// 通过IOC容器获取Bean
Object user = context.getBean("user");
System.out.println(user);
}
}
可以发现 User 类目前还不能获取 Bean,因为我们没有将 User 类定义为组件,所以需要写一个获取 User 类对象的 方法。
package com.lijw.springbootcondition.config;
import com.lijw.springbootcondition.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
@Bean
public User user(){
return new User();
}
}
此时有了这个返回 @Bean 的方法,我们应该就可以获取 user 的 bean 对象了,如下:
好了,既然已经可以获取到 user 类的 bean,那么我们就可以在这个 bean 方法上来控制是否生成 bean 对象。
package com.lijw.springbootcondition.config;
import com.lijw.springbootcondition.condition.ClassCondition;
import com.lijw.springbootcondition.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
@Bean
@Conditional(ClassCondition.class) // 定义ClassCondition设置创建bean的条件规则
public User user(){
return new User();
}
}
package com.lijw.springbootcondition.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class ClassCondition implements Condition {
/**
*
* @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
* @param metadata 注解元对象。 可以用于获取注解定义的属性值
* @return Boolean: false不创建bean, true创建bean
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
此时,ClassCondition 重写的 matches 方法默认返回为 false,所以我们下面使用 IOC 获取 user bean 对象会失败,如下:
如果设置为 true,那么则可以获取到 bean 对象:
那么到这里,我们基本上就可以知道大概如何实现控制是否生成bean的需求了。
package com.lijw.springbootcondition.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class ClassCondition implements Condition {
/**
*
* @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
* @param metadata 注解元对象。 可以用于获取注解定义的属性值
* @return Boolean: false不创建bean, true创建bean
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Boolean flag = true;
try {
// 获取redisTemplate的依赖文件是否存在
Class<?> cls = Class.forName("org.springframework.data.redis.core.RedisTemplate");
} catch (ClassNotFoundException e) {
// 如果异常不存在,则设置为false,不允许创建bean
flag = false;
}
return flag;
}
}
此时,如果我们注释了redis依赖,来看看是否能创建 user 的 bean,如下:
获取 bean 失败了,说明我们的方法是正确的。
下面我们再配置依赖,看看能否获取成功,如下:
成功获取 bean 了。
在上面我们已经通过一个类依赖是否存在,来判断是否创建 bean 对象。但是我们这个类依赖 目前是直接 硬编码写死在代码里头的!
这样肯定不灵活,如果我们想要改其他类,就要每次到代码里面去修改,能不能写到一个注解上,然后我们在注解上配置需要依赖的类呢?
下面我们来看看怎么做。
在Spring的 IOC 容器中创建一个User的bean,现要求:
创建 @ConditionOnClass 继承 @Conditional,可以自定义属性
package com.lijw.springbootcondition.condition;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ClassCondition.class) // 继承 @Conditional 条件注解
public @interface ConditionOnClass {
String[] value(); // 定义数组,方便后续可以写入多个依赖类
}
@Bean
//@Conditional(ClassCondition.class) // 定义ClassCondition设置创建bean的条件规则
@ConditionOnClass("org.springframework.data.redis.core.RedisTemplate")
public User user(){
return new User();
}
在这里我们已经可以将依赖类作为参数写到注解中,那么注解如何将参数传递到条件处理类中呢?
public class ClassCondition implements Condition {
/**
*
* @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
* @param metadata 注解元对象。 可以用于获取注解定义的属性值
* @return Boolean: false不创建bean, true创建bean
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)
也就是我们可以通过 metadata 这个参数获取注解设置的参数。
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// metadata参数使用getAllAnnotationAttributes方法,传入注解的名称,将可以获取注解的属性值
Map<String, Object> map = metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
System.out.println(map);
可以确认已经可以获取 value 属性的 Sting数组了。
下面我们继续来遍历一下 value 属性的字符串数组:
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// metadata参数使用getAllAnnotationAttributes方法,传入注解的名称,将可以获取注解的属性值
Map<String, Object> map = metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
// 获取value属性的String[],并且遍历
String[] value = (String[]) map.get("value");
for (String clazzName : value) {
System.out.println(clazzName);
}
package com.lijw.springbootcondition.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.MultiValueMap;
import java.util.List;
import java.util.Map;
public class ClassCondition implements Condition {
/**
*
* @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
* @param metadata 注解元对象。 可以用于获取注解定义的属性值
* @return Boolean: false不创建bean, true创建bean
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// metadata参数使用getAllAnnotationAttributes方法,传入注解的名称,将可以获取注解的属性值
Map<String, Object> map = metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
// 设置返回的flag
Boolean flag = true;
try {
// 获取value属性的String[],并且遍历
String[] value = (String[]) map.get("value");
for (String clazzName : value) {
System.out.println(clazzName);
// 尝试获取依赖文件,根据异常判断是否存在
Class<?> cls = Class.forName(clazzName);
}
} catch (ClassNotFoundException e) {
// 如果异常不存在,则设置为false,不允许创建bean
flag = false;
}
return flag;
}
}
首先当然是验证设置了依赖的情况,如下:
可以看到成功获取 bean
下面我们来注释依赖,确认异常的情况:
成功了,当依赖不存在,则无法获取 bean
而且目前我们的依赖可以动态在注解类上填写。
在上面我们虽然自己写了一个条件注解类,但是 SpringBoot 其实已经提供了大量的条件注解类了,我们查看源码如下:
可以看到源码其实也是这些写的。
SpringBoot 提供的常用条件注解:
ConditionalOnProperty:判断配置文件中是否有对应属性和值才初始化Bean
ConditionalOnClass:判断环境中是否有对应字节码文件才初始化Bean
ConditionalOnMissingBean:判断环境中没有对应Bean才初始化Bean
其中源码之后 ConditionalOnProperty
这个注解我们可能经常会用到,通过配置文件的属性来判断是否创建 Bean
下面我们来演示一下。
image-20220224204200775
package com.lijw.springbootcondition.config;
import com.lijw.springbootcondition.condition.ClassCondition;
import com.lijw.springbootcondition.condition.ConditionOnClass;
import com.lijw.springbootcondition.domain.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
@Bean
//@Conditional(ClassCondition.class) // 定义ClassCondition设置创建bean的条件规则
@ConditionOnClass("org.springframework.data.redis.core.RedisTemplate")
public User user(){
return new User();
}
@Bean
@ConditionalOnProperty(name = "test", havingValue = "condition") // 配置文件存在 test=condition 则创建bean对象
public User user2(){
return new User();
}
}
package com.lijw.springbootcondition;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import javax.lang.model.element.VariableElement;
@SpringBootApplication
public class SpringbootConditionApplication {
public static void main(String[] args) {
// 启动SpringBoot应用就可以获取 IOC 容器
ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args);
// 通过IOC容器获取Bean
Object user = context.getBean("user2");
System.out.println(user);
}
}
此时没有自动创建 user2 的 bean 对象。
在配置文件设置了属性值之后,成功自动创建 user2 的 bean