Customize #PropertyResource handling behavior for java annotation based configuration - spring

I wish to customize the handling of the property source, while using java annotation based intializing a spring web application.
#Configuration
#PropertySource("ldap.properties")
#Repository
public class LdapDao {
...
#Autowired
public void setEnv(Environment env) throws NamingException {
this.url = env.getProperty("url").trim();
this.user = env.getProperty("user").trim();
this.password = env.getProperty("password).trim();
this.initializeLdapContext();
}
...
}
In this case, spring will look for the property source on classpath. If the property source is declared as:
#PropertySource("file:/${conf.dir}/ldap.properties")
ldap.properties is searched under the directory specified by the system property "conf.dir".
I need the behavior where the property resource is first searched under the directory specified by the system property "conf.dir". If it is not found there, its location defaults to classpath.
Any suggestion on how to achieve this behavior?

Use this
#PropertySource({"ldap.properties", "file:/${conf.dir}/ldap.properties"})
and you will get last one, just add in yours propConfig code
PropertySourcesPlaceholderConfigurer propConfig = new PropertySourcesPlaceholderConfigurer();
//.....
propConfig.setIgnoreResourceNotFound(true);
propConfig.setIgnoreUnresolvablePlaceholders(false);
propConfig.setLocalOverride(true);
//....

Related

How to test #ConfigurationProperties with ApplicationContextRunner from spring-boot-test?

I need to test my autoconfiguration classes that make use of #ConfigurationProperties beans. I'm making use of ApplicationContextRunner as documented in https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-test-autoconfig to make tests faster and avoid starting the servlet container between each variations. However, beans annotated with #AutoconfigurationProperties are not populated with values injected into ApplicationContextRunner.
I suspect that I'm hitting problem similar to https://stackoverflow.com/a/56023100/1484823
#ConfigurationProperties are not managed by the application context you build in tests, although they will be load when the application launches, because you have #EnableConfigurationProperties on your app main class.
How can I enable support for #ConfigurationProperties with ApplicationContextRunner ?
Here is the corresponding code
#Test
void ServiceDefinitionMapperPropertiesAreProperlyLoaded() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
SingleServiceDefinitionAnswerAutoConfig.class,
DynamicCatalogServiceAutoConfiguration.class
))
// .withPropertyValues(DynamicCatalogProperties.OPT_IN_PROPERTY + "=true") //Not sure why this seems ignored
.withSystemProperties(DynamicCatalogConstants.OPT_IN_PROPERTY + "=true",
ServiceDefinitionMapperProperties.PROPERTY_PREFIX
+ServiceDefinitionMapperProperties.SUFFIX_PROPERTY_KEY+ "=suffix")
;
contextRunner.run(context -> {
assertThat(context).hasSingleBean(ServiceDefinitionMapperProperties.class);
ServiceDefinitionMapperProperties serviceDefinitionMapperProperties
= context.getBean(ServiceDefinitionMapperProperties.class);
assertThat(serviceDefinitionMapperProperties.getSuffix()).isEqualTo("suffix");
});
}
which fails with:
org.opentest4j.AssertionFailedError:
Expecting:
<"">
to be equal to:
<"suffix">
but was not.
Expected :suffix
Actual :
<Click to see difference>
at org.springframework.cloud.appbroker.autoconfigure.DynamicCatalogServiceAutoConfigurationTest
public class DynamicCatalogServiceAutoConfiguration {
[...]
#Bean
#ConfigurationProperties(prefix=ServiceDefinitionMapperProperties.PROPERTY_PREFIX, ignoreUnknownFields = false)
public ServiceDefinitionMapperProperties serviceDefinitionMapperProperties() {
return new ServiceDefinitionMapperProperties();
}
[...]
}
Full sources available at https://github.com/orange-cloudfoundry/osb-cmdb-spike/blob/0da641e5f2f811f48b0676a25c8cbe97895168d1/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/DynamicCatalogServiceAutoConfigurationTest.java#L89-L107
ps: I was about to submit an issue to https://github.com/spring-projects/spring-boot/issues to suggest documentation enhancement to warn of such limitation in ApplicationContext, and to ask for ways to turn on support for #ConfigurationProperties. Following guidance at https://raw.githubusercontent.com/spring-projects/spring-boot/master/.github/ISSUE_TEMPLATE.md, I'm first making sure here I'm not misunderstanding the problem.
If you want to populate a bean annotated with #ConfigurationProperties class as part of your test, and you normally depend on a configuration class annotated with #EnableConfigurationProperties to populate that bean, then you can create a trivial configuration class just for the test:
#ConfigurationProperties("app")
public class ConfigProps {
private int meaningOfLife;
public int getMeaningOfLife() { return meaningOfLife; }
public void setMeaningOfLife(int meaning) { this.meaningOfLife = meaning; }
}
class ConfigPropsTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner();
#EnableConfigurationProperties(ConfigProps.class)
static class TrivialConfiguration {
}
#Test
void test() {
runner.withUserConfiguration(TrivialConfiguration.class)
.withPropertyValues("app.meaning-of-life=42")
.run(context -> {
assertEquals(42, context.getBean(ConfigProps.class).getMeaningOfLife());
});
}
}
Passing TrivialConfiguration to the ApplicationContextRunner is sufficient to make it create ConfigProps and populate it using the available properties.
As far as I can tell, none of the classes involved in your test enable configuration property binding. As a result, no properties are bound to ServiceDefinitionMapperProperties. You can enable configuration property binding using #EnableConfigurationProperties. A typical place to add it would be on DynamicCatalogServiceAutoConfiguration as its serviceDefinitionMapperProperties bean relies on configuration properties being enabled.

Why use spring.factories for Spring Boot auto-configuration instead of annotations?

The documentation states:
Developing auto-configuration and using conditions
If you work in a company that develops shared libraries, or if you work on an open-source or commercial library, you might want to develop your own auto-configuration. Auto-configuration classes can be bundled in external jars and still be picked-up by Spring Boot.
If I have annotations for everything else (even #AutoConfigureAfter or #AutoConfigureBefore annotations),
Why maintain a properties file to point to a class with an annotation?
Because we are not going to scan the world to figure out what auto-configuration classes exist in your project. For one, an auto-configuration is just a regular #Configuration class.
The way a Spring component is found is via explicit declaration or component scan but we need to know the list of auto-configuration classes way before we actually start the context.
When SpringBoot app is starting, it will not scan all the classes in jars, So SpringBoot starter should specify which classes are auto-configured. For example, in spring-boot-2.0.4.RELEASE, it initializes like this:
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
//1. method run will call the construtor below
SpringApplication.run(MyApplication.class, args);
}
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = deduceWebApplicationType();
//2. find all the classes whose key is ApplicationContextInitializer in spring.factories and initialize them
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
...
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(
//3. use current thread classcloader to load resources in the classpath
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
//SpringFactoriesLoader.java
public static List<String> loadFactoryNames(Class<?> factoryClass, #Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
// 3.1 first find the configuration file
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(#Nullable ClassLoader classLoader) {
...
try {
Enumeration<URL> urls = (classLoader != null ?
// public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
//4. spring.factories file is defined here
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
...
}
all the entries in spring.factories are loaded by below method -
org.springframework.boot.autoconfigure.ImportAutoConfigurationImportSelector#loadFactoryNames
protected Collection<String> loadFactoryNames(Class<?> source) {
return SpringFactoriesLoader.loadFactoryNames(source, getClass().getClassLoader());
}
SpringFactoriesLoader belongs to the spring-core library see below screen shot
Improve the startup performance by creating a static list of candidates at compilation time.Just scan a static 'spring.factories' file, avoiding scanning a large number of class files in jars. Similar to #indexed in springframework 5.0. --META-INF/spring.components.
Centralized configuration management, concise and compatible.

What configuration enables the evaluation of #Value annotations?

I'm tying to do a very minimal programmatic/annotation based configuration of Spring, to do some command line stuff and I want to be able to inject value of some bean values from System properties.
I'm using the #Value like this:
#Value("${MigrateDb.task:default}")
private String task;
It's sort of working, but it's not evaluating the value definition, I'm just getting "${MigrateDb.task:default}" in the actual field, instead of Spring evaluating it and giving me the value of the Migrate.db.task system property (or default).
What do I need to add to my Configuration class to enable this behaviour?
try using it this way:
#Value("${MigrateDb.task:default}")
private String task;
XML Config:
<context:property-placeholder
location="your.filelocation.properties" />`
Java Config :
#Bean
public static PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() {
PropertyPlaceholderConfigurer propertyPlaceholderConfigurer = new PropertyPlaceholderConfigurer();
propertyPlaceholderConfigurer.setLocation(new ClassPathResource("file.properties"));
return propertyPlaceholderConfigurer;
}
From ShadowRay's answer, the minimum code to enable the requested behaviour is:
#Bean
public static PropertyPlaceholderConfigurer propertyPlaceholderConfigurer(){
return new PropertyPlaceholderConfigurer();
}
Method should be static as per: https://stackoverflow.com/a/14943106/924597

Spring Boot - Detect and terminate if property not set?

Is there any way for a Spring Boot web application to abort at startup if a required property is not set anywhere (neither in the application.properties file nor the other property sources)? Right now, if the property is included in another property, it seem that Spring Boot simply avoids substitution.
For example, in my application.properties file, I have the line:
quartz.datasource.url=jdbc:hsqldb:${my.home}/database/my-jobstore
Right now, if "my.home" is not set elsewhere, Spring Boot is setting the url literally to "jdbc:hsqldb:${my.home}/database/my-jobstore" (no substitution).
I would like to have the application fail to start if the property my.home were not set anywhere else.
To throw a friendly exceptions just put a default null value in property, check and throw a exception in afterProperty method.
#Component
public static class ConfigurationGuard implements InitializingBean {
#Value("${my.home:#{null}}")
private String myHomeValue;
public void afterPropertiesSet() {
if (this.myHomeValue == null or this.myHomeValue.equals("${my.home}") {
throw new IllegalArgumentException("${my.home} must be configured");
}
}
}
Create a bean with a simple #Value(${my.home}) annotated field. - Then Spring will try to inject that value and will fail and therefore stop when the value is not there.
Just #Value(${my.home}) private String myHomeValue; is enough for normal (not Boot) Spring applications for sure! But I do not know whether Boot has some other configuration to handle missing values: If there is an other failure management than you could check that value in an PostCreation method.
#Component
public static class ConfigurationGuard implements InitializingBean {
#Value(${my.home})
private String myHomeValue;
/**
* ONLY needed if there is some crude default handling for missing values!!!!
*
* So try it first without this method (and without implements InitializingBean)
*/
public void afterPropertiesSet() {
if (this.myHomeValue == null or this.myHomeValue.equals("${my.home}") {
throw new IllegalArgumentException("${my.home} must be configured");
}
}
}
The default behaviour in current versions of Spring Boot (1.5.x, 2.0.x, 2.1.x) is to throw an exception if a placeholder can not be resolved.
There will a be an exception like this one :
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'app.foo.undefined' in value "${app.foo.undefined}"
It works because a bean of type PropertySourcesPlaceholderConfigurer (from spring-context) is automatically registered in Spring Boot, in this class : PropertyPlaceholderAutoConfiguration. And by default, the property ignoreUnresolvablePlaceholders in PropertySourcesPlaceholderConfigurer is set to false, which means an exception must be thrown if a placeholder is unresolved (be it nested or not).
Although they work, I think the approach in the foremost answer is somewhat brittle, as it only works for the predefined name(s), and will silently stop checking the when someone changes quartz.datasource.url in the configs to use a different expansion.
Ideally, I want this value of ignoreUnresolvablePlaceholders to be false to get wholesale expansion checking when parsing my configs such as application.properties or its YAML variants, but it's hard-coded to true for these cases. This unfortunately leaves strings such as ${FOO} in its unexpanded form if FOO cannot be found, making troubleshooting extremely painful. This is especially the case for fields that don't readily appear in the logs such as passwords.
While I couldn't find a way of changing ignoreUnresolvablePlaceholders short of modifying Spring Boot's classes, I did find an alternative of using a custom PropertySource implementation and defining a new syntax such as "${!FOO}" to indicate FOO must exist as an environment variable or die. (The OP didn't mention whether my.home is an environment variable but the code below is for environment variables.)
First, an EnvironmentPostProcessor implementation is required for registering the custom PropertySource. This StrictSystemEnvironmentProcessor.java does this as well as holds the implementation of the custom PropertySource:
package some.package;
#Order(Ordered.LOWEST_PRECEDENCE)
class StrictSystemEnvironmentProcessor implements EnvironmentPostProcessor {
private static final String PROPERTY_SOURCE_NAME = "STRICT_" + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
#Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (environment.getPropertySources().contains(PROPERTY_SOURCE_NAME)) {
return;
}
SystemEnvironmentPropertySource delegate = (SystemEnvironmentPropertySource)environment.getPropertySources()
.get(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);
environment.getPropertySources().addLast(new StrictSystemEnvironmentPropertySource(delegate));
}
private static class StrictSystemEnvironmentPropertySource extends SystemEnvironmentPropertySource {
public StrictSystemEnvironmentPropertySource(SystemEnvironmentPropertySource delegate) {
super(PROPERTY_SOURCE_NAME, delegate.getSource());
}
#Override
public Object getProperty(String name) {
if (name.startsWith("!")) {
String variableName = name.substring(1);
Object property = super.getProperty(variableName);
if (property != null) {
return property;
}
throw new IllegalStateException("Environment variable '" + variableName + "' is not set");
}
return null;
}
}
}
Instead of returning null, an exception is thrown for names that start with !.
This META-INF/spring.factories is also required so that Spring initializes our EnvironmentPostProcessor:
org.springframework.boot.env.EnvironmentPostProcessor=some.package.StrictSystemEnvironmentProcessor
Then henceforth, I can write all environment variables substitutions in my configs as ${!FOO} to get strict existance checking.
You can also create a #ConfigurationProperties bean, and decorate it with #Validated and #NotNull. This will throw an exception during startup when the value is not present (or null), e.g.
#Validated
#ConfigurationProperties("my")
public class MyProperties {
#NotNull
private String home;
// getter/setter, or constructor. See #ConstructorBinding.
}
For reference: Spring Boot 2.6 - #ConfigurationProperties Validation.
Note that you may need to add spring-boot-starter-validation, or another validator, depending on your project.
Then, you can just supply it as a dependency when needed, e.g.
#Component
public class AnotherBean {
private final MyProperties myProps;
public AnotherBean(MyProperties myProps) {
this.myProps = myProps;
}
// some code that uses myProps.getHome()
}

creating custom PropertySourcesPlaceholderConfigurer using property loaded dynamically

I am facing some challenge creating PropertySourcesPlaceholderConfigurer based on some value that is available in another property file.
I have a property file, custom-{environment}.property, which contains a value, that is needed to set location of PropertySourcesPlaceholderConfigurer.
My CustomConfiguration looks something like:
#Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
propertySourcesPlaceholderConfigurer.setLocation(customLocation);
//Custom url location based on a String available in the properties file. This is the problem area
return propertySourcesPlaceholderConfigurer;
}
I want to populate this customLocation from the properties file. Tried autowiring Environment, but it's failing as environment is null when placeholderConfigurer() is getting called. Tried using #PropertySource("custom-${environment}.property") and then #Value("**customLocation**"), but that's also not working.
Please let me know how this can be done. Thanks in advance.
I would suggest adding an ApplicationContextInitializer to load your property files instead of a plain #PropertySource. First load your custom-{environment}.properties next your configurable properties file.
public class PropertySourceInitializer implements ApplicationContextInitializer {
private static final String DEFAULT_CONFIG_FILE = "classpath:custom-${environment}.properties";
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
final ConfigurableEnvironment env = applicationContext.getEnvironment();
final MutablePropertySources mps = env.getPropertySources();
//
Resource resource = applicationContext.getResource(env.resolvePlaceholders(DEFAULT_CONFIG_FILE));
mps.addLast(new ResourcePropertySource(resource.getDescription(), resource));
String additional = env.getProperty("name.of.property");
if (StringUtils.hasText(additional) {
Resource additionalResource = applicationContext.getResource(env.resolvePlaceholders(additional));
if (additionalResource.isReadable() ) {
mps.addLast(new ResourcePropertySource(resource.getDescription(), resource));
}
}
}
}
Trying to get it to work with a #PropertySource will be much harder as the phases in which the PropertySourcesPlaceHolderConfigurer is created is different then the one in which the #PropertySource annotations are scanned. Staged loading of #PropertySource (which is basically what you want) is quite difficult. Spring Boot also has its own loading mechanism (which actually is also a ApplicationContextInitializer.
Can you try setting your location as a system property?
#Value("#{ systemProperties['myapp.location'] }")
private String location;
You need to set "myapp.location" as system property.

Resources