使用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 的方式。


作者:木原金
链接:通过Spring SPI机制解决 @PropertySource 注解延迟生效问题 - 掘金