diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index aa0a5ba5e975..5d0b7da05b4d 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -25,6 +25,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -35,6 +36,13 @@ import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aot.generate.AccessControl; +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.support.ClassHintUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.BeanCreationException; @@ -43,20 +51,30 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor; import org.springframework.beans.factory.annotation.InjectionMetadata; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.EmbeddedValueResolver; import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.AutowireCandidateResolver; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.aot.ResourceFieldValueResolver; +import org.springframework.context.aot.ResourceMethodArgumentResolver; import org.springframework.core.BridgeMethodResolver; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; import org.springframework.jndi.support.SimpleJndiBeanFactory; 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; import org.springframework.util.StringValueResolver; @@ -298,6 +316,37 @@ public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, C metadata.checkConfigMembers(beanDefinition); } + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + BeanRegistrationAotContribution parentAotContribution = super.processAheadOfTime(registeredBean); + Class beanClass = registeredBean.getBeanClass(); + String beanName = registeredBean.getBeanName(); + RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); + InjectionMetadata metadata = findResourceMetadata(beanName, beanClass, + beanDefinition.getPropertyValues()); + Collection injectedElements = getInjectedElements(metadata, + beanDefinition.getPropertyValues()); + if (!ObjectUtils.isEmpty(injectedElements)) { + AotContribution aotContribution = new AotContribution(beanClass, injectedElements, + getAutowireCandidateResolver(registeredBean)); + return BeanRegistrationAotContribution.concat(parentAotContribution, aotContribution); + } + return parentAotContribution; + } + + @Nullable + private AutowireCandidateResolver getAutowireCandidateResolver(RegisteredBean registeredBean) { + if (registeredBean.getBeanFactory() instanceof DefaultListableBeanFactory lbf) { + return lbf.getAutowireCandidateResolver(); + } + return null; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Collection getInjectedElements(InjectionMetadata metadata, PropertyValues propertyValues) { + return (Collection) metadata.getInjectedElements(propertyValues); + } + @Override public void resetBeanDefinition(String beanName) { this.injectionMetadataCache.remove(beanName); @@ -789,4 +838,144 @@ public Class getDependencyType() { } } + /** + * {@link BeanRegistrationAotContribution} to inject resources on fields and methods. + */ + private static class AotContribution implements BeanRegistrationAotContribution { + + private static final String REGISTERED_BEAN_PARAMETER = "registeredBean"; + + private static final String INSTANCE_PARAMETER = "instance"; + + private final Class target; + + private final Collection lookupElements; + + @Nullable + private final AutowireCandidateResolver candidateResolver; + + AotContribution(Class target, Collection lookupElements, + @Nullable AutowireCandidateResolver candidateResolver) { + + this.target = target; + this.lookupElements = lookupElements; + this.candidateResolver = candidateResolver; + } + + @Override + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + GeneratedClass generatedClass = generationContext.getGeneratedClasses() + .addForFeatureComponent("ResourceAutowiring", this.target, type -> { + type.addJavadoc("Resource autowiring for {@link $T}.", this.target); + type.addModifiers(javax.lang.model.element.Modifier.PUBLIC); + }); + GeneratedMethod generateMethod = generatedClass.getMethods().add("apply", method -> { + method.addJavadoc("Apply resource autowiring."); + method.addModifiers(javax.lang.model.element.Modifier.PUBLIC, + javax.lang.model.element.Modifier.STATIC); + method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER); + method.addParameter(this.target, INSTANCE_PARAMETER); + method.returns(this.target); + method.addCode(generateMethodCode(generatedClass.getName(), + generationContext.getRuntimeHints())); + }); + beanRegistrationCode.addInstancePostProcessor(generateMethod.toMethodReference()); + + registerHints(generationContext.getRuntimeHints()); + } + + private CodeBlock generateMethodCode(ClassName targetClassName, RuntimeHints hints) { + CodeBlock.Builder code = CodeBlock.builder(); + for (LookupElement lookupElement : this.lookupElements) { + code.addStatement(generateMethodStatementForElement( + targetClassName, lookupElement, hints)); + } + code.addStatement("return $L", INSTANCE_PARAMETER); + return code.build(); + } + + private CodeBlock generateMethodStatementForElement(ClassName targetClassName, + LookupElement lookupElement, RuntimeHints hints) { + + Member member = lookupElement.getMember(); + if (member instanceof Field field) { + return generateMethodStatementForField( + targetClassName, field, lookupElement, hints); + } + if (member instanceof Method method) { + return generateMethodStatementForMethod( + targetClassName, method, lookupElement, hints); + } + throw new IllegalStateException( + "Unsupported member type " + member.getClass().getName()); + } + + private CodeBlock generateMethodStatementForField(ClassName targetClassName, + Field field, LookupElement lookupElement, RuntimeHints hints) { + + hints.reflection().registerField(field); + CodeBlock resolver = generateFieldResolverCode(field, lookupElement); + AccessControl accessControl = AccessControl.forMember(field); + if (!accessControl.isAccessibleFrom(targetClassName)) { + return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, + REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); + } + return CodeBlock.of("$L.$L = $L.resolve($L)", INSTANCE_PARAMETER, + field.getName(), resolver, REGISTERED_BEAN_PARAMETER); + } + + private CodeBlock generateFieldResolverCode(Field field, LookupElement lookupElement) { + if (lookupElement.isDefaultName) { + return CodeBlock.of("$T.$L($S)", ResourceFieldValueResolver.class, + "forField", field.getName()); + } + else { + return CodeBlock.of("$T.$L($S, $S)", ResourceFieldValueResolver.class, + "forField", field.getName(), lookupElement.getName()); + } + } + + private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, + Method method, LookupElement lookupElement, RuntimeHints hints) { + + CodeBlock resolver = generateMethodResolverCode(method, lookupElement); + AccessControl accessControl = AccessControl.forMember(method); + if (!accessControl.isAccessibleFrom(targetClassName)) { + hints.reflection().registerMethod(method, ExecutableMode.INVOKE); + return CodeBlock.of("$L.resolveAndInvoke($L, $L)", resolver, + REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); + } + hints.reflection().registerMethod(method, ExecutableMode.INTROSPECT); + return CodeBlock.of("$L.$L($L.resolve($L))", INSTANCE_PARAMETER, + method.getName(), resolver, REGISTERED_BEAN_PARAMETER); + + } + + private CodeBlock generateMethodResolverCode(Method method, LookupElement lookupElement) { + if (lookupElement.isDefaultName) { + return CodeBlock.of("$T.$L($S, $T.class)", ResourceMethodArgumentResolver.class, + "forMethod", method.getName(), lookupElement.getLookupType()); + } + else { + return CodeBlock.of("$T.$L($S, $T.class, $S)", ResourceMethodArgumentResolver.class, + "forMethod", method.getName(), lookupElement.getLookupType(), lookupElement.getName()); + } + } + + private void registerHints(RuntimeHints runtimeHints) { + this.lookupElements.forEach(lookupElement -> + registerProxyIfNecessary(runtimeHints, lookupElement.getDependencyDescriptor())); + } + + private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescriptor dependencyDescriptor) { + if (this.candidateResolver != null) { + Class proxyClass = + this.candidateResolver.getLazyResolutionProxyClass(dependencyDescriptor, null); + if (proxyClass != null) { + ClassHintUtils.registerProxyIfNecessary(proxyClass, runtimeHints); + } + } + } + } + } diff --git a/spring-context/src/main/java/org/springframework/context/aot/ResourceElementResolver.java b/spring-context/src/main/java/org/springframework/context/aot/ResourceElementResolver.java new file mode 100644 index 000000000000..2b186ad541c4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/ResourceElementResolver.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2023 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.context.aot; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.lang.model.element.Element; + +import jakarta.annotation.Resource; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for resolvers that support injection of named beans on + * an {@link Element}. + * + * @author Stephane Nicoll + * @since 6.1 + * @see Resource + */ +public abstract class ResourceElementResolver { + + protected final String name; + + protected final boolean defaultName; + + protected ResourceElementResolver(String name, boolean defaultName) { + this.name = name; + this.defaultName = defaultName; + } + + /** + * Resolve the field value for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved field value + */ + @Nullable + @SuppressWarnings("unchecked") + public T resolve(RegisteredBean registeredBean) { + return (T) resolveObject(registeredBean); + } + + /** + * Resolve the field value for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved field value + */ + public Object resolveObject(RegisteredBean registeredBean) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + return resolveValue(registeredBean); + } + + + /** + * Create a suitable {@link DependencyDescriptor} for the specified bean. + * @param bean the registered bean + * @return a descriptor for that bean + */ + protected abstract DependencyDescriptor createDependencyDescriptor(RegisteredBean bean); + + /** + * Resolve the value to inject for this instance. + * @param bean the bean registration + * @return the value to inject + */ + protected Object resolveValue(RegisteredBean bean) { + ConfigurableListableBeanFactory factory = bean.getBeanFactory(); + + Object resource; + Set autowiredBeanNames; + DependencyDescriptor descriptor = createDependencyDescriptor(bean); + if (this.defaultName && !factory.containsBean(this.name)) { + autowiredBeanNames = new LinkedHashSet<>(); + resource = factory.resolveDependency(descriptor, bean.getBeanName(), autowiredBeanNames, null); + if (resource == null) { + throw new NoSuchBeanDefinitionException(descriptor.getDependencyType(), "No resolvable resource object"); + } + } + else { + resource = factory.resolveBeanByName(this.name, descriptor); + autowiredBeanNames = Collections.singleton(this.name); + } + + for (String autowiredBeanName : autowiredBeanNames) { + if (factory.containsBean(autowiredBeanName)) { + factory.registerDependentBean(autowiredBeanName, bean.getBeanName()); + } + } + return resource; + } + + + @SuppressWarnings("serial") + protected static class LookupDependencyDescriptor extends DependencyDescriptor { + + private final Class lookupType; + + public LookupDependencyDescriptor(Field field, Class lookupType) { + super(field, true); + this.lookupType = lookupType; + } + + public LookupDependencyDescriptor(Method method, Class lookupType) { + super(new MethodParameter(method, 0), true); + this.lookupType = lookupType; + } + + @Override + public Class getDependencyType() { + return this.lookupType; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/ResourceFieldValueResolver.java b/spring-context/src/main/java/org/springframework/context/aot/ResourceFieldValueResolver.java new file mode 100644 index 000000000000..afda73b49831 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/ResourceFieldValueResolver.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2023 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.context.aot; + +import java.lang.reflect.Field; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Resolver used to support injection of named beans on fields. Typically used in + * AOT-processed applications as a targeted alternative to the + * {@link org.springframework.context.annotation.CommonAnnotationBeanPostProcessor}. + * + *

When resolving arguments in a native image, the {@link Field} being used must + * be marked with an {@link ExecutableMode#INTROSPECT introspection} hint so + * that field annotations can be read. Full {@link ExecutableMode#INVOKE + * invocation} hints are only required if the + * {@link #resolveAndSet(RegisteredBean, Object)} method of this class is being + * used (typically to support private fields). + * + * @author Stephane Nicoll + * @since 6.1 + */ +public final class ResourceFieldValueResolver extends ResourceElementResolver { + + private final String fieldName; + + public ResourceFieldValueResolver(String name, boolean defaultName, String fieldName) { + super(name, defaultName); + this.fieldName = fieldName; + } + + + /** + * Create a new {@link ResourceFieldValueResolver} for the specified field. + * @param fieldName the field name + * @return a new {@link ResourceFieldValueResolver} instance + */ + public static ResourceFieldValueResolver forField(String fieldName) { + return new ResourceFieldValueResolver(fieldName, true, fieldName); + } + + /** + * Create a new {@link ResourceFieldValueResolver} for the specified field and + * resource name. + * @param fieldName the field name + * @param resourceName the resource name + * @return a new {@link ResourceFieldValueResolver} instance + */ + public static ResourceFieldValueResolver forField(String fieldName, String resourceName) { + return new ResourceFieldValueResolver(resourceName, false, fieldName); + } + + @Override + protected DependencyDescriptor createDependencyDescriptor(RegisteredBean bean) { + Field field = getField(bean); + return new LookupDependencyDescriptor(field, field.getType()); + } + + /** + * Resolve the field value for the specified registered bean and set it + * using reflection. + * @param registeredBean the registered bean + * @param instance the bean instance + */ + public void resolveAndSet(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(instance, "'instance' must not be null"); + Field field = getField(registeredBean); + Object resolved = resolveValue(registeredBean); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, instance, resolved); + } + + private Field getField(RegisteredBean registeredBean) { + Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), + this.fieldName); + Assert.notNull(field, () -> "No field '" + this.fieldName + "' found on " + + registeredBean.getBeanClass().getName()); + return field; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/ResourceMethodArgumentResolver.java b/spring-context/src/main/java/org/springframework/context/aot/ResourceMethodArgumentResolver.java new file mode 100644 index 000000000000..6abf88537323 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/ResourceMethodArgumentResolver.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2023 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.context.aot; + +import java.lang.reflect.Method; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Resolver used to support injection of named beans to methods. Typically used in + * AOT-processed applications as a targeted alternative to the + * {@link org.springframework.context.annotation.CommonAnnotationBeanPostProcessor}. + * + *

When resolving arguments in a native image, the {@link Method} being used + * must be marked with an {@link ExecutableMode#INTROSPECT introspection} hint + * so that field annotations can be read. Full {@link ExecutableMode#INVOKE + * invocation} hints are only required if the + * {@link #resolveAndInvoke(RegisteredBean, Object)} method of this class is + * being used (typically to support private methods). + * @author Stephane Nicoll + * @since 6.1 + */ +public final class ResourceMethodArgumentResolver extends ResourceElementResolver { + + private final String methodName; + + private final Class lookupType; + + private ResourceMethodArgumentResolver(String name, boolean defaultName, + String methodName, Class lookupType) { + super(name, defaultName); + this.methodName = methodName; + this.lookupType = lookupType; + } + + + /** + * Create a new {@link ResourceMethodArgumentResolver} for the specified method + * using a resource name that infers from the method name. + * @param methodName the method name + * @param parameterType the parameter type. + * @return a new {@link ResourceMethodArgumentResolver} instance + */ + public static ResourceMethodArgumentResolver forMethod(String methodName, Class parameterType) { + return new ResourceMethodArgumentResolver(defaultResourceName(methodName), true, + methodName, parameterType); + } + + /** + * Create a new {@link ResourceMethodArgumentResolver} for the specified method + * and resource name. + * @param methodName the method name + * @param parameterType the parameter type + * @param resourceName the resource name + * @return a new {@link ResourceMethodArgumentResolver} instance + */ + public static ResourceMethodArgumentResolver forMethod(String methodName, Class parameterType, String resourceName) { + return new ResourceMethodArgumentResolver(resourceName, false, methodName, parameterType); + } + + @Override + protected DependencyDescriptor createDependencyDescriptor(RegisteredBean bean) { + return new LookupDependencyDescriptor(getMethod(bean), this.lookupType); + } + + /** + * Resolve the method argument for the specified registered bean and invoke + * the method using reflection. + * @param registeredBean the registered bean + * @param instance the bean instance + */ + public void resolveAndInvoke(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "'registeredBean' must not be null"); + Assert.notNull(instance, "'instance' must not be null"); + Method method = getMethod(registeredBean); + Object resolved = resolveValue(registeredBean); + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, instance, resolved); + } + + private Method getMethod(RegisteredBean registeredBean) { + Method method = ReflectionUtils.findMethod(registeredBean.getBeanClass(), + this.methodName, this.lookupType); + Assert.notNull(method, () -> + "Method '%s' with parameter type '%s' declared on %s could not be found.".formatted( + this.methodName, this.lookupType.getName(), + registeredBean.getBeanClass().getName())); + return method; + } + + private static String defaultResourceName(String methodName) { + if (methodName.startsWith("set") && methodName.length() > 3) { + return StringUtils.uncapitalizeAsProperty(methodName.substring(3)); + } + return methodName; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java new file mode 100644 index 000000000000..c9bb47dca699 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java @@ -0,0 +1,262 @@ +/* + * Copyright 2002-2023 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.context.annotation; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import javax.lang.model.element.Modifier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.factory.aot.MockBeanRegistrationCode; +import org.springframework.context.testfixture.context.annotation.PackagePrivateFieldResourceSample; +import org.springframework.context.testfixture.context.annotation.PackagePrivateMethodResourceSample; +import org.springframework.context.testfixture.context.annotation.PrivateFieldResourceSample; +import org.springframework.context.testfixture.context.annotation.PrivateMethodResourceSample; +import org.springframework.context.testfixture.context.annotation.PrivateMethodResourceWithCustomNameSample; +import org.springframework.context.testfixture.context.annotation.PublicMethodResourceSample; +import org.springframework.context.testfixture.context.annotation.subpkg.PackagePrivateFieldResourceFromParentSample; +import org.springframework.context.testfixture.context.annotation.subpkg.PackagePrivateMethodResourceFromParentSample; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.Compiled; +import org.springframework.core.test.tools.SourceFile; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for AOT contributions of {@link CommonAnnotationBeanPostProcessor}. + * + * @author Stephane Nicoll + */ +class CommonAnnotationBeanRegistrationAotContributionTests { + + private final TestGenerationContext generationContext; + + private final MockBeanRegistrationCode beanRegistrationCode; + + private final DefaultListableBeanFactory beanFactory; + + private final CommonAnnotationBeanPostProcessor beanPostProcessor; + + CommonAnnotationBeanRegistrationAotContributionTests() { + this.generationContext = new TestGenerationContext(); + this.beanRegistrationCode = new MockBeanRegistrationCode(this.generationContext); + this.beanFactory = new DefaultListableBeanFactory(); + this.beanPostProcessor = new CommonAnnotationBeanPostProcessor(); + this.beanPostProcessor.setBeanFactory(this.beanFactory); + } + + @Test + void contributeWhenPrivateFieldInjectionInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PrivateFieldResourceSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onField(PrivateFieldResourceSample.class, "one")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PrivateFieldResourceSample instance = new PrivateFieldResourceSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PrivateFieldResourceSample.class)) + .contains("resolveAndSet("); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateFieldInjectionInjectsUsingFieldAssignement() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateFieldResourceSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onField(PackagePrivateFieldResourceSample.class, "one")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateFieldResourceSample instance = new PackagePrivateFieldResourceSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PackagePrivateFieldResourceSample.class)) + .contains("instance.one ="); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateFieldInjectionOnParentClassInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateFieldResourceFromParentSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onField(PackagePrivateFieldResourceSample.class, "one")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateFieldResourceFromParentSample instance = new PackagePrivateFieldResourceFromParentSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PackagePrivateFieldResourceFromParentSample.class)) + .contains("resolveAndSet"); + }); + } + + @Test + void contributeWhenPrivateMethodInjectionInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PrivateMethodResourceSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PrivateMethodResourceSample.class, "setOne").invoke()) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PrivateMethodResourceSample instance = new PrivateMethodResourceSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PrivateMethodResourceSample.class)) + .contains("resolveAndInvoke("); + }); + } + + @Test + void contributeWhenPrivateMethodInjectionWithCustomNameInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PrivateMethodResourceWithCustomNameSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PrivateMethodResourceWithCustomNameSample.class, "setText").invoke()) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PrivateMethodResourceWithCustomNameSample instance = new PrivateMethodResourceWithCustomNameSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("text").isEqualTo("1"); + assertThat(getSourceFile(compiled, PrivateMethodResourceWithCustomNameSample.class)) + .contains("resolveAndInvoke("); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateMethodInjectionInjectsUsingMethodInvocation() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateMethodResourceSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PackagePrivateMethodResourceSample.class, "setOne").introspect()) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateMethodResourceSample instance = new PackagePrivateMethodResourceSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PackagePrivateMethodResourceSample.class)) + .contains("instance.setOne("); + }); + } + + @Test + @CompileWithForkedClassLoader + void contributeWhenPackagePrivateMethodInjectionOnParentClassInjectsUsingReflection() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = getAndApplyContribution( + PackagePrivateMethodResourceFromParentSample.class); + assertThat(RuntimeHintsPredicates.reflection() + .onMethod(PackagePrivateMethodResourceSample.class, "setOne")) + .accepts(this.generationContext.getRuntimeHints()); + compile(registeredBean, (postProcessor, compiled) -> { + PackagePrivateMethodResourceFromParentSample instance = new PackagePrivateMethodResourceFromParentSample(); + postProcessor.apply(registeredBean, instance); + assertThat(instance).extracting("one").isEqualTo("1"); + assertThat(getSourceFile(compiled, PackagePrivateMethodResourceFromParentSample.class)) + .contains("resolveAndInvoke("); + }); + } + + @Test + void contributeWhenMethodInjectionHasMatchingPropertyValue() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(PublicMethodResourceSample.class); + beanDefinition.getPropertyValues().addPropertyValue("one", "from-property"); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + BeanRegistrationAotContribution contribution = this.beanPostProcessor + .processAheadOfTime(RegisteredBean.of(this.beanFactory, "test")); + assertThat(contribution).isNull(); + } + + private RegisteredBean getAndApplyContribution(Class beanClass) { + RegisteredBean registeredBean = registerBean(beanClass); + BeanRegistrationAotContribution contribution = this.beanPostProcessor + .processAheadOfTime(registeredBean); + assertThat(contribution).isNotNull(); + contribution.applyTo(this.generationContext, this.beanRegistrationCode); + return registeredBean; + } + + private RegisteredBean registerBean(Class beanClass) { + String beanName = "testBean"; + this.beanFactory.registerBeanDefinition(beanName, + new RootBeanDefinition(beanClass)); + return RegisteredBean.of(this.beanFactory, beanName); + } + + private static SourceFile getSourceFile(Compiled compiled, Class sample) { + return compiled.getSourceFileFromPackage(sample.getPackageName()); + } + + + @SuppressWarnings("unchecked") + private void compile(RegisteredBean registeredBean, + BiConsumer, Compiled> result) { + Class target = registeredBean.getBeanClass(); + MethodReference methodReference = this.beanRegistrationCode.getInstancePostProcessors().get(0); + this.beanRegistrationCode.getTypeBuilder().set(type -> { + CodeBlock methodInvocation = methodReference.toInvokeCodeBlock( + MethodReference.ArgumentCodeGenerator.of(RegisteredBean.class, "registeredBean") + .and(target, "instance"), this.beanRegistrationCode.getClassName()); + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get( + BiFunction.class, RegisteredBean.class, target, target)); + type.addMethod(MethodSpec.methodBuilder("apply") + .addModifiers(Modifier.PUBLIC) + .addParameter(RegisteredBean.class, "registeredBean") + .addParameter(target, "instance") + .returns(target) + .addStatement("return $L", methodInvocation) + .build()); + + }); + this.generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(this.generationContext).printFiles(System.out) + .compile(compiled -> result.accept(compiled.getInstance(BiFunction.class), compiled)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java index f4d95366d96b..23ace1a4f74b 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java @@ -76,8 +76,11 @@ import org.springframework.context.testfixture.context.annotation.LazyAutowiredMethodComponent; import org.springframework.context.testfixture.context.annotation.LazyConstructorArgumentComponent; import org.springframework.context.testfixture.context.annotation.LazyFactoryMethodArgumentComponent; +import org.springframework.context.testfixture.context.annotation.LazyResourceFieldComponent; +import org.springframework.context.testfixture.context.annotation.LazyResourceMethodComponent; import org.springframework.context.testfixture.context.annotation.PropertySourceConfiguration; import org.springframework.context.testfixture.context.annotation.QualifierConfiguration; +import org.springframework.context.testfixture.context.annotation.ResourceComponent; import org.springframework.context.testfixture.context.generator.SimpleComponent; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; @@ -227,6 +230,79 @@ private void testAutowiredComponent(Class type, RootBeanDefinition beanDe } + @Nested + class ResourceAutowiring { + + @Test + void processAheadOfTimeWhenHasResourceAutowiring() { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + registerBeanPostProcessor(applicationContext, + AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, CommonAnnotationBeanPostProcessor.class); + registerStringBean(applicationContext, "text", "hello"); + registerStringBean(applicationContext, "text2", "hello2"); + registerIntegerBean(applicationContext, "number", 42); + applicationContext.registerBeanDefinition("resourceComponent", new RootBeanDefinition(ResourceComponent.class)); + testCompiledResult(applicationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("resourceComponent", "text", "text2", "number"); + ResourceComponent bean = freshApplicationContext.getBean(ResourceComponent.class); + assertThat(bean.getText()).isEqualTo("hello"); + assertThat(bean.getCounter()).isEqualTo(42); + }); + } + + @Test + void processAheadOfTimeWhenHasLazyResourceAutowiringOnField() { + testResourceAutowiringComponent(LazyResourceFieldComponent.class, (bean, generationContext) -> { + Environment environment = bean.getEnvironment(); + assertThat(environment).isInstanceOf(Proxy.class); + ResourceLoader resourceLoader = bean.getResourceLoader(); + assertThat(resourceLoader).isNotInstanceOf(Proxy.class); + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(ResourceLoader.class)); + assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> + assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( + environment.getClass().getInterfaces()))); + + }); + } + + @Test + void processAheadOfTimeWhenHasLazyResourceAutowiringOnMethod() { + testResourceAutowiringComponent(LazyResourceMethodComponent.class, (bean, generationContext) -> { + Environment environment = bean.getEnvironment(); + assertThat(environment).isNotInstanceOf(Proxy.class); + ResourceLoader resourceLoader = bean.getResourceLoader(); + assertThat(resourceLoader).isInstanceOf(Proxy.class); + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + assertThat(runtimeHints.proxies().jdkProxyHints()).satisfies(doesNotHaveProxyFor(Environment.class)); + assertThat(runtimeHints.proxies().jdkProxyHints()).anySatisfy(proxyHint -> + assertThat(proxyHint.getProxiedInterfaces()).isEqualTo(TypeReference.listOf( + resourceLoader.getClass().getInterfaces()))); + }); + } + + private void testResourceAutowiringComponent(Class type, BiConsumer assertions) { + testResourceAutowiringComponent(type, new RootBeanDefinition(type), assertions); + } + + private void testResourceAutowiringComponent(Class type, RootBeanDefinition beanDefinition, + BiConsumer assertions) { + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.getDefaultListableBeanFactory().setAutowireCandidateResolver( + new ContextAnnotationAutowireCandidateResolver()); + registerBeanPostProcessor(applicationContext, + AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME, CommonAnnotationBeanPostProcessor.class); + applicationContext.registerBeanDefinition("testComponent", beanDefinition); + TestGenerationContext generationContext = processAheadOfTime(applicationContext); + testCompiledResult(generationContext, (initializer, compiled) -> { + GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer); + assertThat(freshApplicationContext.getBeanDefinitionNames()).containsOnly("testComponent"); + assertions.accept(freshApplicationContext.getBean("testComponent", type), generationContext); + }); + } + } + @Nested class InitDestroy { @@ -519,6 +595,14 @@ private static void registerBeanPostProcessor(GenericApplicationContext applicat .getBeanDefinition()); } + private static void registerStringBean(GenericApplicationContext applicationContext, + String beanName, String value) { + + applicationContext.registerBeanDefinition(beanName, BeanDefinitionBuilder + .rootBeanDefinition(String.class).addConstructorArgValue(value) + .getBeanDefinition()); + } + private static void registerIntegerBean(GenericApplicationContext applicationContext, String beanName, int value) { diff --git a/spring-context/src/test/java/org/springframework/context/aot/ResourceFieldValueResolverTests.java b/spring-context/src/test/java/org/springframework/context/aot/ResourceFieldValueResolverTests.java new file mode 100644 index 000000000000..a2d425602d03 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/aot/ResourceFieldValueResolverTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2023 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.context.aot; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ResourceFieldValueResolver}. + * + * @author Stephane Nicoll + */ +class ResourceFieldValueResolverTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + + @Test + void resolveWhenFieldIsMissingThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> ResourceFieldValueResolver.forField("missing") + .resolve(registeredBean)) + .withMessage("No field 'missing' found on " + TestBean.class.getName()); + } + + @Test + void resolveReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceFieldValueResolver.forField("one") + .resolve(registeredBean); + assertThat(resolved).isEqualTo("1"); + } + + @Test + void resolveWhenResourceNameAndMatchReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceFieldValueResolver.forField("test", "two") + .resolve(registeredBean); + assertThat(resolved).isEqualTo("2"); + } + + @Test + void resolveWheNoMatchFallbackOnType() { + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceFieldValueResolver.forField("one") + .resolve(registeredBean); + assertThat(resolved).isEqualTo("2"); + } + + @Test + void resolveWhenMultipleCandidatesWithNoNameMatchThrowsException() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceFieldValueResolver.forField("test") + .resolve(registeredBean) + ).isInstanceOf(NoUniqueBeanDefinitionException.class) + .hasMessageContaining(String.class.getName()) + .hasMessageContaining("one").hasMessageContaining("two"); + } + + @Test + void resolveWhenNoCandidateMatchingTypeThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceFieldValueResolver.forField("test") + .resolve(registeredBean) + ).isInstanceOf(NoSuchBeanDefinitionException.class) + .hasMessageContaining(String.class.getName()); + } + + @Test + void resolveWhenInvalidMatchingTypeThrowsException() { + this.beanFactory.registerSingleton("count", "counter"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceFieldValueResolver.forField("count") + .resolve(registeredBean) + ).isInstanceOf(BeanNotOfRequiredTypeException.class) + .hasMessageContaining(Integer.class.getName()) + .hasMessageContaining(String.class.getName()); + } + + @Test + void resolveAndSetSetsValue() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + TestBean testBean = new TestBean(); + ResourceFieldValueResolver.forField("one").resolveAndSet(registeredBean, + testBean); + assertThat(testBean.one).isEqualTo("1"); + } + + @Test + void resolveRegistersDependantBeans() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + ResourceFieldValueResolver.forField("one").resolve(registeredBean); + assertThat(this.beanFactory.getDependentBeans("one")).containsExactly("testBean"); + } + + private RegisteredBean registerTestBean(DefaultListableBeanFactory beanFactory) { + beanFactory.registerBeanDefinition("testBean", + new RootBeanDefinition(TestBean.class)); + return RegisteredBean.of(beanFactory, "testBean"); + } + + static class TestBean { + + String one; + + String test; + + Integer count; + + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/aot/ResourceMethodArgumentResolverTests.java b/spring-context/src/test/java/org/springframework/context/aot/ResourceMethodArgumentResolverTests.java new file mode 100644 index 000000000000..50db06e86769 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/aot/ResourceMethodArgumentResolverTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2023 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.context.aot; + +import java.io.InputStream; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ResourceMethodArgumentResolver}/ + * + * @author Stephane Nicoll + */ +class ResourceMethodArgumentResolverTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void resolveWhenMethodIsMissingThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + ResourceMethodArgumentResolver resolver = ResourceMethodArgumentResolver.forMethod("missing", InputStream.class); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.resolve(registeredBean)) + .withMessage("Method 'missing' with parameter type 'java.io.InputStream' declared on %s could not be found.", + TestBean.class.getName()); + } + + @Test + void resolveReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + ResourceMethodArgumentResolver resolver = ResourceMethodArgumentResolver + .forMethod("setOne", String.class); + Object resolved = resolver.resolve(registeredBean); + assertThat(resolved).isEqualTo("1"); + } + + @Test + void resolveWhenResourceNameAndMatchReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceMethodArgumentResolver.forMethod("setTest", String.class, "two") + .resolve(registeredBean); + assertThat(resolved).isEqualTo("2"); + } + + @Test + void resolveWheNoMatchFallbackOnType() { + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = ResourceMethodArgumentResolver.forMethod("setOne", String.class) + .resolve(registeredBean); + assertThat(resolved).isEqualTo("2"); + } + + @Test + void resolveWhenMultipleCandidatesWithNoNameMatchThrowsException() { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceMethodArgumentResolver.forMethod("setTest", String.class) + .resolve(registeredBean) + ).isInstanceOf(NoUniqueBeanDefinitionException.class) + .hasMessageContaining(String.class.getName()) + .hasMessageContaining("one").hasMessageContaining("two"); + } + + @Test + void resolveWhenNoCandidateMatchingTypeThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceMethodArgumentResolver.forMethod("setTest", String.class) + .resolve(registeredBean) + ).isInstanceOf(NoSuchBeanDefinitionException.class) + .hasMessageContaining(String.class.getName()); + } + + @Test + void resolveWhenInvalidMatchingTypeThrowsException() { + this.beanFactory.registerSingleton("count", "counter"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatThrownBy(() -> ResourceMethodArgumentResolver.forMethod("setCount", Integer.class) + .resolve(registeredBean) + ).isInstanceOf(BeanNotOfRequiredTypeException.class) + .hasMessageContaining(Integer.class.getName()) + .hasMessageContaining(String.class.getName()); + } + + @Test + void resolveAndInvokeInvokesMethod() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + TestBean testBean = new TestBean(); + ResourceMethodArgumentResolver.forMethod("setOne", String.class) + .resolveAndInvoke(registeredBean, testBean); + assertThat(testBean.one).isEqualTo("1"); + } + + @Test + void resolveRegistersDependantBeans() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + ResourceMethodArgumentResolver.forMethod("setOne", String.class).resolve(registeredBean); + assertThat(this.beanFactory.getDependentBeans("one")).containsExactly("testBean"); + } + + private RegisteredBean registerTestBean(DefaultListableBeanFactory beanFactory) { + beanFactory.registerBeanDefinition("testBean", + new RootBeanDefinition(TestBean.class)); + return RegisteredBean.of(beanFactory, "testBean"); + } + + + static class TestBean { + + private String one; + + private String test; + + private Integer count; + + public void setOne(String one) { + this.one = one; + } + + public void setTest(String test) { + this.test = test; + } + + public void setCount(Integer count) { + this.count = count; + } + + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceFieldComponent.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceFieldComponent.java new file mode 100644 index 000000000000..60a4dcedf771 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceFieldComponent.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2023 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.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +public class LazyResourceFieldComponent { + + @Lazy + @Resource + private Environment environment; + + @Resource + private ResourceLoader resourceLoader; + + public Environment getEnvironment() { + return this.environment; + } + + public ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceMethodComponent.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceMethodComponent.java new file mode 100644 index 000000000000..e12fdd48579a --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/LazyResourceMethodComponent.java @@ -0,0 +1,49 @@ +/* + * 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.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +public class LazyResourceMethodComponent { + + private Environment environment; + + private ResourceLoader resourceLoader; + + @Resource + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + public Environment getEnvironment() { + return this.environment; + } + + @Resource + @Lazy + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + public ResourceLoader getResourceLoader() { + return this.resourceLoader; + } +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateFieldResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateFieldResourceSample.java new file mode 100644 index 000000000000..96658671e746 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateFieldResourceSample.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2023 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.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PackagePrivateFieldResourceSample { + + @Resource + String one; + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateMethodResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateMethodResourceSample.java new file mode 100644 index 000000000000..864fb5d6eaf7 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PackagePrivateMethodResourceSample.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2023 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.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PackagePrivateMethodResourceSample { + + private String one; + + @Resource + void setOne(String one) { + this.one = one; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateFieldResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateFieldResourceSample.java new file mode 100644 index 000000000000..7bf479d36c88 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateFieldResourceSample.java @@ -0,0 +1,26 @@ +/* + * 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.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PrivateFieldResourceSample { + + @Resource + private String one; + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceSample.java new file mode 100644 index 000000000000..7703e9773db9 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceSample.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2023 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.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PrivateMethodResourceSample { + + private String one; + + @Resource + private void setOne(String one) { + this.one = one; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceWithCustomNameSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceWithCustomNameSample.java new file mode 100644 index 000000000000..f6350412d8c8 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PrivateMethodResourceWithCustomNameSample.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2023 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.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PrivateMethodResourceWithCustomNameSample { + + private String text; + + @Resource(name = "one") + private void setText(String text) { + this.text = text; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PublicMethodResourceSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PublicMethodResourceSample.java new file mode 100644 index 000000000000..87ba35f421d5 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/PublicMethodResourceSample.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2023 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.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class PublicMethodResourceSample { + + private String one; + + @Resource + public void setOne(String one) { + this.one = one; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ResourceComponent.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ResourceComponent.java new file mode 100644 index 000000000000..98d945b27fc3 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/ResourceComponent.java @@ -0,0 +1,45 @@ +/* + * 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.context.testfixture.context.annotation; + +import jakarta.annotation.Resource; + +public class ResourceComponent { + + private String text; + + private Integer counter; + + public String getText() { + return this.text; + } + + @Resource + public void setText(String text) { + this.text = text; + } + + public Integer getCounter() { + return this.counter; + } + + @Resource + public void setCounter(Integer counter) { + this.counter = counter; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateFieldResourceFromParentSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateFieldResourceFromParentSample.java new file mode 100644 index 000000000000..efa112e80bc4 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateFieldResourceFromParentSample.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2023 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.context.testfixture.context.annotation.subpkg; + +import org.springframework.context.testfixture.context.annotation.PackagePrivateFieldResourceSample; + +public class PackagePrivateFieldResourceFromParentSample extends PackagePrivateFieldResourceSample { + + // see one from parent +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateMethodResourceFromParentSample.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateMethodResourceFromParentSample.java new file mode 100644 index 000000000000..4cf5cad2487b --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/subpkg/PackagePrivateMethodResourceFromParentSample.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2023 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.context.testfixture.context.annotation.subpkg; + +import org.springframework.context.testfixture.context.annotation.PackagePrivateMethodResourceSample; + +public class PackagePrivateMethodResourceFromParentSample extends PackagePrivateMethodResourceSample { + + // see one from parent +}