Extract value code generation to make it reusable

This commit introduces ValueCodeGenerator and its Delegate interface
as a way to generate the code for a particular value. Implementations
in spring-core provides support for common value types such a String,
primitives, Collections, etc.

Additional implementations are provided for code generation of bean
definition property values.

Closes gh-28999
This commit is contained in:
Stéphane Nicoll
2023-12-13 07:05:50 +01:00
parent 75da9c3c47
commit 3c2c9ca186
14 changed files with 1479 additions and 833 deletions

View File

@@ -34,6 +34,9 @@ import java.util.function.Function;
import java.util.function.Predicate;
import org.springframework.aot.generate.GeneratedMethods;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
@@ -89,7 +92,7 @@ class BeanDefinitionPropertiesCodeGenerator {
private final Predicate<String> attributeFilter;
private final BeanDefinitionPropertyValueCodeGenerator valueCodeGenerator;
private final ValueCodeGenerator valueCodeGenerator;
BeanDefinitionPropertiesCodeGenerator(RuntimeHints hints,
@@ -98,8 +101,11 @@ class BeanDefinitionPropertiesCodeGenerator {
this.hints = hints;
this.attributeFilter = attributeFilter;
this.valueCodeGenerator = new BeanDefinitionPropertyValueCodeGenerator(generatedMethods,
(object, type) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), object));
this.valueCodeGenerator = ValueCodeGenerator
.with(new ValueCodeGeneratorDelegateAdapter(customValueCodeGenerator))
.add(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES)
.add(ValueCodeGeneratorDelegates.INSTANCES)
.scoped(generatedMethods);
}
@@ -366,6 +372,22 @@ class BeanDefinitionPropertiesCodeGenerator {
return (castNecessary ? CodeBlock.of("($T) $L", castType, valueCode) : valueCode);
}
static class ValueCodeGeneratorDelegateAdapter implements Delegate {
private final BiFunction<String, Object, CodeBlock> customValueCodeGenerator;
ValueCodeGeneratorDelegateAdapter(BiFunction<String, Object, CodeBlock> customValueCodeGenerator) {
this.customValueCodeGenerator = customValueCodeGenerator;
}
@Override
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
return this.customValueCodeGenerator.apply(PropertyNamesStack.peek(), value);
}
}
static class PropertyNamesStack {
private static final ThreadLocal<ArrayDeque<String>> threadLocal = ThreadLocal.withInitial(ArrayDeque::new);
@@ -384,7 +406,6 @@ class BeanDefinitionPropertiesCodeGenerator {
String value = threadLocal.get().peek();
return ("".equals(value) ? null : value);
}
}
}

View File

@@ -1,600 +0,0 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans.factory.aot;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import org.springframework.aot.generate.GeneratedMethod;
import org.springframework.aot.generate.GeneratedMethods;
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.config.TypedStringValue;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.ManagedSet;
import org.springframework.core.ResolvableType;
import org.springframework.javapoet.AnnotationSpec;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Internal code generator used to generate code for a single value contained in
* a {@link BeanDefinition} property.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Sebastien Deleuze
* @since 6.0
*/
class BeanDefinitionPropertyValueCodeGenerator {
static final CodeBlock NULL_VALUE_CODE_BLOCK = CodeBlock.of("null");
private final GeneratedMethods generatedMethods;
private final List<Delegate> delegates;
BeanDefinitionPropertyValueCodeGenerator(GeneratedMethods generatedMethods,
@Nullable BiFunction<Object, ResolvableType, CodeBlock> customValueGenerator) {
this.generatedMethods = generatedMethods;
this.delegates = new ArrayList<>();
if (customValueGenerator != null) {
this.delegates.add(customValueGenerator::apply);
}
this.delegates.addAll(List.of(
new PrimitiveDelegate(),
new StringDelegate(),
new CharsetDelegate(),
new EnumDelegate(),
new ClassDelegate(),
new ResolvableTypeDelegate(),
new ArrayDelegate(),
new ManagedListDelegate(),
new ManagedSetDelegate(),
new ManagedMapDelegate(),
new ListDelegate(),
new SetDelegate(),
new MapDelegate(),
new BeanReferenceDelegate(),
new TypedStringValueDelegate()
));
}
CodeBlock generateCode(@Nullable Object value) {
ResolvableType type = ResolvableType.forInstance(value);
try {
return generateCode(value, type);
}
catch (Exception ex) {
throw new IllegalArgumentException(buildErrorMessage(value, type), ex);
}
}
private CodeBlock generateCodeForElement(@Nullable Object value, ResolvableType type) {
try {
return generateCode(value, type);
}
catch (Exception ex) {
throw new IllegalArgumentException(buildErrorMessage(value, type), ex);
}
}
private static String buildErrorMessage(@Nullable Object value, ResolvableType type) {
StringBuilder message = new StringBuilder("Failed to generate code for '");
message.append(value).append("'");
if (type != ResolvableType.NONE) {
message.append(" with type ").append(type);
}
return message.toString();
}
private CodeBlock generateCode(@Nullable Object value, ResolvableType type) {
if (value == null) {
return NULL_VALUE_CODE_BLOCK;
}
for (Delegate delegate : this.delegates) {
CodeBlock code = delegate.generateCode(value, type);
if (code != null) {
return code;
}
}
throw new IllegalArgumentException("Code generation does not support " + type);
}
/**
* Internal delegate used to support generation for a specific type.
*/
@FunctionalInterface
private interface Delegate {
@Nullable
CodeBlock generateCode(Object value, ResolvableType type);
}
/**
* {@link Delegate} for {@code primitive} types.
*/
private static class PrimitiveDelegate implements Delegate {
private static final Map<Character, String> CHAR_ESCAPES = Map.of(
'\b', "\\b",
'\t', "\\t",
'\n', "\\n",
'\f', "\\f",
'\r', "\\r",
'\"', "\"",
'\'', "\\'",
'\\', "\\\\"
);
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof Boolean || value instanceof Integer) {
return CodeBlock.of("$L", value);
}
if (value instanceof Byte) {
return CodeBlock.of("(byte) $L", value);
}
if (value instanceof Short) {
return CodeBlock.of("(short) $L", value);
}
if (value instanceof Long) {
return CodeBlock.of("$LL", value);
}
if (value instanceof Float) {
return CodeBlock.of("$LF", value);
}
if (value instanceof Double) {
return CodeBlock.of("(double) $L", value);
}
if (value instanceof Character character) {
return CodeBlock.of("'$L'", escape(character));
}
return null;
}
private String escape(char ch) {
String escaped = CHAR_ESCAPES.get(ch);
if (escaped != null) {
return escaped;
}
return (!Character.isISOControl(ch)) ? Character.toString(ch)
: String.format("\\u%04x", (int) ch);
}
}
/**
* {@link Delegate} for {@link String} types.
*/
private static class StringDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof String) {
return CodeBlock.of("$S", value);
}
return null;
}
}
/**
* {@link Delegate} for {@link Charset} types.
*/
private static class CharsetDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof Charset charset) {
return CodeBlock.of("$T.forName($S)", Charset.class, charset.name());
}
return null;
}
}
/**
* {@link Delegate} for {@link Enum} types.
*/
private static class EnumDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof Enum<?> enumValue) {
return CodeBlock.of("$T.$L", enumValue.getDeclaringClass(),
enumValue.name());
}
return null;
}
}
/**
* {@link Delegate} for {@link Class} types.
*/
private static class ClassDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof Class<?> clazz) {
return CodeBlock.of("$T.class", ClassUtils.getUserClass(clazz));
}
return null;
}
}
/**
* {@link Delegate} for {@link ResolvableType} types.
*/
private static class ResolvableTypeDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof ResolvableType resolvableType) {
return ResolvableTypeCodeGenerator.generateCode(resolvableType);
}
return null;
}
}
/**
* {@link Delegate} for {@code array} types.
*/
private class ArrayDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(@Nullable Object value, ResolvableType type) {
if (type.isArray()) {
ResolvableType componentType = type.getComponentType();
Stream<CodeBlock> elements = Arrays.stream(ObjectUtils.toObjectArray(value)).map(component ->
BeanDefinitionPropertyValueCodeGenerator.this.generateCode(component, componentType));
CodeBlock.Builder code = CodeBlock.builder();
code.add("new $T {", type.toClass());
code.add(elements.collect(CodeBlock.joining(", ")));
code.add("}");
return code.build();
}
return null;
}
}
/**
* Abstract {@link Delegate} for {@code Collection} types.
*/
private abstract class CollectionDelegate<T extends Collection<?>> implements Delegate {
private final Class<?> collectionType;
private final CodeBlock emptyResult;
public CollectionDelegate(Class<?> collectionType, CodeBlock emptyResult) {
this.collectionType = collectionType;
this.emptyResult = emptyResult;
}
@SuppressWarnings("unchecked")
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (this.collectionType.isInstance(value)) {
T collection = (T) value;
if (collection.isEmpty()) {
return this.emptyResult;
}
ResolvableType elementType = type.as(this.collectionType).getGeneric();
return generateCollectionCode(elementType, collection);
}
return null;
}
protected CodeBlock generateCollectionCode(ResolvableType elementType, T collection) {
return generateCollectionOf(collection, this.collectionType, elementType);
}
protected final CodeBlock generateCollectionOf(Collection<?> collection,
Class<?> collectionType, ResolvableType elementType) {
Builder code = CodeBlock.builder();
code.add("$T.of(", collectionType);
Iterator<?> iterator = collection.iterator();
while (iterator.hasNext()) {
Object element = iterator.next();
code.add("$L", BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(element, elementType));
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
return code.build();
}
}
/**
* {@link Delegate} for {@link ManagedList} types.
*/
private class ManagedListDelegate extends CollectionDelegate<ManagedList<?>> {
public ManagedListDelegate() {
super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class));
}
}
/**
* {@link Delegate} for {@link ManagedSet} types.
*/
private class ManagedSetDelegate extends CollectionDelegate<ManagedSet<?>> {
public ManagedSetDelegate() {
super(ManagedSet.class, CodeBlock.of("new $T()", ManagedSet.class));
}
}
/**
* {@link Delegate} for {@link ManagedMap} types.
*/
private class ManagedMapDelegate implements Delegate {
private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class);
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof ManagedMap<?, ?> managedMap) {
return generateManagedMapCode(type, managedMap);
}
return null;
}
private <K, V> CodeBlock generateManagedMapCode(ResolvableType type, ManagedMap<K, V> managedMap) {
if (managedMap.isEmpty()) {
return EMPTY_RESULT;
}
ResolvableType keyType = type.as(Map.class).getGeneric(0);
ResolvableType valueType = type.as(Map.class).getGeneric(1);
CodeBlock.Builder code = CodeBlock.builder();
code.add("$T.ofEntries(", ManagedMap.class);
Iterator<Map.Entry<K, V>> iterator = managedMap.entrySet().iterator();
while (iterator.hasNext()) {
Entry<?, ?> entry = iterator.next();
code.add("$T.entry($L,$L)", Map.class,
BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(entry.getKey(), keyType),
BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(entry.getValue(), valueType));
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
return code.build();
}
}
/**
* {@link Delegate} for {@link List} types.
*/
private class ListDelegate extends CollectionDelegate<List<?>> {
ListDelegate() {
super(List.class, CodeBlock.of("$T.emptyList()", Collections.class));
}
}
/**
* {@link Delegate} for {@link Set} types.
*/
private class SetDelegate extends CollectionDelegate<Set<?>> {
SetDelegate() {
super(Set.class, CodeBlock.of("$T.emptySet()", Collections.class));
}
@Override
protected CodeBlock generateCollectionCode(ResolvableType elementType, Set<?> set) {
if (set instanceof LinkedHashSet) {
return CodeBlock.of("new $T($L)", LinkedHashSet.class,
generateCollectionOf(set, List.class, elementType));
}
return super.generateCollectionCode(elementType, orderForCodeConsistency(set));
}
private Set<?> orderForCodeConsistency(Set<?> set) {
try {
return new TreeSet<Object>(set);
}
catch (ClassCastException ex) {
// If elements are not comparable, just keep the original set
return set;
}
}
}
/**
* {@link Delegate} for {@link Map} types.
*/
private class MapDelegate implements Delegate {
private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.emptyMap()", Collections.class);
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof Map<?, ?> map) {
return generateMapCode(type, map);
}
return null;
}
private <K, V> CodeBlock generateMapCode(ResolvableType type, Map<K, V> map) {
if (map.isEmpty()) {
return EMPTY_RESULT;
}
ResolvableType keyType = type.as(Map.class).getGeneric(0);
ResolvableType valueType = type.as(Map.class).getGeneric(1);
if (map instanceof LinkedHashMap<?, ?>) {
return generateLinkedHashMapCode(map, keyType, valueType);
}
map = orderForCodeConsistency(map);
boolean useOfEntries = map.size() > 10;
CodeBlock.Builder code = CodeBlock.builder();
code.add("$T" + ((!useOfEntries) ? ".of(" : ".ofEntries("), Map.class);
Iterator<Map.Entry<K, V>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Entry<K, V> entry = iterator.next();
CodeBlock keyCode = BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(entry.getKey(), keyType);
CodeBlock valueCode = BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(entry.getValue(), valueType);
if (!useOfEntries) {
code.add("$L, $L", keyCode, valueCode);
}
else {
code.add("$T.entry($L,$L)", Map.class, keyCode, valueCode);
}
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
return code.build();
}
private <K, V> Map<K, V> orderForCodeConsistency(Map<K, V> map) {
try {
return new TreeMap<>(map);
}
catch (ClassCastException ex) {
// If elements are not comparable, just keep the original map
return map;
}
}
private <K, V> CodeBlock generateLinkedHashMapCode(Map<K, V> map,
ResolvableType keyType, ResolvableType valueType) {
GeneratedMethods generatedMethods = BeanDefinitionPropertyValueCodeGenerator.this.generatedMethods;
GeneratedMethod generatedMethod = generatedMethods.add("getMap", method -> {
method.addAnnotation(AnnotationSpec
.builder(SuppressWarnings.class)
.addMember("value", "{\"rawtypes\", \"unchecked\"}")
.build());
method.returns(Map.class);
method.addStatement("$T map = new $T($L)", Map.class,
LinkedHashMap.class, map.size());
map.forEach((key, value) -> method.addStatement("map.put($L, $L)",
BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(key, keyType),
BeanDefinitionPropertyValueCodeGenerator.this
.generateCodeForElement(value, valueType)));
method.addStatement("return map");
});
return CodeBlock.of("$L()", generatedMethod.getName());
}
}
/**
* {@link Delegate} for {@link BeanReference} types.
*/
private static class BeanReferenceDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof RuntimeBeanReference runtimeBeanReference &&
runtimeBeanReference.getBeanType() != null) {
return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class,
runtimeBeanReference.getBeanType());
}
else if (value instanceof BeanReference beanReference) {
return CodeBlock.of("new $T($S)", RuntimeBeanReference.class,
beanReference.getBeanName());
}
return null;
}
}
/**
* {@link Delegate} for {@link TypedStringValue} types.
*/
private class TypedStringValueDelegate implements Delegate {
@Override
public CodeBlock generateCode(Object value, ResolvableType type) {
if (value instanceof TypedStringValue typedStringValue) {
return generateTypeStringValueCode(typedStringValue);
}
return null;
}
private CodeBlock generateTypeStringValueCode(TypedStringValue typedStringValue) {
String value = typedStringValue.getValue();
if (typedStringValue.hasTargetType()) {
return CodeBlock.of("new $T($S, $L)", TypedStringValue.class, value,
generateCode(typedStringValue.getTargetType()));
}
return generateCode(value);
}
private CodeBlock generateCode(@Nullable Object value) {
return BeanDefinitionPropertyValueCodeGenerator.this.generateCode(value);
}
}
}

View File

@@ -0,0 +1,212 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans.factory.aot;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.aot.generate.GeneratedMethod;
import org.springframework.aot.generate.GeneratedMethods;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates.CollectionDelegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates.MapDelegate;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.ManagedSet;
import org.springframework.javapoet.AnnotationSpec;
import org.springframework.javapoet.CodeBlock;
/**
* Code generator {@link Delegate} for common bean definition property values.
*
* @author Stephane Nicoll
* @since 6.1.2
*/
abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
/**
* Return the {@link Delegate} implementations for common bean definition
* property value types. These are:
* <ul>
* <li>{@link ManagedList},</li>
* <li>{@link ManagedSet},</li>
* <li>{@link ManagedMap},</li>
* <li>{@link LinkedHashMap},</li>
* <li>{@link BeanReference},</li>
* <li>{@link TypedStringValue}.</li>
* </ul>
* When combined with {@linkplain ValueCodeGeneratorDelegates#INSTANCES the
* delegates for common value types}, this should be added first as they have
* special handling for list, set, and map.
*/
public static final List<Delegate> INSTANCES = List.of(
new ManagedListDelegate(),
new ManagedSetDelegate(),
new ManagedMapDelegate(),
new LinkedHashMapDelegate(),
new BeanReferenceDelegate(),
new TypedStringValueDelegate()
);
/**
* {@link Delegate} for {@link ManagedList} types.
*/
private static class ManagedListDelegate extends CollectionDelegate<ManagedList<?>> {
public ManagedListDelegate() {
super(ManagedList.class, CodeBlock.of("new $T()", ManagedList.class));
}
}
/**
* {@link Delegate} for {@link ManagedSet} types.
*/
private static class ManagedSetDelegate extends CollectionDelegate<ManagedSet<?>> {
public ManagedSetDelegate() {
super(ManagedSet.class, CodeBlock.of("new $T()", ManagedSet.class));
}
}
/**
* {@link Delegate} for {@link ManagedMap} types.
*/
private static class ManagedMapDelegate implements Delegate {
private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class);
@Override
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof ManagedMap<?, ?> managedMap) {
return generateManagedMapCode(valueCodeGenerator, managedMap);
}
return null;
}
private <K, V> CodeBlock generateManagedMapCode(ValueCodeGenerator valueCodeGenerator,
ManagedMap<K, V> managedMap) {
if (managedMap.isEmpty()) {
return EMPTY_RESULT;
}
CodeBlock.Builder code = CodeBlock.builder();
code.add("$T.ofEntries(", ManagedMap.class);
Iterator<Entry<K, V>> iterator = managedMap.entrySet().iterator();
while (iterator.hasNext()) {
Entry<?, ?> entry = iterator.next();
code.add("$T.entry($L,$L)", Map.class,
valueCodeGenerator.generateCode(entry.getKey()),
valueCodeGenerator.generateCode(entry.getValue()));
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
return code.build();
}
}
/**
* {@link Delegate} for {@link Map} types.
*/
private static class LinkedHashMapDelegate extends MapDelegate {
@Override
protected CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map<?, ?> map) {
GeneratedMethods generatedMethods = valueCodeGenerator.getGeneratedMethods();
if (map instanceof LinkedHashMap<?, ?> && generatedMethods != null) {
return generateLinkedHashMapCode(valueCodeGenerator, generatedMethods, map);
}
return super.generateMapCode(valueCodeGenerator, map);
}
private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerator,
GeneratedMethods generatedMethods, Map<?, ?> map) {
GeneratedMethod generatedMethod = generatedMethods.add("getMap", method -> {
method.addAnnotation(AnnotationSpec
.builder(SuppressWarnings.class)
.addMember("value", "{\"rawtypes\", \"unchecked\"}")
.build());
method.returns(Map.class);
method.addStatement("$T map = new $T($L)", Map.class,
LinkedHashMap.class, map.size());
map.forEach((key, value) -> method.addStatement("map.put($L, $L)",
valueCodeGenerator.generateCode(key),
valueCodeGenerator.generateCode(value)));
method.addStatement("return map");
});
return CodeBlock.of("$L()", generatedMethod.getName());
}
}
/**
* {@link Delegate} for {@link BeanReference} types.
*/
private static class BeanReferenceDelegate implements Delegate {
@Override
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof RuntimeBeanReference runtimeBeanReference &&
runtimeBeanReference.getBeanType() != null) {
return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class,
runtimeBeanReference.getBeanType());
}
else if (value instanceof BeanReference beanReference) {
return CodeBlock.of("new $T($S)", RuntimeBeanReference.class,
beanReference.getBeanName());
}
return null;
}
}
/**
* {@link Delegate} for {@link TypedStringValue} types.
*/
private static class TypedStringValueDelegate implements Delegate {
@Override
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof TypedStringValue typedStringValue) {
return generateTypeStringValueCode(valueCodeGenerator, typedStringValue);
}
return null;
}
private CodeBlock generateTypeStringValueCode(ValueCodeGenerator valueCodeGenerator, TypedStringValue typedStringValue) {
String value = typedStringValue.getValue();
if (typedStringValue.hasTargetType()) {
return CodeBlock.of("new $T($S, $L)", TypedStringValue.class, value,
valueCodeGenerator.generateCode(typedStringValue.getTargetType()));
}
return valueCodeGenerator.generateCode(value);
}
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.aot.generate.AccessControl;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.generate.MethodReference;
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
@@ -51,6 +52,8 @@ import org.springframework.util.function.SingletonSupplier;
*/
class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragments {
private static final ValueCodeGenerator valueCodeGenerator = ValueCodeGenerator.withDefaults();
private final BeanRegistrationsCode beanRegistrationsCode;
private final RegisteredBean registeredBean;
@@ -147,9 +150,9 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
private CodeBlock generateBeanTypeCode(ResolvableType beanType) {
if (!beanType.hasGenerics()) {
return CodeBlock.of("$T.class", ClassUtils.getUserClass(beanType.toClass()));
return valueCodeGenerator.generateCode(ClassUtils.getUserClass(beanType.toClass()));
}
return ResolvableTypeCodeGenerator.generateCode(beanType);
return valueCodeGenerator.generateCode(beanType);
}
private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class<?> beanClass) {

View File

@@ -1,69 +0,0 @@
/*
* 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.aot;
import java.util.Arrays;
import org.springframework.core.ResolvableType;
import org.springframework.javapoet.CodeBlock;
import org.springframework.util.ClassUtils;
/**
* Internal code generator used to support {@link ResolvableType}.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @since 6.0
*/
final class ResolvableTypeCodeGenerator {
private ResolvableTypeCodeGenerator() {
}
public static CodeBlock generateCode(ResolvableType resolvableType) {
return generateCode(resolvableType, false);
}
private static CodeBlock generateCode(ResolvableType resolvableType, boolean allowClassResult) {
if (ResolvableType.NONE.equals(resolvableType)) {
return CodeBlock.of("$T.NONE", ResolvableType.class);
}
Class<?> type = ClassUtils.getUserClass(resolvableType.toClass());
if (resolvableType.hasGenerics() && !resolvableType.hasUnresolvableGenerics()) {
return generateCodeWithGenerics(resolvableType, type);
}
if (allowClassResult) {
return CodeBlock.of("$T.class", type);
}
return CodeBlock.of("$T.forClass($T.class)", ResolvableType.class, type);
}
private static CodeBlock generateCodeWithGenerics(ResolvableType target, Class<?> type) {
ResolvableType[] generics = target.getGenerics();
boolean hasNoNestedGenerics = Arrays.stream(generics).noneMatch(ResolvableType::hasGenerics);
CodeBlock.Builder code = CodeBlock.builder();
code.add("$T.forClassWithGenerics($T.class", ResolvableType.class, type);
for (ResolvableType generic : generics) {
code.add(", $L", generateCode(generic, hasNoNestedGenerics));
}
code.add(")");
return code.build();
}
}

View File

@@ -36,6 +36,8 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.GeneratedClass;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanNameReference;
@@ -47,33 +49,38 @@ import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuild
import org.springframework.core.ResolvableType;
import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.core.testfixture.aot.generate.value.EnumWithClassBody;
import org.springframework.core.testfixture.aot.generate.value.ExampleClass;
import org.springframework.core.testfixture.aot.generate.value.ExampleClass$$GeneratedBy;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.ParameterizedTypeName;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link BeanDefinitionPropertyValueCodeGenerator}.
* Tests for {@link BeanDefinitionPropertyValueCodeGeneratorDelegates}. This
* also tests that code generated by {@link ValueCodeGeneratorDelegates}
* compiles.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Sebastien Deleuze
* @since 6.0
* @see BeanDefinitionPropertyValueCodeGeneratorTests
*/
class BeanDefinitionPropertyValueCodeGeneratorTests {
class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests {
private static BeanDefinitionPropertyValueCodeGenerator createPropertyValuesCodeGenerator(GeneratedClass generatedClass) {
return new BeanDefinitionPropertyValueCodeGenerator(generatedClass.getMethods(), null);
private static ValueCodeGenerator createValueCodeGenerator(GeneratedClass generatedClass) {
return ValueCodeGenerator.with(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES)
.add(ValueCodeGeneratorDelegates.INSTANCES)
.scoped(generatedClass.getMethods());
}
private void compile(Object value, BiConsumer<Object, Compiled> result) {
TestGenerationContext generationContext = new TestGenerationContext();
DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder();
GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder);
CodeBlock generatedCode = createPropertyValuesCodeGenerator(generatedClass).generateCode(value);
CodeBlock generatedCode = createValueCodeGenerator(generatedClass).generateCode(value);
typeBuilder.set(type -> {
type.addModifiers(Modifier.PUBLIC);
type.addSuperinterface(
@@ -101,90 +108,72 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test
void generateWhenBoolean() {
compile(true, (instance, compiled) -> {
assertThat(instance).isEqualTo(Boolean.TRUE);
assertThat(compiled.getSourceFile()).contains("true");
});
compile(true, (instance, compiled) ->
assertThat(instance).isEqualTo(Boolean.TRUE));
}
@Test
void generateWhenByte() {
compile((byte) 2, (instance, compiled) -> {
assertThat(instance).isEqualTo((byte) 2);
assertThat(compiled.getSourceFile()).contains("(byte) 2");
});
compile((byte) 2, (instance, compiled) ->
assertThat(instance).isEqualTo((byte) 2));
}
@Test
void generateWhenShort() {
compile((short) 3, (instance, compiled) -> {
assertThat(instance).isEqualTo((short) 3);
assertThat(compiled.getSourceFile()).contains("(short) 3");
});
compile((short) 3, (instance, compiled) ->
assertThat(instance).isEqualTo((short) 3));
}
@Test
void generateWhenInt() {
compile(4, (instance, compiled) -> {
assertThat(instance).isEqualTo(4);
assertThat(compiled.getSourceFile()).contains("return 4;");
});
compile(4, (instance, compiled) ->
assertThat(instance).isEqualTo(4));
}
@Test
void generateWhenLong() {
compile(5L, (instance, compiled) -> {
assertThat(instance).isEqualTo(5L);
assertThat(compiled.getSourceFile()).contains("5L");
});
compile(5L, (instance, compiled) ->
assertThat(instance).isEqualTo(5L));
}
@Test
void generateWhenFloat() {
compile(0.1F, (instance, compiled) -> {
assertThat(instance).isEqualTo(0.1F);
assertThat(compiled.getSourceFile()).contains("0.1F");
});
compile(0.1F, (instance, compiled) ->
assertThat(instance).isEqualTo(0.1F));
}
@Test
void generateWhenDouble() {
compile(0.2, (instance, compiled) -> {
assertThat(instance).isEqualTo(0.2);
assertThat(compiled.getSourceFile()).contains("(double) 0.2");
});
compile(0.2, (instance, compiled) ->
assertThat(instance).isEqualTo(0.2));
}
@Test
void generateWhenChar() {
compile('a', (instance, compiled) -> {
assertThat(instance).isEqualTo('a');
assertThat(compiled.getSourceFile()).contains("'a'");
});
compile('a', (instance, compiled) ->
assertThat(instance).isEqualTo('a'));
}
@Test
void generateWhenSimpleEscapedCharReturnsEscaped() {
testEscaped('\b', "'\\b'");
testEscaped('\t', "'\\t'");
testEscaped('\n', "'\\n'");
testEscaped('\f', "'\\f'");
testEscaped('\r', "'\\r'");
testEscaped('\"', "'\"'");
testEscaped('\'', "'\\''");
testEscaped('\\', "'\\\\'");
testEscaped('\b');
testEscaped('\t');
testEscaped('\n');
testEscaped('\f');
testEscaped('\r');
testEscaped('\"');
testEscaped('\'');
testEscaped('\\');
}
@Test
void generatedWhenUnicodeEscapedCharReturnsEscaped() {
testEscaped('\u007f', "'\\u007f'");
testEscaped('\u007f');
}
private void testEscaped(char value, String expectedSourceContent) {
compile(value, (instance, compiled) -> {
assertThat(instance).isEqualTo(value);
assertThat(compiled.getSourceFile()).contains(expectedSourceContent);
});
private void testEscaped(char value) {
compile(value, (instance, compiled) ->
assertThat(instance).isEqualTo(value));
}
}
@@ -194,10 +183,8 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test
void generateWhenString() {
compile("test\n", (instance, compiled) -> {
assertThat(instance).isEqualTo("test\n");
assertThat(compiled.getSourceFile()).contains("\n");
});
compile("test\n", (instance, compiled) ->
assertThat(instance).isEqualTo("test\n"));
}
}
@@ -207,10 +194,8 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test
void generateWhenCharset() {
compile(StandardCharsets.UTF_8, (instance, compiled) -> {
assertThat(instance).isEqualTo(Charset.forName("UTF-8"));
assertThat(compiled.getSourceFile()).contains("\"UTF-8\"");
});
compile(StandardCharsets.UTF_8, (instance, compiled) ->
assertThat(instance).isEqualTo(Charset.forName("UTF-8")));
}
}
@@ -220,18 +205,14 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test
void generateWhenEnum() {
compile(ChronoUnit.DAYS, (instance, compiled) -> {
assertThat(instance).isEqualTo(ChronoUnit.DAYS);
assertThat(compiled.getSourceFile()).contains("ChronoUnit.DAYS");
});
compile(ChronoUnit.DAYS, (instance, compiled) ->
assertThat(instance).isEqualTo(ChronoUnit.DAYS));
}
@Test
void generateWhenEnumWithClassBody() {
compile(EnumWithClassBody.TWO, (instance, compiled) -> {
assertThat(instance).isEqualTo(EnumWithClassBody.TWO);
assertThat(compiled.getSourceFile()).contains("EnumWithClassBody.TWO");
});
compile(EnumWithClassBody.TWO, (instance, compiled) ->
assertThat(instance).isEqualTo(EnumWithClassBody.TWO));
}
}
@@ -266,18 +247,16 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test
void generateWhenNoneResolvableType() {
ResolvableType resolvableType = ResolvableType.NONE;
compile(resolvableType, (instance, compiled) -> {
assertThat(instance).isEqualTo(resolvableType);
assertThat(compiled.getSourceFile()).contains("ResolvableType.NONE");
});
compile(resolvableType, (instance, compiled) ->
assertThat(instance).isEqualTo(resolvableType));
}
@Test
void generateWhenGenericResolvableType() {
ResolvableType resolvableType = ResolvableType
.forClassWithGenerics(List.class, String.class);
compile(resolvableType, (instance, compiled) -> assertThat(instance)
.isEqualTo(resolvableType));
compile(resolvableType, (instance, compiled) ->
assertThat(instance).isEqualTo(resolvableType));
}
@Test
@@ -298,28 +277,22 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test
void generateWhenPrimitiveArray() {
byte[] bytes = { 0, 1, 2 };
compile(bytes, (instance, compiler) -> {
assertThat(instance).isEqualTo(bytes);
assertThat(compiler.getSourceFile()).contains("new byte[]");
});
compile(bytes, (instance, compiler) ->
assertThat(instance).isEqualTo(bytes));
}
@Test
void generateWhenWrapperArray() {
Byte[] bytes = { 0, 1, 2 };
compile(bytes, (instance, compiler) -> {
assertThat(instance).isEqualTo(bytes);
assertThat(compiler.getSourceFile()).contains("new Byte[]");
});
compile(bytes, (instance, compiler) ->
assertThat(instance).isEqualTo(bytes));
}
@Test
void generateWhenClassArray() {
Class<?>[] classes = new Class<?>[] { InputStream.class, OutputStream.class };
compile(classes, (instance, compiler) -> {
assertThat(instance).isEqualTo(classes);
assertThat(compiler.getSourceFile()).contains("new Class[]");
});
compile(classes, (instance, compiler) ->
assertThat(instance).isEqualTo(classes));
}
}
@@ -402,10 +375,7 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test
void generateWhenEmptyList() {
List<String> list = List.of();
compile(list, (instance, compiler) -> {
assertThat(instance).isEqualTo(list);
assertThat(compiler.getSourceFile()).contains("Collections.emptyList();");
});
compile(list, (instance, compiler) -> assertThat(instance).isEqualTo(list));
}
}
@@ -423,20 +393,14 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test
void generateWhenEmptySet() {
Set<String> set = Set.of();
compile(set, (instance, compiler) -> {
assertThat(instance).isEqualTo(set);
assertThat(compiler.getSourceFile()).contains("Collections.emptySet();");
});
compile(set, (instance, compiler) -> assertThat(instance).isEqualTo(set));
}
@Test
void generateWhenLinkedHashSet() {
Set<String> set = new LinkedHashSet<>(List.of("a", "b", "c"));
compile(set, (instance, compiler) -> {
assertThat(instance).isEqualTo(set).isInstanceOf(LinkedHashSet.class);
assertThat(compiler.getSourceFile())
.contains("new LinkedHashSet(List.of(");
});
compile(set, (instance, compiler) ->
assertThat(instance).isEqualTo(set).isInstanceOf(LinkedHashSet.class));
}
@Test
@@ -453,10 +417,8 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
@Test
void generateWhenSmallMap() {
Map<String, String> map = Map.of("k1", "v1", "k2", "v2");
compile(map, (instance, compiler) -> {
assertThat(instance).isEqualTo(map);
assertThat(compiler.getSourceFile()).contains("Map.of(");
});
compile(map, (instance, compiler) ->
assertThat(instance).isEqualTo(map));
}
@Test
@@ -465,10 +427,7 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
for (int i = 1; i <= 11; i++) {
map.put("k" + i, "v" + i);
}
compile(map, (instance, compiler) -> {
assertThat(instance).isEqualTo(map);
assertThat(compiler.getSourceFile()).contains("Map.ofEntries(");
});
compile(map, (instance, compiler) -> assertThat(instance).isEqualTo(map));
}
@Test
@@ -518,47 +477,4 @@ class BeanDefinitionPropertyValueCodeGeneratorTests {
}
@Nested
static class ExceptionTests {
@Test
void generateWhenUnsupportedDataTypeThrowsException() {
SampleValue sampleValue = new SampleValue("one");
assertThatIllegalArgumentException().isThrownBy(() -> generateCode(sampleValue))
.withMessageContaining("Failed to generate code for")
.withMessageContaining(sampleValue.toString())
.withMessageContaining(SampleValue.class.getName())
.havingCause()
.withMessageContaining("Code generation does not support")
.withMessageContaining(SampleValue.class.getName());
}
@Test
void generateWhenListOfUnsupportedElement() {
SampleValue one = new SampleValue("one");
SampleValue two = new SampleValue("two");
List<SampleValue> list = List.of(one, two);
assertThatIllegalArgumentException().isThrownBy(() -> generateCode(list))
.withMessageContaining("Failed to generate code for")
.withMessageContaining(list.toString())
.withMessageContaining(list.getClass().getName())
.havingCause()
.withMessageContaining("Failed to generate code for")
.withMessageContaining(one.toString())
.withMessageContaining("?")
.havingCause()
.withMessageContaining("Code generation does not support ?");
}
private void generateCode(Object value) {
TestGenerationContext context = new TestGenerationContext();
GeneratedClass generatedClass = context.getGeneratedClasses()
.addForFeature("Test", type -> {});
createPropertyValuesCodeGenerator(generatedClass).generateCode(value);
}
record SampleValue(String name) {}
}
}

View File

@@ -1,42 +0,0 @@
/*
* 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.aot;
/**
* Test enum that include a class body.
*
* @author Phillip Webb
*/
public enum EnumWithClassBody {
/**
* No class body.
*/
ONE,
/**
* With class body.
*/
TWO {
@Override
public String toString() {
return "2";
}
}
}

View File

@@ -1,26 +0,0 @@
/*
* 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.aot;
/**
* Fake CGLIB generated class.
*
* @author Phillip Webb
*/
class ExampleClass$$GeneratedBy extends ExampleClass {
}

View File

@@ -1,26 +0,0 @@
/*
* 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.aot;
/**
* Public example class used for test.
*
* @author Phillip Webb
*/
public class ExampleClass {
}