Hello all,
I'm working on a task which implies to upgrade our project from Spring 5 to Spring 6. I found the following error/issue when a quartz job is creating and scheduled at runtime:
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.project.JobTask': Unsatisfied dependency expressed through method 'setMapWithEnumKeys' parameter 0: No qualifying bean of type 'java.util.Map<com.project.EnumKeyJobTask, com.project.EnumValueJobTask>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:896)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:849)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:146)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:509)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1459)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:606)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:328)
at org.springframework.scheduling.quartz.SpringBeanJobFactory.createJobInstance(SpringBeanJobFactory.java:89)
at org.springframework.scheduling.quartz.AdaptableJobFactory.newJob(AdaptableJobFactory.java:43)
... 2 common frames omitted
The definition of mapWithEnumKeys bean is following:
<util:map id="mapWithEnumKeys" map-class="java.util.HashMap">
<entry key="ENUM_KEY_A" value-ref="EnumValueJobTaskA"/>
<entry key="ENUM_KEY_B" value-ref="EnumValueJobTaskB"/>
<entry key="ENUM_KEY_C" value-ref="EnumValueJobTaskC"/>
<entry key="ENUM_KEY_D" value-ref="EnumValueJobTaskD"/>
</util:map>
The setter from Job Task looks like following:
@Autowired
public void setMapWithEnumKeys(Map<EnumKeyJobTask, EnumValueJobTask> mapWithEnumKeys) {
this.mapWithEnumKeys = mapWithEnumKeys;
}
The SpringBeanJobFactory type is used for our bean type instance for jobFactory from schedulerFactory definition.
<property name="jobFactory">
<bean class="org.springframework.scheduling.quartz.SpringBeanJobFactory" />
</property>
I'm thinking is an issue regarding to the new Autowire Capability implemented in Spring 5 for Quartz Beans. Because for a normal bean definition in XML everything works fine:
<property name="mapWithEnumKeys" ref="mapWithEnumKeys" />
I tried to use key-type attribute for util:map bean definition but I got the same issue.
Spring version 6.2.11 Quartz Scheduler 2.5.0
Comment From: jhoeller
This looks like a difference between autowiring by type (used by @Autowired) versus autowiring by name (used by the property ref attribute). So this is not really Quartz-specific as far as I can see.
Spring Framework 6 generally comes with stricter generic type matching, and our old util:map definition effectively exposes a raw Map without generics by default. For a complete definition, you should declare not only the key-type but also the value-type on your util:map element. It's like a Java assignment: Your method signature suggests Map<EnumKeyJobTask, EnumValueJobTask> map = ... - with the right-hand side coming from your util:map declaration, and it needs to match without an unchecked warning.
Alternatively, you could declare setMapWithEnumKeys with @Qualifier("mapWithEnumKeys") in order to suggest a specific bean by name. That said, this should actually work as long as the bean property name is equivalent to the target bean name as well: with setMapWithEnumKeys matching a target bean of name mapWithEnumKeys even for a raw Map. Such qualifier and name matches should work even when the generic match is incomplete.
As far as a local unit test shows, either of those steps should make your scenario work. Please give it a try.
Comment From: ciprianbuzduga
Hi @jhoeller,
Unfortunately the proposal solution doesn't work, I put the Qualifier annotation on my setter and still have the issue.
Btw, I'm curios, how is working with List<EnumValueJobTask> as arguments on setter, because also our list use generic type.
Thanks
Comment From: jhoeller
As a general note: List, Set and Map are all ambiguous since they can match a target bean of that particular collection type - but also a collection of multiple beans of the declared element type. We recommend ObjectProvider<...> for referring to multiple beans of a given type in modern applications, but for historical reasons we have the List, Set and Map variants for multiple beans still. That complicates the algorithm for the injection of Map beans, unfortunately.
Please try to provide a repro project for your scenario, showing where even a qualifier does not help (since I cannot reproduce this so far). The above-referenced unit tests indicate that qualified Map injection does work against field/property names, and a variant of those tests also works for me if I replace the name match with a corresponding @Qualifier("mapWithRef") declaration in an annotation-aware BeanFactory setup.
Comment From: ciprianbuzduga
Hi @jhoeller ,thanks for your feedback. I cannot provide a repo project or part of the code base because the raised issue was found in our commercial project, and sure I cannot share the client proprietary project code base.
I will try to enumerate here the structure or composition of these beans:
1. Bean JobTask task/job which extends an abstract class AbstractJobTask.
2. AbstractJobTask extends AbstractJobBean, in the final AbstractJobBean extends org.springframework.scheduling.quartz.QuartzJobBean.
3. AbstractJobTask has a setter setMapWithEnumKeys annotated with @Autowired and @Qualifier("mapWithEnumKeys")
4. My bean named mapWithEnumKeys in configured through xml file like I said in the issue description. first comment.
5. We created the Job Details bean and then submit to scheduled like this:
JobDetail jobDetail = JobBuilder.newJob(jobTaskClass)
.withIdentity(jobGroup + "_" + jobId + "_Job", jobGroup)
.usingJobData(jobDataMap)
.build();
taskScheduler.scheduleJob(jobDetail, jobTrigger);
However, I don't think so matter how we scheduled the job.
May represents a problem if we declared our map in xml configuration and we try to inject it via type with Autowired and Qualifier?
Thanks
Comment From: jhoeller
Trying further variations, it turns out that this is a problem with method-based Map injection with incomplete generics only - not with constructor or field autowiring which was what I tried before. I'll fix this for 6.2.13 - a qualifier match as well as a method parameter name match should resolve fine even for an incomplete Map type then.
For methods, qualifiers can sit at the parameter level - setMapWithEnumKeys(@Qualifier("mapWithEnumKeys") Map....) - as well as at the method level. The fix now makes it check at the method level as well. Since it was already checking for parameter-level annotations before, you could declare a parameter-level qualifier for the time being.
Comment From: github-actions[bot]
Fixed via bce1445d924802fae0b8e9a0595a1489605de940
Comment From: jhoeller
This revision is available in the latest 6.2.13 snapshot now. Please give it an early try!