Spring Boot: Multiple WAR deployment in same tomcat different properties - spring

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

Related

How to add a Property Source to a Spring Cloud Stream Binder's Environment

I have written a PropertySource that enables classpath: prefix for spring.kafka.properties.ssl.truststore.location (which out-of-the-box is not supported).
Essentially, this lets me place a truststore.jks inside my Spring Boot application's src/main/resources folder and reference it from inside the .jar file.
This works nicely for plain Spring Kafka configurations, like these:
spring:
kafka:
properties:
ssl.truststore.location: classpath:myTruststore.jks
It currently fails when the same configurations are given in the context of a Spring Cloud Stream Binder:
spring:
cloud:
stream:
binders:
my-binder:
type: kafka
environment:
spring:
kafka:
properties:
ssl.truststore.location: classpath:myTruststore.jks
My PropertySource is not even called back, when I would have expected it to be called with a poperty name of spring.cloud.stream.binders.my-binder.environment.spring.kafka.properties.ssl.truststore.location.
I think my PropertySource that would do the classpath: resolution is not part of the Environment of the given Spring Cloud Stream binder.
Question: how can one add PropertySources to a specific Binder's environment (or to all of them)?
Thanks!
EDIT
I add my PropertySource in a Spring Boot auto-configuration like this:
#Configuration
#AutoConfigureBefore(KafkaAutoConfiguration.class)
#ConditionalOnProperty(name = "com.acme.kafka.enabled", matchIfMissing = true)
#EnableConfigurationPropertiesAcmeKafkaConfigurations.class)
public class AcmeKafkaAutoConfiguration {
#Bean
ClasspathResourceSupportEnablingPropertySource acmeKafkaClasspathResourceEnablingPropertySource(ConfigurableEnvironment environment) throws IOException {
ClasspathResourcesSupport classpathResourcesSupport = new ClasspathResourcesSupport(Files.createTempDirectory(ACME_KAFKA_PREFIX));
ClasspathResourceSupportEnablingPropertySource propertySource
= new ClasspathResourceSupportEnablingPropertySource(ClasspathResourceSupportEnablingPropertySource.NAME, environment, classpathResourcesSupport);
environment.getPropertySources().addFirst(propertySource);
return propertySource;
}
}
EDIT NO.2: I tried out what Gary Russel suggested below (using a Bean Post Processor declared as a static bean method).
It works but in my case I get a lot of additional warning logs at startup of the form:
Bean '...' of type [...] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
According to this post this can cause some really nasty side effects.
Here is the code I was using (which caused the warnings above):
#Configuration
#AutoConfigureBefore(KafkaAutoConfiguration.class)
#ConditionalOnProperty(name = "com.acme.kafka.enabled", matchIfMissing = true)
#EnableConfigurationProperties(AcmeKafkaConfigurations.class)
public class AcmeKafkaAutoConfiguration {
private static final String ACME_KAFKA_PREFIX = "acme.kafka.";
#Bean
#ConditionalOnMissingBean
public static List<ConnectivityConfigurationsProvider> acmeKafkaTokenProviders(OAuth2TokenClient oAuthClient, AcmeKafkaConfigurations configuration) {
List<ConnectivityConfigurationsProvider> connectivityConfigurationsProviders = new ArrayList<>();
configuration.getInstances().forEach(serviceInstanceConfiguration -> {
TokenProvider tokenProvider = new DefaultOAuth2TokenProvider(oAuthClient, serviceInstanceConfiguration);
ConnectivityConfigurationsProvider connectivityConfigurationsProvider = new ConnectivityConfigurationsProvider(serviceInstanceConfiguration, tokenProvider);
connectivityConfigurationsProviders.add(connectivityConfigurationsProvider);
});
return connectivityConfigurationsProviders;
}
#Bean
#ConditionalOnMissingBean
#Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public static OAuth2TokenClient acmeKafkaOAuth2TokenClient() {
return new DefaultOAuth2TokenClient(new DefaultClientCredentialsTokenResponseClient());
}
#Bean
public static ConnectivityConfigurationsProviders acmeKafkaConnectivityConfigurationsProviders(AcmeKafkaConfigurations configuration, List<ConnectivityConfigurationsProvider> connectivityConfigurationsProviders) {
return new ConnectivityConfigurationsProviders(connectivityConfigurationsProviders);
}
#Bean
static NoOpBeanPostProcessor springKafkaConfigurationsPropertySource(ConfigurableEnvironment environment, ConnectivityConfigurationsProviders connectivityConfigurationsProviders) {
SpringKafkaConfigurationsPropertySource propertySource = new SpringKafkaConfigurationsPropertySource(SpringKafkaConfigurationsPropertySource.NAME, connectivityConfigurationsProviders);
environment.getPropertySources().addLast(propertySource);
return new NoOpBeanPostProcessor();
}
#Bean
#ConditionalOnClass(name = "org.springframework.cloud.stream.binder.BinderConfiguration")
static NoOpBeanPostProcessor springCloudStreamKafkaConfigurationsPropertySource(ConfigurableEnvironment environment, ConnectivityConfigurationsProviders connectivityConfigurationsProviders) {
SpringCloudStreamKafkaConfigurationsPropertySource propertySource = new SpringCloudStreamKafkaConfigurationsPropertySource(SpringCloudStreamKafkaConfigurationsPropertySource.NAME, connectivityConfigurationsProviders);
environment.getPropertySources().addLast(propertySource);
return new NoOpBeanPostProcessor();
}
#Bean
static NoOpBeanPostProcessor acmeKafkaConnectivityConfigurationsPropertySource(ConfigurableEnvironment environment, ConnectivityConfigurationsProviders connectivityConfigurationsProviders) {
AcmeKafkaConnectivityConfigurationsPropertySource propertySource = new AcmeKafkaConnectivityConfigurationsPropertySource(AcmeKafkaConnectivityConfigurationsPropertySource.NAME, connectivityConfigurationsProviders);
environment.getPropertySources().addLast(propertySource);
return new NoOpBeanPostProcessor();
}
#Bean
static NoOpBeanPostProcessor acmeKafkaClasspathResourceEnablingPropertySource(ConfigurableEnvironment environment) throws IOException {
ClasspathResourcesSupport classpathResourcesSupport = new ClasspathResourcesSupport(Files.createTempDirectory(ACME_KAFKA_PREFIX));
ClasspathResourceSupportEnablingPropertySource propertySource
= new ClasspathResourceSupportEnablingPropertySource(ClasspathResourceSupportEnablingPropertySource.NAME, environment, classpathResourcesSupport);
environment.getPropertySources().addFirst(propertySource);
return new NoOpBeanPostProcessor();
}
/**
* This BeanPostProcessor does not really post-process any beans.
* It is a way of getting the bean methods that add the property sources
* above to be called early enough in the lifecycle of Spring ApplicationContext
* creation.
*
* BeanPostProcessors are instantiated by Spring extremely early.
* #Bean methods providing them should be declared as static methods.
* See: https://stackoverflow.com/questions/30874244/bean-annotation-on-a-static-method
*/
static class NoOpBeanPostProcessor implements BeanPostProcessor, Ordered {
#Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
}
EDIT
This seems to work:
#SpringBootApplication
public class So61826877Application {
private static final String KEY = "spring.cloud.stream.binders.my-binder.environment."
+ "spring.kafka.properties.ssl.truststore.location";
public static void main(String[] args) {
SpringApplication.run(So61826877Application.class, args);
}
#Bean
public static BeanPostProcessor configureSource(ConfigurableEnvironment env) {
Properties source = new Properties();
System.out.println(env.getProperty(KEY));
source.setProperty(KEY, "/path/to/myTruststore.jks");
env.getPropertySources().addFirst(new PropertiesPropertySource("cp", source));
return new MyBpp();
}
#Bean
Consumer<String> input() {
return System.out::println;
}
}
class MyBpp implements BeanPostProcessor, Ordered {
#Override
public int getOrder() {
return Integer.MAX_VALUE;
}
}
classpath:myTruststore.jks
...
ConsumerConfig values:
allow.auto.create.topics = true
auto.commit.interval.ms = 100
...
ssl.truststore.location = /path/to/myTruststore.jks
...

Spring Boot externalised configuration for DataSource not working

I have an application - using Spring 4.3.6 and Spring Boot 1.4.4 - which will be exported as a JAR. I want to connect to a remote Oracle database but I am having trouble externalising the configuration without breaking the application.
This is my current workaround:
import org.apache.tomcat.jdbc.pool.DataSource;
#Bean
public DataSource dataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("jdbc:oracle:thin:#ip-address:port:orcl");
dataSource.setUsername("user");
dataSource.setPassword("password");
dataSource.setDriverClassName("oracle.jdbc.OracleDriver");
return dataSource;
}
With the above, my application is able to connect to the database and execute queries successfully. However, when I try to externalise the configuration as follows:
#Bean
#ConfigurationProperties(prefix="app.datasource")
public DataSource dataSource() {
return new DataSource();
}
// application.properties
app.datasource.url=jdbc:oracle:thin:#ip-address:port:orcl
app.datasource.username=user
app.datasource.password=password
app.datasource.driver-class-name=oracle.jdbc.OracleDriver
I will get the following error when trying to execute jdbcTemplate.update(query) in my Spring Boot Controller (note that without externalising the above works):
org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is java.sql.SQLException: The url cannot be null
I have tried to remove #ConfigurationProperties and change app.datasource to spring.datasource. I have also tried to use DataSourceBuilder.create().build() which returns javax.sql.DataSource but the same error is thrown in both cases.
I'm doing something wrong. What's the correct way to successfully externalise the configuration?
Suppose you have two datasources for two different Oracle databases. Then you have the following properties file:
/path/to/config/application.properties
oracle1.username=YourUser1
oracle1.password=YourPassword1
oracle1.url=jdbc:oracle:thin:#localhost:1521:XE
oracle2.username=YourUser2
oracle2.password=YourPassword2
oracle2.url=jdbc:oracle:thin:#192.168.0.3:1521:XE
Then in a configuration file:
import oracle.jdbc.pool.OracleDataSource;
#Configuration
public class DatasourcesConfig {
#Autowired
private Environment env;
#Primary
#Bean(name = "dataSource1")
DataSource oracle1DataSource() throws SQLException {
OracleDataSource dataSource = new OracleDataSource();
dataSource.setUser(env.getProperty("oracle1.username"));
dataSource.setPassword(env.getProperty("oracle1.password"));
dataSource.setURL(env.getProperty("oracle1.url"));
return dataSource;
}
#Bean(name = "dataSource2")
DataSource oracle2DataSource() throws SQLException {
OracleDataSource dataSource = new OracleDataSource();
dataSource.setUser(env.getProperty("oracle2.username"));
dataSource.setPassword(env.getProperty("oracle2.password"));
dataSource.setURL(env.getProperty("oracle2.url"));
return dataSource;
}
}
If you want to specify the external location of your application.properties file when running the jar, then set the spring.config.location as a system property, you can try:
java -jar target/your-application-0.0.1.jar -Dspring.config.location=/path/to/config/
Make sure the application.properties file is excluded when building the jar
There should be no need to create the DataSourceyourself.
Make sure you have the
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
Dependecy in your classpath and the oracle driver and put following properties in your application.properties file:
spring.datasource.url=jdbc:oracle:thin:#ip-address:port:orcl
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
After that you should be able to #Autowired your DataSource
For more information have a look at:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html#boot-features-connect-to-production-database
You cannot override the predefined properties provided by spring boot.
Just use the following properties in application.properties file.
spring.datasource.url=jdbc:oracle:thin:#ip-address:port:orcl
spring.datasource.data-username=user
spring.datasource.data-password=password
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
See Also : https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
Apart from above, to clarify #ConfigurationProperties is used at class level and the prefix "app" not "app.datasource"
#ConfigurationProperties(prefix = "app")
Now you have a class named DbProperties and the properties of the class is same as the last part of the key in application.properties
public class DBProperties {
private String url;
private String username;
private String password;
// setters and getters
}
Now the actual config/component class should look like following.
#Component
#ConfigurationProperties(prefix = "app")
public class MyComponent {
DBProperties datasource = new DBProperties();
public DBProperties getDatasource() {
return datasource;
}
public void setDatasource(DBProperties datasource) {
this.datasource = datasource;
}
}
Please note
The instance variable name is datasource which is same as the second part of the key
datasource is a class level instance

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

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

Can a PropertiesFactoryBean read a value from application.yml?

My project has a dependency that requires a set a properties object that can be read by #Value annotations:
#Value("#{myProps['property.1']}")
To do this in JavaConfig, I'm using the following:
#Bean(name="myProps")
public static PropertiesFactoryBean mapper() {
PropertiesFactoryBean bean = new PropertiesFactoryBean();
bean.setLocation(new ClassPathResource("myprops.properties"));
return bean;
}
This works as expected. My properties look as follows:
property.1=http://localhost/foo/bar
property.2=http://localhost/bar/baz
property.3=http://localhost/foo/baz
I'm using Spring Boot for this project, so I'd love to be able to do something like the following:
myprops.properties:
property.1=${base.url}/foo/bar
property.2=${base.url}/bar/baz
property.3=${base.url}/foo/baz
Then I could configure the base.url based on different profiles.
application.yml:
base:
url: http://localhost
---
spring:
profiles: staging
base:
url: http://staging
---
spring:
profiles: production
base:
url: http://production
I've tried to do this and it doesn't work. As a workaround, I've created three different .properties files (myprops.properties, myprops-staging.properties, etc.) and loaded them with three different #Configuration classes. This works, but seems cumbersome.
#Configuration
public class DefaultConfiguration {
#Bean(name="myProps")
public static PropertiesFactoryBean mapper() {
PropertiesFactoryBean bean = new PropertiesFactoryBean();
bean.setLocation(new ClassPathResource("myprops.properties"));
return bean;
}
}
#Configuration
#Profile("staging")
public class StagingConfiguration {
#Bean(name="myProps")
public static PropertiesFactoryBean mapper() {
PropertiesFactoryBean bean = new PropertiesFactoryBean();
bean.setLocation(new ClassPathResource("myprops-staging.properties"));
return bean;
}
}
#Configuration
#Profile("production")
public class ProductionConfiguration {
#Bean(name="myProps")
public static PropertiesFactoryBean mapper() {
PropertiesFactoryBean bean = new PropertiesFactoryBean();
bean.setLocation(new ClassPathResource("myprops-production.properties"));
return bean;
}
}
Is it possible to configure my PropertiesFactoryBean to read values from application.yml? If not, is there an easier way to configure properties with JavaConfig?
I ended up doing this programmatically and it gives me the behavior I was looking for:
#Value("${base.url}")
private String baseUrl;
#Bean(name = "myProps")
public PropertiesFactoryBean mapper() throws IOException {
PropertiesFactoryBean bean = new PropertiesFactoryBean();
bean.setLocation(new ClassPathResource("myprops.properties"));
bean.afterPropertiesSet();
// replace ${base.url} in values
Properties props = bean.getObject();
Enumeration names = props.propertyNames();
while (names.hasMoreElements()) {
String name = names.nextElement().toString();
String value = props.getProperty(name);
if (value.contains("${base.url}")) {
props.setProperty(name, value.replace("${base.url}", baseUrl));
}
}
bean.setLocalOverride(true);
bean.setProperties(props);
bean.afterPropertiesSet();
if (log.isDebugEnabled()) {
log.debug("Base URL: " + baseUrl);
}
return bean;
}
I'm not 100% sure what you are really looking do there, and you don't really say what doesn't work, but it looks like you want to mix YAML and properties formats for your external config? Why not just use "application.yml"? If I were doing something like that, and I needed to use a properties file for some reason as well, then I would use #PropertySource on one of my SpringApplication source files (that way the placeholders should be replaced when the values are resolved I think).

Spring profile, code based config and JNDI

I am moving to new Spring 3.x code based #Configuration style. I have done good, so far, but when I tried to add a #Profile("production") to select a JNDI data source instead of C3p0 connection pool #Profile("standalone"), I've got:
Error creating bean with name 'dataSource': Requested bean is currently in creation: Is there an unresolvable circular reference?
...
Of course, there is a lot of details (I included some below for reference), but my question is: would someone kindly post/point to a working example?
Thanks,
Details:
Same configuration, as XML, is working fine,
web.xml points to a root context configured in AppConfig.class and to a servlet context configured in WebConfig.class,
AppConfig has an #Import(SomeConfig.class) pulling a config from a service layer project - dependencies are wired by maven,
The SomeConfig class has the #Profile("standalone") on the C3p0 datasource,
The AppConfig class has the #Profile("production") on the JNDI datasource,
web.xml defines spring.profiles.default=production.
Edit: Solved
I moved the profile to the right place. Now they are in the same file and in the same project:
...
other bean definitions
/**
* Stand-alone mode of operation.
*/
#Configuration
#Profile("standalone")
static class StandaloneProfile {
#Autowired
private Environment env;
/**
* Data source.
*/
#Bean
public DataSource dataSource() {
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(env.getRequiredProperty("helianto.jdbc.driverClassName"));
ds.setJdbcUrl(env.getRequiredProperty("helianto.jdbc.url"));
ds.setUser(env.getRequiredProperty("helianto.jdbc.username"));
ds.setPassword(env.getRequiredProperty("helianto.jdbc.password"));
ds.setAcquireIncrement(5);
ds.setIdleConnectionTestPeriod(60);
ds.setMaxPoolSize(100);
ds.setMaxStatements(50);
ds.setMinPoolSize(10);
return ds;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* Production mode of operation.
*/
#Configuration
#Profile("production")
static class ProductionProfile {
#Autowired
private Environment env;
/**
* JNDI data source.
*/
#Bean
public DataSource dataSource() {
try {
JndiObjectFactoryBean jndiFactory = new JndiObjectFactoryBean();
jndiFactory.setJndiName("java:comp/env/jdbc/heliantoDB");
jndiFactory.afterPropertiesSet(); //edited: do not forget this!
return (DataSource) jndiFactory.getObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

Resources