Allow flexible constructor arguments in factory implementations

Update `SpringFactoriesLoader` so that factory implementation classes
can have a constructor with arguments that are resolved dynamically.

Arguments are resolved using a `ArgumentResolver` interface that is
passed to the `loadFactories` method. This strategy interface is
intentionally simple and only allows resolution based on the argument
type. A number of convenience methods are provided to allow resolvers
to be built. For example:

	ArgumentResolver.of(String.class, "tests")
			.and(Integer.class, 123);

Factory implementation classes must have a non-ambiguous constructor
in order to be instantiated. The `SpringFactoriesLoader` uses the same
algorithm as `BeanUtils.getResolvableConstructor`.

See gh-28057

Co-authored-by: Madhura Bhave <bhavem@vmware.com>
Co-authored-by: Andy Wilkinson <wilkinsona@vmware.com>
This commit is contained in:
Phillip Webb
2022-02-15 15:34:38 -08:00
committed by Stephane Nicoll
parent ae1956cac7
commit 0b716c4f90
7 changed files with 760 additions and 37 deletions

View File

@@ -0,0 +1,41 @@
/*
* 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.core.io.support;
/**
* Used by {@link SpringFactoriesLoaderTests}.
*
* @author Andy Wilkinson
*/
class ConstructorArgsDummyFactory implements DummyFactory {
private final String string;
public ConstructorArgsDummyFactory(String string) {
this(string, 0);
}
private ConstructorArgsDummyFactory(String string, int reasonCode) {
this.string = string;
}
@Override
public String getString() {
return this.string;
}
}

View File

@@ -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.core.io.support;
/**
* Used by {@link SpringFactoriesLoaderTests}.
*
* @author Madhura Bhave
*/
class MultipleConstructorArgsDummyFactory implements DummyFactory {
private final String string;
private final Integer age;
MultipleConstructorArgsDummyFactory(String string) {
this(string, null);
}
MultipleConstructorArgsDummyFactory(String string, Integer age) {
this.string = string;
this.age = age;
}
@Override
public String getString() {
return this.string + this.age;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,15 +16,24 @@
package org.springframework.core.io.support;
import java.io.File;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
import org.springframework.core.io.support.SpringFactoriesLoader.FactoryInstantiator;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link SpringFactoriesLoader}.
@@ -32,6 +41,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* @author Arjen Poutsma
* @author Phillip Webb
* @author Sam Brannen
* @author Andy Wilkinson
* @author Madhura Bhave
*/
class SpringFactoriesLoaderTests {
@@ -43,9 +54,11 @@ class SpringFactoriesLoaderTests {
@AfterAll
static void checkCache() {
assertThat(SpringFactoriesLoader.cache).hasSize(1);
assertThat(SpringFactoriesLoader.cache).hasSize(3);
SpringFactoriesLoader.cache.clear();
}
@Test
void loadFactoryNames() {
List<String> factoryNames = SpringFactoriesLoader.loadFactoryNames(DummyFactory.class, null);
@@ -82,4 +95,230 @@ class SpringFactoriesLoaderTests {
+ "[org.springframework.core.io.support.MyDummyFactory1] for factory type [java.lang.String]");
}
@Test
void loadFactoryWithNonDefaultConstructor() {
ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected");
List<DummyFactory> factories = SpringFactoriesLoader.loadFactories(DummyFactory.class, LimitedClassLoader.constructorArgumentFactories, resolver);
assertThat(factories).hasSize(3);
assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class);
assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class);
assertThat(factories.get(2)).isInstanceOf(ConstructorArgsDummyFactory.class);
assertThat(factories).extracting(DummyFactory::getString).containsExactly("Foo", "Bar", "injected");
}
@Test
void loadFactoryWithMultipleConstructors() {
ArgumentResolver resolver = ArgumentResolver.of(String.class, "injected");
assertThatIllegalArgumentException()
.isThrownBy(() -> SpringFactoriesLoader.loadFactories(DummyFactory.class, LimitedClassLoader.multipleArgumentFactories, resolver))
.withMessageContaining("Unable to instantiate factory class "
+ "[org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] for factory type [org.springframework.core.io.support.DummyFactory]")
.havingRootCause().withMessageContaining("Class [org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] has no suitable constructor");
}
@Nested
class ArgumentResolverTests {
@Test
void ofValueResolvesValue() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test");
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isNull();
}
@Test
void ofValueSupplierResolvesValue() {
ArgumentResolver resolver = ArgumentResolver.ofSupplied(CharSequence.class, () -> "test");
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isNull();
}
@Test
void fromAdaptsFunction() {
ArgumentResolver resolver = ArgumentResolver.from(
type -> CharSequence.class.equals(type) ? "test" : null);
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isNull();
}
@Test
void andValueReturnsComposite() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").and(Integer.class, 123);
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isEqualTo(123);
}
@Test
void andValueWhenSameTypeReturnsCompositeResolvingFirst() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").and(CharSequence.class, "ignore");
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
}
@Test
void andValueSupplierReturnsComposite() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").andSupplied(Integer.class, () -> 123);
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isEqualTo(123);
}
@Test
void andValueSupplierWhenSameTypeReturnsCompositeResolvingFirst() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").andSupplied(CharSequence.class, () -> "ignore");
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
}
@Test
void andResolverReturnsComposite() {
ArgumentResolver resolver = ArgumentResolver.of(CharSequence.class, "test").and(Integer.class, 123);
resolver = resolver.and(ArgumentResolver.of(CharSequence.class, "ignore").and(Long.class, 234L));
assertThat(resolver.resolve(CharSequence.class)).isEqualTo("test");
assertThat(resolver.resolve(String.class)).isNull();
assertThat(resolver.resolve(Integer.class)).isEqualTo(123);
assertThat(resolver.resolve(Long.class)).isEqualTo(234L);
}
}
@Nested
class FactoryInstantiatorTests {
private final ArgumentResolver resolver = ArgumentResolver.of(String.class, "test");
@Test
void defaultConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
DefaultConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void singleConstructorWithArgumentsCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
SingleConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void multiplePrivateAndSinglePublicConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
MultiplePrivateAndSinglePublicConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void multiplePackagePrivateAndSinglePublicConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
MultiplePackagePrivateAndSinglePublicConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void singlePackagePrivateConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
SinglePackagePrivateConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void singlePrivateConstructorCreatesInstance() throws Exception {
Object instance = FactoryInstantiator.forClass(
SinglePrivateConstructor.class).instantiate(this.resolver);
assertThat(instance).isNotNull();
}
@Test
void multiplePackagePrivateConstructorsThrowsException() throws Exception {
assertThatIllegalStateException().isThrownBy(
() -> FactoryInstantiator.forClass(MultiplePackagePrivateConstructors.class))
.withMessageContaining("has no suitable constructor");
}
static class DefaultConstructor {
}
static class SingleConstructor {
SingleConstructor(String arg) {
}
}
static class MultiplePrivateAndSinglePublicConstructor {
public MultiplePrivateAndSinglePublicConstructor(String arg) {
this(arg, false);
}
private MultiplePrivateAndSinglePublicConstructor(String arg, boolean extra) {
}
}
static class MultiplePackagePrivateAndSinglePublicConstructor {
public MultiplePackagePrivateAndSinglePublicConstructor(String arg) {
this(arg, false);
}
MultiplePackagePrivateAndSinglePublicConstructor(String arg, boolean extra) {
}
}
static class SinglePackagePrivateConstructor {
SinglePackagePrivateConstructor(String arg) {
}
}
static class SinglePrivateConstructor {
private SinglePrivateConstructor(String arg) {
}
}
static class MultiplePackagePrivateConstructors {
MultiplePackagePrivateConstructors(String arg) {
this(arg, false);
}
MultiplePackagePrivateConstructors(String arg, boolean extra) {
}
}
}
private static class LimitedClassLoader extends URLClassLoader {
private static final ClassLoader constructorArgumentFactories = new LimitedClassLoader("constructor-argument-factories");
private static final ClassLoader multipleArgumentFactories = new LimitedClassLoader("multiple-arguments-factories");
LimitedClassLoader(String location) {
super(new URL[] { toUrl(location) });
}
private static URL toUrl(String location) {
try {
return new File("src/test/resources/org/springframework/core/io/support/" + location + "/").toURI().toURL();
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2002-2019 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.core.io.support
import org.assertj.core.api.Assertions.assertThat
import org.springframework.core.io.support.SpringFactoriesLoader.FactoryInstantiator
import org.junit.jupiter.api.Test
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver
/**
* Kotlin tests for {@link SpringFactoriesLoader}.
*
* @author Phillip Webb
*/
@Suppress("unused", "UNUSED_PARAMETER", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
class KotlinSpringFactoriesLoaderTests {
@Test
fun `Instantiate immutable data class`() {
val resolver = ArgumentResolver.of(java.lang.String::class.java, "test" as java.lang.String)
.and(Integer.TYPE, 123)
val instantiator = FactoryInstantiator.forClass<Immutable>(Immutable::class.java)
val instance = instantiator.instantiate(resolver)
assertThat(instance).isEqualTo(Immutable("test", 123))
}
@Test
fun `Instantiate immutable data class with optional parameter and all arguments specified`() {
val resolver = ArgumentResolver.of(java.lang.String::class.java, "test" as java.lang.String)
val instantiator = FactoryInstantiator.forClass<OneOptionalParameter>(OneOptionalParameter::class.java)
val instance = instantiator.instantiate(resolver)
assertThat(instance).isEqualTo(OneOptionalParameter("test", 12))
}
@Test
fun `Instantiate immutable class with optional argument and only mandatory arguments specified`() {
val resolver = ArgumentResolver.of(java.lang.String::class.java, "test" as java.lang.String)
.and(Integer.TYPE, 345)
val instantiator = FactoryInstantiator.forClass<OneOptionalParameter>(OneOptionalParameter::class.java)
val instance = instantiator.instantiate(resolver)
assertThat(instance).isEqualTo(OneOptionalParameter("test", 345))
}
@Test
fun `Instantiate immutable class with nullable argument`() {
val resolver = ArgumentResolver.of(java.lang.String::class.java, "test" as java.lang.String)
val instantiator = FactoryInstantiator.forClass<NullableParameter>(NullableParameter::class.java)
val instance = instantiator.instantiate(resolver)
assertThat(instance).isEqualTo(NullableParameter("test", null))
}
@Test
fun `Instantiate class with all optional argument`() {
val resolver = ArgumentResolver.none()
val instantiator = FactoryInstantiator.forClass<AllOptionalParameters>(AllOptionalParameters::class.java)
val instance = instantiator.instantiate(resolver)
assertThat(instance).isEqualTo(AllOptionalParameters())
}
@Test
@Suppress("UsePropertyAccessSyntax")
fun `Instantiate class with private constructor`() {
val resolver = ArgumentResolver.none()
val instantiator = FactoryInstantiator.forClass<PrivateConstructor>(PrivateConstructor::class.java)
val instance = instantiator.instantiate(resolver)
assertThat(instance).isNotNull()
}
@Test
fun `Instantiate class with protected constructor`() {
val resolver = ArgumentResolver.none()
val instantiator = FactoryInstantiator.forClass<ProtectedConstructor>(ProtectedConstructor::class.java)
val instance = instantiator.instantiate(resolver)
assertThat(instance).isNotNull()
}
@Test
fun `Instantiate private class`() {
val resolver = ArgumentResolver.none()
val instantiator = FactoryInstantiator.forClass<PrivateClass>(PrivateClass::class.java)
val instance = instantiator.instantiate(resolver)
assertThat(instance).isNotNull()
}
data class Immutable(val param1: String, val param2: Int)
data class OneOptionalParameter(val param1: String, val param2: Int = 12)
data class AllOptionalParameters(var param1: String = "a", var param2: Int = 12)
data class NullableParameter(val param1: String, val param2: Int?)
class PrivateConstructor private constructor()
open class ProtectedConstructor protected constructor()
private class PrivateClass
}

View File

@@ -0,0 +1,2 @@
org.springframework.core.io.support.DummyFactory=\
org.springframework.core.io.support.ConstructorArgsDummyFactory

View File

@@ -0,0 +1,2 @@
org.springframework.core.io.support.DummyFactory=\
org.springframework.core.io.support.MultipleConstructorArgsDummyFactory