From 90ef7ac51474cd0784d6eed10ed70d5584045942 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 13 Nov 2024 13:29:20 +0100 Subject: [PATCH] Cache singleton results for @Lazy injection points Includes consistent use of unmodifiable collections. Closes gh-33841 --- .../beans/factory/ObjectProvider.java | 2 + ...xtAnnotationAutowireCandidateResolver.java | 141 ++++++++++++------ .../context/annotation/Lazy.java | 5 +- ...wiredAnnotationBeanPostProcessorTests.java | 33 +++- 4 files changed, 135 insertions(+), 46 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java index 44e3b55f7d..c008e6537c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java @@ -32,6 +32,8 @@ import org.springframework.lang.Nullable; *

In a {@link BeanFactory} environment, every {@code ObjectProvider} obtained * from the factory will be bound to its {@code BeanFactory} for a specific bean * type, matching all provider calls against factory-registered bean definitions. + * Note that all such calls dynamically operate on the underlying factory state, + * freshly resolving the requested target object on every call. * *

As of 5.1, this interface extends {@link Iterable} and provides {@link Stream} * support. It can be therefore be used in {@code for} loops, provides {@link #forEach} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java index dcf4aa91ae..bc012d0580 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.context.annotation; +import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Collection; @@ -27,7 +28,6 @@ import java.util.Set; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver; import org.springframework.beans.factory.config.DependencyDescriptor; @@ -35,7 +35,6 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** * Complete implementation of the @@ -85,47 +84,13 @@ public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotat } private Object buildLazyResolutionProxy( - final DependencyDescriptor descriptor, @Nullable final String beanName, boolean classOnly) { + DependencyDescriptor descriptor, @Nullable String beanName, boolean classOnly) { - BeanFactory beanFactory = getBeanFactory(); - Assert.state(beanFactory instanceof DefaultListableBeanFactory, - "BeanFactory needs to be a DefaultListableBeanFactory"); - final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory; + if (!(getBeanFactory() instanceof DefaultListableBeanFactory dlbf)) { + throw new IllegalStateException("Lazy resolution only supported with DefaultListableBeanFactory"); + } - TargetSource ts = new TargetSource() { - @Override - public Class getTargetClass() { - return descriptor.getDependencyType(); - } - @Override - @SuppressWarnings("NullAway") - public Object getTarget() { - Set autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null); - Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null); - if (target == null) { - Class type = getTargetClass(); - if (Map.class == type) { - return Collections.emptyMap(); - } - else if (List.class == type) { - return Collections.emptyList(); - } - else if (Set.class == type || Collection.class == type) { - return Collections.emptySet(); - } - throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(), - "Optional dependency not present for lazy injection point"); - } - if (autowiredBeanNames != null) { - for (String autowiredBeanName : autowiredBeanNames) { - if (dlbf.containsBean(autowiredBeanName)) { - dlbf.registerDependentBean(autowiredBeanName, beanName); - } - } - } - return target; - } - }; + TargetSource ts = new LazyDependencyTargetSource(dlbf, descriptor, beanName); ProxyFactory pf = new ProxyFactory(); pf.setTargetSource(ts); @@ -137,4 +102,96 @@ public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotat return (classOnly ? pf.getProxyClass(classLoader) : pf.getProxy(classLoader)); } + + @SuppressWarnings("serial") + private static class LazyDependencyTargetSource implements TargetSource, Serializable { + + private final DefaultListableBeanFactory beanFactory; + + private final DependencyDescriptor descriptor; + + @Nullable + private final String beanName; + + @Nullable + private transient volatile Object cachedTarget; + + public LazyDependencyTargetSource(DefaultListableBeanFactory beanFactory, + DependencyDescriptor descriptor, @Nullable String beanName) { + + this.beanFactory = beanFactory; + this.descriptor = descriptor; + this.beanName = beanName; + } + + @Override + public Class getTargetClass() { + return this.descriptor.getDependencyType(); + } + + @Override + @SuppressWarnings("NullAway") + public Object getTarget() { + Object cachedTarget = this.cachedTarget; + if (cachedTarget != null) { + return cachedTarget; + } + + Set autowiredBeanNames = new LinkedHashSet<>(2); + Object target = this.beanFactory.doResolveDependency( + this.descriptor, this.beanName, autowiredBeanNames, null); + + if (target == null) { + Class type = getTargetClass(); + if (Map.class == type) { + target = Collections.emptyMap(); + } + else if (List.class == type) { + target = Collections.emptyList(); + } + else if (Set.class == type || Collection.class == type) { + target = Collections.emptySet(); + } + else { + throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType(), + "Optional dependency not present for lazy injection point"); + } + } + else { + if (target instanceof Map map && Map.class == getTargetClass()) { + target = Collections.unmodifiableMap(map); + } + else if (target instanceof List list && List.class == getTargetClass()) { + target = Collections.unmodifiableList(list); + } + else if (target instanceof Set set && Set.class == getTargetClass()) { + target = Collections.unmodifiableSet(set); + } + else if (target instanceof Collection coll && Collection.class == getTargetClass()) { + target = Collections.unmodifiableCollection(coll); + } + } + + boolean cacheable = true; + for (String autowiredBeanName : autowiredBeanNames) { + if (!this.beanFactory.containsBean(autowiredBeanName)) { + cacheable = false; + } + else { + if (this.beanName != null) { + this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName); + } + if (!this.beanFactory.isSingleton(autowiredBeanName)) { + cacheable = false; + } + } + if (cacheable) { + this.cachedTarget = target; + } + } + + return target; + } + } + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java b/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java index 19ff1eb1d9..2aad69b606 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,8 @@ import java.lang.annotation.Target; *

In addition to its role for component initialization, this annotation may also be placed * on injection points marked with {@link org.springframework.beans.factory.annotation.Autowired} * or {@link jakarta.inject.Inject}: In that context, it leads to the creation of a - * lazy-resolution proxy for all affected dependencies, as an alternative to using + * lazy-resolution proxy for the affected dependency, caching it on first access in case of + * a singleton or re-resolving it on every access otherwise. This is an alternative to using * {@link org.springframework.beans.factory.ObjectFactory} or {@link jakarta.inject.Provider}. * Please note that such a lazy-resolution proxy will always be injected; if the target * dependency does not exist, you will only be able to find out through an exception on diff --git a/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java index 0eecab3836..9faa292a98 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.Advised; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; @@ -67,7 +69,7 @@ class LazyAutowiredAnnotationBeanPostProcessorTests { } @Test - void lazyResourceInjectionWithField() { + void lazyResourceInjectionWithField() throws Exception { doTestLazyResourceInjection(FieldResourceInjectionBean.class); AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); @@ -84,9 +86,36 @@ class LazyAutowiredAnnotationBeanPostProcessorTests { assertThat(bean.getTestBeans()).isNotEmpty(); assertThat(bean.getTestBeans().get(0).getName()).isNull(); assertThat(ac.getBeanFactory().containsSingleton("testBean")).isTrue(); + TestBean tb = (TestBean) ac.getBean("testBean"); tb.setName("tb"); assertThat(bean.getTestBean().getName()).isSameAs("tb"); + + assertThat(bean.getTestBeans() instanceof Advised).isTrue(); + TargetSource targetSource = ((Advised) bean.getTestBeans()).getTargetSource(); + assertThat(targetSource.getTarget()).isSameAs(targetSource.getTarget()); + + ac.close(); + } + + @Test + void lazyResourceInjectionWithFieldForPrototype() { + doTestLazyResourceInjection(FieldResourceInjectionBean.class); + + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); + RootBeanDefinition abd = new RootBeanDefinition(FieldResourceInjectionBean.class); + abd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + ac.registerBeanDefinition("annotatedBean", abd); + RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class); + tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + tbd.setLazyInit(true); + ac.registerBeanDefinition("testBean", tbd); + ac.refresh(); + + FieldResourceInjectionBean bean = ac.getBean("annotatedBean", FieldResourceInjectionBean.class); + assertThat(bean.getTestBeans()).isNotEmpty(); + TestBean tb = bean.getTestBeans().get(0); + assertThat(bean.getTestBeans().get(0)).isNotSameAs(tb); ac.close(); }