springboot - 自定义配置项实现boot项目的组件开启和关闭开发思路和开发过程记录

自定义配置项实现 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) 注解,很显然这个 excludeEnableAutoConfiguration 类有关,那么我们继续进入 @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 断点

进入AutoConfigurationImportSelector中,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 并且让他拥有 keyspring.autoconfigure.exclude 的环境配置?

通过看书查资料的方式,得到了一种较为完美的解决方案,为什么要说是较为完美,而不是完美。因为我自己还没动手测试过,所以成为较为完美。思路主要是实现 EnvironmentPostProcessor 接口,重写 postProcessEnvironment(ConfigurableEnvironment, SpringApplication) 方法,通过源码注释的描述,我们总结出三点:

  1. 实现该类的 postProcessEnvironment 方法,它能够为应用程序提前设置好环境变量
  2. 实现这个类,需要在 META-INF/spring.factories 里面添加 keyorg.springframework.boot.env.EnvironmentPostProcessorvalue为实现类全限定名的配置
  3. 可以通过 @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.ymlexclude

spring:
  profiles:
    active: dev

3.3 启动项目

此时根据配置,我们在项目中是失去了 Redis 的自动配置,那么项目启动一定会报错

Redis 已经被排除

项目启动,继续进入刚刚测试阶段余留的断点中,发现 org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration 自动配置类已经进入了排除类中,那么接下来会不会报错呢?

redis完全被排除

ok,通过本次测试,可以看出,我们修改了自己的配置,就能对某个组件进行完全的自动配置进行控制,而不需要繁重的去修改,减轻了脚手架使用的一个成本。

3.4 项目结构图

项目结构图

4. 总结

通过本次小功能的开发,我发现我对 spring boot 的掌握还是有一定的欠缺,后续还需要加深对其的学习。此次开发的小功能还是有一定的缺陷的,如果在不明白我这套组件使用的逻辑的时候,无意间修改了其中的配置导致项目启动不起来,可能很难定位到问题,还有就是当新添加一个组件的时候,都需要去修改 AutoConfigurationSwitchSupport 类中定义的静态 Map 类的内容,同时也增加了项目维护的一个难度。后续会将 AutoConfigurationSwitchSupportMap 修改为配置文件的方法,并且在载入配置文件的时候,加强对其内容的校验,尽量做到好用,好维护的目的。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值