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}.
+ *
+ * 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.
+ *
+ * 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 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 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.
+ *
+ * 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.
+ *
+ * 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 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 is 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 not 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 the type of the elements.
+ * @return {@code this} builder.
+ */
+ public 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 the type of the elements.
+ * @return {@code this} builder.
+ */
+ public 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 rules = new ArrayList<>();
+ private final List 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 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 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) {
+
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/javapoet/TypeNames.java b/src/main/java/org/springframework/data/javapoet/TypeNames.java
new file mode 100644
index 000000000..eb9db1a9b
--- /dev/null
+++ b/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.
+ *
+ * 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.
+ *
+ * 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() {}
+
+}
diff --git a/src/main/java/org/springframework/data/javapoet/package-info.java b/src/main/java/org/springframework/data/javapoet/package-info.java
new file mode 100644
index 000000000..c0a6b8c24
--- /dev/null
+++ b/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;
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
index 34fc7f8ff..2720f992a 100644
--- a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
+++ b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
@@ -47,6 +47,7 @@ public class AotQueryMethodGenerationContext {
private final QueryMethod queryMethod;
private final RepositoryInformation repositoryInformation;
private final AotRepositoryFragmentMetadata targetTypeMetadata;
+ private final MethodReturn methodReturn;
private final MethodMetadata targetMethodMetadata;
private final VariableNameFactory variableNameFactory;
private final ExpressionMarker expressionMarker;
@@ -60,6 +61,8 @@ public class AotQueryMethodGenerationContext {
this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = new AotRepositoryFragmentMetadata();
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
+ this.methodReturn = new MethodReturn(queryMethod.getResultProcessor().getReturnedType(),
+ targetMethodMetadata.getReturnType());
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
this.expressionMarker = new ExpressionMarker();
}
@@ -73,6 +76,8 @@ public class AotQueryMethodGenerationContext {
this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = targetTypeMetadata;
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
+ this.methodReturn = new MethodReturn(queryMethod.getResultProcessor().getReturnedType(),
+ targetMethodMetadata.getReturnType());
this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
this.expressionMarker = new ExpressionMarker();
}
@@ -135,6 +140,13 @@ public class AotQueryMethodGenerationContext {
return getRepositoryInformation().getDomainType();
}
+ /**
+ * @return the method return information.
+ */
+ public MethodReturn getMethodReturn() {
+ return methodReturn;
+ }
+
/**
* @return the returned type without considering dynamic projections.
*/
@@ -146,6 +158,7 @@ public class AotQueryMethodGenerationContext {
* @return the actual returned domain type.
* @see org.springframework.data.repository.core.RepositoryMetadata#getReturnedDomainClass(Method)
*/
+ @Deprecated(forRemoval = true)
public ResolvableType getActualReturnType() {
return targetMethodMetadata.getActualReturnType();
}
@@ -154,6 +167,7 @@ public class AotQueryMethodGenerationContext {
* @return the query method return type.
* @see org.springframework.data.repository.core.RepositoryMetadata#getReturnType(Method)
*/
+ @Deprecated(forRemoval = true)
public ResolvableType getReturnType() {
return targetMethodMetadata.getReturnType();
}
@@ -161,6 +175,7 @@ public class AotQueryMethodGenerationContext {
/**
* @return the {@link TypeName} representing the method return type.
*/
+ @Deprecated(forRemoval = true)
public TypeName getReturnTypeName() {
return TypeName.get(getReturnType().getType());
}
@@ -168,6 +183,7 @@ public class AotQueryMethodGenerationContext {
/**
* @return the {@link TypeName} representing the actual (component) method return type.
*/
+ @Deprecated(forRemoval = true)
public TypeName getActualReturnTypeName() {
return TypeName.get(getActualReturnType().getType());
}
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java b/src/main/java/org/springframework/data/repository/aot/generate/MethodReturn.java
new file mode 100644
index 000000000..efc55abfb
--- /dev/null
+++ b/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.
+ *
+ * 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.
+ *
+ * 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;
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java
index 3dbff067f..5ed0352f7 100644
--- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryCreatorUnitTests.java
+++ b/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.Test;
+import org.mockito.Answers;
import org.springframework.aot.generate.Generated;
import org.springframework.aot.hint.TypeReference;
@@ -153,7 +154,7 @@ class AotRepositoryCreatorUnitTests {
repositoryCreator.contributeMethods((method) -> {
- return new MethodContributor<>(mock(QueryMethod.class), null) {
+ return new MethodContributor<>(mock(QueryMethod.class, Answers.RETURNS_MOCKS), null) {
@Override
public MethodSpec contribute(AotQueryMethodGenerationContext context) {