通过Spring SPI机制解决 @PropertySource 注解延迟生效问题

在Springboot 中,通过@PropertySource注解,我们可以让一个.properties文件注册到Spring环境变量中。如果我们想要通过一个通用组件配置多个项目通用的配置文件,这是一个很方便的方法
如:

@Configuration
@PropertySource("classpath:/common.properties")
public class CommonAutoConfiguration {
}
复制代码

common.properties文件在src/resources目录,内容如下

spring.web.locale=zh_CN
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.banner.location=classpath:banner2.txt
复制代码

但是经过测试发现,尽管添加了@PropertySource注解,但是common.properties并不总能生效,spring.jackson.time-zone=GMT+8生效了,但是spring.banner.location=classpath:banner2.txt却没有生效,经过调查发现,原来是因为@PropertySource是在自动配置类被读取时才生效并将其注入环境中的:

org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass()

......
// Process any @PropertySource annotations
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
      sourceClass.getMetadata(), PropertySources.class,
      org.springframework.context.annotation.PropertySource.class)) {
   if (this.environment instanceof ConfigurableEnvironment) {
      processPropertySource(propertySource);
   }
   else {
      logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
            "]. Reason: Environment must implement ConfigurableEnvironment");
   }
}
......
复制代码

配置属性的时机取决于配置类被读取的时间,如果在此之前就需要读取环境配置中的变量,比如控制台打印banner,就会失效,通过注册Bean的方式注入环境变量如果不能确定自动配置类的顺序,都可能发生这种配置失效的情况(配置banner文件基本上无效)。

如果想要配置提前生效,就需要通过 EnvironmentPostProcessor 接口 和META-INF/spring.factories 的SPI机制了:
先创建一个EnvironmentPostProcessor的实现类

public class CommonPropertiesConfig implements EnvironmentPostProcessor {
    private static final Map<String, Object> TEST_PROPERTIES = new HashMap<>();
    private static final Set<String> TEST_PROPERTIES_FILE = new HashSet<>();
    static {
        //最简单的方式就是直接在这里加上配置
        TEST_PROPERTIES.put("spring.jackson.time-zone", "GMT+8");
        TEST_PROPERTIES_FILE.add("classpath:common.properties");
    }
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        environment.getPropertySources().addFirst(new MapPropertySource("CommonPropertiesConfig", TEST_PROPERTIES));
        for (String location : TEST_PROPERTIES_FILE) {
            try {
                environment.getPropertySources().addFirst(new ResourcePropertySource(location));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码

然后在src/main/resources/META-INF/spring.factories中注册它:

org.springframework.boot.env.EnvironmentPostProcessor=\
com.XXX.CommonPropertiesConfig
复制代码

springboot会使用SpringFactoriesLoader.loadFactories()读取EnvironmentPostProcessor的实现类,通过此接口,我们可以让配置文件提前注入到应用程序上下文。

(如果涉及到配置覆盖问题,请额外实现org.springframework.core.Ordered接口,并注意environment.getPropertySources().addFirst()/addLast()方法的插入位置)


彩蛋: 如果你有强迫症,希望把EnvironmentPostProcessor 和被激活的配置文件解耦,希望多个组件都要配置属性时不用每个组件都实现一个EnvironmentPostProcessor 或者配置的注册需要逻辑控制,那么可以这样搞:
这里采用借用 SpringFactoriesLoader ,定义接口和实现类
接口:

public interface BasePropertySource {
    Map<String, Object> getPropertyMap();
    List<String> getPropertyFilePath();
}
复制代码

Spring环境变量工具:

@Slf4j
public class SpringEnvUtil implements EnvironmentPostProcessor, Ordered {
    //最晚注册,防止覆盖或被覆盖
    private static final Integer POST_PROCESSOR_ORDER = Integer.MAX_VALUE;
    private static ConfigurableEnvironment environment = new NoNpeEnv();
    private static final Map<String, Object> MY_PROPERTY_MAP = new HashMap<>();
    private static final Set<String> MY_PROPERTIES_FILE = new HashSet<>();

    static {
        List<BasePropertySource> basePropertySources = SpringFactoriesLoader.loadFactories(BasePropertySource.class, SpringEnvUtil.class.getClassLoader());
        for (BasePropertySource basePropertySource : basePropertySources) {
            if (basePropertySource.getPropertyMap() != null) {
                MY_PROPERTY_MAP.putAll(basePropertySource.getPropertyMap());
            }
            if (basePropertySource.getPropertyFilePath() != null) {
                MY_PROPERTIES_FILE.addAll(basePropertySource.getPropertyFilePath());
            }
        }
    }

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        SpringEnvUtil.environment = environment;
        //加到最后,允许被覆盖
        environment.getPropertySources().addLast(new MapPropertySource("MY_MAP", MY_PROPERTY_MAP));
        for (String location : MY_PROPERTIES_FILE) {
            try {
                //加到最后,允许被覆盖
                environment.getPropertySources().addLast(new ResourcePropertySource(location));
            } catch (IOException e) {
                if (log.isDebugEnabled()) {
                    System.err.println(TraceUtil.getStackTrace());
                    e.printStackTrace();
                }
                log.warn("Properties file:'" + location + "' load failed");
            }
        }
    }

    public static String getProperty(String key) {
        return environment.getProperty(key);
    }

    public static <T> T getProperty(String key, Class<T> targetType) {
        return environment.getProperty(key, targetType);
    }

    public static String getProperty(String key, String defaultValue) {
        return environment.getProperty(key, defaultValue);
    }

    @Override
    public int getOrder() {
        return POST_PROCESSOR_ORDER;
    }

    private static class NoNpeEnv extends AbstractEnvironment {

    }
}
复制代码

将 SpringEnvUtil 注册到spring.factories

org.springframework.boot.env.EnvironmentPostProcessor=\
com.XXX.util.SpringEnvUtil
复制代码

这样如果想要注册一个配置文件,只需要实现BasePropertySource,并也添加到spring.factories中即可

com.XXX.BasePropertySource=\
com.XXX.实现类
复制代码

接口和实现类的好处是,可以通过代码控制配置,比如设定为周末启动项目就把banner换成彩虹猫(笑)

public class CommonPropertySource implements BasePropertySource {

    /**
     * 如果周末,就把启动页面换成 彩虹猫 o(*≧▽≦)ツ
     */
    @Override
    public Map<String, Object> getPropertyMap() {
        LocalDate now = LocalDate.now();
        if (now.getDayOfWeek().equals(DayOfWeek.SATURDAY) || now.getDayOfWeek().equals(DayOfWeek.SUNDAY)) {
            HashMap<String, Object> result = new HashMap<>();
            result.put("spring.banner.location", "classpath:banner-rainbow-cat.txt");
            return result;
        }
        return null;
    }

    @Override
    public List<String> getPropertyFilePath() {
        return Collections.singletonList("classpath:/common.properties");
    }
}
复制代码

image.png

banner-rainbow-cat.txt内容如下:

  ${AnsiColor.BRIGHT_BLUE}████████████████████████████████████████████████████████████████████████████████
  ${AnsiColor.BRIGHT_BLUE}████████████████████████████████████████████████████████████████████████████████
  ${AnsiColor.RED}██████████████████${AnsiColor.BRIGHT_BLUE}████████████████${AnsiColor.BLACK}██████████████████████████████${AnsiColor.BRIGHT_BLUE}████████████████
  ${AnsiColor.RED}████████████████████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████████████████████████████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}██████████████
  ${AnsiColor.BRIGHT_RED}████${AnsiColor.RED}██████████████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████${AnsiColor.MAGENTA}██████████████████████${AnsiColor.WHITE}██████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}████████████
  ${AnsiColor.BRIGHT_RED}██████████████████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}████${AnsiColor.MAGENTA}████████████████${AnsiColor.BLACK}████${AnsiColor.MAGENTA}██████${AnsiColor.WHITE}████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}██${AnsiColor.BLACK}████${AnsiColor.BRIGHT_BLUE}██████
  ${AnsiColor.BRIGHT_RED}██████████████████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.MAGENTA}████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}████${AnsiColor.BLACK}██${AnsiColor.MAGENTA}██████${AnsiColor.WHITE}██${AnsiColor.BLACK}████${AnsiColor.WHITE}████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}████
  ${AnsiColor.BRIGHT_YELLOW}██████████████████${AnsiColor.BRIGHT_RED}████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.MAGENTA}████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████${AnsiColor.MAGENTA}██████${AnsiColor.WHITE}██${AnsiColor.BLACK}██${AnsiColor.WHITE}██████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}████
  ${AnsiColor.BRIGHT_YELLOW}██████████████████████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_YELLOW}██████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.MAGENTA}████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████${AnsiColor.BLACK}████████${AnsiColor.WHITE}████████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}████
  ${AnsiColor.BRIGHT_YELLOW}████████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.BLACK}██${AnsiColor.BRIGHT_YELLOW}████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.MAGENTA}████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████████████████████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}████
  ${AnsiColor.BRIGHT_GREEN}██████████████████${AnsiColor.BRIGHT_YELLOW}██${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.BLACK}████████${AnsiColor.WHITE}██${AnsiColor.MAGENTA}██████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████████████████████████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}██
  ${AnsiColor.BRIGHT_GREEN}██████████████████████${AnsiColor.WHITE}████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.MAGENTA}██████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████${AnsiColor.BRIGHT_YELLOW}██${AnsiColor.WHITE}██████████${AnsiColor.BRIGHT_YELLOW}██${AnsiColor.BLACK}██${AnsiColor.WHITE}████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}██
  ${AnsiColor.BRIGHT_GREEN}██████████████████████${AnsiColor.BLACK}████${AnsiColor.WHITE}████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.MAGENTA}██████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.BLACK}████${AnsiColor.WHITE}████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}██
  ${AnsiColor.BLUE}██████████████████${AnsiColor.BRIGHT_GREEN}████████${AnsiColor.BLACK}██████${AnsiColor.WHITE}██${AnsiColor.MAGENTA}██████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.MAGENTA}████${AnsiColor.WHITE}████████████████${AnsiColor.MAGENTA}████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}██
  ${AnsiColor.BLUE}██████████████████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}████${AnsiColor.MAGENTA}██████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████${AnsiColor.BLACK}████████████${AnsiColor.WHITE}████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}████
  ${AnsiColor.BRIGHT_BLUE}██████████████████${AnsiColor.BLUE}████${AnsiColor.BLUE}██████${AnsiColor.BLACK}████${AnsiColor.WHITE}██████${AnsiColor.MAGENTA}██████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████████████████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}██████
  ${AnsiColor.BRIGHT_BLUE}██████████████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██${AnsiColor.BLACK}████${AnsiColor.WHITE}████████████████████${AnsiColor.BLACK}██████████████████${AnsiColor.BRIGHT_BLUE}████████
  ${AnsiColor.BRIGHT_BLUE}████████████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}██████${AnsiColor.BLACK}████████████████████████████████${AnsiColor.WHITE}██${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}████████████
  ${AnsiColor.BRIGHT_BLUE}████████████████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}██${AnsiColor.BLACK}██${AnsiColor.WHITE}████${AnsiColor.BRIGHT_BLUE}████████████${AnsiColor.BLACK}██${AnsiColor.WHITE}████${AnsiColor.BLACK}████${AnsiColor.WHITE}████${AnsiColor.BLACK}██${AnsiColor.BRIGHT_BLUE}████████████
  ${AnsiColor.BRIGHT_BLUE}████████████████████████${AnsiColor.BLACK}██████${AnsiColor.BRIGHT_BLUE}████${AnsiColor.BLACK}██████${AnsiColor.BRIGHT_BLUE}████████████${AnsiColor.BLACK}██████${AnsiColor.BRIGHT_BLUE}████${AnsiColor.BLACK}██████${AnsiColor.BRIGHT_BLUE}████████████
  ████████████████████████████████████████████████████████████████████████████████
  ${AnsiColor.BRIGHT_BLUE}:: Meow :: Running Spring Boot ${spring-boot.version} :: \ö/${AnsiColor.BLACK}
复制代码

如果不想要这么麻烦也有几个解决办法,比如通过扫描包,获取BasePropertySource实现类,然后反射实例化,缺点是如果包范围很广类比较多,扫描是有性能消耗,可能会拖慢启动速度
扫描实现类方法:

/**
 * 通过父类class和类路径获取该路径下父类的所有子类列表
 * @param parentClass 父类或接口的class
 * @param packagePath 类路径
 * @return 所有该类子类或实现类的列表
 */
@SneakyThrows(ClassNotFoundException.class)
public static <T> List<Class<T>> getSubClasses(final Class<T> parentClass, final String packagePath) {
    final ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
    provider.addIncludeFilter(new AssignableTypeFilter(parentClass));
    final Set<BeanDefinition> components = provider.findCandidateComponents(packagePath);
    final List<Class<T>> subClasses = new ArrayList<>();
    for (final BeanDefinition component : components) {
        @SuppressWarnings("unchecked") final Class<T> cls = (Class<T>) Class.forName(component.getBeanClassName());
        if (Modifier.isAbstract(cls.getModifiers())) {
            continue;
        }
        subClasses.add(cls);
    }
    return subClasses;
}
复制代码

或者参考SpringFactoriesLoader自己设计一个spring.env文件,读取所有组件和依赖的spring.env中指定的配置将其注册到spring上下文中

代码量上最少的两种方法就要么是架构上耦合的通过静态代码块直接注入配置, 要么是架构上复杂的通过接口实现类利用spring.factories的方式

Guess you like

Origin juejin.im/post/7055182726369902605