spring eventListener with external condition - spring

I need a flexible filters for FooEvents for multiple EventListeners all over my code.
I can use #EventListener(condition="event.enabled"), but my filters require many attributes of fooEvent to be analysed.
I was hoping that I could use a Predicate-Bean from my Application Context:
#Component
public class FooPredicate implements Predicate<FooEvent> {
public boolean test(FooEvent event) {...}
}
...
#EventListener(condition="${fooPredicate.test(event)}")
public void handle(FooEvent event) { ... }
But I get:
org.springframework.expression.spel.SpelEvaluationException: EL1011E:
Method call: Attempted to call method
test(org.springframework.context.PayloadApplicationEvent) on null
context object
Is it possible to use external, complex conditions for EventListerns? Or at least to define global listeners with complex conditions and inherit their behavior without repeating the full conditions?

You're using the wrong definition, as fooPredicate is a spring bean you need to use '#' instead of '#' to resolve it as a bean. see 10.5.13 Bean references
#EventListener(condition="#fooPredicate.test(#event)")
public void handle(FooEvent event) {
System.out.println();
}

Related

Access to Actual Class Variable from SpEL

As per spring's Event handling mechanism, we can use SpEL to select a specific handler under some circumstances. Taken from the spring doc.
public class EventXHandler {
private String handlerClassName;
#EventListener(condition = "#event.name == handlerClassName")
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
My question is; if I can access to the handler class' property in the spel.
No; you cannot do that.
If you know the bean name and add a public getter for the property, you can use
#event.name == #beanName.handlerClassName.

Any way to find other annotations on methods that are annotated with EventListeners using the resulting ApplicationListener returned from the context?

It looks like the ApplicationListenerMethodAdapter hides the method it is annotated for making it impossible to look if that method potentially contains other Annotations. There is some other way around this?
if i have an event listener like this
#EventListener
#SomeOtherAnnotation
public void onSomeEvent(SomeEvent e) {
...
}
and a custom event multicaster
public class CustomEventMulticaster extends SimpleApplicationEventMulticaster {
public <T extends ApplicationEvent> void trigger(final T event,
Function<ApplicationListener<T>, Boolean> allowListener) {
...
}
}
i'd like to do something like trigger only if some annotation exists
customEventMulticaster.trigger(someEvent, (listener) -> {
return listener.getClass().getAnnotation(SomeOtherAnnotation.class) == null;
})
There is a hacky solution - just as case study - but please don't go that way.
Since your application listener is in fact ApplicationListenerMethodAdapter you can use reflection to get method or targetMethod from that class. From there you can get method annotations.
More or less (not checked, pure notepad here)
customEventMulticaster.trigger(someEvent, (listener) -> {
Field f=((ApplicationListenerMethodAdapter)listener).getDeclaredField("method"); // or 'targetMethod' - consult ApplicationListenerMethodAdapter to get the difference
f.setAccessible(true);
Method m=f.get(listener); // cast again if required
anno=m.getAnnotation(yourAnno); // here you can access annotation
return anno == null;
})
To make this at least to pretend ot be safe, add nullchecks and check if listener is indeed castable to ApplicationListenerMethodAdapter

Conditional creating a bean based on customized biz logic

Here're my use case:
When my app launched, N beans of type A will be created.
I'd like to have my own biz logic to check these N beans 1 by 1 and:
if none of them satisfy my criteria, I'll create another bean of type A to spring container.
if any of them satisfy my criteria, just do nothing.
I'm not sure whether I can simply use Optional like this:
#Autowired
List<A> beans;
#Bean
public Optional<A> maybeA(){
//check beans and optionally create a A
}
Defining bean that returns type Optional is very unusual and not a good solution.I suggest you check if you could implement your logic as described https://iamninad.com/conditional-bean-creation-in-spring-boot/ or if you insist to follow your solution, you can create a new class as :
public class UnMatchedACriteriaImpl implements A {
public void testMethod() throws UnSatisfiedXCriteria {
throw new UnSatisfiedXCriteria();
}
}
and change your bean definition to :
#Bean
public A maybeA(){
//check beans and optionally create a A
}
When you criteria does not match you return UnMatchedACriteriaImpl version of A. which is clear and manageable when you want to use it in your application.

How to choose bean implementation at runtime for every http request

I am having two implementations of my component.
public interface MyComponent {
}
imple1
#Component("impCompf")
#Lazy
#RequestScope
public class ImpComp1 implements MyComponent {
}
imple2
#Component("impComps")
#Lazy
#RequestScope
public class ImpComp2 implements MyComponent {
}
What I did so far is to create two conditions like so:
imple1
public class FirstCondition implements Condition {
#Override
public boolean matches(ConditionContext arg0, AnnotatedTypeMetadata arg1) {
return staticVariable.contains("impCompf");
}
}
Same goes for imple2
and define a configuration class
#Configuration
public class MyConfiguration {
#Bean
#Conditional(FirstCondition .class)
#Primary
public MyComponent getComp1() {
return new ImpComp1();
}
public static String staticVariable= "impCompf";
and in My main controller:
#RequestMapping(value="api/{co}", method=RequestMethod.POST)
public ResponseEntity<Modelx> postSe(#PathVariable("co") String co) {
if(co.contains("impCompf"))
staticVariable = "impCompf";
else (co.contains("impComps"))
staticVariable = "impComps";
What I want: for every http request I want to load proper implementation
But however what I am getting is the implementation defined first in the static variable.
If is there another elegant and better way, i'd like to know about it.
I think there is some confusion here about the purpose of the conditions. These aren't being used at the time your requests arrive to autowire the candidate bean into your controller. These are being used when the application is started to configure the application context based on the environment and classpath etc...
There is no need for the conditional classes that you have created. This is defining the configuration of the beans when the context starts and not on a per request basis at runtime.
The use of the static variable is also problematic is a scenario with one or more concurrent requests or in a case where multiple threads may observe different values unless some other mechanism in the java memory model is being used (such as volatile or establishing a happens before relationship, e.g. with sychnronized)
There are a number of ways to do what you appear to be trying to achieve. Since ultimately, you appear to be using a path parameter supplied by a client to determine which service you want to invoke you could use a classic factory pattern to return the correct interface implementation based on the string input programmatically.
Alternatively you could create two distinct controller methods which are distinguished by a query parameter or endpoint name or path match etc. You could then have the appropriate service injected by a qualified bean name
Although perhaps generally recommended, you could also inject an application context instance and search the it looking for the relevant bean by name or class: https://brunozambiazi.wordpress.com/2016/01/16/getting-spring-beans-programmatically/ - although This is more cumbersome and you'd need to handle things like org.springframework.beans.factory.NoSuchBeanDefinitionException or casting in some cases - best avoided in favour of one of the other methods.

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

Resources