Spring Boot 使用外部配置

软件对配置的使用,从启动参数、本地配置文件、数据库配置,到外部独立的配置中心。一步一步朝着开放和灵活在迈进。

使用配置中心,软件启动运行的参数都抽离到单独的分布式配置服务中心。Spring Boot在一开始就提供了对配置扩展的支持,非常简单就扩展就适应了。

外部化配置的扩展可以参见 Spring Boot 指定外部启动配置文件, spring boot的官方文档Externalized Configuration章节

问题

用了diamond的配置中心,发现@Value属性配置注入未生效。

@Value("${xx.url}")
private String url;

环境变量不存在对应属性,抛出异常:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'XxBean': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'xx.url' in value "${xx.url}"

具体的加载过程参见:

AutowiredAnnotationBeanPostProcessor
PropertyPlaceholderConfigurer#resolvePlaceholder

方案

有两种方案,一种是实现ApplicationContextInitializer接口

public class ConfigCenterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

    private int order = Ordered.LOWEST_PRECEDENCE - 9;

    public void setOrder(int order) {
        this.order = order;
    }

    @Override
    public int getOrder() {
        return this.order;
    }
 
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        // 加载远程配置省略
        Properties prop = loadRemoteProperties();
        ConfigurableEnvironment env = applicationContext.getEnvironment();
        env.getPropertySources().addLast(new PropertiesPropertySource("remote-prop", prop));
    }

}

在META-INF/spring.factories添加如下配置:

org.springframework.context.ApplicationContextInitializer=\
x.xx.ConfigCenterInitializer

另一种是实现EnvironmentPostProcessor接口

public class ConfigPostProcessor implements EnvironmentPostProcessor, Ordered {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication application) {
       	// 加载远程配置省略
        Properties prop = loadRemoteProperties();
        env.getPropertySources().addLast(new PropertiesPropertySource("remote-prop", prop));
    }

    @Override
    public int getOrder() {
        return ConfigFileApplicationListener.DEFAULT_ORDER - 1;
    }

}

在META-INF/spring.factories添加如下配置:

org.springframework.boot.env.EnvironmentPostProcessor=\
x.xx.ConfigPostProcessor

原理

两种方式都是在spring boot 启动的时候,在Bean加载之前,加载配置合并入Environment中的:

// SpringApplication
public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    FailureAnalyzers analyzers = null;
    this.configureHeadlessProperty();
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    listeners.starting();

    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // EnvironmentPostProcessor 在prepareEnvironment中
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
        Banner printedBanner = this.printBanner(environment);
        context = this.createApplicationContext();
        new FailureAnalyzers(context);
        // ApplicationContextInitializer 在prepareContext中
        this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        this.refreshContext(context);
        this.afterRefresh(context, applicationArguments);
        listeners.finished(context, (Throwable)null);
        stopWatch.stop();
        if (this.logStartupInfo) {
          	(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
        }

      	return context;
    } catch (Throwable var9) {
      	this.handleRunFailure(context, listeners, (FailureAnalyzers)analyzers, var9);
      	throw new IllegalStateException(var9);
    }
}

可以看到EnvironmentPostProcessor加载在ApplicationContextInitializer之前,时机非常早,Spring的Banner都还没打印,日志组件都未初始化的时候。

ApplicationContextInitializer的加载

ApplicationContextInitializer 在SpringApplication#prepareContext() -> applyInitializers()

	protected void applyInitializers(ConfigurableApplicationContext context) {
		for (ApplicationContextInitializer initializer : getInitializers()) {
			Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
					initializer.getClass(), ApplicationContextInitializer.class);
			Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
			initializer.initialize(context);
		}
	}

ApplicationContextInitializer在Spring Boot中使用很广,看看Spring Boot中的spring.factories就可以看到:

#spring-boot
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
#spring-boot-autoconfigure
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

在Spring Boot中使用ApplicationContextInitializer,还有2种方式:

1、使用context.initializer.classes 配置项

application.properties 中

context.initializer.classes=XxApplicationContextInitializer

会由DelegatingApplicationContextInitializer加载;

2、使用SpringApplication.addInitializers()

@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication springApplication = new SpringApplication(DemoApplication.class);
		springApplication.addInitializers(new XxApplicationContextInitializer());
		springApplication.run(args);
	}
}

EnvironmentPostProcessor的加载

在SpringApplication#prepareEnvironment() -> listeners.environmentPrepared(environment) .发布了一个ApplicationEnvironmentPreparedEvent 事件。

ConfigFileApplicationListener 会监听到此事件, onApplicationEvent() -> onApplicationEnvironmentPreparedEvent()

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		postProcessors.add(this);
		AnnotationAwareOrderComparator.sort(postProcessors);
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
			postProcessor.postProcessEnvironment(event.getEnvironment(),
					event.getSpringApplication());
		}
}

扩展

了解了扩展外部配置是在Bean加载之前,修改Environment。按照上述分析的过程,发现还有许多地方也可以扩展并实现配置的修改:

  • 基于ApplicationListener

  • 基于SpringApplicationRunListener#environmentPrepared

  • 基于SpringApplicationRunListener#contextPrepared

  • 基于SpringApplicationRunListener#contextLoaded

SpringApplicationRunListener 的几个接口,可以回顾下SpringApplication#prepareContext()

private void prepareContext(ConfigurableApplicationContext context,
      ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
      ApplicationArguments applicationArguments, Banner printedBanner) {
   context.setEnvironment(environment);
   postProcessApplicationContext(context);
   applyInitializers(context);// 这里调用了ApplicationContextInitializer#initialize
   listeners.contextPrepared(context); // 这里调用了 SpringApplicationRunListener#contextPrepared
   if (this.logStartupInfo) {
      logStartupInfo(context.getParent() == null);
      logStartupProfileInfo(context);
   }

   // Add boot specific singleton beans
   context.getBeanFactory().registerSingleton("springApplicationArguments",
         applicationArguments);
   if (printedBanner != null) {
      context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
   }

   // Load the sources
   Set<Object> sources = getSources();
   Assert.notEmpty(sources, "Sources must not be empty");
   load(context, sources.toArray(new Object[sources.size()]));
   listeners.contextLoaded(context); //这里调用了 SpringApplicationRunListener#contextLoaded
}

参考:

Springboot–扩展外部化配置(一)

Springboot–扩展外部化配置(一)

Springboot–扩展外部化配置(三)

SpringBoot核心机制解读二ApplicationContextInitializer

SpringBoot扩展点之EnvironmentPostProcessor

你可能感兴趣的:(spring,boot,java,spring)