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
This commit is contained in:
Stéphane Nicoll 2024-06-03 15:45:15 +02:00
parent f31113e325
commit 42ace2c2c9
13 changed files with 294 additions and 33 deletions

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}

View File

@ -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(

View File

@ -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) {

View File

@ -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

View File

@ -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);

View File

@ -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");
}

View File

@ -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) { _, _ -> }
}
}

View File

@ -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<BeanFactoryInitializationAotProcessor> processors) {
List<BeanFactoryInitializationAotContribution> 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) {

View File

@ -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");
}
}
}

View File

@ -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() {