Add bean instance generator infrastructure

This commit provides the necessary infrastructure to let components
contribute statements that are used to fully instantiate a bean
instance.

To ease code generation, a dedicated infrastructure to register bean
definition is provided in the o.s.beans.factory.generator package.
BeanDefinitionRegistrar offers a builder style API that provides a way
to hide how injected elements are resolved at runtime and let
contributors provide code that may throw a checked exception.

BeanInstanceContributor is the interface that components can implement
to contribute to a bean instance setup. DefaultBeanInstanceGenerator
generates, for a particular bean definition, the necessary statements
to instantiate a bean.

Closes gh-28047
This commit is contained in:
Stephane Nicoll 2022-02-14 14:31:19 +01:00
parent bfe9d4fc49
commit c5e1a774a5
33 changed files with 4119 additions and 11 deletions

View File

@ -0,0 +1,51 @@
/*
* 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.beans.factory.generator;
import org.springframework.aot.generator.CodeContribution;
/**
* Strategy interface to be implemented by components that participates in a
* bean instance setup so that the generated code provides an equivalent
* setup.
*
* @author Stephane Nicoll
* @since 6.0
*/
@FunctionalInterface
public interface BeanInstanceContributor {
/**
* A {@link BeanInstanceContributor} that does not contribute anything
* to the {@link CodeContribution}.
*/
BeanInstanceContributor NO_OP = contribution -> { };
/**
* Contribute to the specified {@link CodeContribution}.
* <p>Implementation of this interface can assume the following variables
* to be accessible:
* <ul>
* <li>{@code beanFactory}: the general {@code DefaultListableBeanFactory}</li>
* <li>{@code instanceContext}: the {@code BeanInstanceContext} callback</li>
* <li>{@code bean}: the variable that refers to the bean instance</li>
* </ul>
* @param contribution the {@link CodeContribution} to use
*/
void contribute(CodeContribution contribution);
}

View File

@ -0,0 +1,218 @@
/*
* 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.beans.factory.generator;
import java.lang.reflect.Executable;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.aot.generator.ResolvableTypeGenerator;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedSet;
import org.springframework.core.ResolvableType;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.support.MultiCodeBlock;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
/**
* Support for writing parameters.
*
* @author Stephane Nicoll
* @since 6.0
*/
public final class BeanParameterGenerator {
private final ResolvableTypeGenerator typeGenerator = new ResolvableTypeGenerator();
private final BiConsumer<BeanDefinition, Builder> innerBeanDefinitionWriter;
/**
* Create an instance with the callback to use to write an inner bean
* definition.
* @param innerBeanDefinitionWriter the inner bean definition writer
*/
public BeanParameterGenerator(BiConsumer<BeanDefinition, Builder> innerBeanDefinitionWriter) {
this.innerBeanDefinitionWriter = innerBeanDefinitionWriter;
}
/**
* Create an instance with no support for inner bean definitions.
*/
public BeanParameterGenerator() {
this((beanDefinition, builder) -> {
throw new IllegalStateException("Inner bean definition is not supported by this instance");
});
}
/**
* Write the specified parameter {@code value}.
* @param value the value of the parameter
* @return the value of the parameter
*/
public CodeBlock writeParameterValue(@Nullable Object value) {
return writeParameterValue(value, () -> ResolvableType.forInstance(value));
}
/**
* Write the specified parameter {@code value}.
* @param value the value of the parameter
* @param parameterType the type of the parameter
* @return the value of the parameter
*/
public CodeBlock writeParameterValue(@Nullable Object value, Supplier<ResolvableType> parameterType) {
Builder code = CodeBlock.builder();
writeParameterValue(code, value, parameterType);
return code.build();
}
/**
* Write the parameter types of the specified {@link Executable}.
* @param executable the executable
* @return the parameter types of the executable as a comma separated list
*/
public CodeBlock writeExecutableParameterTypes(Executable executable) {
Class<?>[] parameterTypes = Arrays.stream(executable.getParameters())
.map(Parameter::getType).toArray(Class<?>[]::new);
return CodeBlock.of(Arrays.stream(parameterTypes).map(d -> "$T.class")
.collect(Collectors.joining(", ")), (Object[]) parameterTypes);
}
private void writeParameterValue(Builder code, @Nullable Object value, Supplier<ResolvableType> parameterTypeSupplier) {
if (value == null) {
code.add("null");
return;
}
ResolvableType parameterType = parameterTypeSupplier.get();
if (parameterType.isArray()) {
code.add("new $T { ", parameterType.toClass());
code.add(writeAll(Arrays.asList(ObjectUtils.toObjectArray(value)),
item -> parameterType.getComponentType()));
code.add(" }");
}
else if (value instanceof List<?> list) {
if (list.isEmpty()) {
code.add("$T.emptyList()", Collections.class);
}
else {
Class<?> listType = (value instanceof ManagedList ? ManagedList.class : List.class);
code.add("$T.of(", listType);
ResolvableType collectionType = parameterType.as(List.class).getGenerics()[0];
code.add(writeAll(list, item -> collectionType));
code.add(")");
}
}
else if (value instanceof Set<?> set) {
if (set.isEmpty()) {
code.add("$T.emptySet()", Collections.class);
}
else {
Class<?> setType = (value instanceof ManagedSet ? ManagedSet.class : Set.class);
code.add("$T.of(", setType);
ResolvableType collectionType = parameterType.as(Set.class).getGenerics()[0];
code.add(writeAll(set, item -> collectionType));
code.add(")");
}
}
else if (value instanceof Map<?, ?> map) {
if (map.size() <= 10) {
code.add("$T.of(", Map.class);
List<Object> parameters = new ArrayList<>();
map.forEach((mapKey, mapValue) -> {
parameters.add(mapKey);
parameters.add(mapValue);
});
code.add(writeAll(parameters, ResolvableType::forInstance));
code.add(")");
}
}
else if (value instanceof Character character) {
String result = '\'' + characterLiteralWithoutSingleQuotes(character) + '\'';
code.add(result);
}
else if (isPrimitiveOrWrapper(value)) {
code.add("$L", value);
}
else if (value instanceof String) {
code.add("$S", value);
}
else if (value instanceof Enum<?> enumValue) {
code.add("$T.$N", enumValue.getClass(), enumValue.name());
}
else if (value instanceof Class) {
code.add("$T.class", value);
}
else if (value instanceof ResolvableType) {
code.add(this.typeGenerator.generateTypeFor((ResolvableType) value));
}
else if (value instanceof BeanDefinition) {
this.innerBeanDefinitionWriter.accept((BeanDefinition) value, code);
}
else if (value instanceof BeanReference) {
code.add("new $T($S)", RuntimeBeanReference.class, ((BeanReference) value).getBeanName());
}
else {
throw new IllegalArgumentException("Parameter of type " + parameterType + " is not supported");
}
}
private <T> CodeBlock writeAll(Iterable<T> items, Function<T, ResolvableType> elementType) {
MultiCodeBlock multi = new MultiCodeBlock();
items.forEach(item -> multi.add(code ->
writeParameterValue(code, item, () -> elementType.apply(item))));
return multi.join(", ");
}
private boolean isPrimitiveOrWrapper(Object value) {
Class<?> valueType = value.getClass();
return (valueType.isPrimitive() || valueType == Double.class || valueType == Float.class
|| valueType == Long.class || valueType == Integer.class || valueType == Short.class
|| valueType == Character.class || valueType == Byte.class || valueType == Boolean.class);
}
// Copied from com.squareup.javapoet.Util
private static String characterLiteralWithoutSingleQuotes(char c) {
// see https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.6
return switch (c) {
case '\b' -> "\\b"; /* \u0008: backspace (BS) */
case '\t' -> "\\t"; /* \u0009: horizontal tab (HT) */
case '\n' -> "\\n"; /* \u000a: linefeed (LF) */
case '\f' -> "\\f"; /* \u000c: form feed (FF) */
case '\r' -> "\\r"; /* \u000d: carriage return (CR) */
case '\"' -> "\""; /* \u0022: double quote (") */
case '\'' -> "\\'"; /* \u0027: single quote (') */
case '\\' -> "\\\\"; /* \u005c: backslash (\) */
default -> Character.isISOControl(c) ? String.format("\\u%04x", (int) c) : Character.toString(c);
};
}
}

View File

@ -0,0 +1,165 @@
/*
* 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.beans.factory.generator;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.aot.generator.CodeContribution;
import org.springframework.aot.generator.DefaultCodeContribution;
import org.springframework.aot.generator.ProtectedAccess.Options;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.javapoet.CodeBlock;
import org.springframework.util.ClassUtils;
/**
* Write the necessary statements to instantiate a bean.
*
* @author Stephane Nicoll
*/
class DefaultBeanInstanceGenerator {
private static final Options BEAN_INSTANCE_OPTIONS = new Options(false, true);
private final Executable instanceCreator;
private final List<BeanInstanceContributor> contributors;
private final InjectionGenerator injectionGenerator;
DefaultBeanInstanceGenerator(Executable instanceCreator, List<BeanInstanceContributor> contributors) {
this.instanceCreator = instanceCreator;
this.contributors = List.copyOf(contributors);
this.injectionGenerator = new InjectionGenerator();
}
/**
* Return the necessary code to instantiate and post-process the bean
* handled by this instance.
* @param runtimeHints the runtime hints instance to use
* @return a code contribution that provides an initialized bean instance
*/
public CodeContribution generateBeanInstance(RuntimeHints runtimeHints) {
DefaultCodeContribution contribution = new DefaultCodeContribution(runtimeHints);
contribution.protectedAccess().analyze(this.instanceCreator, BEAN_INSTANCE_OPTIONS);
if (this.instanceCreator instanceof Constructor<?> constructor) {
writeBeanInstantiation(contribution, constructor);
}
else if (this.instanceCreator instanceof Method method) {
writeBeanInstantiation(contribution, method);
}
return contribution;
}
private void writeBeanInstantiation(CodeContribution contribution, Constructor<?> constructor) {
Class<?> declaringType = ClassUtils.getUserClass(constructor.getDeclaringClass());
boolean innerClass = isInnerClass(declaringType);
boolean multiStatements = !this.contributors.isEmpty();
int minArgs = isInnerClass(declaringType) ? 2 : 1;
CodeBlock.Builder code = CodeBlock.builder();
// Shortcut for common case
if (!multiStatements && constructor.getParameterTypes().length < minArgs) {
if (innerClass) {
code.add("() -> beanFactory.getBean($T.class).new $L()",
declaringType.getEnclosingClass(), declaringType.getSimpleName());
}
else {
// Only apply the shortcut if there's one candidate
if (declaringType.getDeclaredConstructors().length > 1) {
code.add("() -> new $T()", declaringType);
}
else {
code.add("$T::new", declaringType);
}
}
contribution.statements().addStatement(code.build());
return;
}
contribution.runtimeHints().reflection().registerConstructor(constructor,
hint -> hint.withMode(ExecutableMode.INTROSPECT));
code.add("(instanceContext) ->");
branch(multiStatements, () -> code.beginControlFlow(""), () -> code.add(" "));
if (multiStatements) {
code.add("$T bean = ", declaringType);
}
code.add(this.injectionGenerator.writeInstantiation(constructor));
contribution.statements().addStatement(code.build());
if (multiStatements) {
for (BeanInstanceContributor contributor : this.contributors) {
contributor.contribute(contribution);
}
contribution.statements().addStatement("return bean")
.add(codeBlock -> codeBlock.unindent().add("}"));
}
}
private static boolean isInnerClass(Class<?> type) {
return type.isMemberClass() && !Modifier.isStatic(type.getModifiers());
}
private void writeBeanInstantiation(CodeContribution contribution, Method method) {
// Factory method can be introspected
contribution.runtimeHints().reflection().registerMethod(method,
hint -> hint.withMode(ExecutableMode.INTROSPECT));
List<Class<?>> parameterTypes = new ArrayList<>(Arrays.asList(method.getParameterTypes()));
boolean multiStatements = !this.contributors.isEmpty();
Class<?> declaringType = method.getDeclaringClass();
CodeBlock.Builder code = CodeBlock.builder();
// Shortcut for common case
if (!multiStatements && parameterTypes.isEmpty()) {
code.add("() -> ");
branch(Modifier.isStatic(method.getModifiers()),
() -> code.add("$T", declaringType),
() -> code.add("beanFactory.getBean($T.class)", declaringType));
code.add(".$L()", method.getName());
contribution.statements().addStatement(code.build());
return;
}
code.add("(instanceContext) ->");
branch(multiStatements, () -> code.beginControlFlow(""), () -> code.add(" "));
if (multiStatements) {
code.add("$T bean = ", method.getReturnType());
}
code.add(this.injectionGenerator.writeInstantiation(method));
contribution.statements().addStatement(code.build());
if (multiStatements) {
for (BeanInstanceContributor contributor : this.contributors) {
contributor.contribute(contribution);
}
contribution.statements().addStatement("return bean")
.add(codeBlock -> codeBlock.unindent().add("}"));
}
}
private static void branch(boolean condition, Runnable ifTrue, Runnable ifFalse) {
if (condition) {
ifTrue.run();
}
else {
ifFalse.run();
}
}
}

View File

@ -0,0 +1,239 @@
/*
* 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.beans.factory.generator;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.beans.factory.generator.config.BeanDefinitionRegistrar.BeanInstanceContext;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* Generate the necessary code to {@link #writeInstantiation(Executable)
* create a bean instance} or {@link #writeInjection(Member, boolean)
* inject dependencies}.
* <p/>
* The generator assumes a number of variables to be accessible:
* <ul>
* <li>{@code beanFactory}: the general {@code DefaultListableBeanFactory}</li>
* <li>{@code instanceContext}: the {@link BeanInstanceContext} callback</li>
* <li>{@code bean}: the variable that refers to the bean instance</li>
* </ul>
*
* @author Stephane Nicoll
* @author Brian Clozel
*/
public class InjectionGenerator {
private final BeanParameterGenerator parameterGenerator = new BeanParameterGenerator();
/**
* Write the necessary code to instantiate an object using the specified
* {@link Executable}. The code is suitable to be assigned to a variable
* or used as a {@literal return} statement.
* @param creator the executable to invoke to create an instance of the
* requested object
* @return the code to instantiate an object using the specified executable
*/
public CodeBlock writeInstantiation(Executable creator) {
if (creator instanceof Constructor<?> constructor) {
return write(constructor);
}
if (creator instanceof Method method) {
return writeMethodInstantiation(method);
}
throw new IllegalArgumentException("Could not handle creator " + creator);
}
/**
* Write the code to inject a value resolved by {@link BeanInstanceContext}
* in the specified {@link Member}.
* @param member the field or method to inject
* @param required whether the value is required
* @return a statement that injects a value to the specified membmer
*/
public CodeBlock writeInjection(Member member, boolean required) {
if (member instanceof Method method) {
return writeMethodInjection(method, required);
}
if (member instanceof Field field) {
return writeFieldInjection(field, required);
}
throw new IllegalArgumentException("Could not handle member " + member);
}
private CodeBlock write(Constructor<?> creator) {
Builder code = CodeBlock.builder();
Class<?> declaringType = ClassUtils.getUserClass(creator.getDeclaringClass());
boolean innerClass = isInnerClass(declaringType);
Class<?>[] parameterTypes = Arrays.stream(creator.getParameters()).map(Parameter::getType)
.toArray(Class<?>[]::new);
// Shortcut for common case
if (innerClass && parameterTypes.length == 1) {
code.add("beanFactory.getBean($T.class).new $L()", declaringType.getEnclosingClass(),
declaringType.getSimpleName());
return code.build();
}
if (parameterTypes.length == 0) {
code.add("new $T()", declaringType);
return code.build();
}
boolean isAmbiguous = Arrays.stream(creator.getDeclaringClass().getDeclaredConstructors())
.filter(constructor -> constructor.getParameterCount() == parameterTypes.length).count() > 1;
code.add("instanceContext.create(beanFactory, (attributes) ->");
List<CodeBlock> parameters = resolveParameters(creator.getParameters(), isAmbiguous);
if (innerClass) { // Remove the implicit argument
parameters.remove(0);
}
code.add(" ");
if (innerClass) {
code.add("beanFactory.getBean($T.class).new $L(", declaringType.getEnclosingClass(),
declaringType.getSimpleName());
}
else {
code.add("new $T(", declaringType);
}
for (int i = 0; i < parameters.size(); i++) {
code.add(parameters.get(i));
if (i < parameters.size() - 1) {
code.add(", ");
}
}
code.add(")");
code.add(")");
return code.build();
}
private static boolean isInnerClass(Class<?> type) {
return type.isMemberClass() && !Modifier.isStatic(type.getModifiers());
}
private CodeBlock writeMethodInstantiation(Method injectionPoint) {
if (injectionPoint.getParameterCount() == 0) {
Builder code = CodeBlock.builder();
Class<?> declaringType = injectionPoint.getDeclaringClass();
if (Modifier.isStatic(injectionPoint.getModifiers())) {
code.add("$T", declaringType);
}
else {
code.add("beanFactory.getBean($T.class)", declaringType);
}
code.add(".$L()", injectionPoint.getName());
return code.build();
}
return write(injectionPoint, code -> code.add(".create(beanFactory, (attributes) ->"), true);
}
private CodeBlock writeMethodInjection(Method injectionPoint, boolean required) {
Consumer<Builder> attributesResolver = code -> {
if (required) {
code.add(".invoke(beanFactory, (attributes) ->");
}
else {
code.add(".resolve(beanFactory, false).ifResolved((attributes) ->");
}
};
return write(injectionPoint, attributesResolver, false);
}
private CodeBlock write(Method injectionPoint, Consumer<Builder> attributesResolver, boolean instantiation) {
Builder code = CodeBlock.builder();
code.add("instanceContext");
if (!instantiation) {
code.add(".method($S, ", injectionPoint.getName());
code.add(this.parameterGenerator.writeExecutableParameterTypes(injectionPoint));
code.add(")\n").indent().indent();
}
attributesResolver.accept(code);
List<CodeBlock> parameters = resolveParameters(injectionPoint.getParameters(), false);
code.add(" ");
if (instantiation) {
if (Modifier.isStatic(injectionPoint.getModifiers())) {
code.add("$T", injectionPoint.getDeclaringClass());
}
else {
code.add("beanFactory.getBean($T.class)", injectionPoint.getDeclaringClass());
}
}
else {
code.add("bean");
}
code.add(".$L(", injectionPoint.getName());
code.add(CodeBlock.join(parameters, ", "));
code.add(")");
code.add(")");
if (!instantiation) {
code.unindent().unindent();
}
return code.build();
}
CodeBlock writeFieldInjection(Field injectionPoint, boolean required) {
Builder code = CodeBlock.builder();
code.add("instanceContext.field($S, $T.class", injectionPoint.getName(), injectionPoint.getType());
code.add(")\n").indent().indent();
if (required) {
code.add(".invoke(beanFactory, (attributes) ->");
}
else {
code.add(".resolve(beanFactory, false).ifResolved((attributes) ->");
}
boolean hasAssignment = Modifier.isPrivate(injectionPoint.getModifiers());
if (hasAssignment) {
code.beginControlFlow("");
String fieldName = String.format("%sField", injectionPoint.getName());
code.addStatement("$T $L = $T.findField($T.class, $S, $T.class)", Field.class, fieldName, ReflectionUtils.class,
injectionPoint.getDeclaringClass(), injectionPoint.getName(), injectionPoint.getType());
code.addStatement("$T.makeAccessible($L)", ReflectionUtils.class, fieldName);
code.addStatement("$T.setField($L, bean, attributes.get(0))", ReflectionUtils.class, fieldName);
code.unindent().add("}");
}
else {
code.add(" bean.$L = attributes.get(0)", injectionPoint.getName());
}
code.add(")").unindent().unindent();
return code.build();
}
private List<CodeBlock> resolveParameters(Parameter[] parameters, boolean shouldCast) {
List<CodeBlock> parameterValues = new ArrayList<>();
for (int i = 0; i < parameters.length; i++) {
if (shouldCast) {
parameterValues.add(CodeBlock.of("attributes.get($L, $T.class)", i, parameters[i].getType()));
}
else {
parameterValues.add(CodeBlock.of("attributes.get($L)", i));
}
}
return parameterValues;
}
}

View File

@ -0,0 +1,408 @@
/*
* 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.beans.factory.generator.config;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
/**
* {@link BeanDefinition} registration mechanism offering transparent
* dependency resolution, as well as exception management.
*
* <p>Used by code generators and for internal use within the framework
* only.
*
* @author Stephane Nicoll
* @since 6.0
*/
public final class BeanDefinitionRegistrar {
private static final Log logger = LogFactory.getLog(BeanDefinitionRegistrar.class);
@Nullable
private final String beanName;
private final Class<?> beanClass;
@Nullable
private final ResolvableType beanType;
private final BeanDefinitionBuilder builder;
private final List<Consumer<RootBeanDefinition>> customizers;
@Nullable
private Executable instanceCreator;
@Nullable
private RootBeanDefinition beanDefinition;
private BeanDefinitionRegistrar(@Nullable String beanName, Class<?> beanClass, @Nullable ResolvableType beanType) {
this.beanName = beanName;
this.beanClass = beanClass;
this.beanType = beanType;
this.builder = BeanDefinitionBuilder.rootBeanDefinition(beanClass);
this.customizers = new ArrayList<>();
}
/**
* Initialize the registration of a bean with the specified name and type.
* @param beanName the name of the bean
* @param beanType the type of the bean
* @return a registrar for the specified bean
*/
public static BeanDefinitionRegistrar of(String beanName, ResolvableType beanType) {
return new BeanDefinitionRegistrar(beanName, beanType.toClass(), beanType);
}
/**
* Initialize the registration of a bean with the specified name and type.
* @param beanName the name of the bean
* @param beanType the type of the bean
* @return a registrar for the specified bean
*/
public static BeanDefinitionRegistrar of(String beanName, Class<?> beanType) {
return new BeanDefinitionRegistrar(beanName, beanType, null);
}
/**
* Initialize the registration of an inner bean with the specified type.
* @param beanType the type of the inner bean
* @return a registrar for the specified inner bean
*/
public static BeanDefinitionRegistrar inner(ResolvableType beanType) {
return new BeanDefinitionRegistrar(null, beanType.toClass(), beanType);
}
/**
* Initialize the registration of an inner bean with the specified type.
* @param beanType the type of the inner bean
* @return a registrar for the specified inner bean
*/
public static BeanDefinitionRegistrar inner(Class<?> beanType) {
return new BeanDefinitionRegistrar(null, beanType, null);
}
/**
* Customize the {@link RootBeanDefinition} using the specified consumer.
* @param bd a consumer for the bean definition
* @return {@code this}, to facilitate method chaining
*/
public BeanDefinitionRegistrar customize(ThrowableConsumer<RootBeanDefinition> bd) {
this.customizers.add(bd);
return this;
}
/**
* Specify the factory method to use to instantiate the bean.
* @param declaredType the {@link Method#getDeclaringClass() declared type}
* of the factory method.
* @param name the name of the method
* @param parameterTypes the parameter types of the method
* @return {@code this}, to facilitate method chaining
* @see RootBeanDefinition#getResolvedFactoryMethod()
*/
public BeanDefinitionRegistrar withFactoryMethod(Class<?> declaredType, String name, Class<?>... parameterTypes) {
this.instanceCreator = getMethod(declaredType, name, parameterTypes);
return this;
}
/**
* Specify the constructor to use to instantiate the bean.
* @param parameterTypes the parameter types of the constructor
* @return {@code this}, to facilitate method chaining
*/
public BeanDefinitionRegistrar withConstructor(Class<?>... parameterTypes) {
this.instanceCreator = getConstructor(this.beanClass, parameterTypes);
return this;
}
/**
* Specify how the bean instance should be created and initialized, using
* the {@link BeanInstanceContext} to resolve dependencies if necessary.
* @param instanceContext the {@link BeanInstanceContext} to use
* @return {@code this}, to facilitate method chaining
*/
public BeanDefinitionRegistrar instanceSupplier(ThrowableFunction<BeanInstanceContext, ?> instanceContext) {
return customize(beanDefinition -> beanDefinition.setInstanceSupplier(() ->
instanceContext.apply(createBeanInstanceContext())));
}
/**
* Specify how the bean instance should be created and initialized.
* @return {@code this}, to facilitate method chaining
*/
public BeanDefinitionRegistrar instanceSupplier(ThrowableSupplier<?> instanceSupplier) {
return customize(beanDefinition -> beanDefinition.setInstanceSupplier(instanceSupplier));
}
/**
* Register the {@link RootBeanDefinition} defined by this instance to
* the specified bean factory.
* @param beanFactory the bean factory to use
*/
public void register(DefaultListableBeanFactory beanFactory) {
if (logger.isDebugEnabled()) {
logger.debug("Register bean definition with name '" + this.beanName + "'");
}
BeanDefinition beanDefinition = toBeanDefinition();
if (this.beanName == null) {
throw new IllegalStateException("Bean name not set. Could not register " + beanDefinition);
}
beanFactory.registerBeanDefinition(this.beanName, beanDefinition);
}
/**
* Return the {@link RootBeanDefinition} defined by this instance.
* @return the bean definition
*/
public RootBeanDefinition toBeanDefinition() {
try {
this.beanDefinition = createBeanDefinition();
return this.beanDefinition;
}
catch (Exception ex) {
throw new FatalBeanException("Failed to create bean definition for bean with name '" + this.beanName + "'", ex);
}
}
private RootBeanDefinition createBeanDefinition() {
RootBeanDefinition bd = (RootBeanDefinition) this.builder.getBeanDefinition();
if (this.beanType != null) {
bd.setTargetType(this.beanType);
}
if (this.instanceCreator instanceof Method) {
bd.setResolvedFactoryMethod((Method) this.instanceCreator);
}
this.customizers.forEach(customizer -> customizer.accept(bd));
return bd;
}
private BeanInstanceContext createBeanInstanceContext() {
String resolvedBeanName = this.beanName != null ? this.beanName : createInnerBeanName();
return new BeanInstanceContext(resolvedBeanName, this.beanClass);
}
private String createInnerBeanName() {
return "(inner bean)" + BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR +
(this.beanDefinition != null ? ObjectUtils.getIdentityHexString(this.beanDefinition) : 0);
}
@Nullable
private BeanDefinition resolveBeanDefinition(DefaultListableBeanFactory beanFactory) {
return this.beanDefinition;
}
private static Constructor<?> getConstructor(Class<?> beanType, Class<?>... parameterTypes) {
try {
return beanType.getDeclaredConstructor(parameterTypes);
}
catch (NoSuchMethodException ex) {
String message = String.format("No constructor with type(s) [%s] found on %s",
toCommaSeparatedNames(parameterTypes), beanType.getName());
throw new IllegalArgumentException(message, ex);
}
}
private static Method getMethod(Class<?> declaredType, String methodName, Class<?>... parameterTypes) {
Method method = ReflectionUtils.findMethod(declaredType, methodName, parameterTypes);
if (method == null) {
String message = String.format("No method '%s' with type(s) [%s] found on %s", methodName,
toCommaSeparatedNames(parameterTypes), declaredType.getName());
throw new IllegalArgumentException(message);
}
return MethodIntrospector.selectInvocableMethod(method, declaredType);
}
private static String toCommaSeparatedNames(Class<?>... parameterTypes) {
return Arrays.stream(parameterTypes).map(Class::getName).collect(Collectors.joining(", "));
}
/**
* Callback interface used by instance suppliers that need to resolve
* dependencies for the {@link Executable} used to create the instance
* as well as any {@link Member} that should be handled by the context.
*/
public final class BeanInstanceContext {
private final String beanName;
private final Class<?> beanType;
private BeanInstanceContext(String beanName, Class<?> beanType) {
this.beanName = beanName;
this.beanType = beanType;
}
/**
* Return a bean instance using the specified {@code factory}.
* @param beanFactory the bean factory to use
* @param factory a function that returns a bean instance based on
* the resolved attributes required by its instance creator
* @param <T> the type of the bean
* @return the bean instance
*/
public <T> T create(DefaultListableBeanFactory beanFactory, ThrowableFunction<InjectedElementAttributes, T> factory) {
return resolveInstanceCreator(BeanDefinitionRegistrar.this.instanceCreator).create(beanFactory, factory);
}
private InjectedElementResolver resolveInstanceCreator(@Nullable Executable instanceCreator) {
if (instanceCreator instanceof Method) {
return new InjectedConstructionResolver(instanceCreator, instanceCreator.getDeclaringClass(), this.beanName,
BeanDefinitionRegistrar.this::resolveBeanDefinition);
}
if (instanceCreator instanceof Constructor) {
return new InjectedConstructionResolver(instanceCreator, this.beanType, this.beanName,
BeanDefinitionRegistrar.this::resolveBeanDefinition);
}
throw new IllegalStateException("No factory method or constructor is set");
}
/**
* Create an {@link InjectedElementResolver} for the specified field.
* @param name the name of the field
* @param type the type of the field
* @return a resolved for the specified field
*/
public InjectedElementResolver field(String name, Class<?> type) {
return new InjectedFieldResolver(getField(name, type), this.beanName);
}
/**
* Create an {@link InjectedElementResolver} for the specified bean method.
* @param name the name of the method on the target bean
* @param parameterTypes the method parameter types
* @return a resolved for the specified bean method
*/
public InjectedElementResolver method(String name, Class<?>... parameterTypes) {
return new InjectedMethodResolver(getMethod(this.beanType, name, parameterTypes), this.beanType, this.beanName);
}
private Field getField(String fieldName, Class<?> fieldType) {
Field field = ReflectionUtils.findField(this.beanType, fieldName, fieldType);
if (field == null) {
throw new IllegalArgumentException("No field '" + fieldName + "' with type " + fieldType.getName() + " found on " + this.beanType);
}
return field;
}
}
/**
* A {@link Consumer} that allows to invoke code that throws a checked exception.
*
* @author Stephane Nicoll
* @param <T> the type of the input to the operation
*/
@FunctionalInterface
public interface ThrowableConsumer<T> extends Consumer<T> {
void acceptWithException(T t) throws Exception;
@Override
default void accept(T t) {
try {
acceptWithException(t);
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
}
/**
* A {@link Function} that allows to invoke code that throws a checked exception.
*
* @author Stephane Nicoll
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*/
@FunctionalInterface
public interface ThrowableFunction<T, R> extends Function<T, R> {
R applyWithException(T t) throws Exception;
@Override
default R apply(T t) {
try {
return applyWithException(t);
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
}
/**
* A {@link Supplier} that allows to invoke code that throws a checked exception.
*
* @author Stephane Nicoll
* @param <T> the type of results supplied by this supplier
*/
public interface ThrowableSupplier<T> extends Supplier<T> {
T getWithException() throws Exception;
@Override
default T get() {
try {
return getWithException();
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
}
}

View File

@ -0,0 +1,175 @@
/*
* 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.beans.factory.generator.config;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.BeanDefinitionValueResolver;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.CollectionFactory;
import org.springframework.core.MethodParameter;
/**
* An {@link InjectedElementResolver} for an {@link Executable} that creates
* a bean instance.
*
* @author Stephane Nicoll
*/
class InjectedConstructionResolver implements InjectedElementResolver {
private final Executable executable;
private final Class<?> targetType;
private final String beanName;
private final Function<DefaultListableBeanFactory, BeanDefinition> beanDefinitionResolver;
InjectedConstructionResolver(Executable executable, Class<?> targetType, String beanName,
Function<DefaultListableBeanFactory, BeanDefinition> beanDefinitionResolver) {
this.executable = executable;
this.targetType = targetType;
this.beanName = beanName;
this.beanDefinitionResolver = beanDefinitionResolver;
}
Executable getExecutable() {
return this.executable;
}
@Override
public InjectedElementAttributes resolve(DefaultListableBeanFactory beanFactory, boolean required) {
int argumentCount = this.executable.getParameterCount();
List<Object> arguments = new ArrayList<>();
Set<String> autowiredBeans = new LinkedHashSet<>(argumentCount);
TypeConverter typeConverter = beanFactory.getTypeConverter();
ConstructorArgumentValues argumentValues = resolveArgumentValues(beanFactory);
for (int i = 0; i < argumentCount; i++) {
MethodParameter methodParam = createMethodParameter(i);
ValueHolder valueHolder = argumentValues.getIndexedArgumentValue(i, null);
if (valueHolder != null) {
if (valueHolder.isConverted()) {
arguments.add(valueHolder.getConvertedValue());
}
else {
Object userValue = beanFactory.getTypeConverter()
.convertIfNecessary(valueHolder.getValue(), methodParam.getParameterType());
arguments.add(userValue);
}
}
else {
DependencyDescriptor depDescriptor = new DependencyDescriptor(methodParam, true);
depDescriptor.setContainingClass(this.targetType);
try {
Object arg = resolveDependency(() -> beanFactory.resolveDependency(
depDescriptor, this.beanName, autowiredBeans, typeConverter), methodParam.getParameterType());
arguments.add(arg);
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(null, this.beanName, new InjectionPoint(methodParam), ex);
}
}
}
return new InjectedElementAttributes(arguments);
}
private Object resolveDependency(Supplier<Object> resolvedDependency, Class<?> dependencyType) {
try {
return resolvedDependency.get();
}
catch (NoSuchBeanDefinitionException ex) {
// Single constructor or factory method -> let's return an empty array/collection
// for e.g. a vararg or a non-null List/Set/Map parameter.
if (dependencyType.isArray()) {
return Array.newInstance(dependencyType.getComponentType(), 0);
}
else if (CollectionFactory.isApproximableCollectionType(dependencyType)) {
return CollectionFactory.createCollection(dependencyType, 0);
}
else if (CollectionFactory.isApproximableMapType(dependencyType)) {
return CollectionFactory.createMap(dependencyType, 0);
}
throw ex;
}
}
private ConstructorArgumentValues resolveArgumentValues(DefaultListableBeanFactory beanFactory) {
ConstructorArgumentValues resolvedValues = new ConstructorArgumentValues();
BeanDefinition beanDefinition = this.beanDefinitionResolver.apply(beanFactory);
if (beanDefinition == null || !beanDefinition.hasConstructorArgumentValues()) {
return resolvedValues;
}
ConstructorArgumentValues argumentValues = beanDefinition.getConstructorArgumentValues();
BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(beanFactory,
this.beanName, beanDefinition);
for (Map.Entry<Integer, ValueHolder> entry : argumentValues.getIndexedArgumentValues().entrySet()) {
int index = entry.getKey();
ValueHolder valueHolder = entry.getValue();
if (valueHolder.isConverted()) {
resolvedValues.addIndexedArgumentValue(index, valueHolder);
}
else {
Object resolvedValue =
valueResolver.resolveValueIfNecessary("constructor argument", valueHolder.getValue());
ValueHolder resolvedValueHolder =
new ValueHolder(resolvedValue, valueHolder.getType(), valueHolder.getName());
resolvedValueHolder.setSource(valueHolder);
resolvedValues.addIndexedArgumentValue(index, resolvedValueHolder);
}
}
return resolvedValues;
}
private MethodParameter createMethodParameter(int index) {
if (this.executable instanceof Constructor) {
return new MethodParameter((Constructor<?>) this.executable, index);
}
else {
return new MethodParameter((Method) this.executable, index);
}
}
@Override
public String toString() {
return new StringJoiner(", ", InjectedConstructionResolver.class.getSimpleName() + "[", "]")
.add("executable=" + this.executable)
.toString();
}
}

View File

@ -0,0 +1,93 @@
/*
* 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.beans.factory.generator.config;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Resolved attributes of an injected element.
*
* @author Stephane Nicoll
* @since 6.0
*/
public class InjectedElementAttributes {
@Nullable
private final List<Object> attributes;
InjectedElementAttributes(@Nullable List<Object> attributes) {
this.attributes = attributes;
}
/**
* Specify if the attributes have been resolved.
* @return the resolution of the injection
*/
public boolean isResolved() {
return (this.attributes != null);
}
/**
* Run the specified {@linkplain Runnable task} only if this instance is
* {@link #isResolved() resolved}.
* @param task the task to invoke if attributes are available
*/
public void ifResolved(Runnable task) {
if (isResolved()) {
task.run();
}
}
/**
* Invoke the specified {@link Consumer} with the resolved attributes.
* @param attributes the consumer to invoke if this instance is resolved
*/
public void ifResolved(BeanDefinitionRegistrar.ThrowableConsumer<InjectedElementAttributes> attributes) {
ifResolved(() -> attributes.accept(this));
}
/**
* Return the resolved attribute at the specified index.
* @param index the attribute index
* @param <T> the type of the attribute
* @return the attribute
*/
@SuppressWarnings("unchecked")
public <T> T get(int index) {
Assert.notNull(this.attributes, "Attributes must not be null");
return (T) this.attributes.get(index);
}
/**
* Return the resolved attribute at the specified index.
* @param index the attribute index
* @param type the attribute type
* @param <T> the type of the attribute
* @return the attribute
*/
@SuppressWarnings("unchecked")
public <T> T get(int index, Class<T> type) {
Assert.notNull(this.attributes, "Attributes must not be null");
return (T) this.attributes.get(index);
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.beans.factory.generator.config;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
/**
* Resolve the attributes of an injected element such as a {@code Constructor}
* or a factory {@code Method}.
*
* @author Stephane Nicoll
* @since 6.0
*/
public interface InjectedElementResolver {
/**
* Resolve the attributes using the specified bean factory.
* @param beanFactory the bean factory to use
* @return the resolved attributes
*/
default InjectedElementAttributes resolve(DefaultListableBeanFactory beanFactory) {
return resolve(beanFactory, true);
}
/**
* Resolve the attributes using the specified bean factory.
* @param beanFactory the bean factory to use
* @param required whether the injection point is mandatory
* @return the resolved attributes
*/
InjectedElementAttributes resolve(DefaultListableBeanFactory beanFactory, boolean required);
/**
* Invoke the specified consumer with the resolved
* {@link InjectedElementAttributes attributes}.
* @param beanFactory the bean factory to use to resolve the attributes
* @param attributes a consumer of the resolved attributes
*/
default void invoke(DefaultListableBeanFactory beanFactory,
BeanDefinitionRegistrar.ThrowableConsumer<InjectedElementAttributes> attributes) {
InjectedElementAttributes elements = resolve(beanFactory);
attributes.accept(elements);
}
/**
* Create an instance based on the resolved
* {@link InjectedElementAttributes attributes}.
* @param beanFactory the bean factory to use to resolve the attributes
* @param factory a factory to create the instance based on the resolved attributes
* @param <T> the type of the instance
* @return a new instance
*/
default <T> T create(DefaultListableBeanFactory beanFactory,
BeanDefinitionRegistrar.ThrowableFunction<InjectedElementAttributes, T> factory) {
InjectedElementAttributes attributes = resolve(beanFactory);
return factory.apply(attributes);
}
}

View File

@ -0,0 +1,71 @@
/*
* 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.beans.factory.generator.config;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
/**
* An {@link InjectedElementResolver} for a {@link Field}.
*
* @author Stephane Nicoll
*/
class InjectedFieldResolver implements InjectedElementResolver {
private final Field field;
private final String beanName;
/**
* Create a new instance.
* @param field the field to handle
* @param beanName the name of the bean, or {@code null}
*/
InjectedFieldResolver(Field field, String beanName) {
this.field = field;
this.beanName = beanName;
}
@Override
public InjectedElementAttributes resolve(DefaultListableBeanFactory beanFactory, boolean required) {
DependencyDescriptor desc = new DependencyDescriptor(this.field, required);
desc.setContainingClass(this.field.getType());
Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
TypeConverter typeConverter = beanFactory.getTypeConverter();
try {
Object value = beanFactory.resolveDependency(desc, this.beanName, autowiredBeanNames, typeConverter);
if (value == null && !required) {
return new InjectedElementAttributes(null);
}
return new InjectedElementAttributes(Collections.singletonList(value));
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(null, this.beanName, new InjectionPoint(this.field), ex);
}
}
}

View File

@ -0,0 +1,86 @@
/*
* 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.beans.factory.generator.config;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.MethodParameter;
/**
* An {@link InjectedElementResolver} for a {@link Method}.
*
* @author Stephane Nicoll
*/
class InjectedMethodResolver implements InjectedElementResolver {
private final Method method;
private final Class<?> target;
private final String beanName;
/**
* Create a new instance.
* @param method the method to handle
* @param target the type on which the method is declared
* @param beanName the name of the bean, or {@code null}
*/
InjectedMethodResolver(Method method, Class<?> target, String beanName) {
this.method = method;
this.target = target;
this.beanName = beanName;
}
@Override
public InjectedElementAttributes resolve(DefaultListableBeanFactory beanFactory, boolean required) {
int argumentCount = this.method.getParameterCount();
List<Object> arguments = new ArrayList<>();
Set<String> autowiredBeans = new LinkedHashSet<>(argumentCount);
TypeConverter typeConverter = beanFactory.getTypeConverter();
for (int i = 0; i < argumentCount; i++) {
MethodParameter methodParam = new MethodParameter(this.method, i);
DependencyDescriptor depDescriptor = new DependencyDescriptor(methodParam, required);
depDescriptor.setContainingClass(this.target);
try {
Object arg = beanFactory.resolveDependency(depDescriptor, this.beanName, autowiredBeans, typeConverter);
if (arg == null && !required) {
arguments = null;
break;
}
arguments.add(arg);
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(null, this.beanName, new InjectionPoint(methodParam), ex);
}
}
return new InjectedElementAttributes(arguments);
}
}

View File

@ -0,0 +1,9 @@
/**
* Classes used in generated code to ease bean registration.
*/
@NonNullApi
@NonNullFields
package org.springframework.beans.factory.generator.config;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -0,0 +1,264 @@
/*
* 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.beans.factory.generator;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedSet;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.ResourceLoader;
import org.springframework.javapoet.support.CodeSnippet;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BeanParameterGenerator}.
*
* @author Stephane Nicoll
*/
class BeanParameterGeneratorTests {
private final BeanParameterGenerator generator = new BeanParameterGenerator();
@Test
void writeCharArray() {
char[] value = new char[] { 'v', 'a', 'l', 'u', 'e' };
assertThat(write(value, ResolvableType.forArrayComponent(ResolvableType.forClass(char.class))))
.isEqualTo("new char[] { 'v', 'a', 'l', 'u', 'e' }");
}
@Test
void writeStringArray() {
String[] value = new String[] { "a", "test" };
assertThat(write(value, ResolvableType.forArrayComponent(ResolvableType.forClass(String.class))))
.isEqualTo("new String[] { \"a\", \"test\" }");
}
@Test
void writeStringList() {
List<String> value = List.of("a", "test");
CodeSnippet code = codeSnippet(value, ResolvableType.forClassWithGenerics(List.class, String.class));
assertThat(code.getSnippet()).isEqualTo(
"List.of(\"a\", \"test\")");
assertThat(code.hasImport(List.class)).isTrue();
}
@Test
void writeStringManagedList() {
ManagedList<String> value = ManagedList.of("a", "test");
CodeSnippet code = codeSnippet(value, ResolvableType.forClassWithGenerics(List.class, String.class));
assertThat(code.getSnippet()).isEqualTo(
"ManagedList.of(\"a\", \"test\")");
assertThat(code.hasImport(ManagedList.class)).isTrue();
}
@Test
void writeEmptyList() {
List<String> value = List.of();
CodeSnippet code = codeSnippet(value, ResolvableType.forClassWithGenerics(List.class, String.class));
assertThat(code.getSnippet()).isEqualTo("Collections.emptyList()");
assertThat(code.hasImport(Collections.class)).isTrue();
}
@Test
void writeStringSet() {
Set<String> value = Set.of("a", "test");
CodeSnippet code = codeSnippet(value, ResolvableType.forClassWithGenerics(Set.class, String.class));
assertThat(code.getSnippet()).startsWith("Set.of(").contains("a").contains("test");
assertThat(code.hasImport(Set.class)).isTrue();
}
@Test
void writeStringManagedSet() {
Set<String> value = ManagedSet.of("a", "test");
CodeSnippet code = codeSnippet(value, ResolvableType.forClassWithGenerics(Set.class, String.class));
assertThat(code.getSnippet()).isEqualTo(
"ManagedSet.of(\"a\", \"test\")");
assertThat(code.hasImport(ManagedSet.class)).isTrue();
}
@Test
void writeEmptySet() {
Set<String> value = Set.of();
CodeSnippet code = codeSnippet(value, ResolvableType.forClassWithGenerics(Set.class, String.class));
assertThat(code.getSnippet()).isEqualTo("Collections.emptySet()");
assertThat(code.hasImport(Collections.class)).isTrue();
}
@Test
void writeMap() {
Map<String, Object> value = new LinkedHashMap<>();
value.put("name", "Hello");
value.put("counter", 42);
assertThat(write(value)).isEqualTo("Map.of(\"name\", \"Hello\", \"counter\", 42)");
}
@Test
void writeMapWithEnum() {
Map<String, Object> value = new HashMap<>();
value.put("unit", ChronoUnit.DAYS);
assertThat(write(value)).isEqualTo("Map.of(\"unit\", ChronoUnit.DAYS)");
}
@Test
void writeEmptyMap() {
assertThat(write(Map.of())).isEqualTo("Map.of()");
}
@Test
void writeString() {
assertThat(write("test", ResolvableType.forClass(String.class))).isEqualTo("\"test\"");
}
@Test
void writeCharEscapeBackslash() {
assertThat(write('\\', ResolvableType.forType(char.class))).isEqualTo("'\\\\'");
}
@ParameterizedTest
@MethodSource("primitiveValues")
void writePrimitiveValue(Object value, String parameter) {
assertThat(write(value, ResolvableType.forClass(value.getClass()))).isEqualTo(parameter);
}
private static Stream<Arguments> primitiveValues() {
return Stream.of(Arguments.of((short) 0, "0"), Arguments.of((1), "1"), Arguments.of(2L, "2"),
Arguments.of(2.5d, "2.5"), Arguments.of(2.7f, "2.7"), Arguments.of('c', "'c'"),
Arguments.of((byte) 1, "1"), Arguments.of(true, "true"));
}
@Test
void writeEnum() {
assertThat(write(ChronoUnit.DAYS, ResolvableType.forClass(ChronoUnit.class)))
.isEqualTo("ChronoUnit.DAYS");
}
@Test
void writeClass() {
assertThat(write(Integer.class, ResolvableType.forClass(Class.class)))
.isEqualTo("Integer.class");
}
@Test
void writeResolvableType() {
ResolvableType type = ResolvableType.forClassWithGenerics(Consumer.class, Integer.class);
assertThat(write(type, type))
.isEqualTo("ResolvableType.forClassWithGenerics(Consumer.class, Integer.class)");
}
@Test
void writeExecutableParameterTypesWithConstructor() {
Constructor<?> constructor = TestSample.class.getDeclaredConstructors()[0];
assertThat(CodeSnippet.process(this.generator.writeExecutableParameterTypes(constructor)))
.isEqualTo("String.class, ResourceLoader.class");
}
@Test
void writeExecutableParameterTypesWithNoArgConstructor() {
Constructor<?> constructor = BeanParameterGeneratorTests.class.getDeclaredConstructors()[0];
assertThat(CodeSnippet.process(this.generator.writeExecutableParameterTypes(constructor)))
.isEmpty();
}
@Test
void writeExecutableParameterTypesWithMethod() {
Method method = ReflectionUtils.findMethod(TestSample.class, "createBean", String.class, Integer.class);
assertThat(CodeSnippet.process(this.generator.writeExecutableParameterTypes(method)))
.isEqualTo("String.class, Integer.class");
}
@Test
void writeNull() {
assertThat(write(null)).isEqualTo("null");
}
@Test
void writeBeanReference() {
BeanReference beanReference = mock(BeanReference.class);
given(beanReference.getBeanName()).willReturn("testBean");
assertThat(write(beanReference)).isEqualTo("new RuntimeBeanReference(\"testBean\")");
}
@Test
void writeBeanDefinitionCallsConsumer() {
BeanParameterGenerator customGenerator = new BeanParameterGenerator(
((beanDefinition, builder) -> builder.add("test")));
assertThat(CodeSnippet.process(customGenerator.writeParameterValue(new RootBeanDefinition()))).isEqualTo("test");
}
@Test
void writeBeanDefinitionWithoutConsumerFails() {
BeanParameterGenerator customGenerator = new BeanParameterGenerator();
assertThatIllegalStateException().isThrownBy(() -> customGenerator
.writeParameterValue(new RootBeanDefinition()));
}
@Test
void writeUnsupportedParameter() {
assertThatIllegalArgumentException().isThrownBy(() -> write(new StringWriter()))
.withMessageContaining(StringWriter.class.getName());
}
private String write(Object value) {
return CodeSnippet.process(this.generator.writeParameterValue(value));
}
private String write(Object value, ResolvableType resolvableType) {
return codeSnippet(value, resolvableType).getSnippet();
}
private CodeSnippet codeSnippet(Object value, ResolvableType resolvableType) {
return CodeSnippet.of(this.generator.writeParameterValue(value, () -> resolvableType));
}
@SuppressWarnings("unused")
static class TestSample {
public TestSample(String test, ResourceLoader resourceLoader) {
}
String createBean(String name, Integer counter) {
return "test";
}
}
}

View File

@ -0,0 +1,255 @@
/*
* 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.beans.factory.generator;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generator.CodeContribution;
import org.springframework.aot.hint.ExecutableHint;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeHint;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.EnvironmentAwareComponent;
import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.NoDependencyComponent;
import org.springframework.beans.testfixture.beans.factory.generator.SimpleConfiguration;
import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolderFactoryBean;
import org.springframework.beans.testfixture.beans.factory.generator.factory.SampleFactory;
import org.springframework.beans.testfixture.beans.factory.generator.injection.InjectionComponent;
import org.springframework.beans.testfixture.beans.factory.generator.visibility.ProtectedConstructorComponent;
import org.springframework.beans.testfixture.beans.factory.generator.visibility.ProtectedFactoryMethod;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.support.CodeSnippet;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultBeanInstanceGenerator}.
*
* @author Stephane Nicoll
*/
class DefaultBeanInstanceGeneratorTests {
@Test
void generateUsingDefaultConstructorUsesMethodReference() {
CodeContribution contribution = generate(SimpleConfiguration.class.getDeclaredConstructors()[0]);
assertThat(code(contribution)).isEqualTo("SimpleConfiguration::new");
assertThat(reflectionHints(contribution, SimpleConfiguration.class)).isNull();
}
@Test
void generateUsingConstructorWithoutParameterAndMultipleCandidatesDoesNotUseMethodReference() throws NoSuchMethodException {
CodeContribution contribution = generate(TestBean.class.getConstructor());
assertThat(code(contribution)).isEqualTo("() -> new TestBean()");
assertThat(reflectionHints(contribution, TestBean.class)).isNull();
}
@Test
void generateUsingConstructorWithParameter() {
Constructor<?> constructor = InjectionComponent.class.getDeclaredConstructors()[0];
CodeContribution contribution = generate(constructor);
assertThat(code(contribution).lines()).containsOnly(
"(instanceContext) -> instanceContext.create(beanFactory, (attributes) -> "
+ "new InjectionComponent(attributes.get(0)))");
assertThat(reflectionHints(contribution, InjectionComponent.class))
.satisfies(hasSingleQueryConstructor(constructor));
}
@Test
void generateUsingConstructorWithInnerClassAndNoExtraArg() {
CodeContribution contribution = generate(NoDependencyComponent.class.getDeclaredConstructors()[0]);
assertThat(code(contribution).lines()).containsOnly(
"() -> beanFactory.getBean(InnerComponentConfiguration.class).new NoDependencyComponent()");
assertThat(reflectionHints(contribution, NoDependencyComponent.class)).isNull();
}
@Test
void generateUsingConstructorWithInnerClassAndExtraArg() {
Constructor<?> constructor = EnvironmentAwareComponent.class.getDeclaredConstructors()[0];
CodeContribution contribution = generate(constructor);
assertThat(code(contribution).lines()).containsOnly(
"(instanceContext) -> instanceContext.create(beanFactory, (attributes) -> "
+ "beanFactory.getBean(InnerComponentConfiguration.class).new EnvironmentAwareComponent(attributes.get(1)))");
assertThat(reflectionHints(contribution, EnvironmentAwareComponent.class))
.satisfies(hasSingleQueryConstructor(constructor));
}
@Test
void generateUsingConstructorOfTypeWithGeneric() {
CodeContribution contribution = generate(NumberHolderFactoryBean.class.getDeclaredConstructors()[0]);
assertThat(code(contribution)).isEqualTo("NumberHolderFactoryBean::new");
assertThat(reflectionHints(contribution, NumberHolderFactoryBean.class)).isNull();
}
@Test
void generateUsingNoArgConstructorAndContributorsDoesNotUseMethodReference() {
CodeContribution contribution = generate(SimpleConfiguration.class.getDeclaredConstructors()[0],
contrib -> contrib.statements().add(CodeBlock.of("// hello\n")),
BeanInstanceContributor.NO_OP);
assertThat(code(contribution)).isEqualTo("""
(instanceContext) -> {
SimpleConfiguration bean = new SimpleConfiguration();
// hello
return bean;
}""");
}
@Test
void generateUsingContributorsRegisterHints() {
CodeContribution contribution = generate(SimpleConfiguration.class.getDeclaredConstructors()[0],
contrib -> {
contrib.statements().add(CodeBlock.of("// hello\n"));
contrib.runtimeHints().resources().registerPattern("com/example/*.properties");
},
contrib -> contrib.runtimeHints().reflection().registerType(TypeReference.of(String.class),
hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)));
assertThat(code(contribution)).isEqualTo("""
(instanceContext) -> {
SimpleConfiguration bean = new SimpleConfiguration();
// hello
return bean;
}""");
assertThat(contribution.runtimeHints().resources().resourcePatterns()).singleElement().satisfies(hint ->
assertThat(hint.getIncludes()).containsOnly("com/example/*.properties"));
assertThat(contribution.runtimeHints().reflection().getTypeHint(String.class)).satisfies(hint -> {
assertThat(hint.getType()).isEqualTo(TypeReference.of(String.class));
assertThat(hint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_PUBLIC_METHODS);
});
}
@Test
void generateUsingMethodWithNoArg() {
Method method = method(SimpleConfiguration.class, "stringBean");
CodeContribution contribution = generate(method);
assertThat(code(contribution)).isEqualTo("() -> beanFactory.getBean(SimpleConfiguration.class).stringBean()");
assertThat(reflectionHints(contribution, SimpleConfiguration.class))
.satisfies(hasSingleQueryMethod(method));
}
@Test
void generateUsingStaticMethodWithNoArg() {
Method method = method(SampleFactory.class, "integerBean");
CodeContribution contribution = generate(method);
assertThat(code(contribution)).isEqualTo("() -> SampleFactory.integerBean()");
assertThat(reflectionHints(contribution, SampleFactory.class))
.satisfies(hasSingleQueryMethod(method));
}
@Test
void generateUsingMethodWithArg() {
Method method = method(SampleFactory.class, "create", Number.class, String.class);
CodeContribution contribution = generate(method);
assertThat(code(contribution)).isEqualTo("(instanceContext) -> instanceContext.create(beanFactory, (attributes) -> "
+ "SampleFactory.create(attributes.get(0), attributes.get(1)))");
assertThat(reflectionHints(contribution, SampleFactory.class))
.satisfies(hasSingleQueryMethod(method));
}
@Test
void generateUsingMethodAndContributors() {
CodeContribution contribution = generate(method(SimpleConfiguration.class, "stringBean"),
contrib -> {
contrib.statements().add(CodeBlock.of("// hello\n"));
contrib.runtimeHints().resources().registerPattern("com/example/*.properties");
},
contrib -> contrib.runtimeHints().reflection().registerType(TypeReference.of(String.class),
hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)));
assertThat(code(contribution)).isEqualTo("""
(instanceContext) -> {
String bean = beanFactory.getBean(SimpleConfiguration.class).stringBean();
// hello
return bean;
}""");
assertThat(contribution.runtimeHints().resources().resourcePatterns()).singleElement().satisfies(hint ->
assertThat(hint.getIncludes()).containsOnly("com/example/*.properties"));
assertThat(contribution.runtimeHints().reflection().getTypeHint(String.class)).satisfies(hint -> {
assertThat(hint.getType()).isEqualTo(TypeReference.of(String.class));
assertThat(hint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_PUBLIC_METHODS);
});
}
@Test
void generateUsingProtectedConstructorRegistersProtectedAccess() {
CodeContribution contribution = generate(ProtectedConstructorComponent.class.getDeclaredConstructors()[0]);
assertThat(contribution.protectedAccess().isAccessible("com.example")).isFalse();
assertThat(contribution.protectedAccess().getPrivilegedPackageName("com.example"))
.isEqualTo(ProtectedConstructorComponent.class.getPackageName());
}
@Test
void generateUsingProtectedMethodRegistersProtectedAccess() {
CodeContribution contribution = generate(method(ProtectedFactoryMethod.class, "testBean", Integer.class));
assertThat(contribution.protectedAccess().isAccessible("com.example")).isFalse();
assertThat(contribution.protectedAccess().getPrivilegedPackageName("com.example"))
.isEqualTo(ProtectedFactoryMethod.class.getPackageName());
}
private String code(CodeContribution contribution) {
return CodeSnippet.process(contribution.statements().toCodeBlock());
}
@Nullable
private TypeHint reflectionHints(CodeContribution contribution, Class<?> type) {
return contribution.runtimeHints().reflection().getTypeHint(type);
}
private Consumer<TypeHint> hasSingleQueryConstructor(Constructor<?> constructor) {
return typeHint -> assertThat(typeHint.constructors()).singleElement()
.satisfies(match(constructor, "<init>", ExecutableMode.INTROSPECT));
}
private Consumer<TypeHint> hasSingleQueryMethod(Method method) {
return typeHint -> assertThat(typeHint.methods()).singleElement()
.satisfies(match(method, method.getName(), ExecutableMode.INTROSPECT));
}
private Consumer<ExecutableHint> match(Executable executable, String name, ExecutableMode... modes) {
return hint -> {
assertThat(hint.getName()).isEqualTo(name);
assertThat(hint.getParameterTypes()).hasSameSizeAs(executable.getParameterTypes());
for (int i = 0; i < hint.getParameterTypes().size(); i++) {
assertThat(hint.getParameterTypes().get(i))
.isEqualTo(TypeReference.of(executable.getParameterTypes()[i]));
}
assertThat(hint.getModes()).containsOnly(modes);
};
}
private CodeContribution generate(Executable executable,
BeanInstanceContributor... beanInstanceContributors) {
DefaultBeanInstanceGenerator generator = new DefaultBeanInstanceGenerator(executable,
Arrays.asList(beanInstanceContributors));
return generator.generateBeanInstance(new RuntimeHints());
}
private static Method method(Class<?> type, String methodName, Class<?>... parameterTypes) {
Method method = ReflectionUtils.findMethod(type, methodName, parameterTypes);
assertThat(method).isNotNull();
return method;
}
}

View File

@ -0,0 +1,269 @@
/*
* 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.beans.factory.generator;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.generator.InjectionGeneratorTests.SimpleConstructorBean.InnerClass;
import org.springframework.javapoet.support.CodeSnippet;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link InjectionGenerator}.
*
* @author Stephane Nicoll
*/
class InjectionGeneratorTests {
@Test
void writeInstantiationForConstructorWithNoArgUseShortcut() {
Constructor<?> constructor = SimpleBean.class.getDeclaredConstructors()[0];
assertThat(writeInstantiation(constructor).lines())
.containsExactly("new InjectionGeneratorTests.SimpleBean()");
}
@Test
void writeInstantiationForConstructorWithNonGenericParameter() {
Constructor<?> constructor = SimpleConstructorBean.class.getDeclaredConstructors()[0];
assertThat(writeInstantiation(constructor).lines()).containsExactly(
"instanceContext.create(beanFactory, (attributes) -> new InjectionGeneratorTests.SimpleConstructorBean(attributes.get(0), attributes.get(1)))");
}
@Test
void writeInstantiationForConstructorWithGenericParameter() {
Constructor<?> constructor = GenericConstructorBean.class.getDeclaredConstructors()[0];
assertThat(writeInstantiation(constructor).lines()).containsExactly(
"instanceContext.create(beanFactory, (attributes) -> new InjectionGeneratorTests.GenericConstructorBean(attributes.get(0)))");
}
@Test
void writeInstantiationForAmbiguousConstructor() throws Exception {
Constructor<?> constructor = AmbiguousConstructorBean.class.getDeclaredConstructor(String.class, Number.class);
assertThat(writeInstantiation(constructor).lines()).containsExactly(
"instanceContext.create(beanFactory, (attributes) -> new InjectionGeneratorTests.AmbiguousConstructorBean(attributes.get(0, String.class), attributes.get(1, Number.class)))");
}
@Test
void writeInstantiationForConstructorInInnerClass() {
Constructor<?> constructor = InnerClass.class.getDeclaredConstructors()[0];
assertThat(writeInstantiation(constructor).lines()).containsExactly(
"beanFactory.getBean(InjectionGeneratorTests.SimpleConstructorBean.class).new InnerClass()");
}
@Test
void writeInstantiationForMethodWithNoArgUseShortcut() {
assertThat(writeInstantiation(method(SimpleBean.class, "name")).lines()).containsExactly(
"beanFactory.getBean(InjectionGeneratorTests.SimpleBean.class).name()");
}
@Test
void writeInstantiationForStaticMethodWithNoArgUseShortcut() {
assertThat(writeInstantiation(method(SimpleBean.class, "number")).lines()).containsExactly(
"InjectionGeneratorTests.SimpleBean.number()");
}
@Test
void writeInstantiationForMethodWithNonGenericParameter() {
assertThat(writeInstantiation(method(SampleBean.class, "source", Integer.class)).lines()).containsExactly(
"instanceContext.create(beanFactory, (attributes) -> beanFactory.getBean(InjectionGeneratorTests.SampleBean.class).source(attributes.get(0)))");
}
@Test
void writeInstantiationForStaticMethodWithNonGenericParameter() {
assertThat(writeInstantiation(method(SampleBean.class, "staticSource", Integer.class)).lines()).containsExactly(
"instanceContext.create(beanFactory, (attributes) -> InjectionGeneratorTests.SampleBean.staticSource(attributes.get(0)))");
}
@Test
void writeInstantiationForMethodWithGenericParameters() {
assertThat(writeInstantiation(method(SampleBean.class, "source", ObjectProvider.class)).lines()).containsExactly(
"instanceContext.create(beanFactory, (attributes) -> beanFactory.getBean(InjectionGeneratorTests.SampleBean.class).source(attributes.get(0)))");
}
@Test
void writeInjectionForUnsupportedMember() {
assertThatIllegalArgumentException().isThrownBy(() -> writeInjection(mock(Member.class), false));
}
@Test
void writeInjectionForNonRequiredMethodWithNonGenericParameters() {
Method method = method(SampleBean.class, "sourceAndCounter", String.class, Integer.class);
assertThat(writeInjection(method, false)).isEqualTo("""
instanceContext.method("sourceAndCounter", String.class, Integer.class)
.resolve(beanFactory, false).ifResolved((attributes) -> bean.sourceAndCounter(attributes.get(0), attributes.get(1)))""");
}
@Test
void writeInjectionForRequiredMethodWithGenericParameter() {
Method method = method(SampleBean.class, "nameAndCounter", String.class, ObjectProvider.class);
assertThat(writeInjection(method, true)).isEqualTo("""
instanceContext.method("nameAndCounter", String.class, ObjectProvider.class)
.invoke(beanFactory, (attributes) -> bean.nameAndCounter(attributes.get(0), attributes.get(1)))""");
}
@Test
void writeInjectionForNonRequiredMethodWithGenericParameter() {
Method method = method(SampleBean.class, "nameAndCounter", String.class, ObjectProvider.class);
assertThat(writeInjection(method, false)).isEqualTo("""
instanceContext.method("nameAndCounter", String.class, ObjectProvider.class)
.resolve(beanFactory, false).ifResolved((attributes) -> bean.nameAndCounter(attributes.get(0), attributes.get(1)))""");
}
@Test
void writeInjectionForRequiredField() {
Field field = field(SampleBean.class, "counter");
assertThat(writeInjection(field, true)).isEqualTo("""
instanceContext.field("counter", Integer.class)
.invoke(beanFactory, (attributes) -> bean.counter = attributes.get(0))""");
}
@Test
void writeInjectionForNonRequiredField() {
Field field = field(SampleBean.class, "counter");
assertThat(writeInjection(field, false)).isEqualTo("""
instanceContext.field("counter", Integer.class)
.resolve(beanFactory, false).ifResolved((attributes) -> bean.counter = attributes.get(0))""");
}
@Test
void writeInjectionForRequiredPrivateField() {
Field field = field(SampleBean.class, "source");
assertThat(writeInjection(field, true)).isEqualTo("""
instanceContext.field("source", String.class)
.invoke(beanFactory, (attributes) -> {
Field sourceField = ReflectionUtils.findField(InjectionGeneratorTests.SampleBean.class, "source", String.class);
ReflectionUtils.makeAccessible(sourceField);
ReflectionUtils.setField(sourceField, bean, attributes.get(0));
})""");
}
private Method method(Class<?> type, String name, Class<?>... parameterTypes) {
Method method = ReflectionUtils.findMethod(type, name, parameterTypes);
assertThat(method).isNotNull();
return method;
}
private Field field(Class<?> type, String name) {
Field field = ReflectionUtils.findField(type, name);
assertThat(field).isNotNull();
return field;
}
private String writeInstantiation(Executable creator) {
return CodeSnippet.process(code -> code.add(new InjectionGenerator().writeInstantiation(creator)));
}
private String writeInjection(Member member, boolean required) {
return CodeSnippet.process(code -> code.add(new InjectionGenerator().writeInjection(member, required)));
}
@SuppressWarnings("unused")
static class SampleBean {
private String source;
Integer counter;
void sourceAndCounter(String source, Integer counter) {
}
void nameAndCounter(String name, ObjectProvider<Integer> counter) {
}
String source(Integer counter) {
return "source" + counter;
}
String source(ObjectProvider<Integer> counter) {
return "source" + counter.getIfAvailable(() -> 0);
}
static String staticSource(Integer counter) {
return counter + "source";
}
}
@SuppressWarnings("unused")
static class SimpleBean {
String name() {
return "test";
}
static Integer number() {
return 42;
}
}
@SuppressWarnings("unused")
static class SimpleConstructorBean {
private final String source;
private final Integer counter;
public SimpleConstructorBean(String source, Integer counter) {
this.source = source;
this.counter = counter;
}
class InnerClass {
}
}
@SuppressWarnings("unused")
static class GenericConstructorBean {
private final ObjectProvider<Integer> counter;
GenericConstructorBean(ObjectProvider<Integer> counter) {
this.counter = counter;
}
}
static class AmbiguousConstructorBean {
AmbiguousConstructorBean(String first, String second) {
}
AmbiguousConstructorBean(String first, Number second) {
}
}
}

View File

@ -0,0 +1,458 @@
/*
* 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.beans.factory.generator.config;
import java.io.IOException;
import java.lang.reflect.Field;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.generator.config.BeanDefinitionRegistrar.BeanInstanceContext;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.Environment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BeanDefinitionRegistrar}.
*
* @author Stephane Nicoll
*/
class BeanDefinitionRegistrarTests {
@Test
void beanDefinitionWithBeanClassDoesNotSetTargetType() {
RootBeanDefinition beanDefinition = BeanDefinitionRegistrar.of("test", String.class).toBeanDefinition();
assertThat(beanDefinition.getBeanClass()).isEqualTo(String.class);
assertThat(beanDefinition.getTargetType()).isNull();
}
@Test
void beanDefinitionWithResolvableTypeSetsTargetType() {
ResolvableType targetType = ResolvableType.forClassWithGenerics(NumberHolder.class, Integer.class);
RootBeanDefinition beanDefinition = BeanDefinitionRegistrar.of("test", targetType).toBeanDefinition();
assertThat(beanDefinition.getTargetType()).isNotNull().isEqualTo(NumberHolder.class);
}
@Test
void registerWithSimpleInstanceSupplier() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinitionRegistrar.of("test", InjectionSample.class)
.instanceSupplier(InjectionSample::new).register(beanFactory);
assertBeanFactory(beanFactory, () -> {
assertThat(beanFactory.containsBean("test")).isTrue();
assertThat(beanFactory.getBean(InjectionSample.class)).isNotNull();
});
}
@Test
void registerWithSimpleInstanceSupplierThatThrowsRuntimeException() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
Exception exception = new IllegalArgumentException("test exception");
BeanDefinitionRegistrar.of("testBean", InjectionSample.class)
.instanceSupplier(() -> {
throw exception;
}).register(beanFactory);
assertThatThrownBy(() -> beanFactory.getBean("testBean")).isInstanceOf(BeanCreationException.class)
.getRootCause().isEqualTo(exception);
}
@Test
void registerWithSimpleInstanceSupplierThatThrowsCheckedException() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
Exception exception = new IOException("test exception");
BeanDefinitionRegistrar.of("testBean", InjectionSample.class)
.instanceSupplier(() -> {
throw exception;
}).register(beanFactory);
assertThatThrownBy(() -> beanFactory.getBean("testBean")).isInstanceOf(BeanCreationException.class)
.getRootCause().isEqualTo(exception);
}
@Test
void registerWithoutBeanNameFails() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinitionRegistrar registrar = BeanDefinitionRegistrar.inner(InjectionSample.class)
.instanceSupplier(InjectionSample::new);
assertThatIllegalStateException().isThrownBy(() -> registrar.register(beanFactory))
.withMessageContaining("Bean name not set.");
}
@Test
@SuppressWarnings("unchecked")
void registerWithCustomizer() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinitionRegistrar.ThrowableConsumer<RootBeanDefinition> first = mock(BeanDefinitionRegistrar.ThrowableConsumer.class);
BeanDefinitionRegistrar.ThrowableConsumer<RootBeanDefinition> second = mock(BeanDefinitionRegistrar.ThrowableConsumer.class);
BeanDefinitionRegistrar.of("test", InjectionSample.class)
.instanceSupplier(InjectionSample::new).customize(first).customize(second).register(beanFactory);
assertBeanFactory(beanFactory, () -> {
assertThat(beanFactory.containsBean("test")).isTrue();
InOrder ordered = inOrder(first, second);
ordered.verify(first).accept(any(RootBeanDefinition.class));
ordered.verify(second).accept(any(RootBeanDefinition.class));
});
}
@Test
void registerWithCustomizerThatThrowsRuntimeException() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
Exception exception = new RuntimeException("test exception");
BeanDefinitionRegistrar registrar = BeanDefinitionRegistrar.of("test", InjectionSample.class)
.instanceSupplier(InjectionSample::new).customize(bd -> {
throw exception;
});
assertThatThrownBy(() -> registrar.register(beanFactory)).isInstanceOf(FatalBeanException.class)
.hasMessageContaining("Failed to create bean definition for bean with name 'test'")
.hasMessageContaining("test exception")
.hasCause(exception);
}
@Test
void registerWithCustomizerThatThrowsCheckedException() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
Exception exception = new IOException("test exception");
BeanDefinitionRegistrar registrar = BeanDefinitionRegistrar.of("test", InjectionSample.class)
.instanceSupplier(InjectionSample::new).customize(bd -> {
throw exception;
});
assertThatThrownBy(() -> registrar.register(beanFactory)).isInstanceOf(FatalBeanException.class)
.hasMessageContaining("Failed to create bean definition for bean with name 'test'")
.hasMessageContaining("test exception");
}
@Test
void registerWithConstructorInstantiation() {
ResourceLoader resourceLoader = new DefaultResourceLoader();
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerResolvableDependency(ResourceLoader.class, resourceLoader);
BeanDefinitionRegistrar.of("test", ConstructorSample.class).withConstructor(ResourceLoader.class)
.instanceSupplier(instanceContext -> instanceContext.create(beanFactory, attributes ->
new ConstructorSample(attributes.get(0)))).register(beanFactory);
assertBeanFactory(beanFactory, () -> {
assertThat(beanFactory.containsBean("test")).isTrue();
assertThat(beanFactory.getBean(ConstructorSample.class).resourceLoader).isEqualTo(resourceLoader);
});
}
@Test
void registerWithConstructorInstantiationThatThrowsRuntimeException() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
Exception exception = new RuntimeException("test exception");
BeanDefinitionRegistrar.of("test", ConstructorSample.class).withConstructor(ResourceLoader.class)
.instanceSupplier(instanceContext -> {
throw exception;
}).register(beanFactory);
assertThatThrownBy(() -> beanFactory.getBean("test")).isInstanceOf(BeanCreationException.class)
.getRootCause().isEqualTo(exception);
}
@Test
void registerWithConstructorInstantiationThatThrowsCheckedException() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
Exception exception = new IOException("test exception");
BeanDefinitionRegistrar.of("test", ConstructorSample.class).withConstructor(ResourceLoader.class)
.instanceSupplier(instanceContext -> {
throw exception;
}).register(beanFactory);
assertThatThrownBy(() -> beanFactory.getBean("test")).isInstanceOf(BeanCreationException.class)
.getRootCause().isEqualTo(exception);
}
@Test
void registerWithConstructorOnInnerClass() {
Environment environment = mock(Environment.class);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("environment", environment);
beanFactory.registerBeanDefinition("sample", BeanDefinitionBuilder.rootBeanDefinition(InnerClassSample.class).getBeanDefinition());
BeanDefinitionRegistrar.of("test", InnerClassSample.Inner.class).withConstructor(InnerClassSample.class, Environment.class)
.instanceSupplier(instanceContext -> instanceContext.create(beanFactory, attributes ->
beanFactory.getBean(InnerClassSample.class).new Inner(attributes.get(1))))
.register(beanFactory);
assertBeanFactory(beanFactory, () -> {
assertThat(beanFactory.containsBean("test")).isTrue();
InnerClassSample.Inner bean = beanFactory.getBean(InnerClassSample.Inner.class);
assertThat(bean.environment).isEqualTo(environment);
});
}
@Test
void registerWithInvalidConstructor() {
assertThatThrownBy(() -> BeanDefinitionRegistrar.of("test", ConstructorSample.class).withConstructor(Object.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("No constructor with type(s) [java.lang.Object] found on")
.hasMessageContaining(ConstructorSample.class.getName());
}
@Test
void registerWithFactoryMethod() {
ResourceLoader resourceLoader = new DefaultResourceLoader();
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerResolvableDependency(ResourceLoader.class, resourceLoader);
BeanDefinitionRegistrar.of("configuration", ConfigurationSample.class).instanceSupplier(ConfigurationSample::new)
.register(beanFactory);
BeanDefinitionRegistrar.of("test", ConstructorSample.class)
.withFactoryMethod(ConfigurationSample.class, "sampleBean", ResourceLoader.class)
.instanceSupplier(instanceContext -> instanceContext.create(beanFactory, attributes ->
beanFactory.getBean(ConfigurationSample.class).sampleBean(attributes.get(0))))
.register(beanFactory);
assertBeanFactory(beanFactory, () -> {
assertThat(beanFactory.containsBean("configuration")).isTrue();
assertThat(beanFactory.containsBean("test")).isTrue();
assertThat(beanFactory.getBean(ConstructorSample.class).resourceLoader).isEqualTo(resourceLoader);
RootBeanDefinition bd = (RootBeanDefinition) beanFactory.getBeanDefinition("test");
assertThat(bd.getResolvedFactoryMethod()).isNotNull().isEqualTo(
ReflectionUtils.findMethod(ConfigurationSample.class, "sampleBean", ResourceLoader.class));
});
}
@Test
void registerWithCreateShortcutWithoutFactoryMethod() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinitionRegistrar.of("configuration", ConfigurationSample.class).instanceSupplier(ConfigurationSample::new)
.register(beanFactory);
BeanDefinitionRegistrar.of("test", ConstructorSample.class)
.instanceSupplier(instanceContext -> instanceContext.create(beanFactory, attributes ->
beanFactory.getBean(ConfigurationSample.class).sampleBean(attributes.get(0))))
.register(beanFactory);
assertThatThrownBy(() -> beanFactory.getBean("test")).isInstanceOf(BeanCreationException.class)
.hasMessageContaining("No factory method or constructor is set");
}
@Test
void registerWithInjectedField() {
Environment environment = mock(Environment.class);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("environment", environment);
BeanDefinitionRegistrar.of("test", InjectionSample.class).instanceSupplier(instanceContext -> {
InjectionSample bean = new InjectionSample();
instanceContext.field("environment", Environment.class).invoke(beanFactory,
attributes -> bean.environment = (attributes.get(0)));
return bean;
}).register(beanFactory);
assertBeanFactory(beanFactory, () -> {
assertThat(beanFactory.containsBean("test")).isTrue();
assertThat(beanFactory.getBean(InjectionSample.class).environment).isEqualTo(environment);
});
}
@Test
void registerWithInvalidField() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinitionRegistrar.of("test", InjectionSample.class).instanceSupplier(instanceContext ->
instanceContext.field("doesNotExist", Object.class).resolve(beanFactory)).register(beanFactory);
assertThatThrownBy(() -> beanFactory.getBean(InjectionSample.class)
).isInstanceOf(BeanCreationException.class)
.hasMessageContaining("No field '%s' with type %s found", "doesNotExist", Object.class.getName())
.hasMessageContaining(InjectionSample.class.getName());
}
@Test
void registerWithInjectedMethod() {
Environment environment = mock(Environment.class);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("environment", environment);
BeanDefinitionRegistrar.of("test", InjectionSample.class).instanceSupplier(instanceContext -> {
InjectionSample bean = new InjectionSample();
instanceContext.method("setEnvironment", Environment.class).invoke(beanFactory,
attributes -> bean.setEnvironment(attributes.get(0)));
return bean;
}).register(beanFactory);
assertBeanFactory(beanFactory, () -> {
assertThat(beanFactory.containsBean("test")).isTrue();
assertThat(beanFactory.getBean(InjectionSample.class).environment).isEqualTo(environment);
});
}
@Test
void registerWithInvalidMethod() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
assertThatThrownBy(() -> {
BeanDefinitionRegistrar.of("test", InjectionSample.class).instanceSupplier(instanceContext ->
instanceContext.method("setEnvironment", Object.class).resolve(beanFactory)).register(beanFactory);
beanFactory.getBean(InjectionSample.class);
}
).isInstanceOf(BeanCreationException.class)
.hasMessageContaining("No method '%s' with type(s) [%s] found", "setEnvironment", Object.class.getName())
.hasMessageContaining(InjectionSample.class.getName());
}
@Test
void innerBeanDefinitionWithClass() {
RootBeanDefinition beanDefinition = BeanDefinitionRegistrar.inner(ConfigurationSample.class)
.customize(bd -> bd.setSynthetic(true)).toBeanDefinition();
assertThat(beanDefinition).isNotNull();
assertThat(beanDefinition.getResolvableType().resolve()).isEqualTo(ConfigurationSample.class);
assertThat(beanDefinition.isSynthetic()).isTrue();
}
@Test
void innerBeanDefinitionWithResolvableType() {
RootBeanDefinition beanDefinition = BeanDefinitionRegistrar.inner(ResolvableType.forClass(ConfigurationSample.class))
.customize(bd -> bd.setDescription("test")).toBeanDefinition();
assertThat(beanDefinition).isNotNull();
assertThat(beanDefinition.getResolvableType().resolve()).isEqualTo(ConfigurationSample.class);
assertThat(beanDefinition.getDescription()).isEqualTo("test");
}
@Test
void innerBeanDefinitionHasInnerBeanNameInInstanceSupplier() {
RootBeanDefinition beanDefinition = BeanDefinitionRegistrar.inner(String.class)
.instanceSupplier(instanceContext -> {
Field field = ReflectionUtils.findField(BeanInstanceContext.class, "beanName", String.class);
ReflectionUtils.makeAccessible(field);
return ReflectionUtils.getField(field, instanceContext);
}).toBeanDefinition();
assertThat(beanDefinition).isNotNull();
String beanName = (String) beanDefinition.getInstanceSupplier().get();
assertThat(beanName).isNotNull().startsWith("(inner bean)#");
}
private void assertBeanFactory(DefaultListableBeanFactory beanFactory, Runnable assertions) {
assertions.run();
}
static class ConfigurationSample {
ConstructorSample sampleBean(ResourceLoader resourceLoader) {
return new ConstructorSample(resourceLoader);
}
}
static class ConstructorSample {
private final ResourceLoader resourceLoader;
ConstructorSample(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
static class MultiArgConstructorSample {
private final String name;
private final Integer counter;
public MultiArgConstructorSample(String name, Integer counter) {
this.name = name;
this.counter = counter;
}
}
static class InjectionSample {
private Environment environment;
private String name;
private Integer counter;
void setEnvironment(Environment environment) {
this.environment = environment;
}
void setNameAndCounter(@Value("${test.name:test}") String name, @Value("${test.counter:42}") Integer counter) {
this.name = name;
this.counter = counter;
}
}
static class InnerClassSample {
class Inner {
private Environment environment;
Inner(Environment environment) {
this.environment = environment;
}
}
}
static class GenericFactoryBeanConfiguration {
FactoryBean<NumberHolder<?>> integerHolderFactory() {
return new GenericFactoryBean<>(integerHolder());
}
NumberHolder<?> integerHolder() {
return new NumberHolder<>(42);
}
}
static class GenericFactoryBean<T> implements FactoryBean<T> {
private final T value;
public GenericFactoryBean(T value) {
this.value = value;
}
@Override
public T getObject() {
return this.value;
}
@Override
public Class<?> getObjectType() {
return this.value.getClass();
}
}
static class NumberHolder<N extends Number> {
private final N number;
public NumberHolder(N number) {
this.number = number;
}
}
static class NumberHolderSample {
@Autowired
private NumberHolder<Integer> numberHolder;
}
}

View File

@ -0,0 +1,476 @@
/*
* 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.beans.factory.generator.config;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.assertj.core.util.Arrays;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.env.Environment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link InjectedConstructionResolver}.
*
* @author Stephane Nicoll
*/
class InjectedConstructionResolverTests {
@Test
void resolveNoArgConstructor() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
InjectedElementAttributes attributes = createResolverForConstructor(
InjectedConstructionResolverTests.class).resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
}
@ParameterizedTest
@MethodSource("singleArgConstruction")
void resolveSingleArgConstructor(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("one", "1");
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
assertThat((String) attributes.get(0)).isEqualTo("1");
}
@ParameterizedTest
@MethodSource("singleArgConstruction")
void resolveRequiredDependencyNotPresentThrowsUnsatisfiedDependencyException(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
assertThatThrownBy(() -> resolver.resolve(beanFactory))
.isInstanceOfSatisfying(UnsatisfiedDependencyException.class, ex -> {
assertThat(ex.getBeanName()).isEqualTo("test");
assertThat(ex.getInjectionPoint()).isNotNull();
assertThat(ex.getInjectionPoint().getMember()).isEqualTo(resolver.getExecutable());
});
}
@ParameterizedTest
@MethodSource("arrayOfBeansConstruction")
void resolveArrayOfBeans(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("one", "1");
beanFactory.registerSingleton("two", "2");
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(Arrays.isArray(attribute)).isTrue();
assertThat((Object[]) attribute).containsExactly("1", "2");
}
@ParameterizedTest
@MethodSource("arrayOfBeansConstruction")
void resolveRequiredArrayOfBeansInjectEmptyArray(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(Arrays.isArray(attribute)).isTrue();
assertThat((Object[]) attribute).isEmpty();
}
static Stream<Arguments> arrayOfBeansConstruction() {
return Stream.of(Arguments.of(createResolverForConstructor(BeansCollectionConstructor.class, String[].class)),
Arguments.of(createResolverForFactoryMethod(BeansCollectionFactory.class, "array", String[].class)));
}
@ParameterizedTest
@MethodSource("listOfBeansConstruction")
void resolveListOfBeans(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("one", "1");
beanFactory.registerSingleton("two", "2");
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isInstanceOf(List.class).asList().containsExactly("1", "2");
}
@ParameterizedTest
@MethodSource("listOfBeansConstruction")
void resolveRequiredListOfBeansInjectEmptyList(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isInstanceOf(List.class);
assertThat((List<?>) attribute).isEmpty();
}
static Stream<Arguments> listOfBeansConstruction() {
return Stream.of(Arguments.of(createResolverForConstructor(BeansCollectionConstructor.class, List.class)),
Arguments.of(createResolverForFactoryMethod(BeansCollectionFactory.class, "list", List.class)));
}
@ParameterizedTest
@MethodSource("setOfBeansConstruction")
@SuppressWarnings("unchecked")
void resolveSetOfBeans(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("one", "1");
beanFactory.registerSingleton("two", "2");
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isInstanceOf(Set.class);
assertThat((Set<String>) attribute).containsExactly("1", "2");
}
@ParameterizedTest
@MethodSource("setOfBeansConstruction")
void resolveRequiredSetOfBeansInjectEmptySet(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isInstanceOf(Set.class);
assertThat((Set<?>) attribute).isEmpty();
}
static Stream<Arguments> setOfBeansConstruction() {
return Stream.of(Arguments.of(createResolverForConstructor(BeansCollectionConstructor.class, Set.class)),
Arguments.of(createResolverForFactoryMethod(BeansCollectionFactory.class, "set", Set.class)));
}
@ParameterizedTest
@MethodSource("mapOfBeansConstruction")
@SuppressWarnings("unchecked")
void resolveMapOfBeans(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("one", "1");
beanFactory.registerSingleton("two", "2");
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isInstanceOf(Map.class);
assertThat((Map<String, String>) attribute).containsExactly(entry("one", "1"), entry("two", "2"));
}
@ParameterizedTest
@MethodSource("mapOfBeansConstruction")
void resolveRequiredMapOfBeansInjectEmptySet(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isInstanceOf(Map.class);
assertThat((Map<?, ?>) attribute).isEmpty();
}
static Stream<Arguments> mapOfBeansConstruction() {
return Stream.of(Arguments.of(createResolverForConstructor(BeansCollectionConstructor.class, Map.class)),
Arguments.of(createResolverForFactoryMethod(BeansCollectionFactory.class, "map", Map.class)));
}
@ParameterizedTest
@MethodSource("multiArgsConstruction")
void resolveMultiArgsConstructor(InjectedConstructionResolver resolver) {
ResourceLoader resourceLoader = new DefaultResourceLoader();
Environment environment = mock(Environment.class);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerResolvableDependency(ResourceLoader.class, resourceLoader);
beanFactory.registerSingleton("environment", environment);
beanFactory.registerSingleton("one", "1");
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
assertThat((ResourceLoader) attributes.get(0)).isEqualTo(resourceLoader);
assertThat((Environment) attributes.get(1)).isEqualTo(environment);
ObjectProvider<String> provider = attributes.get(2);
assertThat(provider.getIfAvailable()).isEqualTo("1");
}
@ParameterizedTest
@MethodSource("mixedArgsConstruction")
void resolveMixedArgsConstructorWithUserValue(InjectedConstructionResolver resolver) {
ResourceLoader resourceLoader = new DefaultResourceLoader();
Environment environment = mock(Environment.class);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerResolvableDependency(ResourceLoader.class, resourceLoader);
beanFactory.registerSingleton("environment", environment);
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(MixedArgsConstructor.class)
.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR).getBeanDefinition();
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(1, "user-value");
beanFactory.registerBeanDefinition("test", beanDefinition);
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
assertThat((ResourceLoader) attributes.get(0)).isEqualTo(resourceLoader);
assertThat((String) attributes.get(1)).isEqualTo("user-value");
assertThat((Environment) attributes.get(2)).isEqualTo(environment);
}
@ParameterizedTest
@MethodSource("mixedArgsConstruction")
void resolveMixedArgsConstructorWithUserBeanReference(InjectedConstructionResolver resolver) {
ResourceLoader resourceLoader = new DefaultResourceLoader();
Environment environment = mock(Environment.class);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerResolvableDependency(ResourceLoader.class, resourceLoader);
beanFactory.registerSingleton("environment", environment);
beanFactory.registerSingleton("one", "1");
beanFactory.registerSingleton("two", "2");
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(MixedArgsConstructor.class)
.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR).getBeanDefinition();
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(1, new RuntimeBeanReference("two"));
beanFactory.registerBeanDefinition("test", beanDefinition);
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
assertThat((ResourceLoader) attributes.get(0)).isEqualTo(resourceLoader);
assertThat((String) attributes.get(1)).isEqualTo("2");
assertThat((Environment) attributes.get(2)).isEqualTo(environment);
}
@Test
void resolveUserValueWithTypeConversionRequired() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(CharDependency.class)
.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR).getBeanDefinition();
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, "\\");
beanFactory.registerBeanDefinition("test", beanDefinition);
InjectedElementAttributes attributes = createResolverForConstructor(CharDependency.class, char.class).resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isInstanceOf(Character.class);
assertThat((Character) attribute).isEqualTo('\\');
}
@ParameterizedTest
@MethodSource("singleArgConstruction")
void resolveUserValueWithBeanReference(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("stringBean", "string");
beanFactory.registerBeanDefinition("test", BeanDefinitionBuilder.rootBeanDefinition(SingleArgConstructor.class)
.addConstructorArgReference("stringBean").getBeanDefinition());
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isEqualTo("string");
}
@ParameterizedTest
@MethodSource("singleArgConstruction")
void resolveUserValueWithBeanDefinition(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
AbstractBeanDefinition userValue = BeanDefinitionBuilder.rootBeanDefinition(String.class, () -> "string").getBeanDefinition();
beanFactory.registerBeanDefinition("test", BeanDefinitionBuilder.rootBeanDefinition(SingleArgConstructor.class)
.addConstructorArgValue(userValue).getBeanDefinition());
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isEqualTo("string");
}
@ParameterizedTest
@MethodSource("singleArgConstruction")
void resolveUserValueThatIsAlreadyResolved(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(SingleArgConstructor.class).getBeanDefinition();
ValueHolder valueHolder = new ValueHolder('a');
valueHolder.setConvertedValue("this is an a");
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, valueHolder);
beanFactory.registerBeanDefinition("test", beanDefinition);
InjectedElementAttributes attributes = resolver.resolve(beanFactory);
assertThat(attributes.isResolved()).isTrue();
Object attribute = attributes.get(0);
assertThat(attribute).isEqualTo("this is an a");
}
@ParameterizedTest
@MethodSource("singleArgConstruction")
void createInvokeFactory(InjectedConstructionResolver resolver) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("one", "1");
String instance = resolver.create(beanFactory, attributes -> attributes.get(0));
assertThat(instance).isEqualTo("1");
}
private static InjectedConstructionResolver createResolverForConstructor(Class<?> beanType, Class<?>... parameterTypes) {
try {
Constructor<?> executable = beanType.getDeclaredConstructor(parameterTypes);
return new InjectedConstructionResolver(executable, beanType, "test",
InjectedConstructionResolverTests::safeGetBeanDefinition);
}
catch (NoSuchMethodException ex) {
throw new IllegalStateException(ex);
}
}
private static InjectedConstructionResolver createResolverForFactoryMethod(Class<?> targetType,
String methodName, Class<?>... parameterTypes) {
Method executable = ReflectionUtils.findMethod(targetType, methodName, parameterTypes);
return new InjectedConstructionResolver(executable, targetType, "test",
InjectedConstructionResolverTests::safeGetBeanDefinition);
}
private static BeanDefinition safeGetBeanDefinition(DefaultListableBeanFactory beanFactory) {
try {
return beanFactory.getBeanDefinition("test");
}
catch (NoSuchBeanDefinitionException ex) {
return null;
}
}
static Stream<Arguments> singleArgConstruction() {
return Stream.of(Arguments.of(createResolverForConstructor(SingleArgConstructor.class, String.class)),
Arguments.of(createResolverForFactoryMethod(SingleArgFactory.class, "single", String.class)));
}
@SuppressWarnings("unused")
static class SingleArgConstructor {
public SingleArgConstructor(String s) {
}
}
@SuppressWarnings("unused")
static class SingleArgFactory {
String single(String s) {
return s;
}
}
@SuppressWarnings("unused")
static class BeansCollectionConstructor {
public BeansCollectionConstructor(String[] beans) {
}
public BeansCollectionConstructor(List<String> beans) {
}
public BeansCollectionConstructor(Set<String> beans) {
}
public BeansCollectionConstructor(Map<String, String> beans) {
}
}
@SuppressWarnings("unused")
static class BeansCollectionFactory {
public String array(String[] beans) {
return "test";
}
public String list(List<String> beans) {
return "test";
}
public String set(Set<String> beans) {
return "test";
}
public String map(Map<String, String> beans) {
return "test";
}
}
static Stream<Arguments> multiArgsConstruction() {
return Stream.of(
Arguments.of(createResolverForConstructor(MultiArgsConstructor.class, ResourceLoader.class,
Environment.class, ObjectProvider.class)),
Arguments.of(createResolverForFactoryMethod(MultiArgsFactory.class, "multiArgs", ResourceLoader.class,
Environment.class, ObjectProvider.class)));
}
@SuppressWarnings("unused")
static class MultiArgsConstructor {
public MultiArgsConstructor(ResourceLoader resourceLoader, Environment environment, ObjectProvider<String> provider) {
}
}
@SuppressWarnings("unused")
static class MultiArgsFactory {
String multiArgs(ResourceLoader resourceLoader, Environment environment, ObjectProvider<String> provider) {
return "test";
}
}
static Stream<Arguments> mixedArgsConstruction() {
return Stream.of(
Arguments.of(createResolverForConstructor(MixedArgsConstructor.class, ResourceLoader.class,
String.class, Environment.class)),
Arguments.of(createResolverForFactoryMethod(MixedArgsFactory.class, "mixedArgs", ResourceLoader.class,
String.class, Environment.class)));
}
@SuppressWarnings("unused")
static class MixedArgsConstructor {
public MixedArgsConstructor(ResourceLoader resourceLoader, String test, Environment environment) {
}
}
@SuppressWarnings("unused")
static class MixedArgsFactory {
String mixedArgs(ResourceLoader resourceLoader, String test, Environment environment) {
return "test";
}
}
@SuppressWarnings("unused")
static class CharDependency {
CharDependency(char escapeChar) {
}
}
}

View File

@ -0,0 +1,91 @@
/*
* 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.beans.factory.generator.config;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link InjectedElementAttributes}.
*
* @author Stephane Nicoll
*/
class InjectedElementAttributesTests {
private static final InjectedElementAttributes unresolved = new InjectedElementAttributes(null);
private static final InjectedElementAttributes resolved = new InjectedElementAttributes(Collections.singletonList("test"));
@Test
void isResolvedWithUnresolvedAttributes() {
assertThat(unresolved.isResolved()).isFalse();
}
@Test
void isResolvedWithResoledAttributes() {
assertThat(resolved.isResolved()).isTrue();
}
@Test
void ifResolvedWithUnresolvedAttributesDoesNotInvokeRunnable() {
Runnable runnable = mock(Runnable.class);
unresolved.ifResolved(runnable);
verifyNoInteractions(runnable);
}
@Test
void ifResolvedWithResolvedAttributesInvokesRunnable() {
Runnable runnable = mock(Runnable.class);
resolved.ifResolved(runnable);
verify(runnable).run();
}
@Test
@SuppressWarnings("unchecked")
void ifResolvedWithUnresolvedAttributesDoesNotInvokeConsumer() {
BeanDefinitionRegistrar.ThrowableConsumer<InjectedElementAttributes> consumer = mock(BeanDefinitionRegistrar.ThrowableConsumer.class);
unresolved.ifResolved(consumer);
verifyNoInteractions(consumer);
}
@Test
@SuppressWarnings("unchecked")
void ifResolvedWithResolvedAttributesInvokesConsumer() {
BeanDefinitionRegistrar.ThrowableConsumer<InjectedElementAttributes> consumer = mock(BeanDefinitionRegistrar.ThrowableConsumer.class);
resolved.ifResolved(consumer);
verify(consumer).accept(resolved);
}
@Test
void getWithAvailableAttribute() {
InjectedElementAttributes attributes = new InjectedElementAttributes(Collections.singletonList("test"));
assertThat((String) attributes.get(0)).isEqualTo("test");
}
@Test
void getWithTypeAndAvailableAttribute() {
InjectedElementAttributes attributes = new InjectedElementAttributes(Collections.singletonList("test"));
assertThat(attributes.get(0, String.class)).isEqualTo("test");
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.beans.factory.generator.config;
import java.lang.reflect.Field;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link InjectedFieldResolver}.
*
* @author Stephane Nicoll
*/
class InjectedFieldResolverTests {
@Test
void resolveDependency() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("one", "1");
InjectedElementAttributes attributes = createResolver(TestBean.class, "string",
String.class).resolve(beanFactory, true);
assertThat(attributes.isResolved()).isTrue();
assertThat((String) attributes.get(0)).isEqualTo("1");
}
@Test
void resolveRequiredDependencyNotPresentThrowsUnsatisfiedDependencyException() {
Field field = ReflectionUtils.findField(TestBean.class, "string", String.class);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
assertThatThrownBy(() -> createResolver(TestBean.class, "string", String.class).resolve(beanFactory))
.isInstanceOfSatisfying(UnsatisfiedDependencyException.class, ex -> {
assertThat(ex.getBeanName()).isEqualTo("test");
assertThat(ex.getInjectionPoint()).isNotNull();
assertThat(ex.getInjectionPoint().getField()).isEqualTo(field);
});
}
@Test
void resolveNonRequiredDependency() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
InjectedElementAttributes attributes = createResolver(TestBean.class, "string", String.class).resolve(beanFactory, false);
assertThat(attributes.isResolved()).isFalse();
}
private InjectedFieldResolver createResolver(Class<?> beanType, String fieldName, Class<?> fieldType) {
Field field = ReflectionUtils.findField(beanType, fieldName, fieldType);
assertThat(field).isNotNull();
return new InjectedFieldResolver(field, "test");
}
static class TestBean {
private String string;
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.beans.factory.generator.config;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.env.Environment;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link InjectedMethodResolver}.
*
* @author Stephane Nicoll
*/
class InjectedMethodResolverTests {
@Test
void resolveSingleDependency() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("test", "testValue");
InjectedElementAttributes attributes = createResolver(TestBean.class, "injectString", String.class)
.resolve(beanFactory, true);
assertThat(attributes.isResolved()).isTrue();
assertThat((String) attributes.get(0)).isEqualTo("testValue");
}
@Test
void resolveRequiredDependencyNotPresentThrowsUnsatisfiedDependencyException() {
Method method = ReflectionUtils.findMethod(TestBean.class, "injectString", String.class);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
assertThatThrownBy(() -> createResolver(TestBean.class, "injectString", String.class)
.resolve(beanFactory)).isInstanceOfSatisfying(UnsatisfiedDependencyException.class, ex -> {
assertThat(ex.getBeanName()).isEqualTo("test");
assertThat(ex.getInjectionPoint()).isNotNull();
assertThat(ex.getInjectionPoint().getMember()).isEqualTo(method);
});
}
@Test
void resolveNonRequiredDependency() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
InjectedElementAttributes attributes = createResolver(TestBean.class, "injectString", String.class)
.resolve(beanFactory, false);
assertThat(attributes.isResolved()).isFalse();
}
@Test
void resolveDependencyAndEnvironment() {
Environment environment = mock(Environment.class);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("environment", environment);
beanFactory.registerSingleton("test", "testValue");
InjectedElementAttributes attributes = createResolver(TestBean.class, "injectStringAndEnvironment",
String.class, Environment.class).resolve(beanFactory, true);
assertThat(attributes.isResolved()).isTrue();
String string = attributes.get(0);
assertThat(string).isEqualTo("testValue");
assertThat((Environment) attributes.get(1)).isEqualTo(environment);
}
@Test
@SuppressWarnings("unchecked")
void createWithUnresolvedAttributesDoesNotInvokeCallback() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinitionRegistrar.ThrowableFunction<InjectedElementAttributes, ?> callback = mock(BeanDefinitionRegistrar.ThrowableFunction.class);
assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() ->
createResolver(TestBean.class, "injectString", String.class).create(beanFactory, callback));
verifyNoInteractions(callback);
}
@Test
@SuppressWarnings("unchecked")
void invokeWithUnresolvedAttributesDoesNotInvokeCallback() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinitionRegistrar.ThrowableConsumer<InjectedElementAttributes> callback = mock(BeanDefinitionRegistrar.ThrowableConsumer.class);
assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() ->
createResolver(TestBean.class, "injectString", String.class).invoke(beanFactory, callback));
verifyNoInteractions(callback);
}
private InjectedMethodResolver createResolver(Class<?> beanType, String methodName, Class<?>... parameterTypes) {
Method method = ReflectionUtils.findMethod(beanType, methodName, parameterTypes);
assertThat(method).isNotNull();
return new InjectedMethodResolver(method, beanType, "test");
}
@SuppressWarnings("unused")
static class TestBean {
public void injectString(String string) {
}
public void injectStringAndEnvironment(String string, Environment environment) {
}
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.beans.testfixture.beans.factory.generator;
import org.springframework.core.env.Environment;
public class InnerComponentConfiguration {
public class NoDependencyComponent {
public NoDependencyComponent() {
}
}
public class EnvironmentAwareComponent {
public EnvironmentAwareComponent(Environment environment) {
}
}
}

View File

@ -0,0 +1,32 @@
/*
* 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.beans.testfixture.beans.factory.generator;
public class SimpleConfiguration {
public SimpleConfiguration() {
}
public String stringBean() {
return "Hello";
}
public Integer integerBean() {
return 42;
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.beans.testfixture.beans.factory.generator.factory;
import java.io.Serializable;
/**
* A sample object with a generic type.
*
* @param <T> the number type
* @author Stephane Nicoll
*/
@SuppressWarnings("serial")
public class NumberHolder<T extends Number> implements Serializable {
private final T number;
public NumberHolder(T number) {
this.number = number;
}
}

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.beans.testfixture.beans.factory.generator.factory;
import org.springframework.beans.factory.FactoryBean;
/**
* A sample factory bean with a generic type.
*
* @param <T> the type of the number generated by this factory bean
* @author Stephane Nicoll
*/
public class NumberHolderFactoryBean<T extends Number> implements FactoryBean<NumberHolder<T>> {
private T number;
public void setNumber(T number) {
this.number = number;
}
@Override
public NumberHolder<T> getObject() {
return new NumberHolder<>(this.number);
}
@Override
public Class<?> getObjectType() {
return NumberHolder.class;
}
}

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.beans.testfixture.beans.factory.generator.factory;
public abstract class SampleFactory {
public static String create(String testBean) {
return testBean;
}
public static String create(char character) {
return String.valueOf(character);
}
public static String create(Number number, String test) {
return number + test;
}
public static String create(Class<?> type) {
return type.getName();
}
public static Integer integerBean() {
return 42;
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.beans.testfixture.beans.factory.generator.injection;
import org.springframework.beans.factory.annotation.Autowired;
@SuppressWarnings("unused")
public class InjectionComponent {
private final String bean;
private Integer counter;
public InjectionComponent(String bean) {
this.bean = bean;
}
@Autowired(required = false)
public void setCounter(Integer counter) {
this.counter = counter;
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.beans.testfixture.beans.factory.generator.property;
public class ConfigurableBean {
private String name;
private Integer counter;
public void setName(String name) {
this.name = name;
}
public void setCounter(Integer counter) {
this.counter = counter;
}
}

View File

@ -0,0 +1,24 @@
/*
* 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.beans.testfixture.beans.factory.generator.visibility;
public class ProtectedConstructorComponent {
ProtectedConstructorComponent() {
}
}

View File

@ -0,0 +1,25 @@
/*
* 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.beans.testfixture.beans.factory.generator.visibility;
public class ProtectedFactoryMethod {
String testBean(Integer number) {
return "test-" + number;
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.aot.generator;
import java.util.Arrays;
import org.springframework.core.ResolvableType;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.support.MultiCodeBlock;
import org.springframework.util.ClassUtils;
/**
* Code generator for {@link ResolvableType}.
*
* @author Stephane Nicoll
* @since 6.0
*/
public final class ResolvableTypeGenerator {
/**
* Generate a type signature for the specified {@link ResolvableType}.
* @param target the type to generate
* @return the representation of that type
*/
public CodeBlock generateTypeFor(ResolvableType target) {
CodeBlock.Builder code = CodeBlock.builder();
generate(code, target, false);
return code.build();
}
private void generate(CodeBlock.Builder code, ResolvableType target, boolean forceResolvableType) {
Class<?> type = ClassUtils.getUserClass(target.toClass());
if (!target.hasGenerics()) {
if (forceResolvableType) {
code.add("$T.forClass($T.class)", ResolvableType.class, type);
}
else {
code.add("$T.class", type);
}
}
else {
code.add("$T.forClassWithGenerics($T.class, ", ResolvableType.class, type);
ResolvableType[] generics = target.getGenerics();
boolean hasGenericParameter = Arrays.stream(generics).anyMatch(ResolvableType::hasGenerics);
MultiCodeBlock multi = new MultiCodeBlock();
for (int i = 0; i < generics.length; i++) {
ResolvableType parameter = target.getGeneric(i);
multi.add(parameterCode -> generate(parameterCode, parameter, hasGenericParameter));
}
code.add(multi.join(", ")).add(")");
}
}
}

View File

@ -29,6 +29,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.aot.hint.TypeHint.Builder;
import org.springframework.lang.Nullable;
/**
* Gather the need for reflection at runtime.
@ -49,6 +50,28 @@ public class ReflectionHints {
return this.types.values().stream().map(TypeHint.Builder::build);
}
/**
* Return the reflection hints for the type defined by the specified
* {@link TypeReference}.
* @param type the type to inspect
* @return the reflection hints for this type, or {@code null}
*/
@Nullable
public TypeHint getTypeHint(TypeReference type) {
Builder typeHintBuilder = this.types.get(type);
return (typeHintBuilder != null ? typeHintBuilder.build() : null);
}
/**
* Return the reflection hints for the specified type.
* @param type the type to inspect
* @return the reflection hints for this type, or {@code null}
*/
@Nullable
public TypeHint getTypeHint(Class<?> type) {
return getTypeHint(TypeReference.of(type));
}
/**
* Register or customize reflection hints for the type defined by the
* specified {@link TypeReference}.

View File

@ -47,41 +47,48 @@ public final class MultiStatement {
/**
* Add the specified {@link CodeBlock codeblock} rendered as-is.
* @param codeBlock the code block to add
* @return {@code this}, to facilitate method chaining
* @see #addStatement(CodeBlock) to add a code block that represents
* a statement
*/
public void add(CodeBlock codeBlock) {
public MultiStatement add(CodeBlock codeBlock) {
this.statements.add(Statement.of(codeBlock));
return this;
}
/**
* Add a {@link CodeBlock} rendered as-is using the specified callback.
* @param code the callback to use
* @return {@code this}, to facilitate method chaining
* @see #addStatement(CodeBlock) to add a code block that represents
* a statement
*/
public void add(Consumer<Builder> code) {
public MultiStatement add(Consumer<Builder> code) {
CodeBlock.Builder builder = CodeBlock.builder();
code.accept(builder);
add(builder.build());
return this;
}
/**
* Add a statement.
* @param statement the statement to add
* @return {@code this}, to facilitate method chaining
*/
public void addStatement(CodeBlock statement) {
public MultiStatement addStatement(CodeBlock statement) {
this.statements.add(Statement.ofStatement(statement));
return this;
}
/**
* Add a statement using the specified callback.
* @param code the callback to use
* @return {@code this}, to facilitate method chaining
*/
public void addStatement(Consumer<Builder> code) {
public MultiStatement addStatement(Consumer<Builder> code) {
CodeBlock.Builder builder = CodeBlock.builder();
code.accept(builder);
addStatement(builder.build());
return addStatement(builder.build());
}
/**
@ -89,10 +96,11 @@ public final class MultiStatement {
* arguments.
* @param code the code of the statement
* @param args the arguments for placeholders
* @return {@code this}, to facilitate method chaining
* @see CodeBlock#of(String, Object...)
*/
public void addStatement(String code, Object... args) {
addStatement(CodeBlock.of(code, args));
public MultiStatement addStatement(String code, Object... args) {
return addStatement(CodeBlock.of(code, args));
}
/**
@ -101,9 +109,11 @@ public final class MultiStatement {
* @param items the items to handle, each item is represented as a statement
* @param itemGenerator the item generator
* @param <T> the type of the item
* @return {@code this}, to facilitate method chaining
*/
public <T> void addAll(Iterable<T> items, Function<T, CodeBlock> itemGenerator) {
public <T> MultiStatement addAll(Iterable<T> items, Function<T, CodeBlock> itemGenerator) {
items.forEach(element -> addStatement(itemGenerator.apply(element)));
return this;
}
/**

View File

@ -0,0 +1,62 @@
/*
* 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.aot.generator;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;
import org.springframework.javapoet.support.CodeSnippet;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ResolvableTypeGenerator}.
*
* @author Stephane Nicoll
*/
class ResolvableTypeGeneratorTests {
@Test
void generateTypeForResolvableTypeWithGenericParameter() {
assertThat(generateTypeFor(
ResolvableType.forClassWithGenerics(Function.class,
ResolvableType.forClassWithGenerics(Supplier.class, String.class),
ResolvableType.forClassWithGenerics(Supplier.class, Integer.class))))
.isEqualTo("ResolvableType.forClassWithGenerics(Function.class, "
+ "ResolvableType.forClassWithGenerics(Supplier.class, String.class), "
+ "ResolvableType.forClassWithGenerics(Supplier.class, Integer.class))");
}
@Test
void generateTypeForResolvableTypeWithMixedParameter() {
assertThat(generateTypeFor(
ResolvableType.forClassWithGenerics(Function.class,
ResolvableType.forClassWithGenerics(Supplier.class, String.class),
ResolvableType.forClass(Integer.class))))
.isEqualTo("ResolvableType.forClassWithGenerics(Function.class, "
+ "ResolvableType.forClassWithGenerics(Supplier.class, String.class), "
+ "ResolvableType.forClass(Integer.class))");
}
private String generateTypeFor(ResolvableType type) {
return CodeSnippet.process(new ResolvableTypeGenerator().generateTypeFor(type));
}
}

View File

@ -16,10 +16,13 @@
package org.springframework.aot.hint;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -41,11 +44,34 @@ class ReflectionHintsTests {
typeWithMemberCategories(String.class, MemberCategory.DECLARED_FIELDS));
}
@Test
void getTypeUsingType() {
this.reflectionHints.registerType(TypeReference.of(String.class),
hint -> hint.withMembers(MemberCategory.DECLARED_FIELDS));
assertThat(this.reflectionHints.getTypeHint(String.class)).satisfies(
typeWithMemberCategories(String.class, MemberCategory.DECLARED_FIELDS));
}
@Test
void getTypeUsingTypeReference() {
this.reflectionHints.registerType(String.class,
hint -> hint.withMembers(MemberCategory.DECLARED_FIELDS));
assertThat(this.reflectionHints.getTypeHint(TypeReference.of(String.class))).satisfies(
typeWithMemberCategories(String.class, MemberCategory.DECLARED_FIELDS));
}
@Test
void getTypeForNonExistingType() {
assertThat(this.reflectionHints.getTypeHint(String.class)).isNull();
}
@Test
void registerTypeReuseBuilder() {
this.reflectionHints.registerType(TypeReference.of(String.class),
typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
this.reflectionHints.registerField(ReflectionUtils.findField(String.class, "value"));
Field field = ReflectionUtils.findField(String.class, "value");
assertThat(field).isNotNull();
this.reflectionHints.registerField(field);
assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> {
assertThat(typeHint.getType().getCanonicalName()).isEqualTo(String.class.getCanonicalName());
assertThat(typeHint.fields()).singleElement().satisfies(fieldHint -> assertThat(fieldHint.getName()).isEqualTo("value"));
@ -63,7 +89,9 @@ class ReflectionHintsTests {
@Test
void registerField() {
this.reflectionHints.registerField(ReflectionUtils.findField(TestType.class, "field"));
Field field = ReflectionUtils.findField(TestType.class, "field");
assertThat(field).isNotNull();
this.reflectionHints.registerField(field);
assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> {
assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName());
assertThat(typeHint.fields()).singleElement().satisfies(fieldHint ->
@ -92,7 +120,9 @@ class ReflectionHintsTests {
@Test
void registerMethod() {
this.reflectionHints.registerMethod(ReflectionUtils.findMethod(TestType.class, "setName", String.class));
Method method = ReflectionUtils.findMethod(TestType.class, "setName", String.class);
assertThat(method).isNotNull();
this.reflectionHints.registerMethod(method);
assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> {
assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName());
assertThat(typeHint.fields()).isEmpty();
@ -119,6 +149,7 @@ class ReflectionHintsTests {
@SuppressWarnings("unused")
static class TestType {
@Nullable
private String field;
void setName(String name) {