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:
committed by
Stephane Nicoll
parent
ae1956cac7
commit
0b716c4f90
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.core.io.support.DummyFactory=\
|
||||
org.springframework.core.io.support.ConstructorArgsDummyFactory
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.core.io.support.DummyFactory=\
|
||||
org.springframework.core.io.support.MultipleConstructorArgsDummyFactory
|
||||
Reference in New Issue
Block a user