diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index 9e7fabe5c33..dd644c9dd98 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -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 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 injectedElements; + + private final InjectionGenerator generator; + + AutowiredAnnotationBeanInstanceContributor(Collection 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. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java index 4e205558523..8898dea97a4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java @@ -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 getInjectedElements() { + return Collections.unmodifiableCollection(this.injectedElements); + } + /** * Determine whether this metadata instance needs to be refreshed. * @param clazz the current target class diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanInstanceAotPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanInstanceAotPostProcessor.java new file mode 100644 index 00000000000..af2e87620d4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanInstanceAotPostProcessor.java @@ -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. + * + *

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); + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanInstanceContributorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanInstanceContributorTests.java new file mode 100644 index 00000000000..f0d1b7aa591 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanInstanceContributorTests.java @@ -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) { + + } + + } + + +}