Browse Source

Add JavaPoet enhancements.

Add opinionated builders for return statements and invocations, add introspection type for MethodReturns to reduce checks for e.g. `Optional` and `void` and utilities to construct type names.

Closes: #3357
pull/3363/head
Mark Paluch 3 months ago committed by Christoph Strobl
parent
commit
676e820a1a
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 973
      src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java
  2. 106
      src/main/java/org/springframework/data/javapoet/TypeNames.java
  3. 5
      src/main/java/org/springframework/data/javapoet/package-info.java
  4. 16
      src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
  5. 196
      src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java
  6. 3
      src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java

973
src/main/java/org/springframework/data/javapoet/LordOfTheStrings.java

@ -0,0 +1,973 @@
/*
* Copyright 2025 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.data.javapoet;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ResolvableType;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.javapoet.CodeBlock;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Utility class for generating Java code blocks using a fluent API. This class provides a structured and extensible
* programming model to simplify the creation of method calls, return statements, and complex code structures. It is
* designed to reduce conditional nesting and improve readability in code generation scenarios.
* <p>
* Built on top of JavaPoet, this class introduces additional abstractions such as {@link CodeBlockBuilder},
* {@link InvocationBuilder}, and {@link TypedReturnBuilder} to facilitate the construction of dynamic code blocks.
* These abstractions enable developers to create code with conditional logic, argument concatenation, and control flow
* in a declarative and intuitive manner.
* <p>
* This class is intended for internal use within the framework and is not meant to be used directly by application
* developers.
*
* @author Mark Paluch
* @since 4.0
*/
public abstract class LordOfTheStrings {
/**
* Create a new {@code CodeBlockBuilder} instance.
*
* @return a new {@code CodeBlockBuilder}.
*/
public static CodeBlockBuilder builder() {
return new CodeBlockBuilder(CodeBlock.builder());
}
/**
* Create a new {@code CodeBlockBuilder} instance using the given {@link CodeBlock.Builder}.
*
* @param builder the {@link CodeBlock.Builder} to use.
* @return a new {@code CodeBlockBuilder}.
*/
public static CodeBlockBuilder builder(CodeBlock.Builder builder) {
return new CodeBlockBuilder(builder);
}
/**
* Create a new {@code CodeBlockBuilder} instance with an initial format and arguments.
*
* @param format the format string.
* @param args the arguments for the format string.
* @return a new initialized {@code CodeBlockBuilder}.
*/
public static CodeBlockBuilder builder(String format, @Nullable Object... args) {
return new CodeBlockBuilder(CodeBlock.builder().add(format, args));
}
/**
* Create a {@link InvocationBuilder} for building method invocation code.
* <p>
* The given {@code methodCall} may contain format specifiers as defined in Java Poet. It must additionally contain a
* format specifier (last position) that is used to expand the method arguments, for example:
*
* <pre class="code">
* Sort sort = ;
* MethodCallBuilder method = PoetrySlam.method("$T.by($L)", Sort.class);
*
* method.arguments(sort, (order, builder) -> {
* builder.add("$T.asc($S)", Sort.Order.class, order.getProperty());
* });
*
* method.build();
* </pre>
*
* @param methodCall the invocation (or method call) format string.
* @param arguments the arguments for the method call.
* @return a new {@code MethodCallBuilder}.
*/
public static InvocationBuilder invoke(String methodCall, Object... arguments) {
return new InvocationBuilder(methodCall, arguments);
}
/**
* Create a builder for a return statement targeting the given return type.
*
* @param returnType the method return type.
* @return a new {@code ReturnStatementBuilder}.
*/
public static TypedReturnBuilder returning(ResolvableType returnType) {
return new TypedReturnBuilder(returnType);
}
/**
* Create a builder for a return statement targeting the given return type.
*
* @param returnType the method return type.
* @return a new {@code ReturnStatementBuilder}.
*/
public static TypedReturnBuilder returning(Class<?> returnType) {
return new TypedReturnBuilder(ResolvableType.forType(returnType));
}
private LordOfTheStrings() {
// you shall not instantiate
}
/**
* Builder to create method invocation code supporting argument concatenation.
*/
public static class InvocationBuilder {
private final String name;
private final List<Object> nameArguments;
private final List<CodeTuple> arguments = new ArrayList<>();
InvocationBuilder(String name, Object... arguments) {
this.name = name;
this.nameArguments = List.of(arguments);
}
/**
* Add a single argument to the method call.
*
* @param argument the argument to add.
* @return {@code this} builder.
*/
public InvocationBuilder argument(String argument) {
Assert.hasText(argument, "Argument must not be null or empty");
return argument("$L", argument);
}
/**
* Add multiple arguments to the method call creating a literal for each argument.
*
* @param arguments the collection of arguments to add.
* @return {@code this} builder.
*/
public InvocationBuilder arguments(Iterable<?> arguments) {
for (Object argument : arguments) {
argument("$L", argument);
}
return this;
}
/**
* Add multiple arguments to the method call, applying a builder customizer for each argument.
*
* @param arguments the iterable of arguments to add.
* @param consumer the consumer to apply to each argument.
* @param <T> the type of the arguments.
* @return {@code this} builder.
*/
public <T> InvocationBuilder arguments(Iterable<? extends T> arguments, Function<? super T, CodeBlock> consumer) {
for (T argument : arguments) {
argument(consumer.apply(argument));
}
return this;
}
/**
* Add a {@link CodeBlock} as an argument to the method call.
*
* @param argument the {@link CodeBlock} to add.
* @return {@code this} builder.
*/
public InvocationBuilder argument(CodeBlock argument) {
Assert.notNull(argument, "CodeBlock must not be null");
if (argument.isEmpty()) {
return this;
}
return argument("$L", argument);
}
/**
* Add a formatted argument to the method call.
*
* @param format the format string.
* @param args the arguments for the format string.
* @return {@code this} builder.
*/
public InvocationBuilder argument(String format, @Nullable Object... args) {
Assert.hasText(format, "Format must not be null or empty");
this.arguments.add(new CodeTuple(format, args));
return this;
}
/**
* Build the {@link CodeBlock} representing the method call. The resulting CodeBlock can be used inline or as a
* {@link CodeBlock.Builder#addStatement(CodeBlock) statement}.
*
* @return the constructed {@link CodeBlock}.
*/
public CodeBlock build() {
CodeBlock.Builder builder = CodeBlock.builder();
buildCall(builder);
return builder.build();
}
/**
* Build the {@link CodeBlock} representing the method call and assign it to the given variable, for example:
*
* <pre class="code">
* CodeBlock.Builder builder = ;
* InvocationBuilder invoke = LordOfTheStrings.invoke("getJdbcOperations().update($L)", );
* builder.addStatement(invoke.assignTo("int $L", result));
* </pre>
*
* The resulting CodeBlock should be used as {@link CodeBlock.Builder#addStatement(CodeBlock) statement}.
*
* @param format the format string for the assignment.
* @param args the arguments for the format string.
* @return the constructed {@link CodeBlock}.
*/
public CodeBlock assignTo(String format, @Nullable Object... args) {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add(format.trim() + " = ", args);
buildCall(builder);
return builder.build();
}
private void buildCall(CodeBlock.Builder builder) {
boolean first = true;
CodeBlock.Builder argsBuilder = CodeBlock.builder();
for (CodeTuple argument : arguments) {
if (first) {
first = false;
} else {
argsBuilder.add(", ");
}
argsBuilder.add(argument.format(), argument.args());
}
List<Object> allArguments = new ArrayList<>(nameArguments);
if (!argsBuilder.isEmpty()) {
allArguments.add(argsBuilder.build());
}
builder.add(name, allArguments.toArray());
}
}
/**
* An extended variant of {@link CodeBlock.Builder} that supports building statements in a fluent way and extended for
* functional {@link #addStatement(Consumer) statement creation}.
* <p>
* This builder provides additional methods for creating and managing code blocks, including support for control flow,
* named arguments, and conditional statements. It is designed to enhance the readability and flexibility of code
* block construction.
* <p>
* Use this builder to create complex code structures in a fluent and intuitive manner.
*
* @see CodeBlock.Builder
*/
public static class CodeBlockBuilder {
private final CodeBlock.Builder builder;
CodeBlockBuilder(CodeBlock.Builder builder) {
this.builder = builder;
}
/**
* Determine whether this builder is empty.
*
* @return {@code true} if the builder is empty; {@code false} otherwise.
* @see CodeBlock.Builder#isEmpty()
*/
public boolean isEmpty() {
return builder.isEmpty();
}
/**
* Add a formatted statement to the code block.
*
* @param format the format string.
* @param args the arguments for the format string.
* @return {@code this} builder.
* @see CodeBlock.Builder#add(String, Object...)
*/
public CodeBlockBuilder add(String format, @Nullable Object... args) {
builder.add(format, args);
return this;
}
/**
* Add a {@link CodeBlock} as a statement to the code block.
*
* @param codeBlock the {@link CodeBlock} to add.
* @return {@code this} builder.
* @see CodeBlock.Builder#addStatement(CodeBlock)
*/
public CodeBlockBuilder addStatement(CodeBlock codeBlock) {
builder.addStatement(codeBlock);
return this;
}
/**
* Add a statement to the code block using a {@link Consumer} to configure it.
*
* @param consumer the {@link Consumer} to configure the statement.
* @return {@code this} builder.
*/
public CodeBlockBuilder addStatement(Consumer<StatementBuilder> consumer) {
StatementBuilder statementBuilder = new StatementBuilder();
consumer.accept(statementBuilder);
if (!statementBuilder.isEmpty()) {
this.add("$[");
for (CodeTuple tuple : statementBuilder.tuples) {
builder.add(tuple.format(), tuple.args());
}
this.add(";\n$]");
}
return this;
}
/**
* Add a {@link CodeBlock} to the code block.
*
* @param codeBlock the {@link CodeBlock} to add.
* @return {@code this} builder.
* @see CodeBlock.Builder#addStatement(CodeBlock)
*/
public CodeBlockBuilder add(CodeBlock codeBlock) {
builder.add(codeBlock);
return this;
}
/**
* Add a formatted statement to the code block.
*
* @param format the format string.
* @param args the arguments for the format string.
* @return {@code this} builder.
* @see CodeBlock.Builder#addStatement(String, Object...)
*/
public CodeBlockBuilder addStatement(String format, @Nullable Object... args) {
builder.addStatement(format, args);
return this;
}
/**
* Add named arguments to the code block.
*
* @param format the format string.
* @param arguments the named arguments.
* @return {@code this} builder.
* @see CodeBlock.Builder#addNamed(String, Map)
*/
public CodeBlockBuilder addNamed(String format, Map<String, ?> arguments) {
builder.addNamed(format, arguments);
return this;
}
/**
* Begin a control flow block with the specified format and arguments.
*
* @param controlFlow the control flow format string.
* @param args the arguments for the control flow format string.
* @return {@code this} builder.
* @see CodeBlock.Builder#beginControlFlow(String, Object...)
*/
public CodeBlockBuilder beginControlFlow(String controlFlow, @Nullable Object... args) {
builder.beginControlFlow(controlFlow, args);
return this;
}
/**
* End the current control flow block with the specified format and arguments.
*
* @param controlFlow the control flow format string.
* @param args the arguments for the control flow format string.
* @return {@code this} builder.
* @see CodeBlock.Builder#endControlFlow(String, Object...)
*/
public CodeBlockBuilder endControlFlow(String controlFlow, @Nullable Object... args) {
builder.endControlFlow(controlFlow, args);
return this;
}
/**
* End the current control flow block.
*
* @return {@code this} builder.
* @see CodeBlock.Builder#endControlFlow()
*/
public CodeBlockBuilder endControlFlow() {
builder.endControlFlow();
return this;
}
/**
* Begin the next control flow block with the specified format and arguments.
*
* @param controlFlow the control flow format string.
* @param args the arguments for the control flow format string.
* @return {@code this} builder.
* @see CodeBlock.Builder#nextControlFlow(String, Object...)
*/
public CodeBlockBuilder nextControlFlow(String controlFlow, @Nullable Object... args) {
builder.nextControlFlow(controlFlow, args);
return this;
}
/**
* Indent the current code block.
*
* @return {@code this} builder.
* @see CodeBlock.Builder#indent()
*/
public CodeBlockBuilder indent() {
builder.indent();
return this;
}
/**
* Unindent the current code block.
*
* @return {@code this} builder.
* @see CodeBlock.Builder#unindent()
*/
public CodeBlockBuilder unindent() {
builder.unindent();
return this;
}
/**
* Build the {@link CodeBlock} from the current state of the builder.
*
* @return the constructed {@link CodeBlock}.
*/
public CodeBlock build() {
return builder.build();
}
/**
* Clear the current state of the builder.
*
* @return {@code this} builder.
*/
public CodeBlockBuilder clear() {
builder.clear();
return this;
}
}
/**
* Builder for creating statements including conditional and concatenated variants.
* <p>
* This builder allows for the creation of complex statements with conditional logic and concatenated elements. It is
* designed to simplify the construction of dynamic code blocks.
* <p>
* Use this builder to handle conditional inclusion in a structured and fluent manner instead of excessive conditional
* nesting that would be required otherwise in the calling code.
*/
public static class StatementBuilder {
private final List<CodeTuple> tuples = new ArrayList<>();
/**
* Determine whether this builder is empty.
*
* @return {@code true} if the builder is empty; {@code false} otherwise.
*/
public boolean isEmpty() {
return tuples.isEmpty();
}
/**
* Add a conditional statement to the builder if the condition <b>is</b> met.
*
* @param state the condition to evaluate.
* @return a {@link ConditionalStatementStep} for further configuration.
*/
public ConditionalStatementStep when(boolean state) {
return whenNot(!state);
}
/**
* Add a conditional statement to the builder if the condition is <b>not</b> met.
*
* @param state the condition to evaluate.
* @return a {@link ConditionalStatementStep} for further configuration.
*/
public ConditionalStatementStep whenNot(boolean state) {
return (format, args) -> {
if (state) {
add(format, args);
}
return this;
};
}
/**
* Add a formatted statement to the builder.
*
* @param format the format string.
* @param args the arguments for the format string.
* @return {@code this} builder.
*/
public StatementBuilder add(String format, @Nullable Object... args) {
tuples.add(new CodeTuple(format, args));
return this;
}
/**
* Concatenate elements into the builder with a delimiter.
*
* @param elements the elements to concatenate.
* @param delim the delimiter to use between elements.
* @param builderCustomizer the consumer to apply to each element and {@link CodeBlockBuilder}.
* @param <T> the type of the elements.
* @return {@code this} builder.
*/
public <T> StatementBuilder addAll(Iterable<? extends T> elements, String delim,
BiConsumer<? super T, CodeBlockBuilder> builderCustomizer) {
return addAll(elements, t -> delim, builderCustomizer);
}
/**
* Concatenate elements into the builder with a custom delimiter function.
*
* @param elements the elements to concatenate.
* @param delim the function to determine the delimiter for each element. Delimiters are applied beginning with the
* second iteration element and obtain from the current element.
* @param builderCustomizer the consumer to apply to each element and {@link CodeBlockBuilder}.
* @param <T> the type of the elements.
* @return {@code this} builder.
*/
public <T> StatementBuilder addAll(Iterable<? extends T> elements, Function<? super T, String> delim,
BiConsumer<? super T, CodeBlockBuilder> builderCustomizer) {
boolean first = true;
for (T element : elements) {
if (first) {
first = false;
} else {
tuples.add(new CodeTuple(delim.apply(element)));
}
CodeBlockBuilder builder = new CodeBlockBuilder(CodeBlock.builder());
builderCustomizer.accept(element, builder);
tuples.add(new CodeTuple("$L", builder.build()));
}
return this;
}
/**
* Functional interface for conditional statement steps.
*/
public interface ConditionalStatementStep {
/**
* Add a statement to the builder if the condition is met.
*
* @param format the format string.
* @param args the arguments for the format string.
* @return the {@link StatementBuilder}.
*/
StatementBuilder then(String format, @Nullable Object... args);
}
}
/**
* Builder for constructing return statements based on the target return type. The resulting {@link #build()
* CodeBlock} must be added as a {@link CodeBlock.Builder#addStatement(CodeBlock)}.
*/
public abstract static class ReturnBuilderSupport {
private final List<ReturnRule> rules = new ArrayList<>();
private final List<ReturnRule> fallback = new ArrayList<>();
/**
* Create a new builder.
*/
private ReturnBuilderSupport() {}
/**
* Add a return statement if the given condition is {@code true}.
*
* @param condition the condition to evaluate.
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public ReturnBuilderSupport when(boolean condition, String format, @Nullable Object... args) {
this.rules.add(ruleOf(condition, format, args));
return this;
}
/**
* Add a fallback return statement if no previous return statement was added.
*
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public ReturnBuilderSupport otherwise(String format, @Nullable Object... args) {
this.fallback.add(ruleOf(true, format, args));
return this;
}
/**
* Add a fallback return statement if no previous return statement was added.
*
* @param builderConsumer the code block builder consumer to apply.
* @return {@code this} builder.
*/
ReturnBuilderSupport otherwise(Consumer<CodeBlock.Builder> builderConsumer) {
this.fallback.add(new ReturnRule(true, "", new Object[] {}, builderConsumer));
return this;
}
/**
* Build the code block representing the return statement.
*
* @return the resulting {@code CodeBlock}
*/
public CodeBlock build() {
CodeBlock.Builder builder = CodeBlock.builder();
for (ReturnRule rule : rules) {
if (rule.condition()) {
builder.add("return");
rule.accept(builder);
return builder.build();
}
}
for (ReturnRule rule : fallback) {
if (rule.condition()) {
builder.add("return");
rule.accept(builder);
return builder.build();
}
}
return builder.build();
}
/**
* Add a return statement if the given condition is {@code true}.
*
* @param condition the condition to evaluate.
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
static ReturnRule ruleOf(boolean condition, String format, @Nullable Object... args) {
if (format.startsWith("return")) {
throw new IllegalArgumentException("Return value format '%s' must not contain 'return'".formatted(format));
}
return new ReturnRule(condition, format, args, null);
}
}
record ReturnRule(boolean condition, String format, @Nullable Object[] args,
@Nullable Consumer<CodeBlock.Builder> builderCustomizer) {
public void accept(CodeBlock.Builder builder) {
if (StringUtils.hasText(format()) || builderCustomizer() != null) {
builder.add(" ");
if (StringUtils.hasText(format())) {
builder.add(format(), args());
}
if (builderCustomizer() != null) {
builderCustomizer().accept(builder);
}
}
}
}
/**
* Builder for constructing return statements based on the target return type. The resulting {@link #build()
* CodeBlock} must be added as a {@link CodeBlock.Builder#addStatement(CodeBlock)}.
*/
public static class TypedReturnBuilder extends ReturnBuilderSupport {
private final ResolvableType returnType;
/**
* Create a new builder for the given return type.
*
* @param returnType the method return type
*/
private TypedReturnBuilder(ResolvableType returnType) {
this.returnType = returnType;
// consider early return cases for Void and void.
whenBoxed(Void.class, "null");
when(ReflectionUtils.isVoid(returnType.toClass()), "");
}
/**
* Add return statements for numeric types if the given {@code resultToReturn} points to a {@link Number}. Considers
* primitive and boxed {@code int} and {@code long} type return paths and that {@code resultToReturn} can be
* {@literal null}.
*
* @param resultToReturn the argument or variable name holding the result.
* @return {@code this} builder.
*/
public TypedReturnBuilder number(String resultToReturn) {
return whenBoxedLong("$1L != null ? $1L.longValue() : null", resultToReturn)
.whenLong("$1L != null ? $1L.longValue() : 0L", resultToReturn)
.whenBoxedInteger("$1L != null ? $1L.intValue() : null", resultToReturn)
.whenInt("$1L != null ? $1L.intValue() : 0", resultToReturn);
}
/**
* Add a return statement if the return type is boolean (primitive or box type) returning {@code returnName}.
*
* @param returnName the argument or variable name holding the result.
* @return {@code this} builder.
*/
public TypedReturnBuilder whenBooleanReturn(String returnName) {
return whenBoolean("$L", returnName);
}
/**
* Add a return statement if the return type is boolean (primitive or box type).
*
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder whenBoolean(String format, @Nullable Object... args) {
return when(returnType.isAssignableFrom(boolean.class) || returnType.isAssignableFrom(Boolean.class), format,
args);
}
/**
* Add a return statement if the return type is {@link Long} (boxed {@code long} type).
*
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder whenBoxedLong(String format, @Nullable Object... args) {
return whenBoxed(long.class, format, args);
}
/**
* Add a return statement if the return type is a primitive {@code long} type.
*
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder whenLong(String format, @Nullable Object... args) {
return when(returnType.toClass() == long.class, format, args);
}
/**
* Add a return statement if the return type is {@link Integer} (boxed {@code int} type).
*
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder whenBoxedInteger(String format, @Nullable Object... args) {
return whenBoxed(int.class, format, args);
}
/**
* Add a return statement if the return type is a primitive {@code int} type.
*
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder whenInt(String format, @Nullable Object... args) {
return when(returnType.toClass() == int.class, format, args);
}
/**
* Add a return statement if the return type matches the given boxed wrapper type.
*
* @param primitiveOrWrapper the primitive or wrapper type.
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder whenBoxed(Class<?> primitiveOrWrapper, String format, @Nullable Object... args) {
Class<?> primitiveWrapper = ClassUtils.resolvePrimitiveIfNecessary(primitiveOrWrapper);
return when(returnType.toClass() == primitiveWrapper, format, args);
}
/**
* Add a return statement if the return type matches the given primitive or boxed wrapper type.
*
* @param primitiveType the primitive or wrapper type.
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder whenPrimitiveOrBoxed(Class<?> primitiveType, String format, @Nullable Object... args) {
Class<?> primitiveWrapper = ClassUtils.resolvePrimitiveIfNecessary(primitiveType);
return when(
ClassUtils.isAssignable(ClassUtils.resolvePrimitiveIfNecessary(returnType.toClass()), primitiveWrapper),
format, args);
}
/**
* Add a return statement if the declared return type is assignable from the given {@code returnType}.
*
* @param returnType the candidate return type.
* @param format the code format string.
* @param args the format arguments
* @return {@code this} builder.
*/
public TypedReturnBuilder when(Class<?> returnType, String format, @Nullable Object... args) {
return when(this.returnType.isAssignableFrom(returnType), format, args);
}
/**
* Add a return statement if the given condition is {@code true}.
*
* @param condition the condition to evaluate.
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder when(boolean condition, String format, @Nullable Object... args) {
super.when(condition, format, args);
return this;
}
/**
* Add a fallback return statement considering that the returned value might be nullable and apply conditionally
* {@link Optional#ofNullable(Object)} wrapping if the return type is {@code Optional}.
*
* @param codeBlock the code block result to be returned.
* @return {@code this} builder.
*/
public TypedReturnBuilder optional(CodeBlock codeBlock) {
return optional("$L", codeBlock);
}
/**
* Add a fallback return statement considering that the returned value might be nullable and apply conditionally
* {@link Optional#ofNullable(Object)} wrapping if the return type is {@code Optional}.
*
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder optional(String format, @Nullable Object... args) {
if (Optional.class.isAssignableFrom(returnType.toClass())) {
if (format.startsWith("return")) {
throw new IllegalArgumentException("Return value format '%s' must not contain 'return'".formatted(format));
}
otherwise(builder -> {
builder.add("$T.ofNullable(", Optional.class);
builder.add(format, args);
builder.add(")");
});
return this;
}
return otherwise(format, args);
}
/**
* Add a fallback return statement if no previous return statement was added.
*
* @param codeBlock the code block result to be returned.
* @return {@code this} builder.
*/
public TypedReturnBuilder otherwise(CodeBlock codeBlock) {
return otherwise("$L", codeBlock);
}
/**
* Add a fallback return statement if no previous return statement was added.
*
* @param format the code format string.
* @param args the format arguments.
* @return {@code this} builder.
*/
public TypedReturnBuilder otherwise(String format, @Nullable Object... args) {
super.otherwise(format, args);
return this;
}
}
record CodeTuple(String format, @Nullable Object... args) {
}
}

106
src/main/java/org/springframework/data/javapoet/TypeNames.java

@ -0,0 +1,106 @@
/*
* Copyright 2025 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.data.javapoet;
import org.springframework.core.ResolvableType;
import org.springframework.javapoet.TypeName;
import org.springframework.util.ClassUtils;
/**
* Collection of {@link org.springframework.javapoet.TypeName} transformation utilities.
* <p>
* This class delivers some simple functionality that should be provided by the JavaPoet framework. It also provides
* easy-to-use methods to convert between types.
* <p>
* Mainly for internal use within the framework
*
* @author Mark Paluch
* @since 4.0
*/
public abstract class TypeNames {
/**
* Obtain a {@link TypeName class name} for the given type, resolving primitive wrappers as necessary.
*
* @param type the class to use.
* @return the corresponding {@link TypeName}.
*/
public static TypeName classNameOrWrapper(Class<?> type) {
return ClassUtils.isPrimitiveOrWrapper(type) ? TypeName.get(ClassUtils.resolvePrimitiveIfNecessary(type))
: TypeName.get(type);
}
/**
* Obtain a {@link TypeName class name} for the given {@link ResolvableType}, resolving primitive wrappers as
* necessary. Ideal to represent a type name used as {@code Class} value as generic parameters are not considered.
*
* @param resolvableType the resolvable type to use.
* @return the corresponding {@link TypeName}.
*/
public static TypeName classNameOrWrapper(ResolvableType resolvableType) {
return classNameOrWrapper(resolvableType.toClass());
}
/**
* Obtain a {@link TypeName} for the given {@link ResolvableType}. Ideal to represent a type name used as
* {@code Class} value as generic parameters are not considered.
*
* @param resolvableType the resolvable type to use.
* @return the corresponding {@link TypeName}.
*/
public static TypeName className(ResolvableType resolvableType) {
return TypeName.get(resolvableType.toClass());
}
/**
* Obtain a {@link TypeName} for the underlying type of the given {@link ResolvableType}. Can render a class name, a
* type signature or a generic type variable.
*
* @param resolvableType the resolvable type represent.
* @return the corresponding {@link TypeName}.
*/
public static TypeName typeName(ResolvableType resolvableType) {
return TypeName.get(resolvableType.getType());
}
/**
* Obtain a {@link TypeName} for the given type, resolving primitive wrappers as necessary. Ideal to represent a type
* parameter for parametrized types as primitive boxing is considered.
*
* @param type the class to be represented.
* @return the corresponding {@link TypeName}.
*/
public static TypeName typeNameOrWrapper(Class<?> type) {
return typeNameOrWrapper(ResolvableType.forClass(type));
}
/**
* Obtain a {@link TypeName} for the given {@link ResolvableType}, resolving primitive wrappers as necessary. Can
* render a class name, a type signature or a generic type variable. Ideal to represent a type parameter for
* parametrized types as primitive boxing is considered.
*
* @param resolvableType the resolvable type to be represented.
* @return the corresponding {@link TypeName}.
*/
public static TypeName typeNameOrWrapper(ResolvableType resolvableType) {
return ClassUtils.isPrimitiveOrWrapper(resolvableType.toClass())
? TypeName.get(ClassUtils.resolvePrimitiveIfNecessary(resolvableType.toClass()))
: typeName(resolvableType);
}
private TypeNames() {}
}

5
src/main/java/org/springframework/data/javapoet/package-info.java

@ -0,0 +1,5 @@
/**
* Opinionated extensions to JavaPoet to support Spring Data specific use cases.
*/
@org.jspecify.annotations.NullMarked
package org.springframework.data.javapoet;

16
src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java

@ -47,6 +47,7 @@ public class AotQueryMethodGenerationContext {
private final QueryMethod queryMethod; private final QueryMethod queryMethod;
private final RepositoryInformation repositoryInformation; private final RepositoryInformation repositoryInformation;
private final AotRepositoryFragmentMetadata targetTypeMetadata; private final AotRepositoryFragmentMetadata targetTypeMetadata;
private final MethodReturn methodReturn;
private final MethodMetadata targetMethodMetadata; private final MethodMetadata targetMethodMetadata;
private final VariableNameFactory variableNameFactory; private final VariableNameFactory variableNameFactory;
private final ExpressionMarker expressionMarker; private final ExpressionMarker expressionMarker;
@ -60,6 +61,8 @@ public class AotQueryMethodGenerationContext {
this.repositoryInformation = repositoryInformation; this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = new AotRepositoryFragmentMetadata(); this.targetTypeMetadata = new AotRepositoryFragmentMetadata();
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method); this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
this.methodReturn = new MethodReturn(queryMethod.getResultProcessor().getReturnedType(),
targetMethodMetadata.getReturnType());
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata); this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
this.expressionMarker = new ExpressionMarker(); this.expressionMarker = new ExpressionMarker();
} }
@ -73,6 +76,8 @@ public class AotQueryMethodGenerationContext {
this.repositoryInformation = repositoryInformation; this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = targetTypeMetadata; this.targetTypeMetadata = targetTypeMetadata;
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method); this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
this.methodReturn = new MethodReturn(queryMethod.getResultProcessor().getReturnedType(),
targetMethodMetadata.getReturnType());
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata); this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
this.expressionMarker = new ExpressionMarker(); this.expressionMarker = new ExpressionMarker();
} }
@ -135,6 +140,13 @@ public class AotQueryMethodGenerationContext {
return getRepositoryInformation().getDomainType(); return getRepositoryInformation().getDomainType();
} }
/**
* @return the method return information.
*/
public MethodReturn getMethodReturn() {
return methodReturn;
}
/** /**
* @return the returned type without considering dynamic projections. * @return the returned type without considering dynamic projections.
*/ */
@ -146,6 +158,7 @@ public class AotQueryMethodGenerationContext {
* @return the actual returned domain type. * @return the actual returned domain type.
* @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(Method) * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(Method)
*/ */
@Deprecated(forRemoval = true)
public ResolvableType getActualReturnType() { public ResolvableType getActualReturnType() {
return targetMethodMetadata.getActualReturnType(); return targetMethodMetadata.getActualReturnType();
} }
@ -154,6 +167,7 @@ public class AotQueryMethodGenerationContext {
* @return the query method return type. * @return the query method return type.
* @see org.springframework.data.repository.core.RepositoryMetadata#getReturnType(Method) * @see org.springframework.data.repository.core.RepositoryMetadata#getReturnType(Method)
*/ */
@Deprecated(forRemoval = true)
public ResolvableType getReturnType() { public ResolvableType getReturnType() {
return targetMethodMetadata.getReturnType(); return targetMethodMetadata.getReturnType();
} }
@ -161,6 +175,7 @@ public class AotQueryMethodGenerationContext {
/** /**
* @return the {@link TypeName} representing the method return type. * @return the {@link TypeName} representing the method return type.
*/ */
@Deprecated(forRemoval = true)
public TypeName getReturnTypeName() { public TypeName getReturnTypeName() {
return TypeName.get(getReturnType().getType()); return TypeName.get(getReturnType().getType());
} }
@ -168,6 +183,7 @@ public class AotQueryMethodGenerationContext {
/** /**
* @return the {@link TypeName} representing the actual (component) method return type. * @return the {@link TypeName} representing the actual (component) method return type.
*/ */
@Deprecated(forRemoval = true)
public TypeName getActualReturnTypeName() { public TypeName getActualReturnTypeName() {
return TypeName.get(getActualReturnType().getType()); return TypeName.get(getActualReturnType().getType());
} }

196
src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java

@ -0,0 +1,196 @@
/*
* Copyright 2025 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.data.repository.aot.generate;
import java.util.Optional;
import java.util.stream.Stream;
import org.springframework.core.ResolvableType;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.javapoet.TypeName;
/**
* Value object that encapsulates introspection of a method's return type, providing convenient access to its
* characteristics such as projection, optionality, array status, and actual type information.
* <p>
* Designed to support repository method analysis in the context of Ahead-of-Time (AOT) processing, this class leverages
* {@link ReturnedType}, {@link ResolvableType}, and {@link TypeInformation} to expose both the declared and actual
* return types, including handling of wrapper types, projections, and primitive types.
* <p>
* Typical usage involves querying the return type characteristics to drive code generation or runtime behavior in
* repository infrastructure.
*
* @author Mark Paluch
* @since 4.0
*/
public class MethodReturn {
private final ReturnedType returnedType;
private final Class<?> actualReturnClass;
private final ResolvableType returnType;
private final ResolvableType actualType;
private final TypeName typeName;
private final TypeName className;
private final TypeName actualTypeName;
private final TypeName actualClassName;
/**
* Create a new {@code MethodReturn} instance based on the given {@link ReturnedType} and its {@link ResolvableType
* method return type}.
*
* @param returnedType the returned type to inspect.
* @param returnType the method return type.
*/
public MethodReturn(ReturnedType returnedType, ResolvableType returnType) {
this.returnedType = returnedType;
this.returnType = returnType;
Class<?> returnClass = returnType.toClass();
this.typeName = TypeName.get(returnType.getType());
this.className = TypeName.get(returnClass);
TypeInformation<?> typeInformation = TypeInformation.of(returnType);
TypeInformation<?> actualType = typeInformation.isMap() ? typeInformation
: (typeInformation.getType().equals(Stream.class) ? typeInformation.getComponentType()
: typeInformation.getActualType());
if (actualType != null) {
this.actualType = actualType.toResolvableType();
this.actualTypeName = TypeName.get(actualType.toResolvableType().getType());
this.actualClassName = TypeName.get(actualType.getType());
this.actualReturnClass = actualType.getType();
} else {
this.actualType = returnType;
this.actualTypeName = typeName;
this.actualClassName = className;
this.actualReturnClass = returnClass;
}
}
/**
* Returns whether the method return type is a projection. Query projections (e.g. returning {@code String} or
* {@code int} are not considered.
*
* @return {@literal true} if the return type is a projection.
*/
public boolean isProjecting() {
return returnedType.isProjecting();
}
/**
* Returns whether the method return type is an interface-based projection.
*
* @return {@literal true} if the return type is an interface-based projection.
*/
public boolean isInterfaceProjection() {
return isProjecting() && returnedType.getReturnedType().isInterface();
}
/**
* Returns whether the method return type is {@code Optional}.
*
* @return {@literal true} if the return type is {@code Optional}.
*/
public boolean isOptional() {
return Optional.class.isAssignableFrom(toClass());
}
/**
* Returns whether the method return type is an array.
*
* @return {@literal true} if the return type is an array.
*/
public boolean isArray() {
return toClass().isArray();
}
/**
* Returns whether the method return type is {@code void}. Considers also {@link Void} and Kotlin's {@code Unit}.
*
* @return {@literal true} if the return type is {@code void}.
*/
public boolean isVoid() {
return ReflectionUtils.isVoid(toClass());
}
/**
* Returns the {@link Class} representing the declared return type.
*
* @return the declared return class.
*/
public Class<?> toClass() {
return returnType.toClass();
}
/**
* Returns the actual type (i.e. component type of a collection).
*
* @return the actual type.
*/
public ResolvableType getActualType() {
return actualType;
}
/**
* Returns the {@link TypeName} representing the declared return type.
*
* @return the declared return type name.
*/
public TypeName getTypeName() {
return typeName;
}
/**
* Returns the {@link TypeName} representing the declared return class (i.e. without generics).
*
* @return the declared return class name.
*/
public TypeName getClassName() {
return className;
}
/**
* Returns the actual {@link TypeName} representing the declared return type (component type of collections).
*
* @return the actual return type name.
*/
public TypeName getActualTypeName() {
return actualTypeName;
}
/**
* Returns the actual {@link TypeName} representing the declared return class (component type of collections).
*
* @return the actual return class name.
*/
public TypeName getActualClassName() {
return actualClassName;
}
/**
* Returns the {@link Class} representing the actual return type.
*
* @return the actual return class.
*/
public Class<?> getActualReturnClass() {
return actualReturnClass;
}
}

3
src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java

@ -27,6 +27,7 @@ import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.springframework.aot.generate.Generated; import org.springframework.aot.generate.Generated;
import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.TypeReference;
@ -153,7 +154,7 @@ class AotRepositoryCreatorUnitTests {
repositoryCreator.contributeMethods((method) -> { repositoryCreator.contributeMethods((method) -> {
return new MethodContributor<>(mock(QueryMethod.class), null) { return new MethodContributor<>(mock(QueryMethod.class, Answers.RETURNS_MOCKS), null) {
@Override @Override
public MethodSpec contribute(AotQueryMethodGenerationContext context) { public MethodSpec contribute(AotQueryMethodGenerationContext context) {

Loading…
Cancel
Save