From c5e1a774a5a1e27eb1ca693b663ec37e6583ac41 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 14 Feb 2022 14:31:19 +0100 Subject: [PATCH] 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 --- .../generator/BeanInstanceContributor.java | 51 ++ .../generator/BeanParameterGenerator.java | 218 ++++++++ .../DefaultBeanInstanceGenerator.java | 165 ++++++ .../factory/generator/InjectionGenerator.java | 239 +++++++++ .../config/BeanDefinitionRegistrar.java | 408 +++++++++++++++ .../config/InjectedConstructionResolver.java | 175 +++++++ .../config/InjectedElementAttributes.java | 93 ++++ .../config/InjectedElementResolver.java | 75 +++ .../config/InjectedFieldResolver.java | 71 +++ .../config/InjectedMethodResolver.java | 86 ++++ .../generator/config/package-info.java | 9 + .../BeanParameterGeneratorTests.java | 264 ++++++++++ .../DefaultBeanInstanceGeneratorTests.java | 255 ++++++++++ .../generator/InjectionGeneratorTests.java | 269 ++++++++++ .../config/BeanDefinitionRegistrarTests.java | 458 +++++++++++++++++ .../InjectedConstructionResolverTests.java | 476 ++++++++++++++++++ .../InjectedElementAttributesTests.java | 91 ++++ .../config/InjectedFieldResolverTests.java | 78 +++ .../config/InjectedMethodResolverTests.java | 124 +++++ .../InnerComponentConfiguration.java | 36 ++ .../generator/SimpleConfiguration.java | 32 ++ .../generator/factory/NumberHolder.java | 36 ++ .../factory/NumberHolderFactoryBean.java | 45 ++ .../generator/factory/SampleFactory.java | 41 ++ .../injection/InjectionComponent.java | 37 ++ .../generator/property/ConfigurableBean.java | 33 ++ .../ProtectedConstructorComponent.java | 24 + .../visibility/ProtectedFactoryMethod.java | 25 + .../generator/ResolvableTypeGenerator.java | 68 +++ .../aot/hint/ReflectionHints.java | 23 + .../javapoet/support/MultiStatement.java | 26 +- .../ResolvableTypeGeneratorTests.java | 62 +++ .../aot/hint/ReflectionHintsTests.java | 37 +- 33 files changed, 4119 insertions(+), 11 deletions(-) create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanInstanceContributor.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanParameterGenerator.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/DefaultBeanInstanceGenerator.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/InjectionGenerator.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/config/BeanDefinitionRegistrar.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedConstructionResolver.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedElementAttributes.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedElementResolver.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedFieldResolver.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedMethodResolver.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/generator/config/package-info.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/generator/BeanParameterGeneratorTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/generator/DefaultBeanInstanceGeneratorTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/generator/InjectionGeneratorTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/generator/config/BeanDefinitionRegistrarTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedConstructionResolverTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedElementAttributesTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedFieldResolverTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedMethodResolverTests.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolder.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolderFactoryBean.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/SampleFactory.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/injection/InjectionComponent.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/property/ConfigurableBean.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedConstructorComponent.java create mode 100644 spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedFactoryMethod.java create mode 100644 spring-core/src/main/java/org/springframework/aot/generator/ResolvableTypeGenerator.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generator/ResolvableTypeGeneratorTests.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanInstanceContributor.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanInstanceContributor.java new file mode 100644 index 0000000000..338334bbd2 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanInstanceContributor.java @@ -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}. + *

Implementation of this interface can assume the following variables + * to be accessible: + *

+ * @param contribution the {@link CodeContribution} to use + */ + void contribute(CodeContribution contribution); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanParameterGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanParameterGenerator.java new file mode 100644 index 0000000000..dccc5b660f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/BeanParameterGenerator.java @@ -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 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 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 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 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 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 CodeBlock writeAll(Iterable items, Function 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); + }; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/DefaultBeanInstanceGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/DefaultBeanInstanceGenerator.java new file mode 100644 index 0000000000..8c5006150f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/DefaultBeanInstanceGenerator.java @@ -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 contributors; + + private final InjectionGenerator injectionGenerator; + + + DefaultBeanInstanceGenerator(Executable instanceCreator, List 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> 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(); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/InjectionGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/InjectionGenerator.java new file mode 100644 index 0000000000..c4845b339b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/InjectionGenerator.java @@ -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}. + *

+ * The generator assumes a number of variables to be accessible: + *

    + *
  • {@code beanFactory}: the general {@code DefaultListableBeanFactory}
  • + *
  • {@code instanceContext}: the {@link BeanInstanceContext} callback
  • + *
  • {@code bean}: the variable that refers to the bean instance
  • + *
+ * + * @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 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 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 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 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 resolveParameters(Parameter[] parameters, boolean shouldCast) { + List 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; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/BeanDefinitionRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/BeanDefinitionRegistrar.java new file mode 100644 index 0000000000..149ddcc906 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/BeanDefinitionRegistrar.java @@ -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. + * + *

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> 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 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 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 the type of the bean + * @return the bean instance + */ + public T create(DefaultListableBeanFactory beanFactory, ThrowableFunction 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 the type of the input to the operation + */ + @FunctionalInterface + public interface ThrowableConsumer extends Consumer { + + 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 the type of the input to the function + * @param the type of the result of the function + */ + @FunctionalInterface + public interface ThrowableFunction extends Function { + + 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 the type of results supplied by this supplier + */ + public interface ThrowableSupplier extends Supplier { + + 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); + } + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedConstructionResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedConstructionResolver.java new file mode 100644 index 0000000000..d1257a7eac --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedConstructionResolver.java @@ -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 beanDefinitionResolver; + + InjectedConstructionResolver(Executable executable, Class targetType, String beanName, + Function 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 arguments = new ArrayList<>(); + Set 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 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 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(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedElementAttributes.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedElementAttributes.java new file mode 100644 index 0000000000..a8677f2ee1 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedElementAttributes.java @@ -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 attributes; + + + InjectedElementAttributes(@Nullable List 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 attributes) { + ifResolved(() -> attributes.accept(this)); + } + + /** + * Return the resolved attribute at the specified index. + * @param index the attribute index + * @param the type of the attribute + * @return the attribute + */ + @SuppressWarnings("unchecked") + public 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 the type of the attribute + * @return the attribute + */ + @SuppressWarnings("unchecked") + public T get(int index, Class type) { + Assert.notNull(this.attributes, "Attributes must not be null"); + return (T) this.attributes.get(index); + } +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedElementResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedElementResolver.java new file mode 100644 index 0000000000..5e87e10a28 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedElementResolver.java @@ -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 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 the type of the instance + * @return a new instance + */ + default T create(DefaultListableBeanFactory beanFactory, + BeanDefinitionRegistrar.ThrowableFunction factory) { + + InjectedElementAttributes attributes = resolve(beanFactory); + return factory.apply(attributes); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedFieldResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedFieldResolver.java new file mode 100644 index 0000000000..a7d35c2ff0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedFieldResolver.java @@ -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 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); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedMethodResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedMethodResolver.java new file mode 100644 index 0000000000..c58d616734 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/InjectedMethodResolver.java @@ -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 arguments = new ArrayList<>(); + Set 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); + } + +} + diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/package-info.java new file mode 100644 index 0000000000..821c57180f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/generator/config/package-info.java @@ -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; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/generator/BeanParameterGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/generator/BeanParameterGeneratorTests.java new file mode 100644 index 0000000000..8e85c3cdf0 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/generator/BeanParameterGeneratorTests.java @@ -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 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 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 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 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 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 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 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 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 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"; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/generator/DefaultBeanInstanceGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/generator/DefaultBeanInstanceGeneratorTests.java new file mode 100644 index 0000000000..d780d86814 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/generator/DefaultBeanInstanceGeneratorTests.java @@ -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 hasSingleQueryConstructor(Constructor constructor) { + return typeHint -> assertThat(typeHint.constructors()).singleElement() + .satisfies(match(constructor, "", ExecutableMode.INTROSPECT)); + } + + private Consumer hasSingleQueryMethod(Method method) { + return typeHint -> assertThat(typeHint.methods()).singleElement() + .satisfies(match(method, method.getName(), ExecutableMode.INTROSPECT)); + } + + private Consumer 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; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/generator/InjectionGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/generator/InjectionGeneratorTests.java new file mode 100644 index 0000000000..910e66b6db --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/generator/InjectionGeneratorTests.java @@ -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 counter) { + + } + + String source(Integer counter) { + return "source" + counter; + } + + String source(ObjectProvider 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 counter; + + GenericConstructorBean(ObjectProvider counter) { + this.counter = counter; + } + + } + + static class AmbiguousConstructorBean { + + AmbiguousConstructorBean(String first, String second) { + + } + + AmbiguousConstructorBean(String first, Number second) { + + } + + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/BeanDefinitionRegistrarTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/BeanDefinitionRegistrarTests.java new file mode 100644 index 0000000000..8c97229f6d --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/BeanDefinitionRegistrarTests.java @@ -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 first = mock(BeanDefinitionRegistrar.ThrowableConsumer.class); + BeanDefinitionRegistrar.ThrowableConsumer 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> integerHolderFactory() { + return new GenericFactoryBean<>(integerHolder()); + } + + NumberHolder integerHolder() { + return new NumberHolder<>(42); + } + + } + + static class GenericFactoryBean implements FactoryBean { + + 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 { + + private final N number; + + public NumberHolder(N number) { + this.number = number; + } + + } + + static class NumberHolderSample { + + @Autowired + private NumberHolder numberHolder; + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedConstructionResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedConstructionResolverTests.java new file mode 100644 index 0000000000..ff599eb4ff --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedConstructionResolverTests.java @@ -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 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 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) 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 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) 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 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 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 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 beans) { + + } + + public BeansCollectionConstructor(Set beans) { + + } + + public BeansCollectionConstructor(Map beans) { + + } + + } + + @SuppressWarnings("unused") + static class BeansCollectionFactory { + + public String array(String[] beans) { + return "test"; + } + + public String list(List beans) { + return "test"; + } + + public String set(Set beans) { + return "test"; + } + + public String map(Map beans) { + return "test"; + } + + } + + static Stream 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 provider) { + } + } + + @SuppressWarnings("unused") + static class MultiArgsFactory { + + String multiArgs(ResourceLoader resourceLoader, Environment environment, ObjectProvider provider) { + return "test"; + } + } + + static Stream 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) { + } + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedElementAttributesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedElementAttributesTests.java new file mode 100644 index 0000000000..f2f66811b9 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedElementAttributesTests.java @@ -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 consumer = mock(BeanDefinitionRegistrar.ThrowableConsumer.class); + unresolved.ifResolved(consumer); + verifyNoInteractions(consumer); + } + + @Test + @SuppressWarnings("unchecked") + void ifResolvedWithResolvedAttributesInvokesConsumer() { + BeanDefinitionRegistrar.ThrowableConsumer 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"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedFieldResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedFieldResolverTests.java new file mode 100644 index 0000000000..39f90cca7f --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedFieldResolverTests.java @@ -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; + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedMethodResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedMethodResolverTests.java new file mode 100644 index 0000000000..d9b9e2aee2 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/generator/config/InjectedMethodResolverTests.java @@ -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 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 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) { + + } + + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java new file mode 100644 index 0000000000..28188b2a52 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/InnerComponentConfiguration.java @@ -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) { + + } + } +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java new file mode 100644 index 0000000000..0f5ba99ad0 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/SimpleConfiguration.java @@ -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; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolder.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolder.java new file mode 100644 index 0000000000..d49475656e --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolder.java @@ -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 the number type + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public class NumberHolder implements Serializable { + + private final T number; + + public NumberHolder(T number) { + this.number = number; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolderFactoryBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolderFactoryBean.java new file mode 100644 index 0000000000..a837a8dcaf --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/NumberHolderFactoryBean.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.testfixture.beans.factory.generator.factory; + +import org.springframework.beans.factory.FactoryBean; + +/** + * A sample factory bean with a generic type. + * + * @param the type of the number generated by this factory bean + * @author Stephane Nicoll + */ +public class NumberHolderFactoryBean implements FactoryBean> { + + private T number; + + public void setNumber(T number) { + this.number = number; + } + + @Override + public NumberHolder getObject() { + return new NumberHolder<>(this.number); + } + + @Override + public Class getObjectType() { + return NumberHolder.class; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/SampleFactory.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/SampleFactory.java new file mode 100644 index 0000000000..eca5e0d7bf --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/factory/SampleFactory.java @@ -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; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/injection/InjectionComponent.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/injection/InjectionComponent.java new file mode 100644 index 0000000000..9d772ade5a --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/injection/InjectionComponent.java @@ -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; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/property/ConfigurableBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/property/ConfigurableBean.java new file mode 100644 index 0000000000..0a8319223b --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/property/ConfigurableBean.java @@ -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; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedConstructorComponent.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedConstructorComponent.java new file mode 100644 index 0000000000..5b27a375cd --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedConstructorComponent.java @@ -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() { + + } +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedFactoryMethod.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedFactoryMethod.java new file mode 100644 index 0000000000..7d2fd2b557 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/visibility/ProtectedFactoryMethod.java @@ -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; + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generator/ResolvableTypeGenerator.java b/spring-core/src/main/java/org/springframework/aot/generator/ResolvableTypeGenerator.java new file mode 100644 index 0000000000..339a993bf9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generator/ResolvableTypeGenerator.java @@ -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(")"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java index f459c6a258..1f7be98f33 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java @@ -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}. diff --git a/spring-core/src/main/java/org/springframework/javapoet/support/MultiStatement.java b/spring-core/src/main/java/org/springframework/javapoet/support/MultiStatement.java index 04bd02071e..2442b85850 100644 --- a/spring-core/src/main/java/org/springframework/javapoet/support/MultiStatement.java +++ b/spring-core/src/main/java/org/springframework/javapoet/support/MultiStatement.java @@ -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 code) { + public MultiStatement add(Consumer 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 code) { + public MultiStatement addStatement(Consumer 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 the type of the item + * @return {@code this}, to facilitate method chaining */ - public void addAll(Iterable items, Function itemGenerator) { + public MultiStatement addAll(Iterable items, Function itemGenerator) { items.forEach(element -> addStatement(itemGenerator.apply(element))); + return this; } /** diff --git a/spring-core/src/test/java/org/springframework/aot/generator/ResolvableTypeGeneratorTests.java b/spring-core/src/test/java/org/springframework/aot/generator/ResolvableTypeGeneratorTests.java new file mode 100644 index 0000000000..34ab0f8671 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generator/ResolvableTypeGeneratorTests.java @@ -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)); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java index 6b0f57d85c..8d96f72190 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java @@ -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) {