Conditionally create bean if Map of properties is not empty with SpEL - spring

I have a class with a method that is periodically invoked with the #Scheduled annotation. The method does some bulk operations on a given set of properties.
If there are no properties set, I don't need the scheduled method invocation nor the instantiated class. Therefore, I've added this SpEL expression to check whether the properties are set:
#Service
#ConditionalOnExpression("#(T(java.util.Map)('${myproperties.people:{:}}')).size() > 0")
public class PeopleService { ... }
Example values in the application.yml could be:
myproperties:
people:
uuid1:
name: Mark
age: 32
uuid2:
name: Jeff
age: 36
Unfortunately I get this error message:
Caused by: org.springframework.expression.spel.SpelParseException: Expression [#(T(java.util.Map)('{:}')).size() > 0] #1: EL1043E: Unexpected token. Expected 'identifier' but was 'lparen(()'
Note that I came up with {:} for an empty map as default value here: https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions-inline-maps
If I use this SpEL, I get the following error: "#(T(java.util.Map)(${myproperties.people:})).size() > 0"
Caused by: org.springframework.expression.spel.SpelParseException: Expression [#(T(java.util.Map)()).size() > 0] #1: EL1043E: Unexpected token. Expected 'identifier' but was 'lparen(()'
What is the correct way to accomplish this?

This annotation:
#ConditionalOnExpression("#(T(java.util.Map)('${myproperties.people:{:}}')).size() > 0")
Doesn't work because Spring read LITERALLY all your properties like that:
myproperties.people.uuid1.name=Mark
myproperties.people.uuid1.age=32
myproperties.people.uuid2.name=Jeff
myproperties.people.uuid2.age=36
If you check Spring Boot documentation, this page explains that "the condition matches if spring.example.values is present in the Environment but does not match if spring.example.values[0] is present.".
So for Spring, property "myproperties.people" just doesn't exist. On the same page, they explain that "it is better to use a custom condition for such cases."
A workaround is to define a property class (but it's not wasted, because you will use this class to parse your yaml as a map):
#ConfigurationProperties(prefix = "myproperties")
public class MyPeopleProperties {
private Map<String, String> people;
public Map<String, String> getPeople() {
return people;
}
public void setPeople(Map<String, String> people) {
this.people = people;
}
}
And on your main class:
...
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
...
#Service
#Conditional(PeopleService.PeopleServiceCondition.class)
public class PeopleService {
...
public static class PeopleServiceCondition implements Condition {
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MyPeopleProperties config = Binder.get(context.getEnvironment())
.bind("myproperties", MyPeopleProperties.class).orElse(null);
return config != null && config.getPeople() != null && !config.getPeople().isEmpty();
}
}
}
So myproperties.people will be bound as map, then we check if this map exists on the bean declaration condition.

Circling back around to the SpEL implementation, you actually can do it entirely in SpEL but it's really ugly:
#ConditionalOnExpression(
"(" +
"T(org.springframework.boot.context.properties.bind.Binder)" +
".get(environment)" +
".bind('myproperties.people', T(java.util.Map))" +
".orElse(null)" +
"?.size()" +
"?: 0" +
") > 0"
)
This will work even for deeply-nested config subtrees - the simplistic binding to the Map class just creates a bunch of denormalized/flattened <String,String> entries with keys like uuid1.name. That means that if you are just testing for the existence of any sub-tree elements under the myproperties.people namespace, the above will work fine. It is, in effect, exactly what you probably would have meant when writing #ConditionalOnProperty("myproperties.people")

Related

Trouble with Spring #ConfiurationProperties extending Mao

I am trying to load come properties from config using the Spring #Configuration & #ConfigurationProperties combination. I have a POJO that extends HashMap<Integer, String> and has a single variable inside. (See MyConfigPojo below). From the .yaml file, I have the same shape. However, when booting up the app, I get an error when trying to parse the string for the defaultValue into an Integer.
#Configuration
#ConfigurationPropeties(prefix = "my-config")
public class MyConfigPojo extends HashMap<Integer, String> {
private String defaultValue;
private String getValueForIdOrDefault(int id); // this.get(id) OR ELSE defaultValue
}
In config I have this:
myConfig:
1: "my first value"
23: "my 23rd value"
defaultValue: "cheese"
Which results in a
APPLICATION FAILED TO START
Description:
Failed to bind properties under 'myPackage' to MyConfigPojo:
Property: myConfig[1]
Value: cheese
I thought that was weird, so I turned on TRACE logs and found this:
99 common frames omittedCaused by: java.lang.NumberFormatException: For input string: "defaultValue" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
Is there a way to intercept reading this file or to tell Spring how to set the values properly?
You are trying to read a Map from .yml file, for that, you need #ConfigurationProperties annotation enough, don't need #Configuration annotation(if using spring boot). If you are using spring-framework, you need #Configuration also.
Use this example:
myConfig:
map:
1: "my first value"
23: "my 23rd value"
defaultValue: "cheese"
MyConfigPojo.java
package com.org;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.HashMap;
#Configuration
#ConfigurationProperties(prefix = "my-config")
public class MyConfigPojo {
private HashMap<String, String> map;
public HashMap<String, String> getMap() {
return map;
}
public void setMap(HashMap<String, String> map) {
this.map = map;
}
}
Now, you can Autowire this MyConfigPojo class anywhere(for instance, in controller class) and read those map keys and values.
#Autowired
MyConfigPojo pojo;
Now, you have considered keys & Values as String datatype for that Map, you will not get NumberFormatException.

ConditionalOnExpression fails to compare configuration property to enum type, works as string

I want to load a #Configuration class based on an enum in properties file, so I have the following class:
#Configuration
#ConditionalOnExpression("#{ ${demo.spel.demo-enum} eq T(demo.spel.DemoEnum).VALUE }")
public class DemoConfig {}
And I have: demo.spel.demo-enum=value in application.properties
This does not work, and throws the exception:
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'value' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext' - maybe not public or not valid?
The odd thing is, that if I add single quotes to the property part, and a toString() to the enum part of the expression, there is no exception, the condition is true, and the bean is created (verified by checking console output in debug logging):
#ConditionalOnExpression("#{ '${demo.spel.demo-enum}' eq T(demo.spel.DemoEnum).VALUE.toString() }")
Questions:
Why is comparing an enum like this fails ? How come Spring can successfully convert the values and compare as string but not as their types ?
This is on Spring Boot 2.0.4
It should be pretty obvious, really.
Consider the following Java code:
foo.equals(DemoEnum.VALUE)
It would require an object foo, perhaps a field on this:
this.foo.equals(DemoEnum.VALUE)
If your property placeholder evaluates to 'foo', your first SpEL expression is the equivalent of
#this.foo eq T(DemoEnum).VALUE
So SpEL looks for a property foo on #this
EDIT
If you create a Class:
public class Foo {
#Value("${some.property}")
private DemoEnum enum;
public getEnum() {
return this.enum;
}
}
And add a bean to the context called "foo" you could then use
foo.enum eq ...
Since #this is a BeanExpressionContext allowing you to reference other beans.
I had a similar problem.
I had a feature, which was enabled by default. To disable it, application config file, should have it disabled explicitly. demo.feature.disable:true
I had a spring bean conditional on this property (enabled by default).
#ConditionalOnExpression("#{ ${demo.feature.disable} != true }")
#Component
public class FeatureModule {
}
The problem was, when demo.spel.value was not defined in the config file - application.yml, initialization of this component will fail with
Caused by: org.springframework.expression.spel.SpelParseException: EL1041E: After parsing a valid expression, there is still more data in the expression: 'lcurly({)'
To solve it, I provided default value.
#ConditionalOnExpression("#{ ${demo.feature.disable:false} != true }")
#Component
public class FeatureModule {
}
Now, when I test it.
By default this component is initialized.
If config file does not have demo.feature.disable, this component will be initialized.
If config file has demo.feature.disable:true, this component will not be initialized.
If config file has demo.feature.disable:false this component will be initialized.

Evaluate property from properties file in Spring's #EventListener(condition = "...")

I would like to make the execution of an event handler dependent on whether or not a property is set to true in a properties file.
#EventListener(ContextRefreshedEvent.class, condition = "${service.enabled}")
public void onStartup() { }
However, this does not seem to work. I am getting the following error on startup:
org.springframework.expression.spel.SpelParseException: EL1043E:(pos 1): Unexpected token. Expected 'identifier' but was 'lcurly({)'
Is it possible to use a property from a properties file as a condition here?
The issue is condition argument is expecting a SPEL.
This works try it out.
In your bean where you have this #EventListener, add these lines
public boolean isServiceEnabled() {
return serviceEnabled;
}
#Value("${service.enabled}")
public boolean serviceEnabled;
change your declaration of evnt listener like this
#EventListener(classes = ContextRefreshedEvent.class, condition = "#yourbeanname.isServiceEnabled()")
public void onStartup() { }
change yourbeanname with the correct bean name .
I had the same annoying experience (with Spring Boot 2.4.2 on Java11).
In my case I had the boolean property in a #ConfigurationProperties class anyways in the same java file and still struggled a bit. First the #ConfigurationProperties need to be annotated as #Component to actually be a valid Bean and can be used in SpEL.
And I had to use the same long attributeName for the ConfigurationProperties in the Service itself and the EventListener Annotation for the Bean resolution to work fine. I needed some the ConfigurationProperties values also in another place of the Service, that's why they needed to be (Constructor) Autowired as well...
So this worked for me:
#ConfigurationProperties("my.custom.path")
#Component //Important to make this a proper Spring Bean
#Data //Lombok magic for getters/setters etc.
class MyCustomConfigurationProperties {
boolean refreshAfterStartup = true;
}
#Service
#RequiredArgsConstructor //Lombok for the constructor
#EnableConfigurationProperties(MyCustomConfigurationProperties.class)
#EnableScheduling
public class MyCustomService {
private final MyCustomConfigurationProperties myCustomConfigurationProperties;
#EventListener(value = ApplicationReadyEvent.class, condition = "#myCustomConfigurationProperties.refreshAfterStartup")
public void refresh() {
//the actual code I want to execute on startup conditionally
}
}

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

#Value annotation in child context

I think there are some problems when using #Value annotation and default values. I am using the following annotation on a variable. I expect to see the variable propAVar set with the property value of propA however, its always set to the default value -10
#Component
public class SomeClass {
#Value("${propA:-10}")
public String propAVar;
}
I am using PropertyPlaceholderConfigurer in the parent and child context. The child context has the context:component-scan element which scans the SomeClass. On digging into the Spring code i found the following method where the issue might lie.
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
//...
public String resolveEmbeddedValue(String value) {
String result = value;
for (StringValueResolver resolver : this.embeddedValueResolvers) {
if (result == null) {
return null;
}
result = resolver.resolveStringValue(result);
}
return result;
}
//...
}
Looks like resolveEmbeddedValue will go through all embeddedValueResolvers and ascertain value of that property. However, if we define a default it will look for that property in the first embeddedValueResolvers and return the default value if its not found there. Shouldn't it go through all resolvers and then return the default value? Would like to mention that this works fine without the default value.
Would be great if someone could help explain the expected behavior of #Value with default value in case of parent-child context's.
[cross-posted on spring forum]
I believe you are seeing this bug. There are some workarounds mentioned by other users in there. Also, I would suggest voting for fixing it, in case your situation is the same as the one described there.

Resources