This is a question about customisation of Spring's placeholder resolution in #Value annotations.
We initialise all properties in our app using #Value, normally from servlet context init params, eg:
web.xml
<context-param>
<param-name>app.some.param</param-name>
<param-value>SOME_VALUE</param-value>
</context-param>
Class file
#Value("${app.some.param:DEFAULT_VALUE}")
private String myParameter;
We actually don't use web.xml, we use Tomcat context files or even specify using vmargs.
What we'd like to support is dynamic changes to these properties at runtime. I want to somehow collect a list of property keys that are used in #Value and which also have a new annotation like #Dynamic. For properties marked as #Dynamic the bean may provide a corresponding setter, to do any re-initialisation when the property is modified.
I would then like to create a service that supports updating the property by key, eg:
void setProperty(String key, String value) {
// find all beans that have #Value and #Dynamic and set field or call setter
// NB - should support Spring type coercion, eg. string --> integer, boolean, list, etc.
}
I've been looking at the source for PlaceholderConfigurerSupport and BeanDefinitionVisitor. It seems I might be able to override PlaceholderConfigurerSupport.doProcessProperties and create a custom BeanDefinitionVisitor, but there is quite a lot of code to wade through. I wondered if anyone had looked at this before and found a solution.
I should note that there's more we ultimately want to do. We want to persist changed properties in a backing store, and use these instead of the configuration on startup if they've been modified. In this way we'd have a hierarchy of property sources: default in code, context/property files, peristed config that's been modified. We also want to provide a UI showing a set of all dynamic properties. You get the idea.
Thanks
Related
I am using #value annotation in spring boot to read a property
#Value(value = "${propName:#{null}}")
private String prop;
And based on if it is null or driving some logic in my code. In my dev environment I want to keep it null so I don't add it in that property file (application-dev.properties). But instead of getting it as null, it is reading the value from default application.properties file.
Yes, this is the correct behavior based on Spring's property resolution. If you want to override it in your dev profile, simply add the property to your application-dev.properties, leaving the value empty. This will result in an empty String, not null, which you can convert to null with something like this:
#Value("${#{some_property == \"\"}:#{null}}")
private String prop;
It is probably a good idea to explicitly define these properties and give them values that make sense. Otherwise, you'll likely revisit this code in a few months and wonder what the heck is causing xyz.
You can read up on Spring and the order with which it resolves properties here but generally, you'd want to define variables that apply to all environments in your application.[properties][yaml], and then override them in the environment-specific properties files. In this case, it kind of sounds like this is a flag that's on or off depending on if the dev environment is set; you might consider something like this.
#Value("${property_to_drive_behavior:false}"
private Boolean prop;
This will default to false, so you don't need to add it to any properties file. If you want it set, override it in the environment-specific properties file. Booleans can be set in properties files using either the explicit true or false keywords or the values or 0 for false, 1 for true.
By default, property values of external configuration sources (such as application.properties files) are always injected directly into your beans by using the #Value annotation.
One solution to your problem is moving the propName property from application.properties to application-{profile}.properties.
I have multiple yml files in different folders. All the files in the folder share the same property structure which I mapped with a java bean.
At runtime, with a factory, I want to get the right bean populated with the values of the specific file chosen at runtime. How do I do that?
Thanks
The #ConfigurationProperties annotation or the mechanism behind it is built to be used for configuration of an application at startup, not loading data at runtime.
I'm sure you could somehow start mini spring environments at runtime just to read this data using different spring profiles (this is e.g. how spring-cloud-configserver loads properties) but this seems not right and there are better alternatives.
E.g., if you need that data to be loaded at runtime, you can use jackson's yamlfactory for that, with that you can read your data in 3-4 statements. A good example is here: https://www.baeldung.com/jackson-yaml.
Consider a Bean like this: (Pseudo code, just to explain)
class MyConfigBean {
private Properties currentProperties;
private Map<String, Properties> allPropertiesMap;
void loadAllProperties() { ... }
void switchProperties(String name) {
this.currentProperties = this.allPropertiesMap.get(name);
}
String getProperty(String key) {
return this.currentProperties.get(key);
}
}
You can load all of the Yaml files into a Map in your bean. The Map's key could be the "name" of the properties file and the value would be the Properties object.
A switchProperties(String name) method will "select" the properties file you wish to work with. Using the name, you will get the appropriate Properties object from the Map and assign it to the "currentProperties" object.
This way, each time you get a property by key, it will be fetched from the "currentProperties" according to what you "switched" for.
Important - You'll have to decide what is the default properties after you load all of them.
I find Spring Boot's (or spring in general) handling of yaml collections to be a bit peculiar. Collections according to yaml specs should be written in .yaml files as:
myCollection: ['foo', 'bar']
or
myCollection:
- foo
- bar
But neither #Value("${myCollection}") annotation or Environment.getProperty("myCollection", String[].class) (also tried List.class) can read collection properties (returns null). The only method I know of that works is to use #ConfigurationProperties annotation described in spring boot docs.
The problem with #ConfigurationProperties annotation is that (a) it is too verbose if all I want is a single property and (b) it rely on bean injection to get an instance of the #ConfigurationProperties class. Under some circumstances, bean injection is not available and all we have is a reference to Environment (e.g: thru ApplicationContext).
In my particular case, I want to read some properties during ApplicationEnvironmentPreparedEvent event, since it happens before context is built, the listener has to be manually registered and therefore, no bean injection. Via the event argument, I can get a reference to Environment. So, I can read other properties but cannot read collections.
A couple of "solutions" I noted (quoted because I don't find them very satisfactory):
Specify collections in .yaml file as myCollection: foo, bar. But this is not ideal because, the format isn't really yaml anymore.
Read individual elements using an index, for example Environment.getProperty("myCollection[0]", String.class). Will require some not-so-elegant utility methods to read and put all elements into a List.
So, my questions is - What is a good way to read collection-type properties if I cannot use #ConfigurationProperties? Also curious why comma-separated format works but not yaml-style collections.
EDIT: corrected some typos
Quite Frankly Spring boot application.properties and application.yaml or application.yml is meant to load configuration properties.
The #ConfigurationProperties annotation is designed as an abstraction to hide the implementations of configuration properties and support both .properties and .yaml/.yml.
For yaml/yml however Spring uses org.yaml.snakeyaml.Yaml library underneath to parse the file and load it to a Properties object inside org.springframework.boot.env.YamlPropertySourceLoader and a Collection is mapped as a Set not an array or List. So you try doing the following;
Environment.getProperty("myCollection", Set.class)
Let's imagine we have such a component in Spring:
#Component
public class MyComponent {
#Value("${someProperty}")
private String text;
}
If we define the property placeholder:
<context:property-placeholder location="classpath:myProps.properties"/>
And myPropos.properties contains the value for someProperty the value will be injected to the text field when the context is initialized. That's quite simple and easy.
But let's say that I have a service that enables user to change the value of the someProperty:
public void changeProp(String name, String newValue);
Is there a chance I can re-inject the newValue to text field. I mean it should be quite straight forward.. Basically it's nothing different than the after-initialization injection. I can not imagine that Spring does not have support for this? Can I fire some event or something?
I could do this on my own basically, but I wander is it maybe something there already? If not does anyone know what Spring class is in fact handling the injections at the first place? I could probably reuse the code there do perform this on my own if a solution does not exists.
I expect spring does not have a support for this, because the normal injection is done while creating the bean, but not will it is put in service.
Anyway: in this blog entry "Reloadable Application Properties with Spring 3.1, Java 7 and Google Guava", you can find the idea for an solution.
The key idea is to use a post processor to build a list of all fields with property fields. And if the properties are changed on can use this list to update the fields.
We have a specific format for our config files, where instead of having multiple files - ie, dev.properties, uat.properties, prod.properties - we instead have all the values in one file, but separated by prefixes for each environment. For example:
SERVICE_PORT=9800
DEV_SERVICE_PORT=7800
UAT_SERVICE_PORT=6600
We have an existing class (subclass of PropertyPlaceholderConfigurer) that looks up these values, and decides what prefix to add inside resolvePlaceHolder() based on the IP address where it is executing, ie for a certain IP range, use DEV_ prefix, for another, use UAT_ prefix. These values then get passed on to other beans, in some cases using the context xml, and in some using the #Value${} annotation on some bean constructors. The use of the prefixes is transparent, so all other configs will use SERVICE_PORT (in the example)
We want to change this so that instead of using IPs, we will just detect the active Spring Profile. We have a custom ApplicationContextIniitalizer in our web.xml that detects a java System property that indicates our type of environment.
The problem I have is that at the time resolvePlaceHolder() gets called, there doesn't seem to be any Active Profiles yet! What I"m doing to detect the active profile is:
create an instance of StandardEnvironment
call getActiveProfiles()
(2) always seems to return an empty array. This implies that propertyplaceholder resolution occurs before any Spring profiles are activated. Is this correct??
When does the active profile get set, in relation to other events during Spring context loading, like creating the beans, loading property files, etc?
Is it possible to detect the active profile at the time that resolvePlaceHolder() is called? Should I be extending another class instead?
Beans managed within an ApplicationContext may register to be
EnvironmentAware or #Inject the Environment in order to query profile
state or resolve properties directly.
[Source: Environment javadocs ]
Don't create an instance of StandardEnvironment, inject it into your bean!