JAVA + SPRING AI — INTERMEDIATE
@Tool, @ToolParam & the ChatClient Request/Response Loop, in Practice
The Introduction level covered whySpring AI’s tool-calling support exists. Here’s the actual mechanics, verified directly against Spring AI’s current (2.0.0) reference documentation: the annotation that defines a tool, describing its parameters, registering it on a ChatClientcall, and exactly what runs automatically once that’s done.
The Quick Answer
Annotate a method with @Tool(description = "..."), describe any parameter that needs more context with @ToolParam, and hand an instance of that class to .tools(...) on a ChatClient’s fluent chain. From there, Spring AI runs the entire loop for you — reading the model’s structured tool-call request, invoking your real Java method with the arguments the model supplied, and feeding the result back so the model can produce a final, natural-language answer — none of which you write by hand.
Step 1: Defining a Tool with @Tool
Verified directly against Spring AI’s current (2.0.0) API documentation, the annotation that turns a plain Java method into a callable tool is org.springframework.ai.tool.annotation.Tool, placed directly on the method:
import org.springframework.ai.tool.annotation.Tool;
public class OrderTools {
@Tool(description = "Get the current shipping status of a customer's order, by order ID")
public String getOrderStatus(String orderId) {
// your real logic — a repository lookup, another service call, etc.
return orderService.findStatus(orderId);
}
}Two attributes matter most in practice. namedefaults to the method’s own name if left unset, but must be unique across every tool available to the model in a given request. description is the one attribute genuinely worth writing carefully — Spring AI’s own documentation calls it “paramount for the model to understand the tool’s purpose and how to use it,” since the description is the model’s only real signal for deciding whether this tool is relevant to a given question at all. A vague description (“does order stuff”) gives the model far less to work with than a specific one (“get the current shipping status of a customer’s order, by order ID”).
A less common but genuinely useful third attribute is returnDirect, which controls whether the tool’s result gets handed straight back to the caller as-is, or passed back to the model first so it can turn that raw result into a natural-language answer (the default, and what every example on this page assumes).
Step 2: Describing Parameters with @ToolParam
Spring AI generates a JSON Schema for a tool’s parameters automatically from the method signature — but a bare parameter name like orderIddoesn’t always tell the model everything it needs (what format is expected, whether it’s optional). org.springframework.ai.tool.annotation.ToolParam fills that gap:
import org.springframework.ai.tool.annotation.ToolParam;
@Tool(description = "Get the current weather conditions for a city")
public WeatherReport getCurrentWeather(
@ToolParam(description = "The city name, e.g. 'Pune' or 'Tokyo, Japan'") String city) {
return weatherService.fetch(city);
}description works the same way it does on @Tool itself — extra context the model reads when deciding what value to pass in. required defaults to true, and a parameter annotated @Nullable (from Spring Framework itself) is treated as optional by default without needing @ToolParamat all. Spring AI’s current documentation also notes that standard Jackson (@JsonProperty) and Swagger (@Schema) annotations are recognized too, if a codebase already uses them elsewhere — @ToolParam is simply the annotation built specifically for this.
Step 3: Registering a Tool on a ChatClient Call
With a tool class defined, making it available to a model is one addition to the same fluent chain topic 4 already taught — pass an instance of the class to .tools(...):
String answer = chatClient.prompt()
.tools(new OrderTools(orderService))
.user("What is the status of order 4471?")
.call()
.content();Spring AI scans that instance for @Tool-annotated methods, generates a ToolCallback for each one, and sends their schemas along with the request. A tool registered this way, on .tools(...), is only available for that one specific chat request. If every request through a given ChatClient should have the same tool available, .defaultTools(...) on the ChatClient.Builder does the same thing at build time instead — the same “per-call vs. default” shape defaultSystem(...) already showed for system prompts in the ChatClient topic.
Every example on this page hands .tools(...)a POJO instance, which is the pattern this pillar teaches throughout — but it’s not the only accepted form. .tools(...) also directly accepts already-built ToolCallback or ToolCallbackProvider instances, and collections of any of these types, for cases where a tool isn’t a plain @Tool-annotated class.
What Actually Runs Automatically: The Request/Response Loop
The three steps above are everything a developer writes. What happens next, verified directly against Spring AI’s current documentation, is handled entirely by ChatClient itself, through an advisor called ToolCallingAdvisor, registered automatically in the advisor chain:
That automatic registration is itself a 2.0.0 behavior change, not something that has always been true — Spring AI’s own upgrade notes are explicit that ChatClient“now always auto-registers a ToolCallingAdvisorin the advisor chain (unless explicitly disabled).” If you’re following an older pre-2.0 tutorial that had you add ToolCallingAdvisor to the advisor chain yourself, that same advisor is now registered twice — once from your explicit call, once from auto-registration. The property spring.ai.chat.client.tool-calling.enabled (default true) turns the automatic registration off, for anyone who specifically needs to manage ToolCallingAdvisor registration by hand instead.
- 1
The model reads the question alongside every registered tool’s schema, and decides whether calling one is relevant — or whether to just answer directly, if nothing registered actually applies.
- 2
If it decides to call one, the model produces a structured request naming the tool and its arguments.
ToolCallingAdvisorintercepts that response and hands it to aToolCallingManager, which identifies the real Java method by name and invokes it with those arguments — no manual parsing or dispatch code required. - 3
Whatever the method returns is serialized and sent back to the model as part of the same conversation, and
ToolCallingAdvisorre-enters the chain so the model can read that result and produce a final, natural-language answer.
The result: the fluent chain a developer writes — .tools(...).user(...).call().content() — looks identical whether the model ends up calling a tool or not. That entire loop above runs (or doesn’t, if no tool is needed) before .content() ever returns.
Putting It Together: A Real Endpoint
Here’s all of it in one place — a @RestController (the annotation from topic 2) with a constructor-injected ChatClient(the dependency injection mechanics from topic 1), a separate tool class with a real, simple tool, and an endpoint that lets a model answer a question it has no way of knowing on its own — a specific order’s live shipping status:
// The tool: an ordinary Spring-managed class, annotated for Spring AI.
@Component
public class OrderTools {
private final OrderRepository orderRepository;
public OrderTools(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Tool(description = "Get the current shipping status of a customer's order, by order ID")
public String getOrderStatus(
@ToolParam(description = "The numeric order ID, e.g. '4471'") String orderId) {
return orderRepository.findById(orderId)
.map(Order::getShippingStatus)
.orElse("No order found with ID " + orderId);
}
}
// The controller: registers the tool on every request to this endpoint.
@RestController
@RequestMapping("/support")
public class SupportController {
private final ChatClient chatClient;
private final OrderTools orderTools;
public SupportController(ChatClient.Builder chatClientBuilder, OrderTools orderTools) {
this.chatClient = chatClientBuilder.build();
this.orderTools = orderTools;
}
@GetMapping
public String ask(@RequestParam String question) {
return chatClient.prompt()
.tools(orderTools)
.user(question)
.call()
.content();
}
}A request to /support?question=What is the status of order 4471? now runs the full loop from the previous section: the model recognizes it needs getOrderStatus, Spring AI invokes the real OrderRepositorylookup with the order ID extracted from the question, and the model turns that raw result into a natural-language reply — something no amount of retrieval or training data could have supplied, since this exact order’s current status only exists in your own database, right now.
Try It: Step Through the Loop
Pick a question below and step through what a ToolCallingAdvisor-backed ChatClient call does at each stage — including a question that needs no tool call at all (not a live model or a live method call; see the note under the demo):
Fun Fact
If you’re following an older tutorial and see a tool registered as a @Bean-declared Function<SomeRequest, SomeResponse> instead of an @Tool-annotated method, that’s showing an earlier, now-superseded Spring AI approach — the underlying mechanism the older tutorials called FunctionCallback has been superseded by ToolCallback, with Spring AI’s own current documentation providing a dedicated migration guide between the two. This pillar teaches the current, @Tool-annotated approach throughout, not the older function-bean shape.
Test Yourself
What is the current, correct package for the @Tool annotation?
What does @ToolParam let you specify for an individual tool parameter?
How do you register a tool object with a single ChatClient.prompt() call?
What does Spring AI’s ChatClient handle automatically once a tool is registered on a call?
That endpoint registers exactly one tool and assumes it always runs cleanly. What about registering several tools on one call, what a tool can actually return, what happens when a tool method throws an exception, and how tool calling actually compares to the Advisor-based RAG augmentation from the previous topic? Continue to the Advanced level →