From f64fc4baff2625192aff02e0d192b1c678384eae Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 26 Apr 2022 15:03:54 +0200 Subject: [PATCH] Generate appropriate bean registration code for scoped proxies Closes gh-28383 --- ...yBeanRegistrationContributionProvider.java | 115 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 2 + ...RegistrationContributionProviderTests.java | 113 +++++++++++++++++ .../aop/testfixture/scope/SimpleTarget.java | 20 +++ 4 files changed, 250 insertions(+) create mode 100644 spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationContributionProvider.java create mode 100644 spring-aop/src/main/resources/META-INF/spring.factories create mode 100644 spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationContributionProviderTests.java create mode 100644 spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationContributionProvider.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationContributionProvider.java new file mode 100644 index 000000000000..e236511d93ae --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationContributionProvider.java @@ -0,0 +1,115 @@ +/* + * 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.generator.CodeContribution; +import org.springframework.aot.generator.DefaultCodeContribution; +import org.springframework.aot.generator.ProtectedAccess.Options; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.generator.BeanFactoryContribution; +import org.springframework.beans.factory.generator.BeanInstantiationGenerator; +import org.springframework.beans.factory.generator.BeanRegistrationBeanFactoryContribution; +import org.springframework.beans.factory.generator.BeanRegistrationContributionProvider; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.javapoet.support.MultiStatement; +import org.springframework.lang.Nullable; + +/** + * {@link BeanRegistrationContributionProvider} for {@link ScopedProxyFactoryBean}. + * + * @author Stephane Nicoll + */ +class ScopedProxyBeanRegistrationContributionProvider implements BeanRegistrationContributionProvider { + + private static final Log logger = LogFactory.getLog(ScopedProxyBeanRegistrationContributionProvider.class); + + + private final ConfigurableBeanFactory beanFactory; + + ScopedProxyBeanRegistrationContributionProvider(ConfigurableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Nullable + @Override + public BeanFactoryContribution getContributionFor(String beanName, RootBeanDefinition beanDefinition) { + Class beanType = beanDefinition.getResolvableType().toClass(); + return (beanType.equals(ScopedProxyFactoryBean.class)) + ? createScopedProxyBeanFactoryContribution(beanName, beanDefinition) : null; + } + + @Nullable + private BeanFactoryContribution createScopedProxyBeanFactoryContribution(String beanName, RootBeanDefinition beanDefinition) { + String targetBeanName = getTargetBeanName(beanDefinition); + BeanDefinition targetBeanDefinition = getTargetBeanDefinition(targetBeanName); + if (targetBeanDefinition == null) { + logger.warn("Could not handle " + ScopedProxyFactoryBean.class.getSimpleName() + + ": no target bean definition found with name " + targetBeanName); + return null; + } + RootBeanDefinition processedBeanDefinition = new RootBeanDefinition(beanDefinition); + processedBeanDefinition.setTargetType(targetBeanDefinition.getResolvableType()); + processedBeanDefinition.getPropertyValues().removePropertyValue("targetBeanName"); + return new BeanRegistrationBeanFactoryContribution(beanName, processedBeanDefinition, + getBeanInstantiationGenerator(targetBeanName)); + } + + private BeanInstantiationGenerator getBeanInstantiationGenerator(String targetBeanName) { + return new BeanInstantiationGenerator() { + + @Override + public Executable getInstanceCreator() { + return ScopedProxyFactoryBean.class.getDeclaredConstructors()[0]; + } + + @Override + public CodeContribution generateBeanInstantiation(RuntimeHints runtimeHints) { + CodeContribution codeContribution = new DefaultCodeContribution(runtimeHints); + codeContribution.protectedAccess().analyze(getInstanceCreator(), Options.defaults().build()); + MultiStatement statements = new MultiStatement(); + statements.addStatement("$T factory = new $T()", ScopedProxyFactoryBean.class, ScopedProxyFactoryBean.class); + statements.addStatement("factory.setTargetBeanName($S)", targetBeanName); + statements.addStatement("factory.setBeanFactory(beanFactory)"); + statements.addStatement("return factory.getObject()"); + codeContribution.statements().add(statements.toLambdaBody("() ->")); + return codeContribution; + } + }; + } + + @Nullable + private String getTargetBeanName(BeanDefinition beanDefinition) { + Object value = beanDefinition.getPropertyValues().get("targetBeanName"); + return (value instanceof String targetBeanName) ? targetBeanName : null; + } + + @Nullable + private BeanDefinition getTargetBeanDefinition(@Nullable String targetBeanName) { + if (targetBeanName != null && this.beanFactory.containsBean(targetBeanName)) { + return this.beanFactory.getMergedBeanDefinition(targetBeanName); + } + return null; + } + +} diff --git a/spring-aop/src/main/resources/META-INF/spring.factories b/spring-aop/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..72fff390d168 --- /dev/null +++ b/spring-aop/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.generator.BeanRegistrationContributionProvider= \ +org.springframework.aop.scope.ScopedProxyBeanRegistrationContributionProvider \ No newline at end of file diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationContributionProviderTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationContributionProviderTests.java new file mode 100644 index 000000000000..e303e513ff00 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationContributionProviderTests.java @@ -0,0 +1,113 @@ +/* + * 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.junit.jupiter.api.Test; + +import org.springframework.aop.testfixture.scope.SimpleTarget; +import org.springframework.aot.generator.DefaultGeneratedTypeContext; +import org.springframework.aot.generator.GeneratedType; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.generator.BeanFactoryContribution; +import org.springframework.beans.factory.generator.BeanFactoryInitialization; +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.javapoet.ClassName; +import org.springframework.javapoet.support.CodeSnippet; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ScopedProxyBeanRegistrationContributionProvider}. + * + * @author Stephane Nicoll + */ +class ScopedProxyBeanRegistrationContributionProviderTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void getWithNonScopedProxy() { + BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(PropertiesFactoryBean.class) + .getBeanDefinition(); + assertThat(getBeanFactoryContribution("test", beanDefinition)).isNull(); + } + + @Test + void getWithScopedProxyWithoutTargetBeanName() { + BeanDefinition scopeBean = BeanDefinitionBuilder.rootBeanDefinition(ScopedProxyFactoryBean.class) + .getBeanDefinition(); + assertThat(getBeanFactoryContribution("test", scopeBean)).isNull(); + } + + @Test + void getWithScopedProxyWithInvalidTargetBeanName() { + BeanDefinition scopeBean = BeanDefinitionBuilder.rootBeanDefinition(ScopedProxyFactoryBean.class) + .addPropertyValue("targetBeanName", "testDoesNotExist").getBeanDefinition(); + assertThat(getBeanFactoryContribution("test", scopeBean)).isNull(); + } + + @Test + void getWithScopedProxyWithTargetBeanName() { + BeanDefinition targetBean = BeanDefinitionBuilder.rootBeanDefinition(SimpleTarget.class) + .getBeanDefinition(); + beanFactory.registerBeanDefinition("simpleTarget", targetBean); + BeanDefinition scopeBean = BeanDefinitionBuilder.rootBeanDefinition(ScopedProxyFactoryBean.class) + .addPropertyValue("targetBeanName", "simpleTarget").getBeanDefinition(); + assertThat(getBeanFactoryContribution("test", scopeBean)).isNotNull(); + } + + @Test + void writeBeanRegistrationForScopedProxy() { + RootBeanDefinition targetBean = new RootBeanDefinition(); + targetBean.setTargetType(ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)); + targetBean.setScope("custom"); + this.beanFactory.registerBeanDefinition("numberHolder", targetBean); + BeanDefinition scopeBean = BeanDefinitionBuilder.rootBeanDefinition(ScopedProxyFactoryBean.class) + .addPropertyValue("targetBeanName", "numberHolder").getBeanDefinition(); + assertThat(writeBeanRegistration("test", scopeBean).getSnippet()).isEqualTo(""" + BeanDefinitionRegistrar.of("test", ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class)) + .instanceSupplier(() -> { + ScopedProxyFactoryBean factory = new ScopedProxyFactoryBean(); + factory.setTargetBeanName("numberHolder"); + factory.setBeanFactory(beanFactory); + return factory.getObject(); + }).register(beanFactory); + """); + } + + private CodeSnippet writeBeanRegistration(String beanName, BeanDefinition beanDefinition) { + BeanFactoryContribution contribution = getBeanFactoryContribution(beanName, beanDefinition); + assertThat(contribution).isNotNull(); + BeanFactoryInitialization initialization = new BeanFactoryInitialization(new DefaultGeneratedTypeContext("comp.example", packageName -> GeneratedType.of(ClassName.get(packageName, "Test")))); + contribution.applyTo(initialization); + return CodeSnippet.of(initialization.toCodeBlock()); + } + + @Nullable + BeanFactoryContribution getBeanFactoryContribution(String beanName, BeanDefinition beanDefinition) { + ScopedProxyBeanRegistrationContributionProvider provider = new ScopedProxyBeanRegistrationContributionProvider(this.beanFactory); + return provider.getContributionFor(beanName, (RootBeanDefinition) beanDefinition); + } + +} + diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java new file mode 100644 index 000000000000..ee0370d73520 --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/scope/SimpleTarget.java @@ -0,0 +1,20 @@ +/* + * 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.testfixture.scope; + +public class SimpleTarget { +}