首页 > 解决方案 > 自定义 Spring Boot Starter 库中读取当前属性并根据值设置新属性的最佳方式

问题描述

我正在 Spring Boot 之上构建一组库,打包为 Spring Boot Starters。我需要能够定义在应用程序启动时他们查看环境(即读取属性)的功能,并在此基础上设置其他属性值。我最初尝试在 an 中执行此操作,EnvironmentPostProcessor但这似乎不是正确的地方,因为您遇到了并非所有PropertySources 都可用的订购问题。

我的具体用例是我想寻找该spring.boot.admin.client.url属性的存在。如果未找到,则设置属性spring.boot.admin.client.enabled=false

在配置服务器端,我们为具有不同配置文件的不同应用程序提供不同的配置,其中一些设置 spring.boot.admin.client.url为一个值,而另一些则没有。无论如何,应用程序本身都会捆绑spring-boot-admin-starter-client依赖项。是否启用它只是由应用程序的运行时目标驱动。

我想知道正确的方法是什么?

我想到了一个ApplicationListenerApplicationContextInitializer

启动时触发两次,ApplicationContextInitializer一次用于引导上下文,一次用于主上下文。被ApplicationEnvironmentPreparedEvent触发两次(就在调用每个ApplicationContextInitializers 之前)。那时,配置服务属性源还不存在,我正在寻找的属性值也不存在。

然后触发了一堆不同ApplicationPreparedEvent的 s & s(我数了 3 s(事件的相同实例 id),然后是 2 s(事件的相同实例 id),然后是 2 s(事件的相同实例 id) )。ApplicationStartedEventApplicationPreparedEventApplicationStartedEventApplicationReadyEvent

2018 年 9 月 28 日更新

我想添加一些我也尝试过的测试。我从 Spring Initializr 构建了一个空白应用程序。我构建了一个ApplicationContextInitializr和一个ApplicationListener如下:

application.yml

spring:
  application:
    name: TestEventStartup
  boot:
    admin:
      client:
        url: http://localhost:8888
  jackson:
    serialization:
      write-dates-as-timestamps: false
  resources:
    chain:
      strategy:
        content:
          enabled: true

logging:
  level:
    root: warn
    com.testeventstartup: debug

server:
  compression:
    enabled: true
    mime-types: application/json,text/css,text/html
    min-response-size: 2048

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
  info:
    git:
      mode: full
public class AppContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        System.out.println(String.format("Initializing Context - Got spring.boot.admin.client.url=%s", applicationContext.getEnvironment().getProperty("spring.boot.admin.client.url")));
    }
}

public class AppListener implements ApplicationListener<SpringApplicationEvent> {
    @Override
    public void onApplicationEvent(SpringApplicationEvent event) {
        System.out.println(String.format("Got event: %s", ToStringBuilder.reflectionToString(event)));

        findEnvironment(event)
            .ifPresent(environment -> System.out.println(String.format("%s: spring.boot.admin.client.url=%s", event.getClass().getSimpleName(), environment.getProperty("spring.boot.admin.client.url"))));
    }

    private Optional<Environment> findEnvironment(Object obj) {
        return Optional.ofNullable(Optional.ofNullable(ReflectionUtils.findMethod(obj.getClass(), "getEnvironment"))
            .map(method -> ReflectionUtils.invokeMethod(method, obj))
            .orElseGet(() ->
                Optional.ofNullable(ReflectionUtils.findMethod(obj.getClass(), "getApplicationContext"))
                    .map(method -> ReflectionUtils.invokeMethod(method, obj))
                    .flatMap(this::findEnvironment)
                    .orElse(null)
            ))
            .filter(Environment.class::isInstance)
            .map(Environment.class::cast);
    }
}

/META-INF/spring.factories

org.springframework.context.ApplicationListener=\
com.testeventstartup.listener.AppListener

org.springframework.context.ApplicationContextInitializer=\
com.testeventstartup.listener.AppContextInitializer

当我在没有依赖项的情况下启动应用程序时org.springframework.cloud:spring-cloud-starter-config,我在日志中看到了这一点:

Got event: org.springframework.boot.context.event.ApplicationStartingEvent@32709393[args={},timestamp=1538141292580]
Got event: org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent@399f45b1[environment=StandardServletEnvironment {activeProfiles=[], defaultProfiles=[default], propertySources=[StubPropertySource {name='servletConfigInitParams'}, StubPropertySource {name='servletContextInitParams'}, MapPropertySource {name='systemProperties'}, OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}, RandomValuePropertySource {name='random'}, OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.yml] (document #1)'}]},args={},timestamp=1538141292628]
ApplicationEnvironmentPreparedEvent: spring.boot.admin.client.url=http://localhost:8888

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

Initializing Context - Got spring.boot.admin.client.url=http://localhost:8888
2018-09-28 09:28:12.918  INFO 2534 --- [           main] c.t.TestEventStartupApplication          : Starting TestEventStartupApplication on MAC-22XG8WL with PID 2534 (/Users/edeandre/workspaces/IntelliJ/test-event-startup/build/classes/java/main started by edeandre in /Users/edeandre/workspaces/IntelliJ/test-event-startup)
2018-09-28 09:28:12.920 DEBUG 2534 --- [           main] c.t.TestEventStartupApplication          : Running with Spring Boot v2.0.5.RELEASE, Spring v5.0.9.RELEASE
2018-09-28 09:28:12.922  INFO 2534 --- [           main] c.t.TestEventStartupApplication          : No active profile set, falling back to default profiles: default
Got event: org.springframework.boot.context.event.ApplicationPreparedEvent@6a370f4[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@49e53c76: startup date [Wed Dec 31 19:00:00 EST 1969]; root of context hierarchy,args={},timestamp=1538141292961]
ApplicationPreparedEvent: spring.boot.admin.client.url=http://localhost:8888
2018-09-28 09:28:15.385  INFO 2534 --- [           main] c.t.TestEventStartupApplication          : Started TestEventStartupApplication in 2.809 seconds (JVM running for 3.32)
Got event: org.springframework.boot.context.event.ApplicationStartedEvent@7159139f[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@49e53c76: startup date [Fri Sep 28 09:28:12 EDT 2018]; root of context hierarchy,args={},timestamp=1538141295385]
ApplicationStartedEvent: spring.boot.admin.client.url=http://localhost:8888
Got event: org.springframework.boot.context.event.ApplicationReadyEvent@232cce0[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@49e53c76: startup date [Fri Sep 28 09:28:12 EDT 2018]; root of context hierarchy,args={},timestamp=1538141295387]
ApplicationReadyEvent: spring.boot.admin.client.url=http://localhost:8888

然后,当我org.springframework.cloud:spring-cloud-starter-config在我的配置服务器中添加依赖项并连接时,这就是我在启动时看到的:

Got event: org.springframework.boot.context.event.ApplicationStartingEvent@23faf8f2[args={},timestamp=1538141399719]
Got event: org.springframework.boot.context.event.ApplicationStartingEvent@306279ee[args={},timestamp=1538141399814]
Got event: org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent@7cc0cdad[environment=StandardEnvironment {activeProfiles=[], defaultProfiles=[default], propertySources=[ConfigurationPropertySourcesPropertySource {name='configurationProperties'}, MapPropertySource {name='bootstrap'}, MapPropertySource {name='systemProperties'}, OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}, RandomValuePropertySource {name='random'}, MapPropertySource {name='springCloudClientHostInfo'}, OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/bootstrap.yml]'}]},args={},timestamp=1538141399815]
ApplicationEnvironmentPreparedEvent: spring.boot.admin.client.url=null
Initializing Context - Got spring.boot.admin.client.url=null
Got event: org.springframework.boot.context.event.ApplicationPreparedEvent@4e7912d8[context=org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f: startup date [Wed Dec 31 19:00:00 EST 1969]; root of context hierarchy,args={},timestamp=1538141400176]
ApplicationPreparedEvent: spring.boot.admin.client.url=null
2018-09-28 09:30:00.183  INFO 2568 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f: startup date [Fri Sep 28 09:30:00 EDT 2018]; root of context hierarchy
2018-09-28 09:30:00.362  INFO 2568 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'configurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$f1570cbf] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
Got event: org.springframework.boot.context.event.ApplicationPreparedEvent@4e7912d8[context=org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f: startup date [Fri Sep 28 09:30:00 EDT 2018]; root of context hierarchy,args={},timestamp=1538141400176]
ApplicationPreparedEvent: spring.boot.admin.client.url=null
Got event: org.springframework.boot.context.event.ApplicationStartedEvent@460ebd80[context=org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f: startup date [Fri Sep 28 09:30:00 EDT 2018]; root of context hierarchy,args={},timestamp=1538141400608]
ApplicationStartedEvent: spring.boot.admin.client.url=null
Got event: org.springframework.boot.context.event.ApplicationReadyEvent@16fdec90[context=org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f: startup date [Fri Sep 28 09:30:00 EDT 2018]; root of context hierarchy,args={},timestamp=1538141400609]
ApplicationReadyEvent: spring.boot.admin.client.url=null
Got event: org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent@e8df99a[environment=StandardServletEnvironment {activeProfiles=[], defaultProfiles=[default], propertySources=[ConfigurationPropertySourcesPropertySource {name='configurationProperties'}, StubPropertySource {name='servletConfigInitParams'}, StubPropertySource {name='servletContextInitParams'}, MapPropertySource {name='systemProperties'}, OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}, RandomValuePropertySource {name='random'}, OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.yml] (document #1)'}, ExtendedDefaultPropertySource {name='defaultProperties'}, MapPropertySource {name='springCloudClientHostInfo'}]},args={},timestamp=1538141399781]
ApplicationEnvironmentPreparedEvent: spring.boot.admin.client.url=http://localhost:8888

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

Initializing Context - Got spring.boot.admin.client.url=https://myspringbootadminserver.mycompany.com
2018-09-28 09:30:01.683  INFO 2568 --- [           main] c.t.TestEventStartupApplication          : No active profile set, falling back to default profiles: default
Got event: org.springframework.boot.context.event.ApplicationPreparedEvent@5c87bfe2[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2eee3069: startup date [Wed Dec 31 19:00:00 EST 1969]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f,args={},timestamp=1538141401690]
ApplicationPreparedEvent: spring.boot.admin.client.url=https://myspringbootadminserver.mycompany.com
Got event: org.springframework.boot.context.event.ApplicationPreparedEvent@5c87bfe2[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2eee3069: startup date [Fri Sep 28 09:30:01 EDT 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f,args={},timestamp=1538141401690]
ApplicationPreparedEvent: spring.boot.admin.client.url=https://myspringbootadminserver.mycompany.com
Got event: org.springframework.boot.context.event.ApplicationPreparedEvent@5c87bfe2[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2eee3069: startup date [Fri Sep 28 09:30:01 EDT 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f,args={},timestamp=1538141401690]
ApplicationPreparedEvent: spring.boot.admin.client.url=https://myspringbootadminserver.mycompany.com
2018-09-28 09:30:03.858  INFO 2568 --- [           main] c.t.TestEventStartupApplication          : Started TestEventStartupApplication in 4.143 seconds (JVM running for 4.88)
Got event: org.springframework.boot.context.event.ApplicationStartedEvent@3dfa819[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2eee3069: startup date [Fri Sep 28 09:30:01 EDT 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f,args={},timestamp=1538141403859]
ApplicationStartedEvent: spring.boot.admin.client.url=https://myspringbootadminserver.mycompany.com
Got event: org.springframework.boot.context.event.ApplicationStartedEvent@3dfa819[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2eee3069: startup date [Fri Sep 28 09:30:01 EDT 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f,args={},timestamp=1538141403859]
ApplicationStartedEvent: spring.boot.admin.client.url=https://myspringbootadminserver.mycompany.com
Got event: org.springframework.boot.context.event.ApplicationReadyEvent@4ce94d2f[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2eee3069: startup date [Fri Sep 28 09:30:01 EDT 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f,args={},timestamp=1538141403861]
ApplicationReadyEvent: spring.boot.admin.client.url=https://myspringbootadminserver.mycompany.com
Got event: org.springframework.boot.context.event.ApplicationReadyEvent@4ce94d2f[context=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2eee3069: startup date [Fri Sep 28 09:30:01 EDT 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@815b41f,args={},timestamp=1538141403861]
ApplicationReadyEvent: spring.boot.admin.client.url=https://myspringbootadminserver.mycompany.com

连接到配置服务器后,您会注意到一些事情:

  1. ApplicationContextInitializer触发两次 - 一次用于引导程序,一次用于“主”应用 程序
    • 当“主”应用程序触发时,所有属性都已解析
  2. 各种生命周期事件被多次触发
    • 如果你仔细观察,同一个事件实例会被触发多次(即org.springframework.boot.context.event.ApplicationPreparedEvent@732c2a62(触发两次)、org.springframework.boot.context.event.ApplicationPreparedEvent@6b9ce1bf(触发 3 次)、org.springframework.boot.context.event.ApplicationStartedEvent@1f6917fb(触发两次)、org.springframework.boot.context.event.ApplicationReadyEvent@41eb94bc(触发两次)

我知道生命周期事件应该被触发两次,一次用于引导上下文和“主”上下文,但是在每个上下文的生命周期内,为什么同一个事件实例会被触发多次?

此外 - 这整件事让我觉得我的解决方案应该是我应该使用 anApplicationContextInitializer作为我的要求的最佳解决方案(检查当前Environment的属性值的当前状态,然后有条件地设置一个属性值/添加一个附加PropertySource。如果我的要求只是以编程方式添加属性值,那么继续使用EnvironmentPostProcessor可能是更好的解决方案。

现在的缺点是逻辑ApplicationContextInitializer发生了两次,每个上下文一次。有没有办法让一个ApplicationContextInitializer理解它在哪个上下文中运行,然后只在“主要”上下文中真正做我们想要的?

有人可以权衡我的观察吗?@安迪威尔金森?

标签: springspring-boot

解决方案


您可以使用 Spring 应用程序事件和侦听器。在您的情况下,您可以使用ApplicationEnvironmentPreparedEvent如下:

public class OverridePropertiesListener implements 
        ApplicationListener<ApplicationEnvironmentPreparedEvent> {
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        ConfigurableEnvironment environment = event.getEnvironment();
        Properties props = new Properties();
        props.put("myProperty", "<my value>");
        environment.getPropertySources().addFirst(new PropertiesPropertySource("myProps", props));
   }
}

有关侦听器的更多信息:应用程序事件和侦听器


推荐阅读