Skip to content

Commit 42ace2c

Browse files
committed
Provide dedicated AOT exception hierarchy
This commit adds a number of catch point that provides additional context when an AOT processor fails to execute. Amongst other things, this makes sure that the bean name and its descriptor is consistently provided in the error message when available. Closes gh-32777
1 parent f31113e commit 42ace2c

13 files changed

+294
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2002-2024 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 org.springframework.beans.factory.support.RegisteredBean;
20+
import org.springframework.beans.factory.support.RootBeanDefinition;
21+
import org.springframework.lang.Nullable;
22+
23+
/**
24+
* Thrown when AOT fails to process a bean.
25+
*
26+
* @author Stephane Nicoll
27+
* @since 6.2
28+
*/
29+
@SuppressWarnings("serial")
30+
public class AotBeanProcessingException extends AotProcessingException {
31+
32+
private final RootBeanDefinition beanDefinition;
33+
34+
/**
35+
* Create an instance with the {@link RegisteredBean} that fails to be
36+
* processed, a detail message, and an optional root cause.
37+
* @param registeredBean the registered bean that fails to be processed
38+
* @param msg the detail message
39+
* @param cause the root cause, if any
40+
*/
41+
public AotBeanProcessingException(RegisteredBean registeredBean, String msg, @Nullable Throwable cause) {
42+
super(createErrorMessage(registeredBean, msg), cause);
43+
this.beanDefinition = registeredBean.getMergedBeanDefinition();
44+
}
45+
46+
/**
47+
* Shortcut to create an instance with the {@link RegisteredBean} that fails
48+
* to be processed with only a detail message.
49+
* @param registeredBean the registered bean that fails to be processed
50+
* @param msg the detail message
51+
*/
52+
public AotBeanProcessingException(RegisteredBean registeredBean, String msg) {
53+
this(registeredBean, msg, null);
54+
}
55+
56+
private static String createErrorMessage(RegisteredBean registeredBean, String msg) {
57+
StringBuilder sb = new StringBuilder("Error processing bean with name '");
58+
sb.append(registeredBean.getBeanName()).append("'");
59+
String resourceDescription = registeredBean.getMergedBeanDefinition().getResourceDescription();
60+
if (resourceDescription != null) {
61+
sb.append(" defined in ").append(resourceDescription);
62+
}
63+
sb.append(": ").append(msg);
64+
return sb.toString();
65+
}
66+
67+
/**
68+
* Return the bean definition of the bean that failed to be processed.
69+
*/
70+
public RootBeanDefinition getBeanDefinition() {
71+
return this.beanDefinition;
72+
}
73+
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2002-2024 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 org.springframework.lang.Nullable;
20+
21+
/**
22+
* Abstract superclass for all exceptions thrown by ahead-of-time processing.
23+
*
24+
* @author Stephane Nicoll
25+
* @since 6.2
26+
*/
27+
@SuppressWarnings("serial")
28+
public abstract class AotException extends RuntimeException {
29+
30+
/**
31+
* Create an instance with the specified message and root cause.
32+
* @param msg the detail message
33+
* @param cause the root cause
34+
*/
35+
protected AotException(@Nullable String msg, @Nullable Throwable cause) {
36+
super(msg, cause);
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2002-2024 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 org.springframework.lang.Nullable;
20+
21+
/**
22+
* Throw when an AOT processor failed.
23+
*
24+
* @author Stephane Nicoll
25+
* @since 6.2
26+
*/
27+
@SuppressWarnings("serial")
28+
public class AotProcessingException extends AotException {
29+
30+
/**
31+
* Create a new instance with the detail message and a root cause, if any.
32+
* @param msg the detail message
33+
* @param cause the root cause, if any
34+
*/
35+
public AotProcessingException(String msg, @Nullable Throwable cause) {
36+
super(msg, cause);
37+
}
38+
39+
}

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

+15-6
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,21 @@ private void generateRegisterBeanDefinitionsMethod(MethodSpec.Builder method,
8686
method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME);
8787
CodeBlock.Builder code = CodeBlock.builder();
8888
this.registrations.forEach(registration -> {
89-
MethodReference beanDefinitionMethod = registration.methodGenerator
90-
.generateBeanDefinitionMethod(generationContext, beanRegistrationsCode);
91-
CodeBlock methodInvocation = beanDefinitionMethod.toInvokeCodeBlock(
92-
ArgumentCodeGenerator.none(), beanRegistrationsCode.getClassName());
93-
code.addStatement("$L.registerBeanDefinition($S, $L)",
94-
BEAN_FACTORY_PARAMETER_NAME, registration.beanName(), methodInvocation);
89+
try {
90+
MethodReference beanDefinitionMethod = registration.methodGenerator
91+
.generateBeanDefinitionMethod(generationContext, beanRegistrationsCode);
92+
CodeBlock methodInvocation = beanDefinitionMethod.toInvokeCodeBlock(
93+
ArgumentCodeGenerator.none(), beanRegistrationsCode.getClassName());
94+
code.addStatement("$L.registerBeanDefinition($S, $L)",
95+
BEAN_FACTORY_PARAMETER_NAME, registration.beanName(), methodInvocation);
96+
}
97+
catch (AotException ex) {
98+
throw ex;
99+
}
100+
catch (Exception ex) {
101+
throw new AotBeanProcessingException(registration.registeredBean,
102+
"failed to generate code for bean definition", ex);
103+
}
95104
});
96105
method.addCode(code.build());
97106
}

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

+2-5
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
7979
@Override
8080
public ClassName getTarget(RegisteredBean registeredBean) {
8181
if (hasInstanceSupplier()) {
82-
String resourceDescription = registeredBean.getMergedBeanDefinition().getResourceDescription();
83-
throw new IllegalStateException("Error processing bean with name '" + registeredBean.getBeanName() + "'" +
84-
(resourceDescription != null ? " defined in " + resourceDescription : "") + ": instance supplier is not supported");
82+
throw new AotBeanProcessingException(registeredBean, "instance supplier is not supported");
8583
}
8684
Class<?> target = extractDeclaringClass(registeredBean, this.instantiationDescriptor.get());
8785
while (target.getName().startsWith("java.") && registeredBean.isInnerBean()) {
@@ -236,8 +234,7 @@ public CodeBlock generateSetBeanInstanceSupplierCode(
236234
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
237235
BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
238236
if (hasInstanceSupplier()) {
239-
throw new IllegalStateException("Default code generation is not supported for bean definitions declaring "
240-
+ "an instance supplier callback: " + this.registeredBean.getMergedBeanDefinition());
237+
throw new AotBeanProcessingException(this.registeredBean, "instance supplier is not supported");
241238
}
242239
return new InstanceSupplierCodeGenerator(generationContext, beanRegistrationCode.getClassName(),
243240
beanRegistrationCode.getMethods(), allowDirectSupplierShortcut).generateCode(

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,7 @@ public CodeBlock generateCode(RegisteredBean registeredBean, InstantiationDescri
145145
if (constructorOrFactoryMethod instanceof Method method && !KotlinDetector.isSuspendingFunction(method)) {
146146
return generateCodeForFactoryMethod(registeredBean, method, instantiationDescriptor.targetClass());
147147
}
148-
throw new IllegalStateException(
149-
"No suitable executor found for " + registeredBean.getBeanName());
148+
throw new AotBeanProcessingException(registeredBean, "no suitable constructor or factory method found");
150149
}
151150

152151
private void registerRuntimeHintsIfNecessary(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) {

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

+13-10
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
import org.springframework.util.ReflectionUtils;
7070

7171
import static org.assertj.core.api.Assertions.assertThat;
72-
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
72+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
7373
import static org.assertj.core.api.Assertions.assertThatNoException;
7474

7575
/**
@@ -691,9 +691,10 @@ void generateBeanDefinitionMethodWhenInstanceSupplierWithNoCustomization() {
691691
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
692692
this.methodGeneratorFactory, registeredBean, null,
693693
List.of());
694-
assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod(
695-
this.generationContext, this.beanRegistrationsCode)).withMessage(
696-
"Error processing bean with name 'testBean': instance supplier is not supported");
694+
assertThatExceptionOfType(AotBeanProcessingException.class)
695+
.isThrownBy(() -> generator.generateBeanDefinitionMethod(
696+
this.generationContext, this.beanRegistrationsCode))
697+
.withMessage("Error processing bean with name 'testBean': instance supplier is not supported");
697698
}
698699

699700
@Test
@@ -709,9 +710,10 @@ public ClassName getTarget(RegisteredBean registeredBean) {
709710
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
710711
this.methodGeneratorFactory, registeredBean, null,
711712
List.of(aotContribution));
712-
assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod(
713-
this.generationContext, this.beanRegistrationsCode)).withMessageStartingWith(
714-
"Default code generation is not supported for bean definitions declaring an instance supplier callback");
713+
assertThatExceptionOfType(AotBeanProcessingException.class)
714+
.isThrownBy(() -> generator.generateBeanDefinitionMethod(
715+
this.generationContext, this.beanRegistrationsCode))
716+
.withMessage("Error processing bean with name 'testBean': instance supplier is not supported");
715717
}
716718

717719
@Test
@@ -728,9 +730,10 @@ public CodeBlock generateInstanceSupplierCode(GenerationContext generationContex
728730
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
729731
this.methodGeneratorFactory, registeredBean, null,
730732
List.of(aotContribution));
731-
assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod(
732-
this.generationContext, this.beanRegistrationsCode)).withMessage(
733-
"Error processing bean with name 'testBean': instance supplier is not supported");
733+
assertThatExceptionOfType(AotBeanProcessingException.class)
734+
.isThrownBy(() -> generator.generateBeanDefinitionMethod(
735+
this.generationContext, this.beanRegistrationsCode))
736+
.withMessage("Error processing bean with name 'testBean': instance supplier is not supported");
734737
}
735738

736739
@Test

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

+54
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.aot.generate.GenerationContext;
3030
import org.springframework.aot.generate.MethodReference;
3131
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
32+
import org.springframework.aot.generate.ValueCodeGenerationException;
3233
import org.springframework.aot.hint.MemberCategory;
3334
import org.springframework.aot.test.generate.TestGenerationContext;
3435
import org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.Registration;
@@ -38,6 +39,7 @@
3839
import org.springframework.beans.testfixture.beans.AgeHolder;
3940
import org.springframework.beans.testfixture.beans.Employee;
4041
import org.springframework.beans.testfixture.beans.ITestBean;
42+
import org.springframework.beans.testfixture.beans.NestedTestBean;
4143
import org.springframework.beans.testfixture.beans.TestBean;
4244
import org.springframework.beans.testfixture.beans.factory.aot.MockBeanFactoryInitializationCode;
4345
import org.springframework.core.test.io.support.MockSpringFactoriesLoader;
@@ -50,6 +52,7 @@
5052
import org.springframework.javapoet.ParameterizedTypeName;
5153

5254
import static org.assertj.core.api.Assertions.assertThat;
55+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
5356
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
5457

5558
/**
@@ -156,6 +159,57 @@ void applyToRegisterReflectionHints() {
156159
.accepts(this.generationContext.getRuntimeHints());
157160
}
158161

162+
@Test
163+
void applyToFailingDoesNotWrapAotException() {
164+
RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class);
165+
beanDefinition.setInstanceSupplier(TestBean::new);
166+
RegisteredBean registeredBean = registerBean(beanDefinition);
167+
168+
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory,
169+
registeredBean, null, List.of());
170+
BeanRegistrationsAotContribution contribution = createContribution(registeredBean, generator, "testAlias");
171+
assertThatExceptionOfType(AotProcessingException.class)
172+
.isThrownBy(() -> contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode))
173+
.withMessage("Error processing bean with name 'testBean': instance supplier is not supported")
174+
.withNoCause();
175+
}
176+
177+
@Test
178+
void applyToFailingWrapsValueCodeGeneration() {
179+
RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class);
180+
beanDefinition.getPropertyValues().addPropertyValue("doctor", new NestedTestBean());
181+
RegisteredBean registeredBean = registerBean(beanDefinition);
182+
183+
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory,
184+
registeredBean, null, List.of());
185+
BeanRegistrationsAotContribution contribution = createContribution(registeredBean, generator, "testAlias");
186+
assertThatExceptionOfType(AotProcessingException.class)
187+
.isThrownBy(() -> contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode))
188+
.withMessage("Error processing bean with name 'testBean': failed to generate code for bean definition")
189+
.havingCause().isInstanceOf(ValueCodeGenerationException.class)
190+
.withMessageContaining("Failed to generate code for")
191+
.withMessageContaining(NestedTestBean.class.getName());
192+
}
193+
194+
@Test
195+
void applyToFailingProvidesDedicatedException() {
196+
RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class));
197+
198+
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory,
199+
registeredBean, null, List.of()) {
200+
@Override
201+
MethodReference generateBeanDefinitionMethod(GenerationContext generationContext,
202+
BeanRegistrationsCode beanRegistrationsCode) {
203+
throw new IllegalStateException("Test exception");
204+
}
205+
};
206+
BeanRegistrationsAotContribution contribution = createContribution(registeredBean, generator, "testAlias");
207+
assertThatExceptionOfType(AotProcessingException.class)
208+
.isThrownBy(() -> contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode))
209+
.withMessage("Error processing bean with name 'testBean': failed to generate code for bean definition")
210+
.havingCause().isInstanceOf(IllegalStateException.class).withMessage("Test exception");
211+
}
212+
159213
private RegisteredBean registerBean(RootBeanDefinition rootBeanDefinition) {
160214
String beanName = "testBean";
161215
this.beanFactory.registerBeanDefinition(beanName, rootBeanDefinition);

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
import org.springframework.util.ReflectionUtils;
4949

5050
import static org.assertj.core.api.Assertions.assertThat;
51-
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
51+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
5252
import static org.mockito.Mockito.never;
5353
import static org.mockito.Mockito.spy;
5454
import static org.mockito.Mockito.verify;
@@ -72,7 +72,8 @@ public void getTargetWithInstanceSupplier() {
7272
beanDefinition.setInstanceSupplier(SimpleBean::new);
7373
RegisteredBean registeredBean = registerTestBean(beanDefinition);
7474
BeanRegistrationCodeFragments codeFragments = createInstance(registeredBean);
75-
assertThatIllegalStateException().isThrownBy(() -> codeFragments.getTarget(registeredBean))
75+
assertThatExceptionOfType(AotBeanProcessingException.class)
76+
.isThrownBy(() -> codeFragments.getTarget(registeredBean))
7677
.withMessageContaining("Error processing bean with name 'testBean': instance supplier is not supported");
7778
}
7879

@@ -83,7 +84,8 @@ public void getTargetWithInstanceSupplierAndResourceDescription() {
8384
beanDefinition.setResourceDescription("my test resource");
8485
RegisteredBean registeredBean = registerTestBean(beanDefinition);
8586
BeanRegistrationCodeFragments codeFragments = createInstance(registeredBean);
86-
assertThatIllegalStateException().isThrownBy(() -> codeFragments.getTarget(registeredBean))
87+
assertThatExceptionOfType(AotBeanProcessingException.class)
88+
.isThrownBy(() -> codeFragments.getTarget(registeredBean))
8789
.withMessageContaining("Error processing bean with name 'testBean' defined in my test resource: "
8890
+ "instance supplier is not supported");
8991
}

spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class InstanceSupplierCodeGeneratorKotlinTests {
102102
this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
103103
.genericBeanDefinition(KotlinConfiguration::class.java).beanDefinition
104104
)
105-
Assertions.assertThatIllegalStateException().isThrownBy {
105+
Assertions.assertThatExceptionOfType(AotBeanProcessingException::class.java).isThrownBy {
106106
compile(beanFactory, beanDefinition) { _, _ -> }
107107
}
108108
}

0 commit comments

Comments
 (0)