Skip to content

Commit 735051b

Browse files
committed
Resolve infer destroy method at build-time
This commit allows a RootBeanDefinition to resolve its infer destroy method if necessary. Contrary to BeanInstanceAdapter that uses the actual bean instance, the new method works against the type exposed in the bean definition. The AOT contribution of InitDestroyAnnotationBeanPostProcessor uses the new method to make sure the special '(inferred)' placeholder is handled prior to code generation. Closes gh-28215
1 parent 3f3e37e commit 735051b

File tree

8 files changed

+109
-38
lines changed

8 files changed

+109
-38
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, C
159159
@Override
160160
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
161161
RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition();
162+
beanDefinition.resolveDestroyMethodIfNecessary();
162163
LifecycleMetadata metadata = findInjectionMetadata(beanDefinition, registeredBean.getBeanClass());
163164
if (!CollectionUtils.isEmpty(metadata.initMethods)) {
164165
String[] initMethodNames = safeMerge(beanDefinition.getInitMethodNames(), metadata.initMethods);

spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.util.Arrays;
2525
import java.util.Collections;
2626
import java.util.HashMap;
27-
import java.util.List;
2827
import java.util.Map;
2928
import java.util.Objects;
3029
import java.util.function.BiFunction;
@@ -140,23 +139,16 @@ CodeBlock generateCode(BeanDefinition beanDefinition) {
140139

141140
private void addInitDestroyMethods(Builder builder,
142141
AbstractBeanDefinition beanDefinition, @Nullable String[] methodNames, String format) {
143-
List<String> filteredMethodNames = (!ObjectUtils.isEmpty(methodNames))
144-
? Arrays.stream(methodNames).filter(this::isNotInferredMethod).toList()
145-
: Collections.emptyList();
146-
if (!filteredMethodNames.isEmpty()) {
142+
if (!ObjectUtils.isEmpty(methodNames)) {
147143
Class<?> beanType = ClassUtils.getUserClass(beanDefinition.getResolvableType().toClass());
148-
filteredMethodNames.forEach(methodName -> addInitDestroyHint(beanType, methodName));
149-
CodeBlock arguments = filteredMethodNames.stream()
144+
Arrays.stream(methodNames).forEach(methodName -> addInitDestroyHint(beanType, methodName));
145+
CodeBlock arguments = Arrays.stream(methodNames)
150146
.map(name -> CodeBlock.of("$S", name))
151147
.collect(CodeBlock.joining(", "));
152148
builder.addStatement(format, BEAN_DEFINITION_VARIABLE, arguments);
153149
}
154150
}
155151

156-
private boolean isNotInferredMethod(String candidate) {
157-
return !AbstractBeanDefinition.INFER_METHOD.equals(candidate);
158-
}
159-
160152
private void addInitDestroyHint(Class<?> beanUserClass, String methodName) {
161153
Method method = ReflectionUtils.findMethod(beanUserClass, methodName);
162154
if (method != null) {

spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public DisposableBeanAdapter(Object bean, String beanName, RootBeanDefinition be
105105
this.invokeDisposableBean = (bean instanceof DisposableBean &&
106106
!beanDefinition.hasAnyExternallyManagedDestroyMethod(DESTROY_METHOD_NAME));
107107

108-
String[] destroyMethodNames = inferDestroyMethodsIfNecessary(bean, beanDefinition);
108+
String[] destroyMethodNames = inferDestroyMethodsIfNecessary(bean.getClass(), beanDefinition);
109109
if (!ObjectUtils.isEmpty(destroyMethodNames) &&
110110
!(this.invokeDisposableBean && DESTROY_METHOD_NAME.equals(destroyMethodNames[0])) &&
111111
!beanDefinition.hasAnyExternallyManagedDestroyMethod(destroyMethodNames[0])) {
@@ -325,7 +325,8 @@ protected Object writeReplace() {
325325
* @param beanDefinition the corresponding bean definition
326326
*/
327327
public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) {
328-
return (bean instanceof DisposableBean || inferDestroyMethodsIfNecessary(bean, beanDefinition) != null);
328+
return (bean instanceof DisposableBean
329+
|| inferDestroyMethodsIfNecessary(bean.getClass(), beanDefinition) != null);
329330
}
330331

331332

@@ -343,7 +344,7 @@ public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefin
343344
* interfaces, reflectively calling the "close" method on implementing beans as well.
344345
*/
345346
@Nullable
346-
private static String[] inferDestroyMethodsIfNecessary(Object bean, RootBeanDefinition beanDefinition) {
347+
static String[] inferDestroyMethodsIfNecessary(Class<?> target, RootBeanDefinition beanDefinition) {
347348
String[] destroyMethodNames = beanDefinition.getDestroyMethodNames();
348349
if (destroyMethodNames != null && destroyMethodNames.length > 1) {
349350
return destroyMethodNames;
@@ -352,23 +353,23 @@ private static String[] inferDestroyMethodsIfNecessary(Object bean, RootBeanDefi
352353
String destroyMethodName = beanDefinition.resolvedDestroyMethodName;
353354
if (destroyMethodName == null) {
354355
destroyMethodName = beanDefinition.getDestroyMethodName();
355-
boolean autoCloseable = (bean instanceof AutoCloseable);
356+
boolean autoCloseable = (AutoCloseable.class.isAssignableFrom(target));
356357
if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||
357358
(destroyMethodName == null && autoCloseable)) {
358359
// Only perform destroy method inference in case of the bean
359360
// not explicitly implementing the DisposableBean interface
360361
destroyMethodName = null;
361-
if (!(bean instanceof DisposableBean)) {
362+
if (!(DisposableBean.class.isAssignableFrom(target))) {
362363
if (autoCloseable) {
363364
destroyMethodName = CLOSE_METHOD_NAME;
364365
}
365366
else {
366367
try {
367-
destroyMethodName = bean.getClass().getMethod(CLOSE_METHOD_NAME).getName();
368+
destroyMethodName = target.getMethod(CLOSE_METHOD_NAME).getName();
368369
}
369370
catch (NoSuchMethodException ex) {
370371
try {
371-
destroyMethodName = bean.getClass().getMethod(SHUTDOWN_METHOD_NAME).getName();
372+
destroyMethodName = target.getMethod(SHUTDOWN_METHOD_NAME).getName();
372373
}
373374
catch (NoSuchMethodException ex2) {
374375
// no candidate destroy method found

spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,15 @@ public Set<String> getExternallyManagedInitMethods() {
550550
}
551551
}
552552

553+
/**
554+
* Resolve the inferred destroy method if necessary.
555+
* @since 6.0
556+
*/
557+
public void resolveDestroyMethodIfNecessary() {
558+
setDestroyMethodNames(DisposableBeanAdapter
559+
.inferDestroyMethodsIfNecessary(getResolvableType().toClass(), this));
560+
}
561+
553562
/**
554563
* Register an externally managed configuration destruction method &mdash;
555564
* for example, a method annotated with JSR-250's

spring-beans/src/test/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessorTests.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
import org.junit.jupiter.api.Test;
2020

21+
import org.springframework.beans.factory.support.AbstractBeanDefinition;
2122
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
2223
import org.springframework.beans.factory.support.RegisteredBean;
2324
import org.springframework.beans.factory.support.RootBeanDefinition;
2425
import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.Destroy;
26+
import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.InferredDestroyBean;
2527
import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.Init;
2628
import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.InitDestroyBean;
2729
import org.springframework.beans.testfixture.beans.factory.generator.lifecycle.MultiInitDestroyBean;
@@ -40,7 +42,7 @@ class InitDestroyAnnotationBeanPostProcessorTests {
4042

4143
@Test
4244
void processAheadOfTimeWhenNoCallbackDoesNotMutateRootBeanDefinition() {
43-
RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class);
45+
RootBeanDefinition beanDefinition = new RootBeanDefinition(NoInitDestroyBean.class);
4446
processAheadOfTime(beanDefinition);
4547
RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition();
4648
assertThat(mergedBeanDefinition.getInitMethodNames()).isNull();
@@ -78,6 +80,26 @@ void processAheadOfTimeWhenHasInitDestroyAnnotationsAndOverlappingCustomDefinedM
7880
assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly("destroyMethod");
7981
}
8082

83+
@Test
84+
void processAheadOfTimeWhenHasInferredDestroyMethodAddsDestroyMethodName() {
85+
RootBeanDefinition beanDefinition = new RootBeanDefinition(InferredDestroyBean.class);
86+
beanDefinition.setDestroyMethodNames(AbstractBeanDefinition.INFER_METHOD);
87+
processAheadOfTime(beanDefinition);
88+
RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition();
89+
assertThat(mergedBeanDefinition.getInitMethodNames()).isNull();
90+
assertThat(mergedBeanDefinition.getDestroyMethodNames()).containsExactly("close");
91+
}
92+
93+
@Test
94+
void processAheadOfTimeWhenHasInferredDestroyMethodAndNoCandidateDoesNotMutateRootBeanDefinition() {
95+
RootBeanDefinition beanDefinition = new RootBeanDefinition(NoInitDestroyBean.class);
96+
beanDefinition.setDestroyMethodNames(AbstractBeanDefinition.INFER_METHOD);
97+
processAheadOfTime(beanDefinition);
98+
RootBeanDefinition mergedBeanDefinition = getMergedBeanDefinition();
99+
assertThat(mergedBeanDefinition.getInitMethodNames()).isNull();
100+
assertThat(mergedBeanDefinition.getDestroyMethodNames()).isNull();
101+
}
102+
81103
@Test
82104
void processAheadOfTimeWhenHasMultipleInitDestroyAnnotationsAddsAllMethodNames() {
83105
RootBeanDefinition beanDefinition = new RootBeanDefinition(MultiInitDestroyBean.class);
@@ -110,4 +132,6 @@ private InitDestroyAnnotationBeanPostProcessor createAotBeanPostProcessor() {
110132
return beanPostProcessor;
111133
}
112134

135+
static class NoInitDestroyBean {}
136+
113137
}

spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
3737
import org.springframework.beans.factory.config.RuntimeBeanNameReference;
3838
import org.springframework.beans.factory.config.RuntimeBeanReference;
39-
import org.springframework.beans.factory.support.AbstractBeanDefinition;
4039
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
4140
import org.springframework.beans.factory.support.ManagedList;
4241
import org.springframework.beans.factory.support.ManagedMap;
@@ -225,9 +224,8 @@ void setInitMethodWhenSingleInitMethod() {
225224
}
226225

227226
@Test
228-
void setInitMethodWhenSingleInferredInitMethod() {
227+
void setInitMethodWhenNoInitMethod() {
229228
this.beanDefinition.setTargetType(InitDestroyBean.class);
230-
this.beanDefinition.setInitMethodName(AbstractBeanDefinition.INFER_METHOD);
231229
compile((actual, compiled) -> assertThat(actual.getInitMethodNames()).isNull());
232230
}
233231

@@ -241,13 +239,6 @@ void setInitMethodWhenMultipleInitMethods() {
241239
assertHasMethodInvokeHints(InitDestroyBean.class, methodNames);
242240
}
243241

244-
@Test
245-
void setInitMethodWithInferredMethodFirst() {
246-
this.beanDefinition.setInitMethodNames(AbstractBeanDefinition.INFER_METHOD, "init");
247-
compile((actual, compiled) -> assertThat(compiled.getSourceFile().getContent())
248-
.contains("beanDefinition.setInitMethodNames(\"init\");"));
249-
}
250-
251242
@Test
252243
void setDestroyMethodWhenDestroyInitMethod() {
253244
this.beanDefinition.setTargetType(InitDestroyBean.class);
@@ -260,9 +251,8 @@ void setDestroyMethodWhenDestroyInitMethod() {
260251
}
261252

262253
@Test
263-
void setDestroyMethodWhenSingleInferredInitMethod() {
254+
void setDestroyMethodWhenNoDestroyMethod() {
264255
this.beanDefinition.setTargetType(InitDestroyBean.class);
265-
this.beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
266256
compile((actual, compiled) -> assertThat(actual.getDestroyMethodNames()).isNull());
267257
}
268258

@@ -277,13 +267,6 @@ void setDestroyMethodWhenMultipleDestroyMethods() {
277267
assertHasMethodInvokeHints(InitDestroyBean.class, methodNames);
278268
}
279269

280-
@Test
281-
void setDestroyMethodWithInferredMethodFirst() {
282-
this.beanDefinition.setDestroyMethodNames(AbstractBeanDefinition.INFER_METHOD, "destroy");
283-
compile((actual, compiled) -> assertThat(compiled.getSourceFile().getContent())
284-
.contains("beanDefinition.setDestroyMethodNames(\"destroy\");"));
285-
}
286-
287270
private void assertHasMethodInvokeHints(Class<?> beanType, String... methodNames) {
288271
assertThat(methodNames).allMatch(methodName -> RuntimeHintsPredicates.reflection()
289272
.onMethod(beanType, methodName).invoke()

spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,39 @@ void setInstanceDoesNotOverrideResolvedFactoryMethodWithNull() {
5757
verify(instanceSupplier).getFactoryMethod();
5858
}
5959

60+
@Test
61+
void resolveDestroyMethodWithMatchingCandidateReplacedInferredVaue() {
62+
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithCloseMethod.class);
63+
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
64+
beanDefinition.resolveDestroyMethodIfNecessary();
65+
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close");
66+
}
67+
68+
@Test
69+
void resolveDestroyMethodWithNoCandidateSetDestroyMethodNameToNull() {
70+
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithNoDestroyMethod.class);
71+
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
72+
beanDefinition.resolveDestroyMethodIfNecessary();
73+
assertThat(beanDefinition.getDestroyMethodNames()).isNull();
74+
}
75+
76+
@Test
77+
void resolveDestroyMethodWithNoResolvableType() {
78+
RootBeanDefinition beanDefinition = new RootBeanDefinition();
79+
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
80+
beanDefinition.resolveDestroyMethodIfNecessary();
81+
assertThat(beanDefinition.getDestroyMethodNames()).isNull();
82+
}
83+
84+
static class BeanWithCloseMethod {
85+
86+
public void close() {
87+
}
88+
89+
}
90+
91+
static class BeanWithNoDestroyMethod {
92+
93+
}
94+
6095
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.beans.testfixture.beans.factory.generator.lifecycle;
18+
19+
public class InferredDestroyBean {
20+
21+
public void close() {
22+
23+
}
24+
}
25+
26+

0 commit comments

Comments
 (0)