Affects: 5.1.6.RELEASE, similar problem appeared trying with spring boot 2.3.2.RELEASE and 1.5.22.RELEASE
Trying to create a way to use Strategy Pattern in my service I constructed this set of classes:
MathOperProcessor is the base class for - MathOperAdd - MathOperDivide - MathOperMultiply
Each of them have a different set of Services @Autowired with setter injection.
And to use them as a service an Enum/@Configuration/@Service classes whose snippets are shown below (real implementation can be found in my github project strategy-calculator branch minimum).
public enum MathOperEnum {
// 2 parameters
// 1 - A key to recognize the operation
// 2 - An implementation of MathOperProcessor abstract class
// each implementation has a different set of @Autowired setters.
ADD("add", MathOperAdd.class),
MULTIPLY("mult", MathOperMultiply.class),
DIVIDE("div", MathOperDivide.class);
public String getKey() {
return this.key;
}
public Class<? extends MathOperProcessor> getProcessorClass() {
return this.processorClass;
}
}
@Configuration
public class SpringConfiguration {
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public MathOperProcessor getMathProcessor(final MathOperEnum mathOper) throws Exception {
return mathOper.getProcessorClass().getConstructor(String.class, long.class)
.newInstance("user", 101L);
}
.....
@Service
public class MathOperService {
@Autowired
ObjectProvider<MathOperProcessor> provider;
public BigDecimal mathOperation( final MathOperEnum operation
, final BigDecimal x
, final BigDecimal y
) {
return this.provider.getObject(operation).execute(x, y);
}
If I call MathOperService.mathOperation(...)
from multiple threads at the same time, @Autowired services with setter injection in MathOperProcessor
implementations do not get populated, in fact some setters are not called while others do, randomly I should say.
A couple of interesting things I found:
- If I call the ObjectProvider from within a synchronized method (MathOperStragetyFactory) in my github project everything works ok.
- Once the error start to occur, next calls to obtain new beans fail to populate @Autowired setters even if called synchronized.
- I tried to get the Beans once and then run the multithread test hoping a kind of cache will fix the error, but it didn't, which make myself asked, shouldn't Spring annotations for given class be cached ? (Just a doubt).
Finally I tried to search information to see if I was missing some "restrictions" on constructing beans the way I do in @Configuration, and wasn't able to find anything discouraging my approach, or doing a similar thing.
1st. issue report sorry if some info is missing Thanks.
Comment From: mdeinum
Looking at the code, I suspect the culprit lies in the AutowiredAnnotationBeanPostProcessor
and especially in this piece of code. It does a lookup based on the beanName
then checks if the metadata needs to be refreshed.
As it acts as a factory and potentially returning a different class for each invocation it might be that the InjectionMetadata
gets cleared and thus loses the auto wiring information. This only happens in a multi-threaded environment and when using synchronized
on your factory (or factory method) only 1 thread is allowed access and it will work.
Comment From: jhoeller
This smells like some race condition around injection metadata refreshing indeed, where concurrent refreshing may interfere in unfortunate ways... although I yet need to understand where exactly. Admittedly the cache is optimized for a bean class that remains the same, so in case of a per-operation class change, we'll recompute and update the cache every time that class changes. This is generally not efficient but should work reliably, and we should find out where the consistency issue is there.
That said, from an application design perspective, I'd avoid this as far as possible. Couldn't you rather design the specific operation implementations as independently defined beans (with a fixed implementation class per bean definition) and then have a custom lookup method that chooses one of those target beans according to the given enum value? Any alternative arrangement that avoids a commonly used bean definition with an ever-changing implementation class would help here.
Comment From: toniocus
@jhoeller, thanks for the info.
Yes after @mdeinum answer, I realized we need to do what you clearly explain in your 2nd. paragraph, I will go for that solution, one of it is already mentioned in my stackoverflow question (A factory using ApplicationContext).