Add AOT support for @Resource

This commit adds ahead of time support for @Resource on fields and
methods. Lookup elements are discovered and code is generated to replace
that introspection at runtime.

Closes gh-29614
This commit is contained in:
Stéphane Nicoll
2023-10-16 16:36:16 +02:00
parent b510bc3bab
commit e2c2268c39
19 changed files with 1557 additions and 0 deletions

View File

@@ -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<BiFunction<RegisteredBean, Object, Object>, 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));
}
}

View File

@@ -24,6 +24,7 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -76,8 +77,11 @@ import org.springframework.context.testfixture.context.annotation.LazyAutowiredF
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 +231,80 @@ class ApplicationContextAotGeneratorTests {
}
@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
@Disabled("gh-31447")
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 <T> void testResourceAutowiringComponent(Class<T> type, BiConsumer<T, GenerationContext> assertions) {
testResourceAutowiringComponent(type, new RootBeanDefinition(type), assertions);
}
private <T> void testResourceAutowiringComponent(Class<T> type, RootBeanDefinition beanDefinition,
BiConsumer<T, GenerationContext> 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 +597,14 @@ class ApplicationContextAotGeneratorTests {
.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) {

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}