Allow MethodReference to define a more flexible signature

This commit moves MethodReference to an interface with a default
implementation that relies on a MethodSpec. Such an arrangement avoid
the need of specifying attributes of the method such as whether it is
static or not.

The resolution of the invocation block now takes an
ArgumentCodeGenerator rather than the raw arguments. Doing so gives
the opportunity to create more flexible signatures.

See gh-29005
This commit is contained in:
Stephane Nicoll
2022-08-24 07:53:38 +02:00
parent 8a4a89b9d9
commit ae706f3954
16 changed files with 469 additions and 428 deletions

View File

@@ -0,0 +1,199 @@
/*
* 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.generate;
import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.MethodSpec.Builder;
import org.springframework.javapoet.TypeName;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link DefaultMethodReference}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
class DefaultMethodReferenceTests {
private static final String EXPECTED_STATIC = "org.springframework.aot.generate.DefaultMethodReferenceTests::someMethod";
private static final String EXPECTED_ANONYMOUS_INSTANCE = "<instance>::someMethod";
private static final String EXPECTED_DECLARED_INSTANCE = "<org.springframework.aot.generate.DefaultMethodReferenceTests>::someMethod";
private static final ClassName TEST_CLASS_NAME = ClassName.get("com.example", "Test");
private static final ClassName INITIALIZER_CLASS_NAME = ClassName.get("com.example", "Initializer");
@Test
void createWithStringCreatesMethodReference() {
MethodSpec method = createTestMethod("someMethod", new TypeName[0]);
MethodReference reference = new DefaultMethodReference(method, null);
assertThat(reference).hasToString(EXPECTED_ANONYMOUS_INSTANCE);
}
@Test
void createWithClassNameAndStringCreateMethodReference() {
ClassName declaringClass = ClassName.get(DefaultMethodReferenceTests.class);
MethodReference reference = createMethodReference("someMethod", new TypeName[0], declaringClass);
assertThat(reference).hasToString(EXPECTED_DECLARED_INSTANCE);
}
@Test
void createWithStaticAndClassAndStringCreatesMethodReference() {
ClassName declaringClass = ClassName.get(DefaultMethodReferenceTests.class);
MethodReference reference = createStaticMethodReference("someMethod", declaringClass);
assertThat(reference).hasToString(EXPECTED_STATIC);
}
@Test
void toCodeBlock() {
assertThat(createLocalMethodReference("methodName").toCodeBlock())
.isEqualTo(CodeBlock.of("this::methodName"));
}
@Test
void toCodeBlockWithStaticMethod() {
assertThat(createStaticMethodReference("methodName", TEST_CLASS_NAME).toCodeBlock())
.isEqualTo(CodeBlock.of("com.example.Test::methodName"));
}
@Test
void toCodeBlockWithStaticMethodRequiresDeclaringClass() {
MethodSpec method = createTestMethod("methodName", new TypeName[0], Modifier.STATIC);
MethodReference methodReference = new DefaultMethodReference(method, null);
assertThatIllegalArgumentException().isThrownBy(methodReference::toCodeBlock)
.withMessage("static method reference must define a declaring class");
}
@Test
void toInvokeCodeBlockWithNullDeclaringClassAndTargetClass() {
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
MethodReference methodReference = new DefaultMethodReference(method, null);
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), TEST_CLASS_NAME))
.isEqualTo(CodeBlock.of("methodName()"));
}
@Test
void toInvokeCodeBlockWithNullDeclaringClassAndNullTargetClass() {
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
MethodReference methodReference = new DefaultMethodReference(method, null);
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none()))
.isEqualTo(CodeBlock.of("methodName()"));
}
@Test
void toInvokeCodeBlockWithDeclaringClassAndNullTargetClass() {
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
MethodReference methodReference = new DefaultMethodReference(method, TEST_CLASS_NAME);
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none()))
.isEqualTo(CodeBlock.of("new com.example.Test().methodName()"));
}
@Test
void toInvokeCodeBlockWithMatchingTargetClass() {
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
MethodReference methodReference = new DefaultMethodReference(method, TEST_CLASS_NAME);
CodeBlock invocation = methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), TEST_CLASS_NAME);
// Assume com.example.Test is in a `test` variable.
assertThat(CodeBlock.of("$L.$L", "test", invocation)).isEqualTo(CodeBlock.of("test.methodName()"));
}
@Test
void toInvokeCodeBlockWithNonMatchingDeclaringClass() {
MethodSpec method = createTestMethod("methodName", new TypeName[0]);
MethodReference methodReference = new DefaultMethodReference(method, TEST_CLASS_NAME);
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), INITIALIZER_CLASS_NAME))
.isEqualTo(CodeBlock.of("new com.example.Test().methodName()"));
}
@Test
void toInvokeCodeBlockWithMatchingArg() {
MethodReference methodReference = createLocalMethodReference("methodName", ClassName.get(String.class));
ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator.of(String.class, "stringArg");
assertThat(methodReference.toInvokeCodeBlock(argCodeGenerator))
.isEqualTo(CodeBlock.of("methodName(stringArg)"));
}
@Test
void toInvokeCodeBlockWithMatchingArgs() {
MethodReference methodReference = createLocalMethodReference("methodName",
ClassName.get(Integer.class), ClassName.get(String.class));
ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator.of(String.class, "stringArg")
.and(Integer.class, "integerArg");
assertThat(methodReference.toInvokeCodeBlock(argCodeGenerator))
.isEqualTo(CodeBlock.of("methodName(integerArg, stringArg)"));
}
@Test
void toInvokeCodeBlockWithNonMatchingArg() {
MethodReference methodReference = createLocalMethodReference("methodName",
ClassName.get(Integer.class), ClassName.get(String.class));
ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator.of(Integer.class, "integerArg");
assertThatIllegalArgumentException().isThrownBy(() -> methodReference.toInvokeCodeBlock(argCodeGenerator))
.withMessageContaining("parameter 1 of type java.lang.String is not supported");
}
@Test
void toInvokeCodeBlockWithStaticMethodAndMatchingDeclaringClass() {
MethodReference methodReference = createStaticMethodReference("methodName", TEST_CLASS_NAME);
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), TEST_CLASS_NAME))
.isEqualTo(CodeBlock.of("methodName()"));
}
@Test
void toInvokeCodeBlockWithStaticMethodAndSeparateDeclaringClass() {
MethodReference methodReference = createStaticMethodReference("methodName", TEST_CLASS_NAME);
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), INITIALIZER_CLASS_NAME))
.isEqualTo(CodeBlock.of("com.example.Test.methodName()"));
}
private MethodReference createLocalMethodReference(String name, TypeName... argumentTypes) {
return createMethodReference(name, argumentTypes, null);
}
private MethodReference createMethodReference(String name, TypeName[] argumentTypes, @Nullable ClassName declaringClass) {
MethodSpec method = createTestMethod(name, argumentTypes);
return new DefaultMethodReference(method, declaringClass);
}
private MethodReference createStaticMethodReference(String name, ClassName declaringClass, TypeName... argumentTypes) {
MethodSpec method = createTestMethod(name, argumentTypes, Modifier.STATIC);
return new DefaultMethodReference(method, declaringClass);
}
private MethodSpec createTestMethod(String name, TypeName[] argumentTypes, Modifier... modifiers) {
Builder method = MethodSpec.methodBuilder(name);
for (int i = 0; i < argumentTypes.length; i++) {
method.addParameter(argumentTypes[i], "args" + i);
}
method.addModifiers(modifiers);
return method.build();
}
}

View File

@@ -22,6 +22,7 @@ import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec;
@@ -67,8 +68,8 @@ class GeneratedMethodTests {
GeneratedMethod generatedMethod = create(emptyMethod);
MethodReference methodReference = generatedMethod.toMethodReference();
assertThat(methodReference).isNotNull();
assertThat(methodReference.toInvokeCodeBlock("test"))
.isEqualTo(CodeBlock.of("test.spring()"));
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), TEST_CLASS_NAME))
.isEqualTo(CodeBlock.of("spring()"));
}
@Test
@@ -76,7 +77,8 @@ class GeneratedMethodTests {
GeneratedMethod generatedMethod = create(method -> method.addModifiers(Modifier.STATIC));
MethodReference methodReference = generatedMethod.toMethodReference();
assertThat(methodReference).isNotNull();
assertThat(methodReference.toInvokeCodeBlock())
ClassName anotherDeclaringClass = ClassName.get("com.example", "Another");
assertThat(methodReference.toInvokeCodeBlock(ArgumentCodeGenerator.none(), anotherDeclaringClass))
.isEqualTo(CodeBlock.of("com.example.Test.spring()"));
}

View File

@@ -1,226 +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.aot.generate;
import org.junit.jupiter.api.Test;
import org.springframework.javapoet.ClassName;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link MethodReference}.
*
* @author Phillip Webb
*/
class MethodReferenceTests {
private static final String EXPECTED_STATIC = "org.springframework.aot.generate.MethodReferenceTests::someMethod";
private static final String EXPECTED_ANONYMOUS_INSTANCE = "<instance>::someMethod";
private static final String EXPECTED_DECLARED_INSTANCE = "<org.springframework.aot.generate.MethodReferenceTests>::someMethod";
@Test
void ofWithStringWhenMethodNameIsNullThrowsException() {
String methodName = null;
assertThatIllegalArgumentException()
.isThrownBy(() -> MethodReference.of(methodName))
.withMessage("'methodName' must not be empty");
}
@Test
void ofWithStringCreatesMethodReference() {
String methodName = "someMethod";
MethodReference reference = MethodReference.of(methodName);
assertThat(reference).hasToString(EXPECTED_ANONYMOUS_INSTANCE);
}
@Test
void ofWithClassAndStringWhenDeclaringClassIsNullThrowsException() {
Class<?> declaringClass = null;
String methodName = "someMethod";
assertThatIllegalArgumentException()
.isThrownBy(() -> MethodReference.of(declaringClass, methodName))
.withMessage("'declaringClass' must not be null");
}
@Test
void ofWithClassAndStringWhenMethodNameIsNullThrowsException() {
Class<?> declaringClass = MethodReferenceTests.class;
String methodName = null;
assertThatIllegalArgumentException()
.isThrownBy(() -> MethodReference.of(declaringClass, methodName))
.withMessage("'methodName' must not be empty");
}
@Test
void ofWithClassAndStringCreatesMethodReference() {
Class<?> declaringClass = MethodReferenceTests.class;
String methodName = "someMethod";
MethodReference reference = MethodReference.of(declaringClass, methodName);
assertThat(reference).hasToString(EXPECTED_DECLARED_INSTANCE);
}
@Test
void ofWithClassNameAndStringWhenDeclaringClassIsNullThrowsException() {
ClassName declaringClass = null;
String methodName = "someMethod";
assertThatIllegalArgumentException()
.isThrownBy(() -> MethodReference.of(declaringClass, methodName))
.withMessage("'declaringClass' must not be null");
}
@Test
void ofWithClassNameAndStringWhenMethodNameIsNullThrowsException() {
ClassName declaringClass = ClassName.get(MethodReferenceTests.class);
String methodName = null;
assertThatIllegalArgumentException()
.isThrownBy(() -> MethodReference.of(declaringClass, methodName))
.withMessage("'methodName' must not be empty");
}
@Test
void ofWithClassNameAndStringCreateMethodReference() {
ClassName declaringClass = ClassName.get(MethodReferenceTests.class);
String methodName = "someMethod";
MethodReference reference = MethodReference.of(declaringClass, methodName);
assertThat(reference).hasToString(EXPECTED_DECLARED_INSTANCE);
}
@Test
void ofStaticWithClassAndStringWhenDeclaringClassIsNullThrowsException() {
Class<?> declaringClass = null;
String methodName = "someMethod";
assertThatIllegalArgumentException()
.isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName))
.withMessage("'declaringClass' must not be null");
}
@Test
void ofStaticWithClassAndStringWhenMethodNameIsEmptyThrowsException() {
Class<?> declaringClass = MethodReferenceTests.class;
String methodName = null;
assertThatIllegalArgumentException()
.isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName))
.withMessage("'methodName' must not be empty");
}
@Test
void ofStaticWithClassAndStringCreatesMethodReference() {
Class<?> declaringClass = MethodReferenceTests.class;
String methodName = "someMethod";
MethodReference reference = MethodReference.ofStatic(declaringClass, methodName);
assertThat(reference).hasToString(EXPECTED_STATIC);
}
@Test
void ofStaticWithClassNameAndGeneratedMethodNameWhenDeclaringClassIsNullThrowsException() {
ClassName declaringClass = null;
String methodName = "someMethod";
assertThatIllegalArgumentException()
.isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName))
.withMessage("'declaringClass' must not be null");
}
@Test
void ofStaticWithClassNameAndGeneratedMethodNameWhenMethodNameIsEmptyThrowsException() {
ClassName declaringClass = ClassName.get(MethodReferenceTests.class);
String methodName = null;
assertThatIllegalArgumentException()
.isThrownBy(() -> MethodReference.ofStatic(declaringClass, methodName))
.withMessage("'methodName' must not be empty");
}
@Test
void ofStaticWithClassNameAndGeneratedMethodNameCreatesMethodReference() {
ClassName declaringClass = ClassName.get(MethodReferenceTests.class);
String methodName = "someMethod";
MethodReference reference = MethodReference.ofStatic(declaringClass, methodName);
assertThat(reference).hasToString(EXPECTED_STATIC);
}
@Test
void toCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNull() {
MethodReference reference = MethodReference.of("someMethod");
assertThat(reference.toCodeBlock(null)).hasToString("this::someMethod");
}
@Test
void toCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNotNull() {
MethodReference reference = MethodReference.of("someMethod");
assertThat(reference.toCodeBlock("myInstance"))
.hasToString("myInstance::someMethod");
}
@Test
void toCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNull() {
MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class,
"someMethod");
assertThat(reference.toCodeBlock(null)).hasToString(EXPECTED_STATIC);
}
@Test
void toCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNotNullThrowsException() {
MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class,
"someMethod");
assertThatIllegalArgumentException()
.isThrownBy(() -> reference.toCodeBlock("myInstance")).withMessage(
"'instanceVariable' must be null for static method references");
}
@Test
void toInvokeCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNull() {
MethodReference reference = MethodReference.of("someMethod");
assertThat(reference.toInvokeCodeBlock()).hasToString("someMethod()");
}
@Test
void toInvokeCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNullAndHasDecalredClass() {
MethodReference reference = MethodReference.of(MethodReferenceTests.class,
"someMethod");
assertThat(reference.toInvokeCodeBlock()).hasToString(
"new org.springframework.aot.generate.MethodReferenceTests().someMethod()");
}
@Test
void toInvokeCodeBlockWhenInstanceMethodReferenceAndInstanceVariableIsNotNull() {
MethodReference reference = MethodReference.of("someMethod");
assertThat(reference.toInvokeCodeBlock("myInstance"))
.hasToString("myInstance.someMethod()");
}
@Test
void toInvokeCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNull() {
MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class,
"someMethod");
assertThat(reference.toInvokeCodeBlock()).hasToString(
"org.springframework.aot.generate.MethodReferenceTests.someMethod()");
}
@Test
void toInvokeCodeBlockWhenStaticMethodReferenceAndInstanceVariableIsNotNullThrowsException() {
MethodReference reference = MethodReference.ofStatic(MethodReferenceTests.class,
"someMethod");
assertThatIllegalArgumentException()
.isThrownBy(() -> reference.toInvokeCodeBlock("myInstance")).withMessage(
"'instanceVariable' must be null for static method references");
}
}