Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way in custom Spring Boot Starter library to read current properties and set new ones based on values

I’m building a set of libraries on top of Spring Boot, packaged as Spring Boot Starters. I need to be able to define functionality where on application startup they look at the environment (i.e. read properties) and based on that will set other property values. I originally tried to do this in an EnvironmentPostProcessor but that doesn’t seem to be the correct place because you run into ordering issues where not all PropertySources are available yet.

My specific use case is I want to look for for the presence of the spring.boot.admin.client.url property. If not found then set the property spring.boot.admin.client.enabled=false.

On the config server side we have different configurations for different apps with different profiles, some of which set spring.boot.admin.client.url to a value and some which don't. The application itself bundles the spring-boot-admin-starter-client dependency regardless. Whether or not to enable it is simply driven by the application's runtime target.

I’m wondering what is the correct approach for this?

I thought about either an ApplicationListener or ApplicationContextInitializer.

The ApplicationContextInitializer is fired twice at startup, once for the bootstrap context & once for the main context. The ApplicationEnvironmentPreparedEvent is fired twice (just before the call to each of the ApplicationContextInitializers). At that point the config service property sources are not yet present and the property values I'm looking for are not yet present.

There is then a bunch of different ApplicationPreparedEvents & ApplicationStartedEvents fired (I counted 3 ApplicationPreparedEvents (same instance id of the event), followed by 2 ApplicationStartedEvents (same instance id of the event), followed by 2 ApplicationReadyEvents (same instance id of the event)).

UPDATED September 28, 2018

I wanted to add on some of the tests I've tried as well. I built a blank application from the Spring Initializr. I built an ApplicationContextInitializr and an ApplicationListener as follows:

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

When I start up the application without the org.springframework.cloud:spring-cloud-starter-config dependency, I see this in my log:

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

When I then add in the org.springframework.cloud:spring-cloud-starter-config dependency and wire in my config server, this is what I see at startup:

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

You'll notice a few things once hooking into the config server:

  1. The ApplicationContextInitializer fires twice - once for the bootstrap & once for the "main" application
    • When the "main" application fires the properties are all resolved
  2. The various lifecycle events are fired multiple times
    • If you look closely though, the same instance of the events are fired multiple times (i.e. org.springframework.boot.context.event.ApplicationPreparedEvent@732c2a62 (fired twice), org.springframework.boot.context.event.ApplicationPreparedEvent@6b9ce1bf (fired 3 times), org.springframework.boot.context.event.ApplicationStartedEvent@1f6917fb (fired twice), org.springframework.boot.context.event.ApplicationReadyEvent@41eb94bc (fired twice)

I understand that the lifecycle events should be fired twice, once for the bootstrap context and the "main" context, but within each context's lifecycle, why is the same instance of the event fired multiple times?

Furthermore - this whole thing is leading me down the path towards feeling like my solution here should be that I should use an ApplicationContextInitializer as the best solution to my requirement (inspect the current Environment for current state of property values then conditionally set a property value/add an additional PropertySource. If my requirement is just to programmatically add property values, then continuing to use EnvironmentPostProcessor is probably the better solution.

The downside now is that the logic in ApplicationContextInitializer happens twice, once in each context. Is there a way for an ApplicationContextInitializer to understand for which context it is running in and then only actually do what we want in the "main" one?

Can someone please weigh in on my observations? @AndyWilkinson?

like image 322
Eric Avatar asked Sep 05 '25 06:09

Eric


1 Answers

You can make use of Spring application Events and Listeners. In your case, you can use ApplicationEnvironmentPreparedEventlike:

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));
   }
}

For more information on listeners: Application Events and Listeners

like image 84
Sukhpal Singh Avatar answered Sep 07 '25 20:09

Sukhpal Singh