Add MethodReference support

Add a `MethodReference` class which can be used to refer to a
static or instance method.

See gh-28414
This commit is contained in:
Phillip Webb 2022-04-13 18:15:41 -07:00
parent 1816c77c51
commit c5c68a4662
2 changed files with 463 additions and 0 deletions

View File

@ -0,0 +1,237 @@
/*
* 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.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A reference to a static or instance method.
*
* @author Phillip Webb
* @since 6.0
*/
public final class MethodReference {
private final Kind kind;
private final ClassName declaringClass;
private final String methodName;
private MethodReference(Kind kind, @Nullable ClassName declaringClass,
String methodName) {
this.kind = kind;
this.declaringClass = declaringClass;
this.methodName = methodName;
}
/**
* Create a new method reference that refers to the given instance method.
* @param methodName the method name
* @return a new {@link MethodReference} instance
*/
public static MethodReference of(String methodName) {
Assert.hasLength(methodName, "'methodName' must not be empty");
return new MethodReference(Kind.INSTANCE, null, methodName);
}
/**
* Create a new method reference that refers to the given instance method.
* @param declaringClass the declaring class
* @param methodName the method name
* @return a new {@link MethodReference} instance
*/
public static MethodReference of(Class<?> declaringClass, String methodName) {
Assert.notNull(declaringClass, "'declaringClass' must not be null");
Assert.hasLength(methodName, "'methodName' must not be empty");
return new MethodReference(Kind.INSTANCE, ClassName.get(declaringClass),
methodName);
}
/**
* Create a new method reference that refers to the given instance method.
* @param declaringClass the declaring class
* @param methodName the method name
* @return a new {@link MethodReference} instance
*/
public static MethodReference of(ClassName declaringClass, String methodName) {
Assert.notNull(declaringClass, "'declaringClass' must not be null");
Assert.hasLength(methodName, "'methodName' must not be empty");
return new MethodReference(Kind.INSTANCE, declaringClass, methodName);
}
/**
* Create a new method reference that refers to the given static method.
* @param declaringClass the declaring class
* @param methodName the method name
* @return a new {@link MethodReference} instance
*/
public static MethodReference ofStatic(Class<?> declaringClass, String methodName) {
Assert.notNull(declaringClass, "'declaringClass' must not be null");
Assert.hasLength(methodName, "'methodName' must not be empty");
return new MethodReference(Kind.STATIC, ClassName.get(declaringClass),
methodName);
}
/**
* Create a new method reference that refers to the given static method.
* @param declaringClass the declaring class
* @param methodName the method name
* @return a new {@link MethodReference} instance
*/
public static MethodReference ofStatic(ClassName declaringClass, String methodName) {
Assert.notNull(declaringClass, "'declaringClass' must not be null");
Assert.hasLength(methodName, "'methodName' must not be empty");
return new MethodReference(Kind.STATIC, declaringClass, methodName);
}
/**
* Return the referenced declaring class.
* @return the declaring class
*/
public ClassName getDeclaringClass() {
return this.declaringClass;
}
/**
* Return the referenced method name.
* @return the method name
*/
public String getMethodName() {
return this.methodName;
}
/**
* Return this method reference as a {@link CodeBlock}. If the reference is
* to an instance method then {@code this::<method name>} will be returned.
* @return a code block for the method reference.
* @see #toCodeBlock(String)
*/
public CodeBlock toCodeBlock() {
return toCodeBlock(null);
}
/**
* Return this method reference as a {@link CodeBlock}. If the reference is
* to an instance method and {@code instanceVariable} is {@code null} then
* {@code this::<method name>} will be returned. No {@code instanceVariable}
* can be specified for static method references.
* @param instanceVariable the instance variable or {@code null}
* @return a code block for the method reference.
* @see #toCodeBlock(String)
*/
public CodeBlock toCodeBlock(@Nullable String instanceVariable) {
return switch (this.kind) {
case INSTANCE -> toCodeBlockForInstance(instanceVariable);
case STATIC -> toCodeBlockForStatic(instanceVariable);
};
}
private CodeBlock toCodeBlockForInstance(String instanceVariable) {
instanceVariable = (instanceVariable != null) ? instanceVariable : "this";
return CodeBlock.of("$L::$L", instanceVariable, this.methodName);
}
private CodeBlock toCodeBlockForStatic(@Nullable String instanceVariable) {
Assert.isTrue(instanceVariable == null,
"'instanceVariable' must be null for static method references");
return CodeBlock.of("$T::$L", this.declaringClass, this.methodName);
}
/**
* Return this method reference as an invocation {@link CodeBlock}.
* @param arguments the method arguments
* @return a code back to invoke the method
*/
public CodeBlock toInvokeCodeBlock(CodeBlock... arguments) {
return toInvokeCodeBlock(null, arguments);
}
/**
* Return this method reference as an invocation {@link CodeBlock}.
* @param instanceVariable the instance variable or {@code null}
* @param arguments the method arguments
* @return a code back to invoke the method
*/
public CodeBlock toInvokeCodeBlock(@Nullable String instanceVariable,
CodeBlock... arguments) {
return switch (this.kind) {
case INSTANCE -> toInvokeCodeBlockForInstance(instanceVariable, arguments);
case STATIC -> toInvokeCodeBlockForStatic(instanceVariable, arguments);
};
}
private CodeBlock toInvokeCodeBlockForInstance(@Nullable String instanceVariable,
CodeBlock[] arguments) {
CodeBlock.Builder builder = CodeBlock.builder();
if (instanceVariable != null) {
builder.add("$L.", instanceVariable);
}
else if (this.declaringClass != null) {
builder.add("new $T().", this.declaringClass);
}
builder.add("$L", this.methodName);
addArguments(builder, arguments);
return builder.build();
}
private CodeBlock toInvokeCodeBlockForStatic(@Nullable String instanceVariable,
CodeBlock[] arguments) {
Assert.isTrue(instanceVariable == null,
"'instanceVariable' must be null for static method references");
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("$T.$L", this.declaringClass, this.methodName);
addArguments(builder, arguments);
return builder.build();
}
private void addArguments(CodeBlock.Builder builder, CodeBlock[] arguments) {
builder.add("(");
for (int i = 0; i < arguments.length; i++) {
if (i != 0) {
builder.add(", ");
}
builder.add(arguments[i]);
}
builder.add(")");
}
@Override
public String toString() {
return switch (this.kind) {
case INSTANCE -> ((this.declaringClass != null) ? "<" + this.declaringClass + ">"
: "<instance>") + "::" + this.methodName;
case STATIC -> this.declaringClass + "::" + this.methodName;
};
}
private enum Kind {
INSTANCE, STATIC
}
}

View File

@ -0,0 +1,226 @@
/*
* 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");
}
}