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

@@ -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 {
}