I'm currently trying to move an existing spring-application to spring-boot and therefore recreate things that worked without boot.
I want to configure some properties (like spring.datasource.*) from an external source. specificly a folder with several properties files.
I set up a configuration class that creates propertyPlaceholder configurers like this:
#Configuration
public class PropertySourceConfiguration {
#Bean
public static PropertySourcesPlaceholderConfigurer defaultsPlaceHolderConfigurer() throws IOException {
PropertySourcesPlaceholderConfigurer propertyConfigurer = new PropertySourcesPlaceholderConfigurer();
propertyConfigurer.setLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/*-defaults.properties"));
propertyConfigurer.setIgnoreUnresolvablePlaceholders(true);
return propertyConfigurer;
}
#Bean
public static PropertySourcesPlaceholderConfigurer externalPlaceHolderConfigurer() throws IOException {
PropertySourcesPlaceholderConfigurer propertyConfigurer = new PropertySourcesPlaceholderConfigurer();
propertyConfigurer.setLocations(new
PathMatchingResourcePatternResolver().getResources("file:/my-config-path/*.properties"));
propertyConfigurer.setOrder(1);
propertyConfigurer.setIgnoreUnresolvablePlaceholders(true);
return propertyConfigurer;
}
This seems to work for most things (like amqp or my own config properties) but when i try to use spring-data-jpa they are ignored. basicly setting spring.datasource.url (and other things used for auto-config) in those files has no effect.
looking through the logs of the PropertySourcesPropertyResolver i figured out that these configurer fall under the localProperties group which is not used when looking for spring.datasource.*.
is there a way to fix this or a better way to add external properties files to my context?
I know i could set spring.config.location to do something similar but i can not pass command-line properties to my application and need to do this config from within my application. afaik this is not possible with this property.
EDIT: setting spring.config.location:
Attempt 1:
public class ServletInitializer extends SpringBootServletInitializer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(CampaignServiceStarter.class);
}
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
servletContext.setInitParameter("spring.config.location", "file:/my-config-path/*.properties");
}
}
Attempt 2:
public class ServletInitializer extends SpringBootServletInitializer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(CampaignServiceStarter.class).properties("spring.config.location=file:/my-config-path/*.properties");
}
}
in both cases the external properties were not picked up at all (even in places where it worked before, like the amqp config)
How to use external configuration is explained in this section of the Spring Boot Reference Guide.
The spring.config.location is a path to the directory which contains your application.properties file. It takes a comma separated list of values so you could specify multiple paths. It doesn't take the wildcard. It is a path so not an expression to match multiple property files. If you want to change the default application then use the spring.config.name to make it something else.
The defaults of Spring Boot are opinionated as the rest of Spring Boot (with the default configuration etc.).
If you want to do more extensive (pre) configuration you should use an ApplicationContextInitializer to manually add the PropertySources to the Environment. This is mentioned here in the Spring Boot Reference Guide.
An example of how the initializer might look.
public class ConfigurationInitializer implements ApplicationContextInitializer {
private static final String DEFAULT_PROPS = "classpath*:/*-defaults.properties";
private static final String EXTERNAL_PROPS = "file:/my-config-path/*.properties";
public void initialize(ConfigurableApplicationContext applicationContext) {
final Resource[] defaultConfigs = applicationContext.getResources(DEFAULT_PROPS);
final Resource[] externalConfigs = applicationContext.getResources(EXTERNAL_PROPS);
final ConfigurableEnvironment env = applicationContext.getEnvironment();
final MutablePropertySources mps = env.getPropertySources();
for (Resource r : externalConfigs) {
mps.addLast(new ResourcePropertySource(r.getFilename(), r);
}
for (Resource r : defaultConfigs) {
mps.addLast(new ResourcePropertySource(r.getFilename(), r);
}
}
}
Then when building your application object add it as follows.
public class ServletInitializer extends SpringBootServletInitializer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(CampaignServiceStarter.class)
.initializers(new ConfigurationInitializer());
}
}
Now the configs should be added to the list of property sources.
Related
tldr
How to best "put" runtime application.properties values from a spring boot app in a javascript file
[Admittedly this is probably a stupid question for the experienced spring dev...so please share how to solve this problem "the spring way"]
Context
We have tiny app which a front-end developer put together and "threw over the wall"..It defines a properties file settings.js:
var SERVERROOT = 'http://solr:8983/solr/operations/select/';
referenced by html :
<script type='text/javascript' src="js/settings.js"></script>
I would like to define the solr path in application.yml at runtime as follows:
app:
solr:
path: 'http://solr:8983/solr/operations/select/'
Question
What is the best way to "populate" this settings.js value from application.properties (or equivalent, i.e. command line arguments, e.g.:
-Dapp.solr.path=...
)
Possible
I thought about putting 'settings.js' as a thymeleaf template (settings.js.html) and having a spring controller populate the model from application.properties.
I didn't know if a more "native spring" method existed.
Thanks!
You could use a ResourceTransformer:
#Component
public class InjectSolrPathResourceTransformer implements ResourceTransformer {
private final MySetting settings; // inject via constructor
#Override
public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain) throws IOException {
if (request.getServletPath().equals("/js/settings.js")) {
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
String content = new String(bytes, StandardCharsets.UTF_8);
content = content.replace("var SERVERROOT = SERVERROOT_VALUE",
"var SERVERROOT = '" + settings.getSolrPath() + "'");
return new TransformedResource(resource, content.getBytes(StandardCharsets.UTF_8));
} else {
return resource;
}
}
}
This assumes that you change settings.js to be:
var SERVERROOT = SERVERROOT_VALUE
To use the ResourceTransformer, register it in your WebMvcConfigurer:
#Configuration
public class WebConfig implements WebMvcConfigurer {
// inject this via constructor
private final InjectSolrPathResourceTransformer resourceTransformer;
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**")
.addResourceLocations("classpath:/public/js/")
.addTransformer(resourceTransformer);
}
}
The answer of #Wim above is correct but with new spring boot edition use the below code to register your transformer;
#Configuration
public class WebConfig implements WebMvcConfigurer {
// inject this via constructor
private final InjectSolrPathResourceTransformer resourceTransformer;
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**")
.addResourceLocations("classpath:/public/js/")
.resourceChain(false)
.addTransformer(resourceTransformer);
}
}
Can something like this be accomplished using Spring Boot?
The idea is to group properties and assign the same value to all of them, so instead of all of the properties ending with 'test*' i would like to change just one property 'my.flag'. I know that such functionality works in case of loggers, but can I define my own group?
I am not sure whether your problem has been solved or not, but I want to provide a solution to achieve what you want by using spring.factories and implementing ApplicationListener as following steps.
STEP 1
Create a class MyPropertiesListener which implements ApplicationListener and read the value of my.flag in application.properties first, then set it to all the properties whose key starts with my.flag..
public class MyPropertiesListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
#Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment env = event.getEnvironment();
String myFlag = env.getProperty("my.flag");
Properties props = new Properties();
MutablePropertySources propSrcs = env.getPropertySources();
StreamSupport.stream(propSrcs.spliterator(), false)
.filter(ps -> ps instanceof EnumerablePropertySource)
.map(ps -> ((MapPropertySource) ps).getPropertyNames())
.flatMap(Arrays::<String>stream)
.forEach(propName -> {
if (propName.toString().startsWith("my.flag.")) {
props.put(propName, myFlag);
}
});
env.getPropertySources().addFirst(new PropertiesPropertySource("myProps", props));
}
}
STEP 2
Create a file named spring.factories under src/main/resource/META-INF and configure MyPropertiesListener into it.
org.springframework.context.ApplicationListener=xxx.xxx.xxx.MyPropertiesListener
TEST
The value of my.flag.test3 is false in application.properties originally, but it is going to be overwritten as true while application starts.
#SpringBootApplication
public class Application implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Value("${my.flag.test3}")
private String test3;
#Override
public void run(String... args) throws Exception {
System.out.println(test3); //true
}
}
See also
Creating a Custom Starter with Spring Boot
I have 1 spring boot application. In this application i have configured 1 eCommerce system as elastic path(Configure the end point url of elastic path in application.properties file). Now i have to give my spring boot application to some other guys. which will be deploy on tomcat server. I don't want to give the source code. So i can make the war file but now problem is that they have their own elastic path eCommerce and they wants to configure their own eCommerce.
I want to externalize some properties which will override the existing property.
My springboot application have 2 modules :
1) elasticpath module which is having elasticpath-application.properties
2) salesforce - salesforce-application.properties
Now i have to externalize "C:\apache-tomcat-8.5.29\conf\ep-external.properties" file which will override the existing property. Now problem is that #PropertySource is loading in last position. So my external file is not able to override the property.
#SpringBootApplication
#PropertySource(value = {"classpath:application.properties", "classpath:elasticpath-application.properties", "classpath:salesforce-application.properties")
public class SpringBootDemo extends SpringBootServletInitializer implements CommandLineRunner {
private static final Logger LOG = LoggerFactory.getLogger(SpringBootDemo.class);
private ServletContext servletContext;
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//application = application.properties("file:C:\\apache-tomcat-8.5.29\\conf\\ep-external.properties");
return application.sources(SpringBootDemo.class);
}
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
this.servletContext = servletContext;
super.onStartup(servletContext);
}
public static void main(String[] args) {
SpringApplication.run(SpringBootDemo.class, args);
}
#Override
public void run(String... args) throws Exception {
}
}
Yes absolutely possible. Basically what you need is changing the property value as needed without changing jar/war
Passing command line args for jar
Package your spring boot application as jar and put the external application.properties file at any location and pass on the same location as command line argument as below :
java -jar app.jar --spring.config.location=file:<property_file_location>
This will pick up the external properties.
Passing command-line/dynamic args for war
1. Extend the SpringBootServletInitializer as below
#SpringBootApplication
class DemoApp extends SpringBootServletInitializer {
private ServletContext servletContext;
public static void main(String[] args){SpringApplication.run(DemoApp.class,args);}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
builder = builder.properties("test.property:${test_property:defaultValue}");
return builder.sources(DemoApp.class)
}
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
this.servletContext = servletContext;
super.onStartup(servletContext);
}
}
Access the property as usual like below anywhere you want :
#Value("${test.property}")
Before starting tomcat set the env variable named test_property. that's it
Additionally:
You can pass on the property like below as well if you want to supply complete file as external file.
.properties("spring.config.location:${config:null}")
Further reading about externalized configuration : https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html
I followed the exact steps in this documentation.
I have the following entries in my META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor=com.mygroup.myapp.CustomEnvironmentPostProcessor
My post processor:
public class CustomEnvironmentPostProcessor
implements EnvironmentPostProcessor, Ordered {
..
}
I don't see anything in the logs as if it didn't get registered or not existing.
I unzipped the JAR and I can see META-INF/spring.factories. I can also see BOOT-INF/classes directly from the root.
What am I missing here?
There is no elegant way to solve this. You can make something like this :
#Component
public class CustomEnvironmentPostProcessor implements
EnvironmentPostProcessor, ApplicationListener<ApplicationEvent> {
private static final DeferredLog log = new DeferredLog();
#Override
public void postProcessEnvironment(
ConfigurableEnvironment env, SpringApplication app) {
log.error("This should be printed");
}
#Override
public void onApplicationEvent(ApplicationEvent event) {
log.replayTo(CustomEnvironmentPostProcessor.class);
}
}
define spring.factories file
Environment Post Processor
org.springframework.boot.env.EnvironmentPostProcessor=\
class name with package
As of Spring Boot 2.4, there are now optional constructor parameters that provide access to deferred logs:
public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor {
private final Log log;
public CustomEnvironmentPostProcessor(Log log) {
this.log = log;
}
#Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
log.info("CustomEnvironmentPostProcessor!!");
}
}
NOTE: The log messages are deferred so they will appear in the log output only after the logging system has been initialized.
See https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/env/EnvironmentPostProcessor.html
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.