Skip to content

Commit 90ef7ac

Browse files
committed
Cache singleton results for @lazy injection points
Includes consistent use of unmodifiable collections. Closes gh-33841
1 parent c399139 commit 90ef7ac

File tree

4 files changed

+135
-46
lines changed

4 files changed

+135
-46
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* <p>In a {@link BeanFactory} environment, every {@code ObjectProvider} obtained
3333
* from the factory will be bound to its {@code BeanFactory} for a specific bean
3434
* type, matching all provider calls against factory-registered bean definitions.
35+
* Note that all such calls dynamically operate on the underlying factory state,
36+
* freshly resolving the requested target object on every call.
3537
*
3638
* <p>As of 5.1, this interface extends {@link Iterable} and provides {@link Stream}
3739
* support. It can be therefore be used in {@code for} loops, provides {@link #forEach}

spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java

+99-42
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.context.annotation;
1818

19+
import java.io.Serializable;
1920
import java.lang.annotation.Annotation;
2021
import java.lang.reflect.Method;
2122
import java.util.Collection;
@@ -27,15 +28,13 @@
2728

2829
import org.springframework.aop.TargetSource;
2930
import org.springframework.aop.framework.ProxyFactory;
30-
import org.springframework.beans.factory.BeanFactory;
3131
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3232
import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver;
3333
import org.springframework.beans.factory.config.DependencyDescriptor;
3434
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
3535
import org.springframework.core.MethodParameter;
3636
import org.springframework.core.annotation.AnnotationUtils;
3737
import org.springframework.lang.Nullable;
38-
import org.springframework.util.Assert;
3938

4039
/**
4140
* Complete implementation of the
@@ -85,47 +84,13 @@ protected Object buildLazyResolutionProxy(DependencyDescriptor descriptor, @Null
8584
}
8685

8786
private Object buildLazyResolutionProxy(
88-
final DependencyDescriptor descriptor, @Nullable final String beanName, boolean classOnly) {
87+
DependencyDescriptor descriptor, @Nullable String beanName, boolean classOnly) {
8988

90-
BeanFactory beanFactory = getBeanFactory();
91-
Assert.state(beanFactory instanceof DefaultListableBeanFactory,
92-
"BeanFactory needs to be a DefaultListableBeanFactory");
93-
final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory;
89+
if (!(getBeanFactory() instanceof DefaultListableBeanFactory dlbf)) {
90+
throw new IllegalStateException("Lazy resolution only supported with DefaultListableBeanFactory");
91+
}
9492

95-
TargetSource ts = new TargetSource() {
96-
@Override
97-
public Class<?> getTargetClass() {
98-
return descriptor.getDependencyType();
99-
}
100-
@Override
101-
@SuppressWarnings("NullAway")
102-
public Object getTarget() {
103-
Set<String> autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null);
104-
Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null);
105-
if (target == null) {
106-
Class<?> type = getTargetClass();
107-
if (Map.class == type) {
108-
return Collections.emptyMap();
109-
}
110-
else if (List.class == type) {
111-
return Collections.emptyList();
112-
}
113-
else if (Set.class == type || Collection.class == type) {
114-
return Collections.emptySet();
115-
}
116-
throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(),
117-
"Optional dependency not present for lazy injection point");
118-
}
119-
if (autowiredBeanNames != null) {
120-
for (String autowiredBeanName : autowiredBeanNames) {
121-
if (dlbf.containsBean(autowiredBeanName)) {
122-
dlbf.registerDependentBean(autowiredBeanName, beanName);
123-
}
124-
}
125-
}
126-
return target;
127-
}
128-
};
93+
TargetSource ts = new LazyDependencyTargetSource(dlbf, descriptor, beanName);
12994

13095
ProxyFactory pf = new ProxyFactory();
13196
pf.setTargetSource(ts);
@@ -137,4 +102,96 @@ else if (Set.class == type || Collection.class == type) {
137102
return (classOnly ? pf.getProxyClass(classLoader) : pf.getProxy(classLoader));
138103
}
139104

105+
106+
@SuppressWarnings("serial")
107+
private static class LazyDependencyTargetSource implements TargetSource, Serializable {
108+
109+
private final DefaultListableBeanFactory beanFactory;
110+
111+
private final DependencyDescriptor descriptor;
112+
113+
@Nullable
114+
private final String beanName;
115+
116+
@Nullable
117+
private transient volatile Object cachedTarget;
118+
119+
public LazyDependencyTargetSource(DefaultListableBeanFactory beanFactory,
120+
DependencyDescriptor descriptor, @Nullable String beanName) {
121+
122+
this.beanFactory = beanFactory;
123+
this.descriptor = descriptor;
124+
this.beanName = beanName;
125+
}
126+
127+
@Override
128+
public Class<?> getTargetClass() {
129+
return this.descriptor.getDependencyType();
130+
}
131+
132+
@Override
133+
@SuppressWarnings("NullAway")
134+
public Object getTarget() {
135+
Object cachedTarget = this.cachedTarget;
136+
if (cachedTarget != null) {
137+
return cachedTarget;
138+
}
139+
140+
Set<String> autowiredBeanNames = new LinkedHashSet<>(2);
141+
Object target = this.beanFactory.doResolveDependency(
142+
this.descriptor, this.beanName, autowiredBeanNames, null);
143+
144+
if (target == null) {
145+
Class<?> type = getTargetClass();
146+
if (Map.class == type) {
147+
target = Collections.emptyMap();
148+
}
149+
else if (List.class == type) {
150+
target = Collections.emptyList();
151+
}
152+
else if (Set.class == type || Collection.class == type) {
153+
target = Collections.emptySet();
154+
}
155+
else {
156+
throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType(),
157+
"Optional dependency not present for lazy injection point");
158+
}
159+
}
160+
else {
161+
if (target instanceof Map<?, ?> map && Map.class == getTargetClass()) {
162+
target = Collections.unmodifiableMap(map);
163+
}
164+
else if (target instanceof List<?> list && List.class == getTargetClass()) {
165+
target = Collections.unmodifiableList(list);
166+
}
167+
else if (target instanceof Set<?> set && Set.class == getTargetClass()) {
168+
target = Collections.unmodifiableSet(set);
169+
}
170+
else if (target instanceof Collection<?> coll && Collection.class == getTargetClass()) {
171+
target = Collections.unmodifiableCollection(coll);
172+
}
173+
}
174+
175+
boolean cacheable = true;
176+
for (String autowiredBeanName : autowiredBeanNames) {
177+
if (!this.beanFactory.containsBean(autowiredBeanName)) {
178+
cacheable = false;
179+
}
180+
else {
181+
if (this.beanName != null) {
182+
this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName);
183+
}
184+
if (!this.beanFactory.isSingleton(autowiredBeanName)) {
185+
cacheable = false;
186+
}
187+
}
188+
if (cacheable) {
189+
this.cachedTarget = target;
190+
}
191+
}
192+
193+
return target;
194+
}
195+
}
196+
140197
}

spring-context/src/main/java/org/springframework/context/annotation/Lazy.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -45,7 +45,8 @@
4545
* <p>In addition to its role for component initialization, this annotation may also be placed
4646
* on injection points marked with {@link org.springframework.beans.factory.annotation.Autowired}
4747
* or {@link jakarta.inject.Inject}: In that context, it leads to the creation of a
48-
* lazy-resolution proxy for all affected dependencies, as an alternative to using
48+
* lazy-resolution proxy for the affected dependency, caching it on first access in case of
49+
* a singleton or re-resolving it on every access otherwise. This is an alternative to using
4950
* {@link org.springframework.beans.factory.ObjectFactory} or {@link jakarta.inject.Provider}.
5051
* Please note that such a lazy-resolution proxy will always be injected; if the target
5152
* dependency does not exist, you will only be able to find out through an exception on

spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java

+31-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,8 @@
2222

2323
import org.junit.jupiter.api.Test;
2424

25+
import org.springframework.aop.TargetSource;
26+
import org.springframework.aop.framework.Advised;
2527
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
2628
import org.springframework.beans.factory.annotation.Autowired;
2729
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
@@ -67,7 +69,7 @@ private void doTestLazyResourceInjection(Class<? extends TestBeanHolder> annotat
6769
}
6870

6971
@Test
70-
void lazyResourceInjectionWithField() {
72+
void lazyResourceInjectionWithField() throws Exception {
7173
doTestLazyResourceInjection(FieldResourceInjectionBean.class);
7274

7375
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
@@ -84,9 +86,36 @@ void lazyResourceInjectionWithField() {
8486
assertThat(bean.getTestBeans()).isNotEmpty();
8587
assertThat(bean.getTestBeans().get(0).getName()).isNull();
8688
assertThat(ac.getBeanFactory().containsSingleton("testBean")).isTrue();
89+
8790
TestBean tb = (TestBean) ac.getBean("testBean");
8891
tb.setName("tb");
8992
assertThat(bean.getTestBean().getName()).isSameAs("tb");
93+
94+
assertThat(bean.getTestBeans() instanceof Advised).isTrue();
95+
TargetSource targetSource = ((Advised) bean.getTestBeans()).getTargetSource();
96+
assertThat(targetSource.getTarget()).isSameAs(targetSource.getTarget());
97+
98+
ac.close();
99+
}
100+
101+
@Test
102+
void lazyResourceInjectionWithFieldForPrototype() {
103+
doTestLazyResourceInjection(FieldResourceInjectionBean.class);
104+
105+
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
106+
RootBeanDefinition abd = new RootBeanDefinition(FieldResourceInjectionBean.class);
107+
abd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
108+
ac.registerBeanDefinition("annotatedBean", abd);
109+
RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class);
110+
tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
111+
tbd.setLazyInit(true);
112+
ac.registerBeanDefinition("testBean", tbd);
113+
ac.refresh();
114+
115+
FieldResourceInjectionBean bean = ac.getBean("annotatedBean", FieldResourceInjectionBean.class);
116+
assertThat(bean.getTestBeans()).isNotEmpty();
117+
TestBean tb = bean.getTestBeans().get(0);
118+
assertThat(bean.getTestBeans().get(0)).isNotSameAs(tb);
90119
ac.close();
91120
}
92121

0 commit comments

Comments
 (0)