自定义配置项实现 boot
项目的组件开启和关闭开发思路和开发过程记录
需求描述:
由于
spring boot
项目拥有自动配置(AutoConfiguration)
的功能,但是开发者不清楚spring boot
配置原理的情况下,很难掌握到关闭某个组件在spring boot
项目中自动配置。所以,在这种情况下,诞生了自定义配置项,来开启和关闭某个组件在spring boot
项目中自动配置的功能
该想法来自于项目开发中,多数项目组使用同一个脚手架进行二开的情况,很多项目根本就使用不到某些组件,从而增加了开发项目运行占用多余的开销
1. 思路描述
我们都知道
spring boot
启动的时候,是通过main
函数进行启动的,主函数所在的类需要添加注解@SpringbootApplication
,那么在这两个必要条件的加持下,运行主函数spring boot
就会被启动,那么此需求的入口,很显然就在这个主函数所在类中。而另外还有一个我们都应该知道的信息,spring boot
项目推崇的是注解开发,那么综合这两个信息,确定我要找的入口就在@SpringbootApplication
中。
1.1 进入 @SpringbootApplication
中
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
...
进入
@SpringbootApplication
内,映入眼帘的就是@EnableAutoConfiguration
注解,而此注解就是spring boot
三大神器之一,spring boot
自动装配,如果对spring boot
有所了解的都知道, 在@SpringbootApplication
注解中是能配置exclude
的,而通过上面的代码中能看出,@SpringBootApplication
注解中确实提供了Class<?>[] exclude() default {}
这么一个属性,并且在属性上添加了一个@AliasFor(annotation = EnableAutoConfiguration.class)
注解,很显然这个exclude
于EnableAutoConfiguration
类有关,那么我们继续进入@EnableAutoConfiguration
注解中
1.2 进入注解 @EnableAutoConfiguration
中
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
通过上面的代码中,直观的查看到
@Import(AutoConfigurationImportSelector.class)
了这个类,借名之意(自动配置导入选择器),那么很现然这个类就是我们要找的最终目标类,进入该类中。
1.3 进入 AutoConfigurationImportSelector
中
这里我只放了最重要,且直观的一些代码
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}
protected List<String> getExcludeAutoConfigurationsProperty() {
Environment environment = getEnvironment();
if (environment == null) {
return Collections.emptyList();
}
if (environment instanceof ConfigurableEnvironment) {
Binder binder = Binder.get(environment);
return binder.bind(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class).map(Arrays::asList)
.orElse(Collections.emptyList());
}
String[] excludes = environment.getProperty(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class);
return (excludes != null) ? Arrays.asList(excludes) : Collections.emptyList();
}
}
1.4 使用 DEBUG
模式,在 application.yml
中配置移除 RabbitMQ
,来查看 AutoConfigurationImportSelector
这个类中代码执行的情况
1.4.1 导入 RabbitMQ
starter 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
1.4.2 yml
中移除 RabbitMQ
自动配置类
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
1.4.3 运行项目,进入 DEBUG
断点
通过
DEBUG
模式进行调试,发现最终在getExcludeAutoConfigurationsProperty()
方法内获取到了被移除的自动配置类,那么我们先看看这个方法内部的代码情况
1.4.4 getExcludeAutoConfigurationsProperty()
内部结构
protected List<String> getExcludeAutoConfigurationsProperty() {
// 获取成员变量 Environment
Environment environment = getEnvironment();
// 如果环境变量为 null 的情况下,就返回的是 emptyList
if (environment == null) {
return Collections.emptyList();
}
// 如果不为 null 并且属于可配置的环境变量,那么就从这里面获取到所有被移除的自动配置类
if (environment instanceof ConfigurableEnvironment) {
// 这里是通过 Binder 进行获取的
Binder binder = Binder.get(environment);
return binder.bind(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class).map(Arrays::asList)
.orElse(Collections.emptyList());
}
String[] excludes = environment.getProperty(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class);
return (excludes != null) ? Arrays.asList(excludes) : Collections.emptyList();
}
那么问题来了,
ConfigurableEnvironment
这个类是什么时候被初始化的,并且是什么时候开始工作的,又是什么时候被放入了所有的配置的,在这里篇幅有限,就不详细讲解了,可以通过DEBUG
来针对该成员变量的setter
方法的所有代码进行断点测试
2 解决问题
2.1 怎么样能在项目启动的时候去修改 Environment
并且让他拥有 key
为 spring.autoconfigure.exclude
的环境配置?
通过看书查资料的方式,得到了一种较为完美的解决方案,为什么要说是较为完美,而不是完美。因为我自己还没动手测试过,所以成为较为完美。思路主要是实现
EnvironmentPostProcessor
接口,重写postProcessEnvironment(ConfigurableEnvironment, SpringApplication)
方法,通过源码注释的描述,我们总结出三点:
- 实现该类的
postProcessEnvironment
方法,它能够为应用程序提前设置好环境变量- 实现这个类,需要在
META-INF/spring.factories
里面添加key
为org.springframework.boot.env.EnvironmentPostProcessor
,value
为实现类全限定名的配置- 可以通过
@Ordered
注解来实现优先加载
/**
* 允许在应用程序上下文刷新之前,来自定义应用程序的环境
* Allows for customization of the application's {@link Environment} prior to the
* application context being refreshed.
*
* <p>
* 实现EnvironmentPostProcessor类,需要在将该类注册到 META-INF/spring.factories 里面,使用本类的全限定名作为key,实现者全限定名作为value
* EnvironmentPostProcessor implementations have to be registered in
* {@code META-INF/spring.factories}, using the fully qualified name of this class as the
* key.
* <p>
*
* 支持Ordered 注解,根据配置得顺序,进行相应得顺序进行加载调用
* {@code EnvironmentPostProcessor} processors are encouraged to detect whether Spring's
* {@link org.springframework.core.Ordered Ordered} interface has been implemented or if
* the {@link org.springframework.core.annotation.Order @Order} annotation is present and
* to sort instances accordingly if so prior to invocation.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 1.3.0
*/
public interface EnvironmentPostProcessor {
/**
* 通过给定环境,进行后置处理
* 好像真的很符合我们的需求,并且类型也是我们需要的类型
* Post-process the given {@code environment}.
* @param environment the environment to post-process
* 需要处理的环境
* @param application the application to which the environment belongs
* 环境所在的应用程序
*/
void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);
}
2.2 准备好自己的 autoExcludeConfig.properties
配置文件,放入 classpath
下
## true: 关闭该模块的自动配置
## false: 开启模块自动配置
cm.module.enable.rabbitMq=true
2.3 实现 EnvironmentPostProcessor
类,重写唯一的抽象方法
package com.example.oauth.system.config.exclude;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.util.*;
/**
* <p>
* 本类主要实现的功能如下:
* boot 项目启动加载 spring.factories 配置文件阶段,会来执行该类,因为在该项目 META-INF/spring.factories 中,配置了
* org.springframework.boot.env.EnvironmentPostProcessor={@code this.getClass().getName},在该类中,主要是完成
* classpath:autoExcludeConfig.properties 文件的读取和解析,当 prefix=cm.module.enable 的 key 设置为 false 的情况
* 则表示该模块对应的自动配置类将会在 springboot 中被排除加载
*
* eg: cm.module.enable.redis=false,那么本项目中 Redis 的配置将不会生效,也就是本项目中不会使用该中间件
*
* 后续的处理逻辑则是在 {@code AutoConfigurationImportSelector#getExcludeAutoConfigurationsProperty()} 内完成的,
* 此方法,主要是通过读取 {@link Environment} 类,来寻找所有配置为 {@see spring.autoconfigure.exclude} 的自动配置,
* 最终会执行 {@code org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationEntry(AnnotationMetadata)}
* 方法,然后在方法内,获取到扫描出来的全部自动配置项,在需要自动配置项类,去移除调不被自动配置的类,这样本类目的就达到了
* </p>
*
* @author zyred
* @since v 0.1
**/
public class GlobalAutoConfigurationSwitch extends AutoConfigurationSwitchSupport implements EnvironmentPostProcessor {
/**
* {@code org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE}
* 该配置前缀,主要是 spring boot 针对自动配置项进行移除的关键配置,如果 spring boot 中配置了该项,那么将会在 boot 项目初始化
* 读取配置文件之后,来扫描该配置 {@see org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getExclu
* -deAutoConfigurationsProperty()},该方法主要作用是通过对全局 Environment 对象的扫描,读取 {@code PROPERTY_NAME_AUTOCONFIGURE
* _EXCLUDE} 的所有字符串,最终返回一个 {@link List<String>} 的,被排除的自动配置类全路径
**/
private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
/**
* 字符串切分正则表达式,切分目的 cm.module.enable.redis -> 切分为 redis, 从 {@see definitionExcludeContainers} 结果中获取全路径
*/
public static final String REGEX_COMMAND = "\\.";
/**
* 被扫描到所有配置为 false 的自动配置类,将会被添加到 {@see excludeContainer} 中,但是环境变量 {@see ConfigurableEnvironment} 内
* 包含的所有被移除的类却是一个字符串,例如: org....RedisAutoConfiguration,org....RabbitAutoConfiguration,在这里我们扫描到的类装入了
* {@see excludeContainer} 中,所以需要进行处理成一个字符串,使用 {@see JOIN_COMMAND} 进行连接
*/
public static final String JOIN_COMMAND = ",";
/** 在 {@see GlobalAutoConfigurationSwitch#profiles} 文件中所有被排除的类(val=true),将会进入到此容器中 **/
private static final List<String> excludeContainer = new ArrayList<>();
/** 读取配置文件到本类中 **/
private final String[] profiles = { "autoExcludeConfig.properties" };
/** 将 properties 文件转换为此对象,方便存取 **/
private final Properties properties = new Properties();
/**
* 由 {@see org.springframework.boot.env.EnvironmentPostProcessor} 提供的方法,
* 主要目的是完成初始化阶段,读取配置文件spring.factories 的方法,并且在此方法内,可
* 以自定义逻辑
* @param environment 装配 spring.factories 的配置环境变量
* @param application springboot 应用程序配置信息
*/
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
for (String profile : this.profiles) {
// 读取配置文件为 Resource 对象
Resource resource = new ClassPathResource(profile);
// 加载并且解析出配置文件里面的信息
loadAndParseProfiles(resource);
if (CollectionUtils.isEmpty(excludeContainer)) { continue; }
// 强行为当前 Properties 设置属性 spring.autoconfigure.exclude
properties.setProperty(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, arrayJoinSeparator());
// 将该属性添加到全局环境变量中
environment.getPropertySources().addLast(new PropertiesPropertySource(Objects.requireNonNull(resource.getFilename()), properties));
}
}
/**
* 加载,并且解析配置文件,如果又扫描到的类配置为 false,那么就将
* 被扫描到的类装入容器 {@see excludeContainer} 中如果没有,那
* 么直接跳过处理
*
* @param resource 被读取的配置文件所指向的 Resource 类
*/
private void loadAndParseProfiles(Resource resource) {
if (!resource.exists()) {
throw new IllegalArgumentException("File " + resource + " not exist.");
}
String key = null;
try {
// 加载配置文件
this.properties.load(resource.getInputStream());
// 循环所有的配置项
Set<String> set = this.properties.stringPropertyNames();
if (CollectionUtils.isEmpty(set)) {
return;
}
for (String a : set) {
// cm.module.enable.redis
key = a;
// true/false
String property = properties.getProperty(a);
boolean bool = Boolean.parseBoolean(property);
// true: 开启目标自动配置
if (!bool) {
continue;
}
// 关闭目标自动配置
// cm.module.enable.redis -> ["cm", "module", "enable", "redis"]
String[] split = key.split(REGEX_COMMAND);
int len = split.length - 1;
// ["cm", "module", "enable", "redis"] -> redis
String containerKey = split[len];
// 拿到被定义出来的目标自动配置类
String clazzName = definitionExcludeContainers.get(containerKey);
excludeContainer.add(clazzName);
}
} catch (Exception ex) {
if (StringUtils.isNotBlank(key)) {
throw new ClassCastException("case: " + resource.getFilename() + " -> " + key + "can not cast bool.");
}
throw new IllegalArgumentException("case: File " + resource.getFilename() + " load failure.");
}
}
/**
* 集合切分重新组合为字符串
*
* @return target not empty: ["1", "2"] --> 1,2 / target empty : ""
*/
private static String arrayJoinSeparator() {
int singleSize = 1, firstIndex = 0; String defaultStr = "";
if (CollectionUtils.isEmpty(GlobalAutoConfigurationSwitch.excludeContainer)) {
return defaultStr;
}
if (GlobalAutoConfigurationSwitch.excludeContainer.size() == singleSize) {
return GlobalAutoConfigurationSwitch.excludeContainer.get(firstIndex);
}
StringBuilder builder = new StringBuilder();
for (String str : GlobalAutoConfigurationSwitch.excludeContainer) {
builder.append(GlobalAutoConfigurationSwitch.JOIN_COMMAND).append(str);
}
// before : ,"1","2"
// after : "1","2"
return builder.toString().substring(singleSize);
}
}
/**
* <p>
* 本类中,主要是在开发阶段完成类型的定义,例如 redis -> org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
* 起到一个通过别名,获取到对应组件的自动装配对象
* </p>
*
* @author zyred
* @since v 0.1
**/
public class AutoConfigurationSwitchSupport {
/**
* 开发阶段,定义好的所有 {@see key} 自定义类型和 {@see val} 类型对应的自动配置类全路径名称
* key: 自定义类型 val: 组件在spring boot 中自动配置的类全路径名称
*/
protected static final Map<String, String> definitionExcludeContainers = new ConcurrentHashMap<>();
static {
// key: 自定义类型 val: 组件在spring boot 中自动配置的类全路径名称
definitionExcludeContainers.put("redis", "org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration");
definitionExcludeContainers.put("rabbitMq", "org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration");
}
}
2.4 新增文件 META-INF/spring.factories
文件,并添加内容
org.springframework.boot.env.EnvironmentPostProcessor=com.example.oauth.system.config.exclude.GlobalAutoConfigurationSwitch
到此,思路也屡清楚了,代码也写好了,就缺测试了
3. 测试功能
描述: 在该项目中依赖了 oauth2,在 oauth2 中添加了 redis 作为 TokenStore ,那么本项目如果在不依赖 Redis 自动配置的情况下,是一定启动失败的,所以这里我们将会采用对 Redis 的开关来测试
3.1 修改 autoExcludeConfig.properties
配置,新增 Redis
开关
## true: 关闭该模块的自动配置
## false: 开启模块自动配置
cm.module.enable.redis=false
cm.module.enable.rabbitMq=true
3.2 删除 application.yml
中 exclude
项
spring:
profiles:
active: dev
3.3 启动项目
此时根据配置,我们在项目中是失去了
Redis
的自动配置,那么项目启动一定会报错
项目启动,继续进入刚刚测试阶段余留的断点中,发现
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
自动配置类已经进入了排除类中,那么接下来会不会报错呢?
ok
,通过本次测试,可以看出,我们修改了自己的配置,就能对某个组件进行完全的自动配置进行控制,而不需要繁重的去修改,减轻了脚手架使用的一个成本。
3.4 项目结构图
4. 总结
通过本次小功能的开发,我发现我对
spring boot
的掌握还是有一定的欠缺,后续还需要加深对其的学习。此次开发的小功能还是有一定的缺陷的,如果在不明白我这套组件使用的逻辑的时候,无意间修改了其中的配置导致项目启动不起来,可能很难定位到问题,还有就是当新添加一个组件的时候,都需要去修改AutoConfigurationSwitchSupport
类中定义的静态Map
类的内容,同时也增加了项目维护的一个难度。后续会将AutoConfigurationSwitchSupport
中Map
修改为配置文件的方法,并且在载入配置文件的时候,加强对其内容的校验,尽量做到好用,好维护的目的。