Introduce AOT variant of BeanPostProcessor

This commit introduces an optional interface that a BeanPostProcessor
can implement to opt-in for providing a generated code equivalent of
what it does with a regular runtime.

This commit has a first implementation of this interface with
AutowiredAnnotationBeanPostProcessor replacing its processing of
autowired annotations by generated code.

Closes gh-27921
This commit is contained in:
Stephane Nicoll 2022-02-15 09:00:47 +01:00
parent 5bbc7dbce2
commit 2141373c43
4 changed files with 292 additions and 2 deletions

View File

@ -22,10 +22,12 @@ import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
@ -37,6 +39,8 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aot.generator.CodeContribution;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
@ -48,9 +52,13 @@ import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.annotation.InjectionMetadata.InjectedElement;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.generator.BeanInstanceAotPostProcessor;
import org.springframework.beans.factory.generator.BeanInstanceContributor;
import org.springframework.beans.factory.generator.InjectionGenerator;
import org.springframework.beans.factory.support.LookupOverride;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
@ -65,6 +73,7 @@ import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@ -131,7 +140,7 @@ import org.springframework.util.StringUtils;
* @see Value
*/
public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
MergedBeanDefinitionPostProcessor, BeanInstanceAotPostProcessor, PriorityOrdered, BeanFactoryAware {
protected final Log logger = LogFactory.getLog(getClass());
@ -255,8 +264,22 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
findInjectionMetadata(beanName, beanType, beanDefinition);
}
@Override
public BeanInstanceContributor buildAotContributor(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
InjectionMetadata metadata = findInjectionMetadata(beanName, beanType, beanDefinition);
Collection<InjectedElement> injectedElements = metadata.getInjectedElements();
return (!ObjectUtils.isEmpty(injectedElements)
? new AutowiredAnnotationBeanInstanceContributor(injectedElements)
: BeanInstanceContributor.NO_OP);
}
private InjectionMetadata findInjectionMetadata(String beanName, Class<?> beanType, RootBeanDefinition beanDefinition) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
metadata.checkConfigMembers(beanDefinition);
return metadata;
}
@Override
@ -798,6 +821,52 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
}
}
private static final class AutowiredAnnotationBeanInstanceContributor implements BeanInstanceContributor {
private final Collection<InjectedElement> injectedElements;
private final InjectionGenerator generator;
AutowiredAnnotationBeanInstanceContributor(Collection<InjectedElement> injectedElements) {
this.injectedElements = injectedElements;
this.generator = new InjectionGenerator();
}
@Override
public void contribute(CodeContribution contribution) {
this.injectedElements.forEach(element -> {
boolean isRequired = isRequired(element);
Member member = element.getMember();
analyzeMember(contribution, member);
contribution.statements().addStatement(this.generator.writeInjection(member, isRequired));
});
}
private boolean isRequired(InjectedElement element) {
if (element instanceof AutowiredMethodElement injectedMethod) {
return injectedMethod.required;
}
else if (element instanceof AutowiredFieldElement injectedField) {
return injectedField.required;
}
return true;
}
private void analyzeMember(CodeContribution contribution, Member member) {
if (member instanceof Method method) {
contribution.runtimeHints().reflection().registerMethod(method,
hint -> hint.setModes(ExecutableMode.INTROSPECT));
contribution.protectedAccess().analyze(member,
this.generator.getProtectedAccessInjectionOptions(member));
}
else if (member instanceof Field field) {
contribution.runtimeHints().reflection().registerField(field);
contribution.protectedAccess().analyze(member,
this.generator.getProtectedAccessInjectionOptions(member));
}
}
}
/**
* DependencyDescriptor variant with a pre-resolved target bean name.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@ -88,6 +88,14 @@ public class InjectionMetadata {
}
/**
* Return the {@link InjectedElement elements} to inject.
* @return the elements to inject
*/
public Collection<InjectedElement> getInjectedElements() {
return Collections.unmodifiableCollection(this.injectedElements);
}
/**
* Determine whether this metadata instance needs to be refreshed.
* @param clazz the current target class

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-2022 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.generator;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
/**
* Strategy interface to be implemented by a {@link BeanPostProcessor} that
* participates in a bean instance setup and can provide an equivalent setup
* using generated code.
*
* <p>Contrary to other bean post processors, implementations of this interface
* are instantiated at build-time and should not rely on other beans in the
* context.
*
* @author Stephane Nicoll
* @since 6.0
*/
@FunctionalInterface
public interface BeanInstanceAotPostProcessor extends BeanPostProcessor {
/**
* Build a {@link BeanInstanceContributor} for the given bean definition.
* @param beanDefinition the merged bean definition for the bean
* @param beanType the actual type of the managed bean instance
* @param beanName the name of the bean
* @return the contributor to use
*/
BeanInstanceContributor buildAotContributor(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName);
}

View File

@ -0,0 +1,167 @@
/*
* Copyright 2002-2022 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.annotation;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generator.CodeContribution;
import org.springframework.aot.generator.DefaultCodeContribution;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessorTests.ResourceInjectionBean;
import org.springframework.beans.factory.generator.BeanInstanceContributor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.core.env.Environment;
import org.springframework.javapoet.support.CodeSnippet;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for code contribution of {@link AutowiredAnnotationBeanPostProcessor}.
*
* @author Stephane Nicoll
*/
class AutowiredAnnotationBeanInstanceContributorTests {
@Test
void buildAotContributorWithPackageProtectedFieldInjection() {
CodeContribution contribution = contribute(PackageProtectedFieldInjectionSample.class);
assertThat(CodeSnippet.process(contribution.statements().toCodeBlock())).isEqualTo("""
instanceContext.field("environment", Environment.class)
.invoke(beanFactory, (attributes) -> bean.environment = attributes.get(0))""");
assertThat(contribution.runtimeHints().reflection().typeHints()).singleElement().satisfies(typeHint -> {
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(PackageProtectedFieldInjectionSample.class));
assertThat(typeHint.fields()).singleElement().satisfies(fieldHint -> {
assertThat(fieldHint.getName()).isEqualTo("environment");
assertThat(fieldHint.isAllowWrite()).isTrue();
assertThat(fieldHint.isAllowUnsafeAccess()).isFalse();
});
});
assertThat(contribution.protectedAccess().getPrivilegedPackageName("com.example"))
.isEqualTo(PackageProtectedFieldInjectionSample.class.getPackageName());
}
@Test
void buildAotContributorWithPrivateFieldInjection() {
CodeContribution contribution = contribute(PrivateFieldInjectionSample.class);
assertThat(CodeSnippet.process(contribution.statements().toCodeBlock())).isEqualTo("""
instanceContext.field("environment", Environment.class)
.invoke(beanFactory, (attributes) -> {
Field environmentField = ReflectionUtils.findField(AutowiredAnnotationBeanInstanceContributorTests.PrivateFieldInjectionSample.class, "environment", Environment.class);
ReflectionUtils.makeAccessible(environmentField);
ReflectionUtils.setField(environmentField, bean, attributes.get(0));
})""");
assertThat(contribution.runtimeHints().reflection().typeHints()).singleElement().satisfies(typeHint -> {
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(PrivateFieldInjectionSample.class));
assertThat(typeHint.fields()).singleElement().satisfies(fieldHint -> {
assertThat(fieldHint.getName()).isEqualTo("environment");
assertThat(fieldHint.isAllowWrite()).isTrue();
assertThat(fieldHint.isAllowUnsafeAccess()).isFalse();
});
});
assertThat(contribution.protectedAccess().isAccessible("com.example")).isTrue();
}
@Test
void buildAotContributorWithPublicMethodInjection() {
CodeContribution contribution = contribute(PublicMethodInjectionSample.class);
assertThat(CodeSnippet.process(contribution.statements().toCodeBlock())).isEqualTo("""
instanceContext.method("setTestBean", TestBean.class)
.invoke(beanFactory, (attributes) -> bean.setTestBean(attributes.get(0)))""");
assertThat(contribution.runtimeHints().reflection().typeHints()).singleElement().satisfies(typeHint -> {
assertThat(typeHint.getType()).isEqualTo(TypeReference.of(PublicMethodInjectionSample.class));
assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> {
assertThat(methodHint.getName()).isEqualTo("setTestBean");
assertThat(methodHint.getModes()).contains(ExecutableMode.INTROSPECT);
});
});
assertThat(contribution.protectedAccess().isAccessible("com.example")).isTrue();
}
@Test
void buildAotContributorWithInjectionPoints() {
CodeContribution contribution = contribute(ResourceInjectionBean.class);
assertThat(CodeSnippet.process(contribution.statements().toCodeBlock())).isEqualTo("""
instanceContext.field("testBean", TestBean.class)
.resolve(beanFactory, false).ifResolved((attributes) -> {
Field testBeanField = ReflectionUtils.findField(AutowiredAnnotationBeanPostProcessorTests.ResourceInjectionBean.class, "testBean", TestBean.class);
ReflectionUtils.makeAccessible(testBeanField);
ReflectionUtils.setField(testBeanField, bean, attributes.get(0));
});
instanceContext.method("setTestBean2", TestBean.class)
.invoke(beanFactory, (attributes) -> bean.setTestBean2(attributes.get(0)));""");
assertThat(contribution.runtimeHints().reflection().typeHints()).singleElement().satisfies(typeHint -> {
assertThat(typeHint.fields()).singleElement().satisfies(fieldHint ->
assertThat(fieldHint.getName()).isEqualTo("testBean"));
assertThat(typeHint.methods()).singleElement().satisfies(methodHint ->
assertThat(methodHint.getName()).isEqualTo("setTestBean2"));
});
assertThat(contribution.protectedAccess().isAccessible("com.example")).isTrue();
}
@Test
void buildAotContributorWithoutInjectionPoints() {
BeanInstanceContributor contributor = createAotContributor(String.class);
assertThat(contributor).isNotNull().isSameAs(BeanInstanceContributor.NO_OP);
}
private DefaultCodeContribution contribute(Class<?> type) {
BeanInstanceContributor contributor = createAotContributor(type);
assertThat(contributor).isNotNull();
DefaultCodeContribution contribution = new DefaultCodeContribution(new RuntimeHints());
contributor.contribute(contribution);
return contribution;
}
private BeanInstanceContributor createAotContributor(Class<?> type) {
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
RootBeanDefinition beanDefinition = new RootBeanDefinition(type);
return bpp.buildAotContributor(beanDefinition, type, "test");
}
public static class PackageProtectedFieldInjectionSample {
@Autowired
Environment environment;
}
public static class PrivateFieldInjectionSample {
@Autowired
private Environment environment;
}
public static class PublicMethodInjectionSample {
@Autowired
public void setTestBean(TestBean testBean) {
}
public void setUnrelated(String unrelated) {
}
}
}