Affects: 5.2+
My organization has multiple high-traffic, customer-facing applications that create many beans per request. The individual creations are fast enough but in aggregate these beans contribute noticeably to the request's critical path latency, as well as the per-request cpu. The key portion of the stack trace is:
org.springframework.beans.factory.support.ConstructorResolver.resolvePreparedArguments
org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod
org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance
Eventual leaf nodes of the call graph are:
java.lang.Class.forName0
java.lang.reflect.Constructor.getParameterAnnotations
java.lang.reflect.Method.getParameterAnnotations
org.springframework.core.annotation.AnnotationUtils.getValue
Annotations and classes don't change during runtime, yet they are repeatedly processed.
Solution
Some level of caching is appropriate, either multiple caches (one per leaf) or a single larger one around ConstructorResolver.resolvePreparedArguments
. Multiple specific caches seems preferable because of lower contention and simpler keys.
Supporting Data
Below are further, detailed stack traces that illustrate the path from ConstructorResolver.resolvePreparedArguments to the leaf nodes.
org.springframework.core.annotation.AnnotationUtils.getValue
org.springframework.core.annotation.AnnotationUtils.getValue
org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver.checkQualifiers
org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver.isAutowireCandidate
org.springframework.beans.factory.support.DefaultListableBeanFactory.isAutowireCandidate
org.springframework.beans.factory.support.DefaultListableBeanFactory.isAutowireCandidate
org.springframework.beans.factory.support.DefaultListableBeanFactory.isAutowireCandidate
org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates
org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency
org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency
org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument
org.springframework.beans.factory.support.ConstructorResolver.resolvePreparedArguments
java.lang.reflect.Constructor.getParameterAnnotations
org.springframework.core.MethodParameter.getParameterAnnotations
org.springframework.beans.factory.InjectionPoint.getAnnotations
org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver.isLazy
org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver.getLazyResolutionProxyIfNecessary
org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency
org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument
org.springframework.beans.factory.support.ConstructorResolver.resolvePreparedArguments
java.lang.reflect.Method.getParameterAnnotations
org.springframework.core.MethodParameter.getParameterAnnotations
org.springframework.beans.factory.InjectionPoint.getAnnotations
org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver.isLazy
org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver.getLazyResolutionProxyIfNecessary
org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency
org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument
org.springframework.beans.factory.support.ConstructorResolver.resolvePreparedArguments
java.lang.Class.forName0
java.lang.Class.forName
org.springframework.util.ClassUtils.forName
org.springframework.beans.factory.config.MethodInvokingBean.resolveClassName
org.springframework.util.MethodInvoker.prepare
org.springframework.beans.factory.config.MethodInvokingFactoryBean.afterPropertiesSet
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.lambda$invokeInitMethods$5
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$Lambda.run
java.security.AccessController.doPrivileged
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean
<proprietary BeanFactory subclass>.createBean
org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$1
org.springframework.beans.factory.support.AbstractBeanFactory$Lambda.getObject
<proprietary request scope subclass>.createBean
<proprietary request scope subclass>.get
org.springframework.beans.factory.support.AbstractBeanFactory.getBean
org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference
org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary
org.springframework.beans.factory.support.ConstructorResolver.resolvePreparedArguments
Comment From: jengebr
I created a pull request to address three of the four stacks listed in this issue, which represent 95% of the problem and are sufficient to satisfy this issue.
Comment From: jhoeller
We cache a ConstructorDependencyDescriptor
per autowired constructor or factory method argument now, including shortcut bean retrieval (aligned with AutowiredAnnotationBeanPostProcessor
). This should not only address parameter annotations caching across all scoped instances of the same bean but also shortcut even further, going straight via getBean
for a pre-resolved target bean. As a minor improvement, this also bypasses a conversion attempt for prepared autowired arguments now.
Comment From: jhoeller
I've also included a minor MethodInvoker
optimization for a pre-resolved targetClass, e.g. for targetClass+staticMethod configuration (even if that isn't recommended since targetClass+targetMethod is preferable there).
Last but not least, the checkQualifiers
hotspot should disappear as well now since the shortcut bean retrieval (mentioned above) bypasses this for a pre-resolved target bean now, just like it does for @Autowired
fields and methods already.
Comment From: jhoeller
This is in 5.3.x as well now, feel to give it a try! Should be straightforward enough to patch 5.2.x with it but I strongly recommend an upgrade to 5.3.30 (and trying a 5.3.30 build snapshot for the time being).
FWIW the 5.2.x line has reached its end of open source support a while ago already and will reach its ultimate end of life by the end of this year.
Comment From: jhoeller
Aside from some alignment in related classes, the core part of the change is in org.springframework.beans.factory.support.ConstructorResolver
. Patching any 5.2.x version of the framework with just the latest ConstructorResolver
file from the 5.3.x branch should be sufficient to address the annotation hotspots above. That said, I'd appreciate a test against the actual latest 5.3.30 snapshot :-)
Comment From: jengebr
Thank you so much!!! The relevant applications are planning the 5.3 upgrade by October, but I'll do some 5.2.x testing this week and get back to you ASAP.
Comment From: jengebr
I confirmed ConstructorResolver works on 5.2. Won't be able to test 5.3 for at least another month but will raise a new, linked issue if something comes up.
Thank you very much!