From 588d4d877626495cd9195a0be3b9810b53135223 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 26 Apr 2022 20:48:24 -0700 Subject: [PATCH] Update scoped proxy AOT support Add `ScopedProxyBeanRegistrationCodeGeneratorFactory` to supply custom scoped proxy bean registration code. See gh-28414 --- spring-aop/spring-aop.gradle | 1 + ...anRegistrationCodeFragmentsCustomizer.java | 184 ++++++++++++++++++ .../resources/META-INF/spring/aot.factories | 2 + .../aop/scope/MockBeanRegistrationsCode.java | 55 ++++++ ...istrationCodeFragmentsCustomizerTests.java | 176 +++++++++++++++++ 5 files changed, 418 insertions(+) create mode 100644 spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationCodeFragmentsCustomizer.java create mode 100644 spring-aop/src/main/resources/META-INF/spring/aot.factories create mode 100644 spring-aop/src/test/java/org/springframework/aop/scope/MockBeanRegistrationsCode.java create mode 100644 spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationCodeFragmentsCustomizerTests.java diff --git a/spring-aop/spring-aop.gradle b/spring-aop/spring-aop.gradle index f87eceff6a..87a9998502 100644 --- a/spring-aop/spring-aop.gradle +++ b/spring-aop/spring-aop.gradle @@ -5,6 +5,7 @@ dependencies { api(project(":spring-core")) optional("org.aspectj:aspectjweaver") optional("org.apache.commons:commons-pool2") + testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-core"))) testFixturesImplementation(testFixtures(project(":spring-beans"))) diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationCodeFragmentsCustomizer.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationCodeFragmentsCustomizer.java new file mode 100644 index 0000000000..3a616cc9d4 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationCodeFragmentsCustomizer.java @@ -0,0 +1,184 @@ +/* + * 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.aop.scope; + +import java.lang.reflect.Executable; +import java.util.function.Predicate; + +import javax.lang.model.element.Modifier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsCustomizer; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.javapoet.CodeBlock; +import org.springframework.lang.Nullable; + +/** + * {@link BeanRegistrationCodeFragmentsCustomizer} for + * {@link ScopedProxyFactoryBean}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 6.0 + */ +class ScopedProxyBeanRegistrationCodeFragmentsCustomizer + implements BeanRegistrationCodeFragmentsCustomizer { + + private static final Log logger = LogFactory + .getLog(ScopedProxyBeanRegistrationCodeFragmentsCustomizer.class); + + + @Override + public BeanRegistrationCodeFragments customizeBeanRegistrationCodeFragments( + RegisteredBean registeredBean, BeanRegistrationCodeFragments codeFragments) { + + Class beanType = registeredBean.getBeanType().toClass(); + if (!beanType.equals(ScopedProxyFactoryBean.class)) { + return codeFragments; + } + String targetBeanName = getTargetBeanName( + registeredBean.getMergedBeanDefinition()); + BeanDefinition targetBeanDefinition = getTargetBeanDefinition( + registeredBean.getBeanFactory(), targetBeanName); + if (targetBeanDefinition == null) { + logger.warn("Could not handle " + ScopedProxyFactoryBean.class.getSimpleName() + + ": no target bean definition found with name " + targetBeanName); + return codeFragments; + } + return new ScopedProxyBeanRegistrationCodeFragments(codeFragments, registeredBean, + targetBeanName, targetBeanDefinition); + } + + @Nullable + private String getTargetBeanName(BeanDefinition beanDefinition) { + Object value = beanDefinition.getPropertyValues().get("targetBeanName"); + return (value instanceof String) ? (String) value : null; + } + + @Nullable + private BeanDefinition getTargetBeanDefinition(ConfigurableBeanFactory beanFactory, + @Nullable String targetBeanName) { + + if (targetBeanName != null && beanFactory.containsBean(targetBeanName)) { + return beanFactory.getMergedBeanDefinition(targetBeanName); + } + return null; + } + + + private static class ScopedProxyBeanRegistrationCodeFragments + extends BeanRegistrationCodeFragments { + + private static final String REGISTERED_BEAN_PARAMETER_NAME = "registeredBean"; + + + private final RegisteredBean registeredBean; + + private final String targetBeanName; + + private final BeanDefinition targetBeanDefinition; + + + ScopedProxyBeanRegistrationCodeFragments( + BeanRegistrationCodeFragments codeGenerator, + RegisteredBean registeredBean, String targetBeanName, + BeanDefinition targetBeanDefinition) { + + super(codeGenerator); + this.registeredBean = registeredBean; + this.targetBeanName = targetBeanName; + this.targetBeanDefinition = targetBeanDefinition; + } + + + @Override + public Class getTarget(RegisteredBean registeredBean, + Executable constructorOrFactoryMethod) { + return this.targetBeanDefinition.getResolvableType().toClass(); + } + + @Override + public CodeBlock generateNewBeanDefinitionCode( + GenerationContext generationContext, ResolvableType beanType, + BeanRegistrationCode beanRegistrationCode) { + + return super.generateNewBeanDefinitionCode(generationContext, + this.targetBeanDefinition.getResolvableType(), beanRegistrationCode); + } + + @Override + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { + + RootBeanDefinition processedBeanDefinition = new RootBeanDefinition( + beanDefinition); + processedBeanDefinition + .setTargetType(this.targetBeanDefinition.getResolvableType()); + processedBeanDefinition.getPropertyValues() + .removePropertyValue("targetBeanName"); + return super.generateSetBeanDefinitionPropertiesCode(generationContext, + beanRegistrationCode, processedBeanDefinition, attributeFilter); + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, + Executable constructorOrFactoryMethod, + boolean allowDirectSupplierShortcut) { + + GeneratedMethod method = beanRegistrationCode.getMethodGenerator() + .generateMethod("get", "scopedProxyInstance").using(builder -> { + Class beanClass = this.targetBeanDefinition.getResolvableType() + .toClass(); + builder.addJavadoc( + "Create the scoped proxy bean instance for '$L'.", + this.registeredBean.getBeanName()); + builder.addModifiers(Modifier.PRIVATE, Modifier.STATIC); + builder.returns(beanClass); + builder.addParameter(RegisteredBean.class, + REGISTERED_BEAN_PARAMETER_NAME); + builder.addStatement("$T factory = new $T()", + ScopedProxyFactoryBean.class, + ScopedProxyFactoryBean.class); + builder.addStatement("factory.setTargetBeanName($S)", + this.targetBeanName); + builder.addStatement( + "factory.setBeanFactory($L.getBeanFactory())", + REGISTERED_BEAN_PARAMETER_NAME); + builder.addStatement("return ($T) factory.getObject()", + beanClass); + }); + return CodeBlock.of("$T.of($T::$L)", InstanceSupplier.class, + beanRegistrationCode.getClassName(), method.getName()); + } + + } + +} diff --git a/spring-aop/src/main/resources/META-INF/spring/aot.factories b/spring-aop/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 0000000000..e71dbaf9cd --- /dev/null +++ b/spring-aop/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.aot.registration.BeanRegistrationCodeFragmentsCustomizer=\ +org.springframework.aop.scope.ScopedProxyBeanRegistrationCodeFragmentsCustomizer diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/MockBeanRegistrationsCode.java b/spring-aop/src/test/java/org/springframework/aop/scope/MockBeanRegistrationsCode.java new file mode 100644 index 0000000000..2d3ce3eb8d --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/scope/MockBeanRegistrationsCode.java @@ -0,0 +1,55 @@ +/* + * 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.aop.scope; + +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.MethodGenerator; +import org.springframework.beans.factory.aot.BeanRegistrationsCode; +import org.springframework.javapoet.ClassName; + +/** + * Mock {@link BeanRegistrationsCode} implementation. + * + * @author Phillip Webb + */ +class MockBeanRegistrationsCode implements BeanRegistrationsCode { + + private final ClassName className; + + private final GeneratedMethods generatedMethods = new GeneratedMethods(); + + + MockBeanRegistrationsCode(ClassName className) { + this.className = className; + } + + + @Override + public ClassName getClassName() { + return this.className; + } + + @Override + public MethodGenerator getMethodGenerator() { + return this.generatedMethods; + } + + GeneratedMethods getGeneratedMethods() { + return this.generatedMethods; + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationCodeFragmentsCustomizerTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationCodeFragmentsCustomizerTests.java new file mode 100644 index 0000000000..4c43a3be3b --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationCodeFragmentsCustomizerTests.java @@ -0,0 +1,176 @@ +/* + * 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.aop.scope; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.generate.MethodGenerator; +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.test.generator.compile.Compiled; +import org.springframework.aot.test.generator.compile.TestCompiler; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.aot.TestBeanRegistrationsAotProcessor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolder; +import org.springframework.core.ResolvableType; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link ScopedProxyBeanRegistrationCodeFragmentsCustomizer}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 6.0 + */ +class ScopedProxyBeanRegistrationCodeFragmentsCustomizerTests { + + private DefaultListableBeanFactory beanFactory; + + private TestBeanRegistrationsAotProcessor processor; + + private InMemoryGeneratedFiles generatedFiles; + + private DefaultGenerationContext generationContext; + + private MockBeanFactoryInitializationCode beanFactoryInitializationCode; + + @BeforeEach + void setup() { + this.beanFactory = new DefaultListableBeanFactory(); + this.processor = new TestBeanRegistrationsAotProcessor(); + this.generatedFiles = new InMemoryGeneratedFiles(); + this.generationContext = new DefaultGenerationContext(this.generatedFiles); + this.beanFactoryInitializationCode = new MockBeanFactoryInitializationCode(); + } + + @Test + void getBeanRegistrationCodeGeneratorWhenNotScopedProxy() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(PropertiesFactoryBean.class).getBeanDefinition(); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + testCompile((freshBeanFactory, compiled) -> { + Object bean = freshBeanFactory.getBean("test"); + assertThat(bean).isInstanceOf(Properties.class); + }); + } + + @Test + void getBeanRegistrationCodeGeneratorWhenScopedProxyWithoutTargetBeanName() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(ScopedProxyFactoryBean.class).getBeanDefinition(); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + testCompile((freshBeanFactory, + compiled) -> assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> freshBeanFactory.getBean("test")) + .withMessageContaining("'targetBeanName' is required")); + } + + @Test + void getBeanRegistrationCodeGeneratorWhenScopedProxyWithInvalidTargetBeanName() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(ScopedProxyFactoryBean.class) + .addPropertyValue("targetBeanName", "testDoesNotExist") + .getBeanDefinition(); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + testCompile((freshBeanFactory, + compiled) -> assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> freshBeanFactory.getBean("test")) + .withMessageContaining("No bean named 'testDoesNotExist'")); + } + + @Test + void getBeanRegistrationCodeGeneratorWhenScopedProxyWithTargetBeanName() { + RootBeanDefinition targetBean = new RootBeanDefinition(); + targetBean.setTargetType( + ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); + targetBean.setScope("custom"); + this.beanFactory.registerBeanDefinition("numberHolder", targetBean); + BeanDefinition scopedBean = BeanDefinitionBuilder + .rootBeanDefinition(ScopedProxyFactoryBean.class) + .addPropertyValue("targetBeanName", "numberHolder").getBeanDefinition(); + this.beanFactory.registerBeanDefinition("test", scopedBean); + testCompile((freshBeanFactory, compiled) -> { + Object bean = freshBeanFactory.getBean("test"); + assertThat(bean).isNotNull().isInstanceOf(NumberHolder.class) + .isInstanceOf(AopInfrastructureBean.class); + }); + } + + private void testCompile(BiConsumer result) { + BeanFactoryInitializationAotContribution contribution = this.processor + .processAheadOfTime(this.beanFactory); + contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); + this.generationContext.writeGeneratedContent(); + TestCompiler.forSystem().withFiles(this.generatedFiles).printFiles(System.out) + .compile(compiled -> { + MethodReference reference = this.beanFactoryInitializationCode + .getInitializers().get(0); + Object instance = compiled.getInstance(Object.class, + reference.getDeclaringClass().toString()); + Method method = ReflectionUtils.findMethod(instance.getClass(), + reference.getMethodName(), DefaultListableBeanFactory.class); + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); + freshBeanFactory.setBeanClassLoader(compiled.getClassLoader()); + ReflectionUtils.invokeMethod(method, instance, freshBeanFactory); + result.accept(freshBeanFactory, compiled); + }); + } + + + class MockBeanFactoryInitializationCode implements BeanFactoryInitializationCode { + + private final GeneratedMethods generatedMethods = new GeneratedMethods(); + + private final List initializers = new ArrayList<>(); + + @Override + public MethodGenerator getMethodGenerator() { + return this.generatedMethods; + } + + @Override + public void addInitializer(MethodReference methodReference) { + this.initializers.add(methodReference); + } + + List getInitializers() { + return initializers; + } + + } + +}