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 PropertySource
s 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 ApplicationContextInitializer
s). 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 ApplicationPreparedEvent
s & ApplicationStartedEvent
s fired (I counted 3 ApplicationPreparedEvent
s (same instance id of the event), followed by 2 ApplicationStartedEvent
s (same instance id of the event), followed by 2 ApplicationReadyEvent
s (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:
ApplicationContextInitializer
fires twice - once for the bootstrap & once for the "main" application
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?
You can make use of Spring application Events and Listeners. In your case, you can use ApplicationEnvironmentPreparedEvent
like:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With