How can I build a database-based Spring Boot environment / property source? - spring

The goal is running Spring Boot application with an Environment containing keys & values loaded and generated by a database connection (DataSource).
Or, more abstract defined: While a configuration by files only should be preferred (faster, easier, more tolerant, ...), sometimes you will find use cases where a non-static files based configuration is required.
Spring 3.1 introduces Environment which is actually a property resolver (extends PropertyResolver) and is based on a list of objects PropertySource. Such a source is a wrapper/adapter for a properties (file or object), a map or something else. It really looks like this is the way how to get.
Properties properties = new Properties();
properties.put("mykey", "in-config");
PropertiesPropertySource propertySource = new PropertiesPropertySource("myProperties", properties);
However, this cannot be done in #Configuration classes since it must be available for the configuration phase. Think about something like
#Bean public MyService myService() {
if ("one".equals(env.getProperty("key")) {
return new OneService();
} else {
return new AnotherService();
}
}
// alternatively via
#Value("${key}")
private String serviceKey;
Additionally, the more recent Spring releases support Condition as well.
With a OneCondition like
public class OneCondition implements Condition {
#Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
return "one".equals(context.getEnvironment().getProperty("key"));
}
}
This can be used like
#Bean
#Conditional(OneCondition.class)
public MyService myService() {
return new OneService();
}
My non working ideas:
Option 1: #PropertySource
The corresponding annotation processor handles files only. This is fine, but not for this use case.
Option 2: PropertySourcesPlaceholderConfigurer
An example with a custom property source would be
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() throws IOException {
PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
pspc.setIgnoreUnresolvablePlaceholders(Boolean.TRUE);
// create a custom property source and apply into pspc
MutablePropertySources propertySources = new MutablePropertySources();
Properties properties = new Properties();
properties.put("key", "myvalue");
final PropertiesPropertySource propertySource = new PropertiesPropertySource("pspc", properties);
propertySources.addFirst(propertySource);
pspc.setPropertySources(propertySources);
pspc.setLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:application.properties"));
return pspc;
}
However, this only configures the placeholders (i.e. #Value. Any environment.getProperty() will not profit.
This is more or less the same as Option 1 (less magic, more options).
Do you know a better option? Ideally, the solution would use the context datasource. However, this is conceptually an issue since the datasource bean creation relies on properties itself...

Spring Boot provides some different extensions point for this early processing step: http://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-boot-application.html#howto-customize-the-environment-or-application-context
Internally, these options are realised with implementations of standard Spring ApplicationContextInitializer.
Depending on the priority of the source, the key/value will be available both in environment.getProperty() as well in property placeholders.
Because these is a pre-config-context listeners, no other beans are available, like a DataSource. So if the properties should be read from a database, the datasource and connection have to be build manually (eventually a separated datasource connection lookup).
Option: ApplicationListener for ApplicationEnvironmentPreparedEvent
Build an implementation of an application listener consuming ApplicationEnvironmentPreparedEvents and
register it in META-INF/spring.factories and the key org.springframework.context.ApplicationListener
- or -
use the SpringApplicationBuilder:
new SpringApplicationBuilder(App.class)
.listeners(new MyListener())
.run(args);
Example
public class MyListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
#Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
final ConfigurableEnvironment env = event.getEnvironment();
final Properties props = loadPropertiesFromDatabaseOrSo();
final PropertiesPropertySource source = new PropertiesPropertySource("myProps", props);
environment.getPropertySources().addFirst(source);
}
}
Reference: http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-spring-application.html#boot-features-application-events-and-listeners
Option: SpringApplicationRunListener
Besides the special event, there is also a more general event listener containing hooks for several types of events.
Build an implementation of SpringApplicationRunListener and register it in META-INF/spring.factories and the key org.springframework.boot.SpringApplicationRunListener.
Example
public class MyAppRunListener implements SpringApplicationRunListener {
// this constructor is required!
public MyAppRunListener(SpringApplication application, String... args) {}
#Override
public void environmentPrepared(final ConfigurableEnvironment environment) {
MutablePropertySources propertySources = environment.getPropertySources();
Properties props = loadPropertiesFromDatabaseOrSo();
PropertiesPropertySource propertySource = new PropertiesPropertySource("myProps", props);
propertySources.addFirst(propertySource);
}
// and some empty method stubs of the interface…
}
Reference: http://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-boot-application.html#howto-customize-the-environment-or-application-context
Option: ApplicationContextInitializer
This is an old friend for all "non Boot" Spring developers. However, SpringApplication mocks a configuration away -- at first.
Build an implementation of ApplicationContextInitializer and
register it in META-INF/spring.factories and the key org.springframework.context.ApplicationContextInitializer.
- or -
use the SpringApplicationBuilder:
new SpringApplicationBuilder(App.class)
.initializers(new MyContextInitializer())
.run(args);
Example
public class MyContextInitializer implements ApplicationContextInitializer {
#Override
public void initialize(final ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
Properties props = loadPropertiesFromDatabaseOrSo();
PropertiesPropertySource propertySource = new PropertiesPropertySource("myProps", props);
propertySources.addFirst(propertySource);
}
}

Related

Spring Boot: Multiple WAR deployment in same tomcat different properties

So I have to deploy the same springboot app as multiple apps in the same tomcat server.
eg /app1 /app2 /app3.
They share most of the same configuration except for datasource configuration.
I've been searching for a way to externalise the datasource configuration based on the servlet-context or something like that.
Using springs externalised configuration, I am able to get it to load the same external data source file for all apps, but they need to be different. eg.
#PropertySource(value = "file:${HOME}/datasource-override.properties", ignoreResourceNotFound=false)
Using the embedded tomcat mode, ie via say .\gradlew bootRun I think I can achieve it.
I just need to use the following as the application.properties for that profile sets the server.context-path property. (as this is a single app) eg.
#PropertySource(value = "file:${HOME}/${server.context-path}/datasource-override.properties", ignoreResourceNotFound=false),
Searching around, I thought it might be something like (and combinations of) the following, but it didn't work. eg
#PropertySource(value = "file:${HOME}/${server.servlet.context-path}/datasource-override.properties", ignoreResourceNotFound=false)
All examples I've found so far deal with either the embedded tomcat, or a single externalised property file for a single app.
Ideally I would like it to find the file in either it's own directory
file:${HOME}/${server.servlet.context-path}/datasource.properties
So for the three apps it would be something like the following, where it detects from it's deployed context, what the location for it's property file is. eg:
file:${HOME}/app1/datasource.properties
file:${HOME}/app2/datasource.properties
file:${HOME}/app3/datasource.properties
Obviously if the app was deployed as /funky_chicken then it would have a matching funky_chicken/datasource.properties
Any thoughts ? I know I am probably close, and I've tried dumping all the environmental properties. (you are probably are going to tell me to get it from JNDI as it's the only one I haven't dumped looking for the context)
Now I know ${HOME} is not the best place to store config items, it really is just to make it easier to describe the issue.
Update:
Thanks to the suggestions to use profiles, is there a way to have three active profiles in the one tomcat server, /app1, /app2 and /app3 from the same WAR file?
Why you want to deploy in tomcat? Springboot app can work lonely. Hope below steps helpful to you.
add application.yml(application.properties is ok too) in /resources. In this file, you configure common setting here.
Then you add files named from application-app1.yml to application-app3.yml in /resources too. In these files, you configure different db setting.
launch your app: for example, I suppose app1 using port 10000, app2 using port 10001...
after maven well,
app1: java -jar target/[***SNAPSHOT].jar --server.port=10000 --spring.profiles.active=app1
app2: java -jar target/[***SNAPSHOT].jar --server.port=10001 --spring.profiles.active=app2
app3: java -jar target/[***SNAPSHOT].jar --server.port=10002 --spring.profiles.active=app3
You can solve problem with spring profiles and there is no need to use #PropertySource
for application 1 just activate profiles: spring.profiles.active=app1 - this assume that in classpath you have application-app1.properties file. Same for app2, app3..appN. And file application.properies will contains common properties for all of services
I had a similar requirement. My approach is to pass the config directory to tomcat as environment variable, lets assume you pass
spring.config.location=/home/user/config
Then under /home/user/config folder you should have files matching the contextPath for each application instance. In your case you should have
app1.properties
app2.properties
app3.properties
If you don't want to duplicate common parameters, you can put all common properties in a separate file and use "spring.config.import" to import it from each application specific configuration file. Note that importing another file is supported since Spring Boot 2.4 See section "Importing Additional Configuration"
For Spring Boot application to load the properties file according to the context path, you should override "createRootApplicationContext" to get the context path, and override "configure" to set it as the properties file name as below.
#SpringBootApplication
public class TestApp extends SpringBootServletInitializer {
private static Class<TestApp> applicationClass = TestApp.class;
private String contextPath;
public static void main(String[] args) {
try {
SpringApplication.run(applicationClass, args);
}
catch (Exception e) {
// Handle error
}
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(applicationClass).properties("spring.config.name:" + contextPath);
}
#Override
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
contextPath = servletContext.getContextPath();
return super.createRootApplicationContext(servletContext);
}
}
You can try the RoutingDataSource approach, This lets you switch the datasource at runtime/realtime.
To implement this you have to pass some datasource identifier initially (You can set it in your auth token for rest based requests or in a session)
eg -
localhost:80/yourcontext/{app1}
localhost:80/yourcontext/{app2}
localhost:80/yourcontext/app3
Here app1, app2, app3 will be your datasource identifiers
App controller
#Controller
public class AppController extends SuperController{
#GetMapping("/{client}")
public String login(ModelMap map, #PathVariable("client") String client, HttpServletRequest request){
//Logic to set the path variable
return "loginPage";
}
}
Routing datasource Config
#Configuration
#EnableTransactionManagement
#ComponentScan({ "com.components_package" })
#PropertySource(value = { "classpath:database.properties" })
public class RoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
String tenant = DbContextHolder.getDbType();
LoginDetails LoginDetails = currentUser();
if(LoginDetails != null){
tenant = LoginDetails.getTenantId();
}
logger.debug("tenant >>>> "+tenant);
return tenant;
}
private LoginDetails currentUser() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof UserAuthentication) {
return ((UserAuthentication) authentication).getDetails();
}
//If not authenticated return anonymous user
return null;
}
}
Hibernate config
#PropertySource(value = { "classpath:database.properties" })
public class HibernateConfiguration {
#Autowired
private Environment environment;
#Bean
#Primary
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dynamicDataSource());
em.setPackagesToScan("com.entities_package");
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(hibernateProperties());
return em;
}
/** RoutingDataSource datasource initialized enabling the application
* to switch multiple databases at runtime
* #return
*/
private RoutingDataSource dynamicDataSource(){
RoutingDataSource routingDataSource = new RoutingDataSource();
/*Set default datasource*/
routingDataSource.setDefaultTargetDataSource(defaultDataSource());
/*Set all datasource map*/
routingDataSource.setTargetDataSources(fetchDatasourceMap());
routingDataSource.afterPropertiesSet();
return routingDataSource;
}
/** This is a default datasource
* #return
*/
private DataSource defaultDataSource() {
final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
DataSource dataSource = dsLookup.getDataSource("jdbc/defaultDs");
return dataSource;
}
/** This method sets all predefined client specific datasources in a map
* #return Map<Object, Object>
*/
private Map<Object, Object> fetchDatasourceMap(){
Map<Object, Object> dataSourcesMap = new HashMap<>();
//database.clients=app1,app2,app3
String client = environment.getRequiredProperty("database.clients");
String[] allClients = client.split(",");
if(allClients != null && allClients.length > 0){
for (Integer i = 0; i < allClients.length; i++) {
String clientKey = allClients[i].trim();
dataSourcesMap.put(clientKey, dataSource(clientKey));
}
}
return dataSourcesMap;
}
private DataSource dataSource(String clientKey) {
final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
String lookupKey = "jdbc/"+clientKey;
DataSource dataSource = dsLookup.getDataSource(lookupKey);
return dataSource;
}
private Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.dialect", environment.getRequiredProperty("hibernate.dialect"));
properties.put("hibernate.show_sql", environment.getRequiredProperty("hibernate.show_sql"));
// properties.put("hibernate.format_sql",
// environment.getRequiredProperty("hibernate.format_sql"));
return properties;
}
#Bean
JpaTransactionManager transactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
return transactionManager;
}
}
You have to define your datasource properties in context.xml for JNDI lookup.
Hope this helps

Why does the ConfigurationPropertiesBindingPostProcessor considers only one PropertySourcesPlaceholderConfigurer?

The ConfigurationPropertiesBindingPostProcessor is used whenever a bean is annotated with #ConfigurationProperties in order to bind property values to a bean.
The implementation of ConfigurationPropertiesBindingPostProcessor (Spring Boot version 1.3.1) determines its property sources in the method deducePropertySources() as shown below:
private PropertySources deducePropertySources() {
PropertySourcesPlaceholderConfigurer configurer = getSinglePropertySourcesPlaceholderConfigurer();
if (configurer != null) {
// Flatten the sources into a single list so they can be iterated
return new FlatPropertySources(configurer.getAppliedPropertySources());
}
...
}
When looking into getSinglePropertySourcesPlaceholderConfigurer() you'll discover that only the first PropertySourcesPlaceholderConfigurer is taken and the others are ignored.
What is the reason for this?
In my Spring configuration I have multiple PropertySourcesPlaceholderConfigurer beans and I would expect that all of them are used when binding the property values.
Of course I can create a workaround by defining an interface MyPropertySourceResources that looks like this:
public interface MyPropertySourceResources {
List<Resource> getResources();
}
and in my Spring configuration I define only one PropertySourcesPlaceholderConfigurer that takes a list of MyResourceLocartion as parameter:
#Bean
public static PropertySourcesPlaceholderConfigurer gdaPropertySourcesPlaceholderConfigurer(
List<MyPropertySourceResources> propertySourceResources) {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
List<Resource> allResources = propertySourceResources.stream()
.flatMap(p -> p.getResources().stream())
.collect(Collectors.toList());
propertySourcesPlaceholderConfigurer.setLocations(allResources.toArray(new Resource[allResources.size()]));
return propertySourcesPlaceholderConfigurer;
}
#Bean
public MyPropertySourceResources gdaPropertySourceResources() {
return () -> Arrays.asList(new FileSystemResource(new File("path/to/my.properties")));
}
In this solution each module defines its own MyPropertySourceResources bean and all of them will be used to build up the only PropertySourcesPlaceholderConfigurer.
But back to my question: Is there a good reason why the ConfigurationPropertiesBindingPostProcessor takes only one PropertySourcesPlaceholderConfigurer into account?

What's the best way to add a new property source in Spring?

I'd like to add a new property source that could be used to read property values in an application. I'd like to do this using Spring. I have a piece of code like this in a #Configuration class:
#Bean
public static PropertySourcesPlaceholderConfigurer placeHolderConfigurer() {
PropertySourcesPlaceholderConfigurer properties = new PropertySourcesPlaceholderConfigurer();
MutablePropertySources sources = new MutablePropertySources();
MyCustomPropertySource propertySource = new MyCustomPropertySource("my custom property source");
sources.addFirst(propertySource);
properties.setPropertySources(sources);
return properties;
}
This seems to work pretty well. However, what it is also doing is overriding other property values (e.g. server.port property in application.properties file used by spring boot) which I don't want to overwrite. So the basic question is what's the best way to add this propertysource but not have it override other properties. Any way to grab the existing propertysources and simply add on to it?
I got this working by adding a custom initiailizer to my spring boot app:
#SpringBootApplication
public class MyApp {
public static void main(String[] args) {
new SpringApplicationBuilder(MyApp.class)
.initializers(new MyContextInitializer()) // <---- here
.run(args);
}
}
Where MyContextInitializer contains: -
public class MyContextInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment();
// Create map for properites and add first (important)
Map<String, Object> myProperties = new HashMap<>();
myProperties.put("some-prop", "custom-value");
environment.getPropertySources().addFirst(
new MapPropertySource("my-props", myProperties));
}
}
Note, if your application.yaml contains: -
some-prop: some-value
another-prop: this is ${some-prop} property
Then the initialize method will update the some-prop to custom-value and when the app loads it will have the following values at run-time:
some-prop: custom-value
another-prop: this is custom-value property
Note, if the initialize method did a simple System.setProperty call i.e.
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment();
System.setProperty("some-prop", "custom-value");
}
... then the another-prop would be equal to this is some-value property which is not what we generally want (and we lose the power of Spring config property resolution).
Try setting IgnoreUnresolvablePlaceholders to TRUE. I had a similar problem which I was able to resolve in this way. In my case, I had another placeholderconfigurer, which was working - but properties in the second one were not being resolved unless I set this property to TRUE.
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
propertySourcesPlaceholderConfigurer.setIgnoreUnresolvablePlaceholders(Boolean.TRUE);
propertySourcesPlaceholderConfigurer.setIgnoreResourceNotFound(Boolean.TRUE);
return propertySourcesPlaceholderConfigurer;
}
Yet another possibility (after lots of experimentation, it's what worked for me) would be to declare your PropertySource inside a ApplicationContextInitializer and then inject that one in your SpringBootServletInitializer:
public class MyPropertyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final Logger logger = LoggerFactory.getLogger(ApplicationPropertyInitializer.class);
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
MyPropertySource ps = new MyPropertySource();
applicationContext.getEnvironment().getPropertySources().addFirst(ps);
}
}
public class MyInitializer extends SpringBootServletInitializer{
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return super.configure(builder.initializers(new MyPropertyInitializer()));
}
}
You can perhaps add your propertySource straight into environment once it is initialized.
EDIT: As this is done AFTER the class is processed, you cannot expect the #Value annotations to pick up anything from this particular PropertySource in the same #Configuration class - or any other that is loaded before.
#Configuration
public class YourPropertyConfigClass{
#Value("${fromCustomSource}")
String prop; // failing - property source not yet existing
#Autowired
ConfigurableEnvironment env;
#PostConstruct
public void init() throws Exception {
env.getPropertySources().addFirst(
new MyCustomPropertySource("my custom property source"));
}
}
#Configuration
#DependsOn("YourPropertyConfigClass")
public class PropertyUser {
#Value("${fromCustomSource}")
String prop; // not failing
}
You could move the #PostConstruct to a separate #Configuration class and mark other classes using those properties #DependOn("YourPropertyConfigClass") (this works - but perhaps there is a better way to force configuration order?).
Anyway - it is only worth it, if MyCustomPropertySource cannot be simply added using #PropertySource("file.properties") annotation - which would solve the problem for simple property files.
If you implement PropertySourceFactory as such:
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
public class CustomPropertySourceFactory implements PropertySourceFactory {
#Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) {
...
}
}
You can use the following property source:
#PropertySource(name="custom-prop-source", value="", factory=CustomPropertySourceFactory.class)
Kind of hack-ish, but it works.

How do I set #Qualifier without XML in Spring 3+

I'm using the below configuration setup. The #configuration class loads the property file, and then there is an arraylist that produced which extracts the relevant chunks of the property file in a way that the classes that depend on barUserList and fooUserList can consume easily. They don't even know that it came from a property file. Huzzah for DI!
My problem comes when I try to tell Spring which one of these I want. Class Foo wants fooUserList so I should be able to use the #Qualifier annotation, but I can't find a way to /set/ the qualifier outside of XML.
So my question is this, how do I set the Qualifier for these two Spring beans in Javaland? Zero XML config is a big goal for me. I know that you can set #name and the #qualifier mechanism for Spring will default to the #name, but I'd like to avoid using that. I don't like things that "default to" other things.
I'm using Spring 3.2.5.RELEASE
#Configuration
public class AppConfig {
#Bean
Properties loadProperties() throws IOException {
Properties properties = new Properties();
properties.load(new FileInputStream("checker.properties"));
return properties;
}
#Bean
#Autowired
ArrayList<String> barUserList(Properties properties) {
ArrayList<String> barUsernames = new ArrayList<String>();
Collections.addAll(barUsernames, properties.getProperty("site.bar.watchedUsernames", "").split(","));
return barUsernames;
}
#Bean
#Autowired
ArrayList<String> fooUserList(Properties properties) {
ArrayList<String> fooUsernames = new ArrayList<String>();
Collections.addAll(fooUsernames, properties.getProperty("site.foo.watchedUsernames", "").split(","));
return fooUsernames;
}
}
One way could be by defining a name for the #Bean and using it on #Qualifier as follows:
#Bean(name="barUserList")
#Autowired
ArrayList<String> barUserList(Properties properties) {
ArrayList<String> barUsernames = new ArrayList<String>();
Collections.addAll(barUsernames, properties.getProperty("site.bar.watchedUsernames", "").split(","));
return barUsernames;
}
and within the use you could have something like:
// ...
#Autowired
#Qualifier("barUserList")
private List<String> userList;
// ...

Custom spring property source does not resolve placeholders in #Value

I'm trying to build a Spring 3.1 PropertySource which reads its values from Zookeeper nodes. For connecting to Zookeeper I am using Curator from Netflix.
For that I've built a custom property source which reads the value of a property from Zookeeper and returns it. This works fine when I am resolving the property like this
ZookeeperPropertySource zkPropertySource = new ZookeeperPropertySource(zkClient);
ctx.getEnvironment().getPropertySources().addLast(zkPropertySource);
ctx.getEnvironment().getProperty("foo"); // returns 'from zookeeper'
However, when I try to instantiate a bean which has a field with an #Value annotation then this fails:
#Component
public class MyBean {
#Value("${foo}") public String foo;
}
MyBean b = ctx.getBean(MyBean.class); // fails with BeanCreationException
This problem has most likely nothing to do with Zookeeper but with the way I'm registering the property sources and creating the beans.
Any insight is highly appreciated.
Update 1:
I'm creating the app context from an XML file like this:
public class Main {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ctx.registerShutdownHook();
}
}
The class which connects to Zookeeper is a #Component.
#Component
public class Server {
CuratorFramework zkClient;
public void connectToZookeeper() {
zkClient = ... (curator magic) ...
}
public void registerPropertySource() {
ZookeeperPropertySource zkPropertySource = new ZookeeperPropertySource(zkClient);
ctx.getEnvironment().getPropertySources().addLast(zkPropertySource);
ctx.getEnvironment().getProperty("foo"); // returns 'from zookeeper'
}
#PostConstruct
public void start() {
connectToZookeeper();
registerPropertySource();
MyBean b = ctx.getBean(MyBean.class);
}
}
Update 2
This seems to work when I'm using XML-less configuration, i.e. #Configuration, #ComponentScan and #PropertySource in combination with an AnnotationConfigApplicationContext. Why doesn't it work with a ClassPathXmlApplicationContext?
#Configuration
#ComponentScan("com.goleft")
#PropertySource({"classpath:config.properties","classpath:version.properties"})
public class AppConfig {
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
Answering to your Update 2: This does not work with your original configuration(registering a PropertySource using #PostConstruct) because the PropertySource is being registered very late, by this time your target bean has already been constructed and initialized.
Typically the injection of the placeholders happens via a BeanFactoryPostProcessor which is very early in the Spring lifecycle(beans have not been created at this stage) and if a PropertySource is registered at that stage, then placeholders should be resolved.
The best approach though is to use a ApplicationContextInitializer, get a handle on the applicationContext and to register the propertySource there:
public class CustomInitializer implements ApplicationContextInitializer<ConfigurableWebApplicationContext> {
public void initialize(ConfigurableWebApplicationContext ctx) {
ZookeeperPropertySource zkPropertySource = new ZookeeperPropertySource(zkClient);
ctx.getEnvironment().getPropertySources().addFirst(zkPropertySource);
}
}

Resources