From 42ace2c2c91bede9b1b91ae376f8e172124fa59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 3 Jun 2024 15:45:15 +0200 Subject: [PATCH] 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 --- .../aot/AotBeanProcessingException.java | 74 +++++++++++++++++++ .../beans/factory/aot/AotException.java | 39 ++++++++++ .../factory/aot/AotProcessingException.java | 39 ++++++++++ .../aot/BeanRegistrationsAotContribution.java | 21 ++++-- .../DefaultBeanRegistrationCodeFragments.java | 7 +- .../aot/InstanceSupplierCodeGenerator.java | 3 +- .../BeanDefinitionMethodGeneratorTests.java | 23 +++--- ...BeanRegistrationsAotContributionTests.java | 54 ++++++++++++++ ...ultBeanRegistrationCodeFragmentsTests.java | 8 +- ...nstanceSupplierCodeGeneratorKotlinTests.kt | 2 +- ...FactoryInitializationAotContributions.java | 22 +++++- .../ApplicationContextAotGeneratorTests.java | 26 +++++++ ...actoryInitializationAotProcessorTests.java | 9 ++- 13 files changed, 294 insertions(+), 33 deletions(-) create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java new file mode 100644 index 0000000000..03173bf9a9 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java @@ -0,0 +1,74 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.aot; + +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.lang.Nullable; + +/** + * Thrown when AOT fails to process a bean. + * + * @author Stephane Nicoll + * @since 6.2 + */ +@SuppressWarnings("serial") +public class AotBeanProcessingException extends AotProcessingException { + + private final RootBeanDefinition beanDefinition; + + /** + * Create an instance with the {@link RegisteredBean} that fails to be + * processed, a detail message, and an optional root cause. + * @param registeredBean the registered bean that fails to be processed + * @param msg the detail message + * @param cause the root cause, if any + */ + public AotBeanProcessingException(RegisteredBean registeredBean, String msg, @Nullable Throwable cause) { + super(createErrorMessage(registeredBean, msg), cause); + this.beanDefinition = registeredBean.getMergedBeanDefinition(); + } + + /** + * Shortcut to create an instance with the {@link RegisteredBean} that fails + * to be processed with only a detail message. + * @param registeredBean the registered bean that fails to be processed + * @param msg the detail message + */ + public AotBeanProcessingException(RegisteredBean registeredBean, String msg) { + this(registeredBean, msg, null); + } + + private static String createErrorMessage(RegisteredBean registeredBean, String msg) { + StringBuilder sb = new StringBuilder("Error processing bean with name '"); + sb.append(registeredBean.getBeanName()).append("'"); + String resourceDescription = registeredBean.getMergedBeanDefinition().getResourceDescription(); + if (resourceDescription != null) { + sb.append(" defined in ").append(resourceDescription); + } + sb.append(": ").append(msg); + return sb.toString(); + } + + /** + * Return the bean definition of the bean that failed to be processed. + */ + public RootBeanDefinition getBeanDefinition() { + return this.beanDefinition; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java new file mode 100644 index 0000000000..0b1f810726 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java @@ -0,0 +1,39 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.aot; + +import org.springframework.lang.Nullable; + +/** + * Abstract superclass for all exceptions thrown by ahead-of-time processing. + * + * @author Stephane Nicoll + * @since 6.2 + */ +@SuppressWarnings("serial") +public abstract class AotException extends RuntimeException { + + /** + * Create an instance with the specified message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + protected AotException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java new file mode 100644 index 0000000000..87613d63da --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java @@ -0,0 +1,39 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.aot; + +import org.springframework.lang.Nullable; + +/** + * Throw when an AOT processor failed. + * + * @author Stephane Nicoll + * @since 6.2 + */ +@SuppressWarnings("serial") +public class AotProcessingException extends AotException { + + /** + * Create a new instance with the detail message and a root cause, if any. + * @param msg the detail message + * @param cause the root cause, if any + */ + public AotProcessingException(String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java index b588dd8171..8b1066060d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java @@ -86,12 +86,21 @@ class BeanRegistrationsAotContribution method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME); CodeBlock.Builder code = CodeBlock.builder(); this.registrations.forEach(registration -> { - MethodReference beanDefinitionMethod = registration.methodGenerator - .generateBeanDefinitionMethod(generationContext, beanRegistrationsCode); - CodeBlock methodInvocation = beanDefinitionMethod.toInvokeCodeBlock( - ArgumentCodeGenerator.none(), beanRegistrationsCode.getClassName()); - code.addStatement("$L.registerBeanDefinition($S, $L)", - BEAN_FACTORY_PARAMETER_NAME, registration.beanName(), methodInvocation); + try { + MethodReference beanDefinitionMethod = registration.methodGenerator + .generateBeanDefinitionMethod(generationContext, beanRegistrationsCode); + CodeBlock methodInvocation = beanDefinitionMethod.toInvokeCodeBlock( + ArgumentCodeGenerator.none(), beanRegistrationsCode.getClassName()); + code.addStatement("$L.registerBeanDefinition($S, $L)", + BEAN_FACTORY_PARAMETER_NAME, registration.beanName(), methodInvocation); + } + catch (AotException ex) { + throw ex; + } + catch (Exception ex) { + throw new AotBeanProcessingException(registration.registeredBean, + "failed to generate code for bean definition", ex); + } }); method.addCode(code.build()); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java index 50564ee23a..01f006c0f8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java @@ -79,9 +79,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme @Override public ClassName getTarget(RegisteredBean registeredBean) { if (hasInstanceSupplier()) { - String resourceDescription = registeredBean.getMergedBeanDefinition().getResourceDescription(); - throw new IllegalStateException("Error processing bean with name '" + registeredBean.getBeanName() + "'" + - (resourceDescription != null ? " defined in " + resourceDescription : "") + ": instance supplier is not supported"); + throw new AotBeanProcessingException(registeredBean, "instance supplier is not supported"); } Class target = extractDeclaringClass(registeredBean, this.instantiationDescriptor.get()); while (target.getName().startsWith("java.") && registeredBean.isInnerBean()) { @@ -236,8 +234,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { if (hasInstanceSupplier()) { - throw new IllegalStateException("Default code generation is not supported for bean definitions declaring " - + "an instance supplier callback: " + this.registeredBean.getMergedBeanDefinition()); + throw new AotBeanProcessingException(this.registeredBean, "instance supplier is not supported"); } return new InstanceSupplierCodeGenerator(generationContext, beanRegistrationCode.getClassName(), beanRegistrationCode.getMethods(), allowDirectSupplierShortcut).generateCode( diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java index c65ab4ed1f..d6ee4124db 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java @@ -145,8 +145,7 @@ public class InstanceSupplierCodeGenerator { if (constructorOrFactoryMethod instanceof Method method && !KotlinDetector.isSuspendingFunction(method)) { return generateCodeForFactoryMethod(registeredBean, method, instantiationDescriptor.targetClass()); } - throw new IllegalStateException( - "No suitable executor found for " + registeredBean.getBeanName()); + throw new AotBeanProcessingException(registeredBean, "no suitable constructor or factory method found"); } private void registerRuntimeHintsIfNecessary(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java index e7c986925a..0aa194ff9a 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java @@ -69,7 +69,7 @@ import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; /** @@ -691,9 +691,10 @@ class BeanDefinitionMethodGeneratorTests { BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( this.methodGeneratorFactory, registeredBean, null, List.of()); - assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod( - this.generationContext, this.beanRegistrationsCode)).withMessage( - "Error processing bean with name 'testBean': instance supplier is not supported"); + assertThatExceptionOfType(AotBeanProcessingException.class) + .isThrownBy(() -> generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode)) + .withMessage("Error processing bean with name 'testBean': instance supplier is not supported"); } @Test @@ -709,9 +710,10 @@ class BeanDefinitionMethodGeneratorTests { BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( this.methodGeneratorFactory, registeredBean, null, List.of(aotContribution)); - assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod( - this.generationContext, this.beanRegistrationsCode)).withMessageStartingWith( - "Default code generation is not supported for bean definitions declaring an instance supplier callback"); + assertThatExceptionOfType(AotBeanProcessingException.class) + .isThrownBy(() -> generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode)) + .withMessage("Error processing bean with name 'testBean': instance supplier is not supported"); } @Test @@ -728,9 +730,10 @@ class BeanDefinitionMethodGeneratorTests { BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( this.methodGeneratorFactory, registeredBean, null, List.of(aotContribution)); - assertThatIllegalStateException().isThrownBy(() -> generator.generateBeanDefinitionMethod( - this.generationContext, this.beanRegistrationsCode)).withMessage( - "Error processing bean with name 'testBean': instance supplier is not supported"); + assertThatExceptionOfType(AotBeanProcessingException.class) + .isThrownBy(() -> generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode)) + .withMessage("Error processing bean with name 'testBean': instance supplier is not supported"); } @Test diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java index 6b7ff497e6..68a3d68c68 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java @@ -29,6 +29,7 @@ import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.generate.ValueCodeGenerationException; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.Registration; @@ -38,6 +39,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.testfixture.beans.AgeHolder; import org.springframework.beans.testfixture.beans.Employee; import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.NestedTestBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.factory.aot.MockBeanFactoryInitializationCode; import org.springframework.core.test.io.support.MockSpringFactoriesLoader; @@ -50,6 +52,7 @@ import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; /** @@ -156,6 +159,57 @@ class BeanRegistrationsAotContributionTests { .accepts(this.generationContext.getRuntimeHints()); } + @Test + void applyToFailingDoesNotWrapAotException() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); + beanDefinition.setInstanceSupplier(TestBean::new); + RegisteredBean registeredBean = registerBean(beanDefinition); + + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory, + registeredBean, null, List.of()); + BeanRegistrationsAotContribution contribution = createContribution(registeredBean, generator, "testAlias"); + assertThatExceptionOfType(AotProcessingException.class) + .isThrownBy(() -> contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode)) + .withMessage("Error processing bean with name 'testBean': instance supplier is not supported") + .withNoCause(); + } + + @Test + void applyToFailingWrapsValueCodeGeneration() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestBean.class); + beanDefinition.getPropertyValues().addPropertyValue("doctor", new NestedTestBean()); + RegisteredBean registeredBean = registerBean(beanDefinition); + + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory, + registeredBean, null, List.of()); + BeanRegistrationsAotContribution contribution = createContribution(registeredBean, generator, "testAlias"); + assertThatExceptionOfType(AotProcessingException.class) + .isThrownBy(() -> contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode)) + .withMessage("Error processing bean with name 'testBean': failed to generate code for bean definition") + .havingCause().isInstanceOf(ValueCodeGenerationException.class) + .withMessageContaining("Failed to generate code for") + .withMessageContaining(NestedTestBean.class.getName()); + } + + @Test + void applyToFailingProvidesDedicatedException() { + RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class)); + + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory, + registeredBean, null, List.of()) { + @Override + MethodReference generateBeanDefinitionMethod(GenerationContext generationContext, + BeanRegistrationsCode beanRegistrationsCode) { + throw new IllegalStateException("Test exception"); + } + }; + BeanRegistrationsAotContribution contribution = createContribution(registeredBean, generator, "testAlias"); + assertThatExceptionOfType(AotProcessingException.class) + .isThrownBy(() -> contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode)) + .withMessage("Error processing bean with name 'testBean': failed to generate code for bean definition") + .havingCause().isInstanceOf(IllegalStateException.class).withMessage("Test exception"); + } + private RegisteredBean registerBean(RootBeanDefinition rootBeanDefinition) { String beanName = "testBean"; this.beanFactory.registerBeanDefinition(beanName, rootBeanDefinition); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java index 1333b2f160..7379fc2226 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java @@ -48,7 +48,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -72,7 +72,8 @@ class DefaultBeanRegistrationCodeFragmentsTests { beanDefinition.setInstanceSupplier(SimpleBean::new); RegisteredBean registeredBean = registerTestBean(beanDefinition); BeanRegistrationCodeFragments codeFragments = createInstance(registeredBean); - assertThatIllegalStateException().isThrownBy(() -> codeFragments.getTarget(registeredBean)) + assertThatExceptionOfType(AotBeanProcessingException.class) + .isThrownBy(() -> codeFragments.getTarget(registeredBean)) .withMessageContaining("Error processing bean with name 'testBean': instance supplier is not supported"); } @@ -83,7 +84,8 @@ class DefaultBeanRegistrationCodeFragmentsTests { beanDefinition.setResourceDescription("my test resource"); RegisteredBean registeredBean = registerTestBean(beanDefinition); BeanRegistrationCodeFragments codeFragments = createInstance(registeredBean); - assertThatIllegalStateException().isThrownBy(() -> codeFragments.getTarget(registeredBean)) + assertThatExceptionOfType(AotBeanProcessingException.class) + .isThrownBy(() -> codeFragments.getTarget(registeredBean)) .withMessageContaining("Error processing bean with name 'testBean' defined in my test resource: " + "instance supplier is not supported"); } diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt index 63501a601c..451c77b37a 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt @@ -102,7 +102,7 @@ class InstanceSupplierCodeGeneratorKotlinTests { this.beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder .genericBeanDefinition(KotlinConfiguration::class.java).beanDefinition ) - Assertions.assertThatIllegalStateException().isThrownBy { + Assertions.assertThatExceptionOfType(AotBeanProcessingException::class.java).isThrownBy { compile(beanFactory, beanDefinition) { _, _ -> } } } diff --git a/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationAotContributions.java b/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationAotContributions.java index 878ed204c0..d2d5ec73be 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationAotContributions.java +++ b/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationAotContributions.java @@ -21,11 +21,14 @@ import java.util.Collections; import java.util.List; import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.AotException; +import org.springframework.beans.factory.aot.AotProcessingException; import org.springframework.beans.factory.aot.AotServices; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.lang.Nullable; /** * A collection of {@link BeanFactoryInitializationAotContribution AOT @@ -63,8 +66,7 @@ class BeanFactoryInitializationAotContributions { List processors) { List contributions = new ArrayList<>(); for (BeanFactoryInitializationAotProcessor processor : processors) { - BeanFactoryInitializationAotContribution contribution = processor - .processAheadOfTime(beanFactory); + BeanFactoryInitializationAotContribution contribution = processAheadOfTime(processor, beanFactory); if (contribution != null) { contributions.add(contribution); } @@ -72,6 +74,22 @@ class BeanFactoryInitializationAotContributions { return Collections.unmodifiableList(contributions); } + @Nullable + private BeanFactoryInitializationAotContribution processAheadOfTime(BeanFactoryInitializationAotProcessor processor, + DefaultListableBeanFactory beanFactory) { + + try { + return processor.processAheadOfTime(beanFactory); + } + catch (AotException ex) { + throw ex; + } + catch (Exception ex) { + throw new AotProcessingException("Error executing '" + + processor.getClass().getName() + "': " + ex.getMessage(), ex); + } + } + void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { for (BeanFactoryInitializationAotContribution contribution : this.contributions) { diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java index d3d67fd9b6..104d6b0761 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java @@ -40,6 +40,7 @@ import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.aot.AotProcessingException; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; @@ -94,6 +95,7 @@ import org.springframework.mock.env.MockEnvironment; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link ApplicationContextAotGenerator}. @@ -587,6 +589,22 @@ class ApplicationContextAotGeneratorTests { } + @Nested + class ExceptionHanding { + + @Test + void failureProcessingBeanFactoryAotContribution() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.registerBeanDefinition("test", + new RootBeanDefinition(FailingBeanFactoryInitializationAotContribution.class)); + assertThatExceptionOfType(AotProcessingException.class) + .isThrownBy(() -> processAheadOfTime(applicationContext)) + .withMessageStartingWith("Error executing '") + .withMessageContaining(FailingBeanFactoryInitializationAotContribution.class.getName()) + .withMessageContaining("Test exception"); + } + } + private static void registerBeanPostProcessor(GenericApplicationContext applicationContext, String beanName, Class beanPostProcessorClass) { @@ -676,4 +694,12 @@ class ApplicationContextAotGeneratorTests { } + static class FailingBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotProcessor { + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + throw new IllegalStateException("Test exception"); + } + } + } diff --git a/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java index c3542b630f..bb4aa59ff8 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java @@ -31,6 +31,7 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.factory.aot.AotProcessingException; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.Bean; @@ -40,7 +41,7 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link RuntimeHintsBeanFactoryInitializationAotProcessor}. @@ -119,9 +120,9 @@ class RuntimeHintsBeanFactoryInitializationAotProcessorTests { void shouldRejectRuntimeHintsRegistrarWithoutDefaultConstructor() { GenericApplicationContext applicationContext = createApplicationContext( ConfigurationWithIllegalRegistrar.class); - assertThatThrownBy(() -> this.generator.processAheadOfTime( - applicationContext, this.generationContext)) - .isInstanceOf(BeanInstantiationException.class); + assertThatExceptionOfType(AotProcessingException.class) + .isThrownBy(() -> this.generator.processAheadOfTime(applicationContext, this.generationContext)) + .havingCause().isInstanceOf(BeanInstantiationException.class); } private void assertThatSampleRegistrarContributed() {