12

I am defining conditions that I will check to dynamically load one of the two implementations of my service interface later.

@Component
public class IsPolicyEnabled implements Condition {

    @Autowired
    private MyProperties props;

    @Override
    public boolean matches(ConditionContext arg0, AnnotatedTypeMetadata arg1) {
        return props.isPolicyEnabled();
    }

}

And

@Component
public class MyProperties {...}

And

@Service
@Conditional(IsPolicyEnabled.class)
public class ServiceA implements Service {...}

However, I am running into a runtime error as.

java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: java.lang.NullPointerException
at com.xyz.utils.IsPolicyEnabled.matches(IsPolicyEnabled.java:9)
at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108)
at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:88)
at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:71)
at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.isConditionMatch(ClassPathScanningCandidateComponentProvider.java:515)

Basically, it failed to initialize props object that has been auto-wired inside the condition implementation. Is that not allowed?

How can I auto wire another dependency inside the condition implementation since my condition evaluation depends on a value provided by that dependency?

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
WillMcavoy
  • 1,795
  • 4
  • 22
  • 34

2 Answers2

10

Conditions are checked immediately before the bean-definition is due to be registered [...]

Condition, Spring Framework 5.0.8.RELEASE API documentation

You can't inject a bean into a Condition instance because there are no bean-definitions in the context yet1.

Moreover, you are not supposed to work with beans within Condition classes:

Conditions must follow the same restrictions as BeanFactoryPostProcessor and take care to never interact with bean instances.

Condition, Spring Framework 5.0.8.RELEASE API documentation

You should rethink the design because

[...] my condition evaluation depends on a value provided by that dependency.

indicates that it's not quite right.

1 Precisely speaking, there are a few beans already registered by Spring for its own needs.

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
  • 1
    "my condition evaluation depends on a value provided by that dependency." Why do you say so? As an example, say there's a DB table that contains a value that drives which type of service to connect to (say JMS or Kafka). In order to tell spring "initialize JMS" or "initialize Kafka", I need a bean that can access DB to read that value, which in turn may need e.g. `JdbcTemplate` or so. How would that initialization be conditioned without those beans? – levant pied Aug 16 '19 at 18:31
4

There are two issues:

1) There is no injection for Condition classes

Solution is to retrieve beans from ConditionContext:

@Component
public class IsPolicyEnabled implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MyProperties props = context.getBeanFactory().getBean(MyProperties.class);

        return props.isPolicyEnabled();
    }
}

2) Condition initialization happens very early

Trying to retrieve beans from ConditionContext fails with NoSuchBeanDefinitionException: No qualifying bean of type ... because condition check happens very early in the Spring lifecycle.

A solution is to have two Spring contexts:

  • A parentContext which defines only MyProperties
  • A childContext which defines the rest of the application and has parentContext as its parent

So when the Condition is invoked, MyProperties is already created in the parent context:

ApplicationContext parentContext = new AnnotationConfigApplicationContext(MyProperties.class);
ApplicationContext childContext = new AnnotationConfigApplicationContext();
childContext.setParent(parent);
childContext.register(ApplicationConfiguration.class);
childContext.refresh();

Service service = childContext.getBean(Service.class);
// do something with service
Arend v. Reinersdorff
  • 4,110
  • 2
  • 36
  • 40
  • I understand your points, but for the part (2), I don't understand where to place that code to initialize the parent context with the properties class, neither how to make it initialized then for the part (1). Thx! – Gerard Bosch Dec 17 '20 at 19:05