Skip to content

Commit 96b3258

Browse files
committed
Handle deprecation warning in AOT-generated code
Closes gh-29597
2 parents 2754da1 + ea66883 commit 96b3258

File tree

14 files changed

+782
-69
lines changed

14 files changed

+782
-69
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,14 @@ private GeneratedMethod generateBeanDefinitionMethod(GenerationContext generatio
164164

165165
this.aotContributions.forEach(aotContribution -> aotContribution.applyTo(generationContext, codeGenerator));
166166

167+
CodeWarnings codeWarnings = new CodeWarnings();
168+
codeWarnings.detectDeprecation(this.registeredBean.getBeanClass());
167169
return generatedMethods.add("getBeanDefinition", method -> {
168170
method.addJavadoc("Get the $L definition for '$L'.",
169171
(this.registeredBean.isInnerBean() ? "inner-bean" : "bean"),
170172
getName());
171173
method.addModifiers(modifier, Modifier.STATIC);
174+
codeWarnings.suppress(method);
172175
method.returns(BeanDefinition.class);
173176
method.addCode(codeGenerator.generateCode(generationContext));
174177
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2002-2023 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.factory.aot;
18+
19+
import java.lang.reflect.AnnotatedElement;
20+
import java.util.Collections;
21+
import java.util.LinkedHashSet;
22+
import java.util.Set;
23+
import java.util.StringJoiner;
24+
import java.util.stream.Stream;
25+
26+
import org.springframework.javapoet.AnnotationSpec;
27+
import org.springframework.javapoet.CodeBlock;
28+
import org.springframework.javapoet.MethodSpec;
29+
import org.springframework.lang.Nullable;
30+
31+
/**
32+
* Helper class to register warnings that the compiler may trigger on
33+
* generated code.
34+
*
35+
* @author Stephane Nicoll
36+
* @see SuppressWarnings
37+
*/
38+
class CodeWarnings {
39+
40+
private final Set<String> warnings = new LinkedHashSet<>();
41+
42+
/**
43+
* Register a warning to be included for this block. Does nothing if
44+
* the warning is already registered.
45+
* @param warning the warning to register, if it hasn't been already
46+
*/
47+
public void register(String warning) {
48+
this.warnings.add(warning);
49+
}
50+
51+
/**
52+
* Detect the presence of {@link Deprecated} on the specified elements.
53+
* @param elements the elements to check
54+
* @return {@code this} instance
55+
*/
56+
public CodeWarnings detectDeprecation(AnnotatedElement... elements) {
57+
for (AnnotatedElement element : elements) {
58+
register(element.getAnnotation(Deprecated.class));
59+
}
60+
return this;
61+
}
62+
63+
/**
64+
* Detect the presence of {@link Deprecated} on the specified elements.
65+
* @param elements the elements to check
66+
* @return {@code this} instance
67+
*/
68+
public CodeWarnings detectDeprecation(Stream<AnnotatedElement> elements) {
69+
elements.forEach(element -> register(element.getAnnotation(Deprecated.class)));
70+
return this;
71+
}
72+
73+
/**
74+
* Include a {@link SuppressWarnings} on the specified method if necessary.
75+
* @param method the method to update
76+
*/
77+
public void suppress(MethodSpec.Builder method) {
78+
if (this.warnings.isEmpty()) {
79+
return;
80+
}
81+
method.addAnnotation(buildAnnotationSpec());
82+
}
83+
84+
/**
85+
* Return the currently registered warnings.
86+
* @return the warnings
87+
*/
88+
protected Set<String> getWarnings() {
89+
return Collections.unmodifiableSet(this.warnings);
90+
}
91+
92+
private void register(@Nullable Deprecated annotation) {
93+
if (annotation != null) {
94+
if (annotation.forRemoval()) {
95+
register("removal");
96+
}
97+
else {
98+
register("deprecation");
99+
}
100+
}
101+
}
102+
103+
private AnnotationSpec buildAnnotationSpec() {
104+
return AnnotationSpec.builder(SuppressWarnings.class)
105+
.addMember("value", generateValueCode()).build();
106+
}
107+
108+
private CodeBlock generateValueCode() {
109+
if (this.warnings.size() == 1) {
110+
return CodeBlock.of("$S", this.warnings.iterator().next());
111+
}
112+
CodeBlock values = CodeBlock.join(this.warnings.stream()
113+
.map(warning -> CodeBlock.of("$S", warning)).toList(), ", ");
114+
return CodeBlock.of("{ $L }", values);
115+
}
116+
117+
@Override
118+
public String toString() {
119+
return new StringJoiner(", ", CodeWarnings.class.getSimpleName(), "")
120+
.add(this.warnings.toString())
121+
.toString();
122+
}
123+
124+
}
125+

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.lang.reflect.Member;
2222
import java.lang.reflect.Method;
2323
import java.lang.reflect.Modifier;
24+
import java.lang.reflect.Parameter;
2425
import java.lang.reflect.Proxy;
2526
import java.util.Arrays;
2627
import java.util.function.Consumer;
@@ -189,11 +190,15 @@ private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class<?>
189190
private CodeBlock generateCodeForInaccessibleConstructor(String beanName, Class<?> beanClass,
190191
Constructor<?> constructor, boolean dependsOnBean, Consumer<ReflectionHints> hints) {
191192

193+
CodeWarnings codeWarnings = new CodeWarnings();
194+
codeWarnings.detectDeprecation(beanClass, constructor)
195+
.detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType));
192196
hints.accept(this.generationContext.getRuntimeHints().reflection());
193197

194198
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> {
195199
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
196200
method.addModifiers(PRIVATE_STATIC);
201+
codeWarnings.suppress(method);
197202
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass));
198203
int parameterOffset = (!dependsOnBean) ? 0 : 1;
199204
method.addStatement(generateResolverForConstructor(beanClass, constructor, parameterOffset));
@@ -206,8 +211,12 @@ private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method,
206211
String beanName, Class<?> beanClass, Constructor<?> constructor, Class<?> declaringClass,
207212
boolean dependsOnBean, javax.lang.model.element.Modifier... modifiers) {
208213

214+
CodeWarnings codeWarnings = new CodeWarnings();
215+
codeWarnings.detectDeprecation(beanClass, constructor, declaringClass)
216+
.detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType));
209217
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
210218
method.addModifiers(modifiers);
219+
codeWarnings.suppress(method);
211220
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, beanClass));
212221

213222
int parameterOffset = (!dependsOnBean) ? 0 : 1;
@@ -300,9 +309,13 @@ private void buildGetInstanceMethodForFactoryMethod(MethodSpec.Builder method,
300309

301310
String factoryMethodName = factoryMethod.getName();
302311
Class<?> suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType());
312+
CodeWarnings codeWarnings = new CodeWarnings();
313+
codeWarnings.detectDeprecation(declaringClass, factoryMethod, suppliedType)
314+
.detectDeprecation(Arrays.stream(factoryMethod.getParameters()).map(Parameter::getType));
303315

304316
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
305317
method.addModifiers(modifiers);
318+
codeWarnings.suppress(method);
306319
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, suppliedType));
307320

308321
CodeBlock.Builder code = CodeBlock.builder();

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import javax.lang.model.element.Modifier;
2727
import javax.xml.parsers.DocumentBuilderFactory;
2828

29+
import org.junit.jupiter.api.Nested;
2930
import org.junit.jupiter.api.Test;
3031

3132
import org.springframework.aot.generate.GeneratedMethod;
@@ -52,6 +53,7 @@
5253
import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.Implementation;
5354
import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.One;
5455
import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.Two;
56+
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedBean;
5557
import org.springframework.core.ResolvableType;
5658
import org.springframework.core.test.io.support.MockSpringFactoriesLoader;
5759
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
@@ -66,6 +68,7 @@
6668

6769
import static org.assertj.core.api.Assertions.assertThat;
6870
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
71+
import static org.assertj.core.api.Assertions.assertThatNoException;
6972

7073
/**
7174
* Tests for {@link BeanDefinitionMethodGenerator} and
@@ -740,6 +743,32 @@ public CodeBlock generateInstanceSupplierCode(GenerationContext generationContex
740743
});
741744
}
742745

746+
@Nested
747+
@SuppressWarnings("deprecation")
748+
class DeprecationTests {
749+
750+
private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem()
751+
.withCompilerOptions("-Xlint:all", "-Xlint:-rawtypes", "-Werror");
752+
753+
@Test
754+
void generateBeanDefinitionMethodWithDeprecatedTargetClass() {
755+
RootBeanDefinition beanDefinition = new RootBeanDefinition(DeprecatedBean.class);
756+
RegisteredBean registeredBean = registerBean(beanDefinition);
757+
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
758+
methodGeneratorFactory, registeredBean, null,
759+
Collections.emptyList());
760+
MethodReference method = generator.generateBeanDefinitionMethod(
761+
generationContext, beanRegistrationsCode);
762+
compileAndCheckWarnings(method);
763+
}
764+
765+
private void compileAndCheckWarnings(MethodReference methodReference) {
766+
assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, methodReference,
767+
((instanceSupplier, compiled) -> {})));
768+
}
769+
770+
}
771+
743772
private void testBeanDefinitionMethodInCurrentFile(Class<?> targetType, RootBeanDefinition beanDefinition) {
744773
RegisteredBean registeredBean = registerBean(new RootBeanDefinition(beanDefinition));
745774
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
@@ -764,6 +793,10 @@ private RegisteredBean registerBean(RootBeanDefinition beanDefinition) {
764793
}
765794

766795
private void compile(MethodReference method, BiConsumer<RootBeanDefinition, Compiled> result) {
796+
compile(TestCompiler.forSystem(), method, result);
797+
}
798+
799+
private void compile(TestCompiler testCompiler, MethodReference method, BiConsumer<RootBeanDefinition, Compiled> result) {
767800
this.beanRegistrationsCode.getTypeBuilder().set(type -> {
768801
CodeBlock methodInvocation = method.toInvokeCodeBlock(ArgumentCodeGenerator.none(),
769802
this.beanRegistrationsCode.getClassName());
@@ -775,7 +808,7 @@ private void compile(MethodReference method, BiConsumer<RootBeanDefinition, Comp
775808
.addCode("return $L;", methodInvocation).build());
776809
});
777810
this.generationContext.writeGeneratedContent();
778-
TestCompiler.forSystem().with(this.generationContext).compile(compiled ->
811+
testCompiler.with(this.generationContext).compile(compiled ->
779812
result.accept((RootBeanDefinition) compiled.getInstance(Supplier.class).get(), compiled));
780813
}
781814

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2002-2023 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.factory.aot;
18+
19+
import java.util.function.Consumer;
20+
21+
import javax.lang.model.element.Modifier;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.aot.test.generate.TestGenerationContext;
26+
import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder;
27+
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedBean;
28+
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalBean;
29+
import org.springframework.core.test.tools.Compiled;
30+
import org.springframework.core.test.tools.TestCompiler;
31+
import org.springframework.javapoet.MethodSpec;
32+
import org.springframework.javapoet.MethodSpec.Builder;
33+
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
36+
/**
37+
* Tests for {@link CodeWarnings}.
38+
*
39+
* @author Stephane Nicoll
40+
*/
41+
class CodeWarningsTests {
42+
43+
private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem()
44+
.withCompilerOptions("-Xlint:all", "-Werror");
45+
46+
private final CodeWarnings codeWarnings;
47+
48+
private final TestGenerationContext generationContext;
49+
50+
CodeWarningsTests() {
51+
this.codeWarnings = new CodeWarnings();
52+
this.generationContext = new TestGenerationContext();
53+
}
54+
55+
@Test
56+
void registerNoWarningDoesNotIncludeAnnotation() {
57+
compile(method -> {
58+
this.codeWarnings.suppress(method);
59+
method.addStatement("$T bean = $S", String.class, "Hello");
60+
}, compiled -> assertThat(compiled.getSourceFile()).doesNotContain("@SuppressWarnings"));
61+
}
62+
63+
@Test
64+
@SuppressWarnings("deprecation")
65+
void registerWarningSuppressesIt() {
66+
this.codeWarnings.register("deprecation");
67+
compile(method -> {
68+
this.codeWarnings.suppress(method);
69+
method.addStatement("$T bean = new $T()", DeprecatedBean.class, DeprecatedBean.class);
70+
}, compiled -> assertThat(compiled.getSourceFile())
71+
.contains("@SuppressWarnings(\"deprecation\")"));
72+
}
73+
74+
@Test
75+
@SuppressWarnings({ "deprecation", "removal" })
76+
void registerSeveralWarningsSuppressesThem() {
77+
this.codeWarnings.register("deprecation");
78+
this.codeWarnings.register("removal");
79+
compile(method -> {
80+
this.codeWarnings.suppress(method);
81+
method.addStatement("$T bean = new $T()", DeprecatedBean.class, DeprecatedBean.class);
82+
method.addStatement("$T another = new $T()", DeprecatedForRemovalBean.class, DeprecatedForRemovalBean.class);
83+
}, compiled -> assertThat(compiled.getSourceFile())
84+
.contains("@SuppressWarnings({ \"deprecation\", \"removal\" })"));
85+
}
86+
87+
@Test
88+
@SuppressWarnings("deprecation")
89+
void detectDeprecationOnAnnotatedElementWithDeprecated() {
90+
this.codeWarnings.detectDeprecation(DeprecatedBean.class);
91+
assertThat(this.codeWarnings.getWarnings()).containsExactly("deprecation");
92+
}
93+
94+
@Test
95+
@SuppressWarnings("removal")
96+
void detectDeprecationOnAnnotatedElementWithDeprecatedForRemoval() {
97+
this.codeWarnings.detectDeprecation(DeprecatedForRemovalBean.class);
98+
assertThat(this.codeWarnings.getWarnings()).containsExactly("removal");
99+
}
100+
101+
@Test
102+
void toStringIncludeWarnings() {
103+
this.codeWarnings.register("deprecation");
104+
this.codeWarnings.register("rawtypes");
105+
assertThat(this.codeWarnings).hasToString("CodeWarnings[deprecation, rawtypes]");
106+
}
107+
108+
private void compile(Consumer<Builder> method,
109+
Consumer<Compiled> result) {
110+
DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder();
111+
this.generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder);
112+
typeBuilder.set(type -> {
113+
type.addModifiers(Modifier.PUBLIC);
114+
Builder methodBuilder = MethodSpec.methodBuilder("apply")
115+
.addModifiers(Modifier.PUBLIC);
116+
method.accept(methodBuilder);
117+
type.addMethod(methodBuilder.build());
118+
});
119+
this.generationContext.writeGeneratedContent();
120+
TEST_COMPILER.with(this.generationContext).compile(result);
121+
}
122+
123+
}

0 commit comments

Comments
 (0)