为了手写 OpenRouter Starter,我先读了 Spring AI 的源码 (一)
为了手写 OpenRouter Starter,我先读了 Spring AI 的源码(一)
为什么要手写 OpenRouter Starter?
相信大家看到这个标题就不理解了,为什么要手写 OpenRouter Starter ?
在官方 Spring AI: Chat Models Comparison :: Spring AI Reference 中我们可以看到,当前 Spring AI 并不支持 OpenRouter !!!!


但这有什么关系?
AI 调用不应该支持 OpenAI API 请求格式吗?
在 Spring AI 中使用 OpenAI 相关方法,来调用 OpenRouter API 会出现无法获得思考内容即 reasoning_content 的情况。
在 Spring AI 1.1.0 开始就支持可以在 OpenAI 的 AssistantMessage 消息类使用 getMetadata().get("reasoningContent") 来获取兼容 OpenAI API 的返回体了
但还是无法通过该字段获取 OpenRouter 的思考内容,在 Spring AI 官方的 GitHub 中也有人提交了 Issues : https://github.com/spring-projects/spring-ai/issues/5265
同时也有人提出了 Spring AI 会"丢弃"不认识的字段 : https://github.com/spring-projects/spring-ai/issues/4901

那么我们来使用 Spring AI 的 OpenAI 方法来调用方法,给大家演示一下,实现代码如下:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.1.4</version>
</dependency>
@Resource
private OpenAiChatModel openAiChatModel;
/**
* 流式聊天
*
* @return
*/
@GetMapping(value = "/generateStream", produces = "text/event-stream;charset=utf-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
OpenAiChatModel openAiChatModel = OpenAiChatModel
.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(baseUrl)
.apiKey(apiKey)
.build())
.build();
// 创建 ChatClient
ChatClient.ChatClientRequestSpec chatClientRequestSpec = ChatClient.create(openAiChatModel)
.prompt()
// 构建配置
.options(OpenAiChatOptions.builder()
.model("google/gemini-3.1-pro-preview")
.build())
.user(message);
return chatClientRequestSpec.stream()
.chatResponse()
.mapNotNull(chatResponse -> {
// 获取响应内容
AssistantMessage assistantMessage = chatResponse.getResult().getOutput();
// 获取输出内容
String text = assistantMessage.getText();
// 获取思考内容
String reasoning = assistantMessage.getMetadata().get("reasoningContent").toString();
log.info("==> 输出内容: {}", text);
log.info("==> 思考内容: {}", reasoning);
return text;
});
}
执行结果如下:

可以看到输出的日志是没有思考结果的。
什么问题太简单?不可能思考?

gemini-3.1-pro-preview 在不发癫的情况下,基本都是强思考的,可以看到 ApiFox 的调用结果:

可以看到是一样会思考的,在调用 OpenRouter API 返回的结果中,思考内容是 reasoning 字段,但使用assistantMessage.getMetadata().get("reasoning").toString() 获取思考内容也是一样的甚至会出现下面的情况。
代码修改:
/**
* 流式聊天
*
* @return
*/
@GetMapping(value = "/generateStream", produces = "text/event-stream;charset=utf-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
// 省略.....
return chatClientRequestSpec.stream()
.chatResponse()
.mapNotNull(chatResponse -> {
// 获取响应内容
AssistantMessage assistantMessage = chatResponse.getResult().getOutput();
// 获取输出内容
String text = assistantMessage.getText();
// 获取思考内容
String reasoning = assistantMessage.getMetadata().get("reasoning").toString();
log.info("==> 输出内容: {}", text);
log.info("==> 思考内容: {}", reasoning);
return text;
});
}
输出结果如下:

如果我们直接打印 assistantMessage.getMetadata() 获取出的对象可以看到
/**
* 流式聊天
*
* @return
*/
@GetMapping(value = "/generateStream", produces = "text/event-stream;charset=utf-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
// 省略.....
return chatClientRequestSpec.stream()
.chatResponse()
.mapNotNull(chatResponse -> {
// 获取响应内容
AssistantMessage assistantMessage = chatResponse.getResult().getOutput();
// 获取输出内容
String text = assistantMessage.getText();
// 获取思考内容
String reasoning = assistantMessage.getMetadata().get("reasoning").toString();
log.info("==> 输出内容: {}", text);
log.info("=== Metadata: {}", assistantMessage.getMetadata());
return text;
});
}
输出结果如下:

可以看到是无法获取OpenRouter API 返回的结果中,思考内容是 reasoning 字段。
那么我们该如何使用 Spring AI 调用 OpenRouter API 时能获取到思考内容呢?

| 方案 | 优点 ✅ | 缺点 ❌ |
|---|---|---|
| 手写 HTTP 请求 |
|
|
| 手写 OpenRouter Starter |
|
|
还有 LangChain4j,但我对 LangChain4j 了解有限,此处不做深入评价。仅从官方文档粗略来看,它通过 customParameters 提供了传递额外参数的通道,标准 OpenAI 参数覆盖较全;是否完整支持 OpenRouter 的全部返回参数和嵌套结构,有待进一步验证。
想手写 HTTP 请求的同学可以看看 OpenRouter 官方的 API 文档:OpenRouter 官方 API 文档
LangChain4j 的官方文档在这里:LangChain4j 官方文档
那么我们开始进入查看源码环节,准备手写 OpenRouter Starter 吧
查看源码准备
在查看源码时,我将使用 spring-ai-starter-model-deepseek 来进行阅读,因为 deepseek 同样支持思考模式,并且有属于自己的 AssistantMessage(DeepSeekAssistantMessage),所以研读源码时,就是读 deepseek 相关代码实现了
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
<version>1.1.4</version>
</dependency>
在看源码时我们在有一个例子的情况下来读会更清晰,我们的例子就如下:
@Resource
private DeepSeekChatModel deepseekChatModel;
/**
* 流式聊天
*
* @return
*/
@GetMapping(value = "/generateStream", produces = "text/event-stream;charset=utf-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
// 创建 ChatClient
ChatClient.ChatClientRequestSpec chatClientRequestSpec = ChatClient.create(deepseekChatModel)
.prompt()
// 构建配置
.options(DeepSeekChatOptions.builder()
.model(DeepSeekApi.ChatModel.DEEPSEEK_CHAT)
.build())
.user(message);
// 创建 advisors
List<Advisor> advisors = new ArrayList<>();
// 添加自定义的 ChatMemoryAdvisor
advisors.add(new CustomChatMemoryAdvisor());
// 创建 tools
List<Object> tools = new ArrayList<>();
// 添加 DateTimeTools
tools.add(new DateTimeTools());
// 添加 advisors
chatClientRequestSpec.advisors(advisors);
// 添加 tools
chatClientRequestSpec.tools(tools);
return chatClientRequestSpec.stream()
.chatResponse()
.mapNotNull(chatResponse -> {
// 获取响应内容
DeepSeekAssistantMessage assistantMessage = (DeepSeekAssistantMessage) chatResponse.getResult().getOutput();
log.info("==> 输出内容: {}", assistantMessage.getText());
log.info("==> 思考内容: {}", assistantMessage.getReasoningContent());
return assistantMessage.getText();
});
}
当前请求进入到接口中会先执行下面的代码:
// 创建 ChatClient
ChatClient.ChatClientRequestSpec chatClientRequestSpec = ChatClient.create(deepseekChatModel)
.prompt()
// 构建配置
.options(DeepSeekChatOptions.builder()
.model(DeepSeekApi.ChatModel.DEEPSEEK_CHAT)
.build())
.user(message);
这里先执行 ChatClient.create(deepseekChatModel).prompt().options(...).user(...);,那 ChatClient 有什么?他是用来干什么的呢?
ChatClient
相关源码阅读
我们在 IDEA 中使用 Ctrl + 左键,可以看看里面的具体实现,我们这里就截取根据例子的重要代码来看:
/**
* Spring AI 的聊天客户端顶层门面(Facade)接口。
*
* 定位:作为业务代码与底层 ChatModel 之间的"一站式柜台",屏蔽不同模型厂商的差异,
* 统一提供 Prompt 构建、选项配置、Advisor 拦截、工具注册、同步/流式调用等能力。
*
* 设计特点:
* 1. 工厂模式:通过静态 create()/builder() 方法快速创建默认实现(DefaultChatClient)。
* 2. 双层构建:外层 Builder 配置全局默认值(defaultSystem、defaultAdvisors 等),
* 内层 ChatClientRequestSpec 配置单次请求的运行时参数(user、options、advisors 等)。
* 3. 宽接口设计:对外暴露丰富的链式 API,但底层实现只需关注最终的 Prompt → ChatModel 调用。
* 4. 响应多态:同步返回 CallResponseSpec,流式返回 StreamResponseSpec,均支持 String / ChatResponse / 结构化实体 多种取法。
*/
public interface ChatClient {
/**
* 快速创建 ChatClient 实例,使用 NOOP 观察注册表(不采集监控指标)。
* 最简工厂方法,适合不需要 Micrometer 监控的场景。
*/
static ChatClient create(ChatModel chatModel) {
return create(chatModel, ObservationRegistry.NOOP);
}
/**
* 创建 ChatClient 实例,并指定 ObservationRegistry 用于采集调用链路和性能指标。
* 适合生产环境接入监控体系。
*/
static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry) {
return create(chatModel, observationRegistry, (ChatClientObservationConvention)null, (AdvisorObservationConvention)null);
}
/**
* 完整版工厂方法:指定 ChatModel、ObservationRegistry,以及自定义的观察约定。
* 允许替换默认的 ChatClientObservationConvention 和 AdvisorObservationConvention,
* 用于自定义监控埋点字段和 Advisor 观测行为。
*/
static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry, @Nullable ChatClientObservationConvention chatClientObservationConvention, @Nullable AdvisorObservationConvention advisorObservationConvention) {
Assert.notNull(chatModel, "chatModel cannot be null");
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
return builder(chatModel, observationRegistry, chatClientObservationConvention, advisorObservationConvention).build();
}
/**
* 创建 Builder,使用 NOOP 观察注册表和默认观察约定。
* 用于需要手动配置全局默认值后再 build() 的场景。
*/
static Builder builder(ChatModel chatModel) {
return builder(chatModel, ObservationRegistry.NOOP, (ChatClientObservationConvention)null, (AdvisorObservationConvention)null);
}
/**
* 完整版 Builder 工厂:支持自定义 ObservationRegistry 和观察约定。
* 所有 create() 方法最终都委托到这里创建 DefaultChatClientBuilder。
*/
static Builder builder(ChatModel chatModel, ObservationRegistry observationRegistry, @Nullable ChatClientObservationConvention chatClientObservationConvention, @Nullable AdvisorObservationConvention advisorObservationConvention) {
Assert.notNull(chatModel, "chatModel cannot be null");
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
return new DefaultChatClientBuilder(chatModel, observationRegistry, chatClientObservationConvention, advisorObservationConvention);
}
/**
* 开启一次新的请求构建,返回空的 ChatClientRequestSpec。
* 链式调用的起点:chatClient.prompt().user("你好").call()...
*/
ChatClientRequestSpec prompt();
// ...省略
// .... AdvisorSpec、 Builder 和 CallResponseSpec 的接口
/**
* 单次请求构建规范(运行时构建器)。
*
* 职责:链式组装一次具体请求所需的所有要素:消息、选项、Advisor、工具。
* 所有配置均为"运行时覆盖",优先级高于 Builder 配置的全局默认值。
*
* 调用 .call() 或 .stream() 时,会将累积的配置打包为 Prompt 并提交给 ChatModel。
*/
public interface ChatClientRequestSpec {
// ...省略
/**
* 添加 Advisor 列表到本次请求。
*/
ChatClientRequestSpec advisors(List<Advisor> advisors);
/**
* 添加 Message 列表到本次请求。
*/
ChatClientRequestSpec messages(List<Message> messages);
/**
* 配置本次请求的模型选项(运行时选项,会覆盖全局默认值)。
*/
<T extends ChatOptions> ChatClientRequestSpec options(T options);
// ...省略
/**
* 添加工具对象到本次请求(自动注册为 ToolCallback)。
*/
ChatClientRequestSpec tools(Object... toolObjects);
// ...省略
/**
* 设置本次请求的用户文本(单轮对话最常用)。
*/
ChatClientRequestSpec user(String text);
// ...省略
}
// ...PromptSystemSpec、PromptUserSpec 和 StreamResponseSpec 接口
}
可以看到 ChatClient 也仅是一个接口,规定接入实现类需要重写什么方法,并给了快速创建的静态方法 create,我们可以继续往下看,看实现类做了什么
/**
* ChatClient 接口的默认实现类,是 Spring AI 聊天调用的核心编排器。
*
* 定位:作为业务代码与 ChatModel 之间的"总指挥",负责:
* 1. 接收并累积用户通过链式 API 配置的参数(消息、选项、Advisor、工具等);
* 2. 构建 Advisor 调用链(Chain of Responsibility),在请求前后插入横切逻辑;
* 3. 自动在 Advisor 链尾部追加 ChatModelCallAdvisor / ChatModelStreamAdvisor,确保最终一定会调用底层模型;
* 4. 包装 Micrometer Observation,采集 ChatClient 级别的监控指标;
* 5. 提供同步(DefaultCallResponseSpec)和流式(DefaultStreamResponseSpec)两种响应封装。
*
* 内部结构:
* - DefaultChatClientRequestSpec:攒参数的"大仓库",链式调用的实际载体;
* - DefaultCallResponseSpec / DefaultStreamResponseSpec:结果取法的"包装器";
* - DefaultPromptUserSpec / DefaultPromptSystemSpec / DefaultAdvisorSpec:辅助构建器。
*/
public class DefaultChatClient implements ChatClient {
// 默认的 ChatClient 观察约定,用于 Micrometer 监控埋点
private static final ChatClientObservationConvention DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION = new DefaultChatClientObservationConvention();
// 默认模板渲染器,使用 StringTemplate(ST)引擎处理 {{变量}} 占位符
private static final TemplateRenderer DEFAULT_TEMPLATE_RENDERER = StTemplateRenderer.builder().build();
// 流式消息聚合器,负责把 Flux 中的分片消息拼接为完整的 ChatClientResponse
private static final ChatClientMessageAggregator CHAT_CLIENT_MESSAGE_AGGREGATOR = new ChatClientMessageAggregator();
// 全局默认请求规范,包含 Builder 阶段配置的所有默认值(默认系统提示、默认 Advisor、默认工具等)
// 每次 prompt() 都会基于它克隆出新的 DefaultChatClientRequestSpec,保证全局配置不被污染
private final DefaultChatClientRequestSpec defaultChatClientRequest;
/**
* 构造方法:依赖一个已经组装好的 DefaultChatClientRequestSpec(通常由 DefaultChatClientBuilder 构建)。
*/
public DefaultChatClient(DefaultChatClientRequestSpec defaultChatClientRequest){
Assert.notNull(defaultChatClientRequest, "defaultChatClientRequest cannot be null");
this.defaultChatClientRequest = defaultChatClientRequest;
}
/**
* 开启新的请求构建:基于全局默认值克隆出一个新的 DefaultChatClientRequestSpec。
* 这是无参 prompt() 的入口,返回后可继续链式配置。
*/
public ChatClient.ChatClientRequestSpec prompt() {
return new DefaultChatClientRequestSpec(this.defaultChatClientRequest);
}
// ...省略
// ... PromptUserSpec 的默认实现:攒用户消息的各个零件。
// 包含:文本、多媒体附件(Media)、模板参数(params)、元数据(metadata)。
// 最终被 DefaultChatClientRequestSpec.user(Consumer<PromptUserSpec>) 消费,合并到主 Spec。
// ... PromptSystemSpec 的默认实现:攒系统提示的各个零件。
// 结构与 DefaultPromptUserSpec 类似,但不支持多媒体(SystemMessage 通常纯文本)。
// ... AdvisorSpec 的默认实现:收集 Advisor 实例及其参数。
// 用于 Builder.defaultAdvisors(Consumer<AdvisorSpec>) 或 RequestSpec.advisors(Consumer<AdvisorSpec>) 场景。
// ... 同步调用响应的默认实现:封装 advisorChain.nextCall() 的结果,
// 提供 content() / chatResponse() / entity() / responseEntity() 等多种取数方式。
// ... 流式调用响应的默认实现:封装 advisorChain.nextStream() 的结果,
// 返回 Flux 系列,支持背压和逐块消费。
/**
* ChatClientRequestSpec 的默认实现:Spring AI 链式调用的"大仓库"。
*
* 职责:累积一次请求所需的所有要素:
* - 用户/系统文本、模板参数、多媒体
* - ChatOptions(模型、温度等)
* - Advisors(拦截器链)
* - Tools(工具名称、回调、Provider、上下文)
* - 已有 Message 列表
*
* 最终通过 call() 或 stream() 触发:
* 1. buildAdvisorChain() 组装 Advisor 链(自动追加 ChatModelCallAdvisor / ChatModelStreamAdvisor);
* 2. 将累积的参数打包为 ChatClientRequest;
* 3. 创建 DefaultCallResponseSpec 或 DefaultStreamResponseSpec 返回。
*/
public static class DefaultChatClientRequestSpec implements ChatClient.ChatClientRequestSpec {
private final ObservationRegistry observationRegistry;
private final ChatClientObservationConvention chatClientObservationConvention;
@Nullable private final AdvisorObservationConvention advisorObservationConvention;
private final ChatModel chatModel; // 底层模型
private final List<Media> media; // 用户多媒体附件
private final List<String> toolNames; // 按名称引用的工具
private final List<ToolCallback> toolCallbacks; // 直接注册的工具回调
private final List<ToolCallbackProvider> toolCallbackProviders; // 批量提供工具回调
private final List<Message> messages; // 已有的消息列表
private final Map<String, Object> userParams; // 用户文本模板参数
private final Map<String, Object> userMetadata; // 用户消息元数据
private final Map<String, Object> systemParams; // 系统文本模板参数
private final Map<String, Object> systemMetadata; // 系统消息元数据
private final List<Advisor> advisors; // 本次请求的 Advisor 列表
private final Map<String, Object> advisorParams; // Advisor 共享参数
private final Map<String, Object> toolContext; // 工具上下文
private TemplateRenderer templateRenderer; // 模板渲染器
@Nullable private String userText; // 用户文本
@Nullable private String systemText; // 系统文本
@Nullable private ChatOptions chatOptions; // 运行时模型选项
/**
* 拷贝构造方法:基于另一个 DefaultChatClientRequestSpec 深拷贝所有字段。
* 用于 prompt() 时从全局默认值克隆出独立的运行时 Spec。
*/
DefaultChatClientRequestSpec(DefaultChatClientRequestSpec ccr) {
this(ccr.chatModel, ccr.userText, ccr.userParams, ccr.userMetadata, ccr.systemText, ccr.systemParams, ccr.systemMetadata, ccr.toolCallbacks, ccr.toolCallbackProviders, ccr.messages, ccr.toolNames, ccr.media, ccr.chatOptions, ccr.advisors, ccr.advisorParams, ccr.observationRegistry, ccr.chatClientObservationConvention, ccr.toolContext, ccr.templateRenderer, ccr.advisorObservationConvention);
}
/**
* 全量构造方法:由 DefaultChatClientBuilder 调用,一次性传入所有配置项。
* 内部对 ChatOptions 做防御性拷贝(copy()),防止外部修改污染内部状态。
*/
public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userText, Map<String, Object> userParams, Map<String, Object> userMetadata, @Nullable String systemText, Map<String, Object> systemParams, Map<String, Object> systemMetadata, List<ToolCallback> toolCallbacks, List<ToolCallbackProvider> toolCallbackProviders, List<Message> messages, List<String> toolNames, List<Media> media, @Nullable ChatOptions chatOptions, List<Advisor> advisors, Map<String, Object> advisorParams, ObservationRegistry observationRegistry, @Nullable ChatClientObservationConvention chatClientObservationConvention, Map<String, Object> toolContext, @Nullable TemplateRenderer templateRenderer, @Nullable AdvisorObservationConvention advisorObservationConvention) {
// ...省略
this.chatOptions = chatOptions != null ? chatOptions.copy() : (chatModel.getDefaultOptions() != null ? chatModel.getDefaultOptions().copy() : null);
// ...省略
this.chatClientObservationConvention = chatClientObservationConvention != null ? chatClientObservationConvention : DefaultChatClient.DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION;
// ...省略
this.templateRenderer = templateRenderer != null ? templateRenderer : DefaultChatClient.DEFAULT_TEMPLATE_RENDERER;
// ..省略
}
// ...各字段的 getter,供外部(如 DefaultChatClientUtils)读取内部状态
// ...省略
/**
* 添加 Advisor 列表。
*/
public ChatClient.ChatClientRequestSpec advisors(List<Advisor> advisors) {
Assert.notNull(advisors, "advisors cannot be null");
Assert.noNullElements(advisors, "advisors cannot contain null elements");
this.advisors.addAll(advisors);
return this;
}
/**
* 添加一个或多个 Message(如历史消息)。
*/
public ChatClient.ChatClientRequestSpec messages(Message... messages) {
Assert.notNull(messages, "messages cannot be null");
Assert.noNullElements(messages, "messages cannot contain null elements");
this.messages.addAll(List.of(messages));
return this;
}
/**
* 设置运行时模型选项(覆盖 Builder 阶段的全局默认值)。
*/
public <T extends ChatOptions> ChatClient.ChatClientRequestSpec options(T options) {
Assert.notNull(options, "options cannot be null");
this.chatOptions = options;
return this;
}
// ...省略
/**
* 添加工具对象,内部通过 ToolCallbacks.from() 自动转换为 ToolCallback。
*/
public ChatClient.ChatClientRequestSpec tools(Object... toolObjects) {
Assert.notNull(toolObjects, "toolObjects cannot be null");
Assert.noNullElements(toolObjects, "toolObjects cannot contain null elements");
this.toolCallbacks.addAll(Arrays.asList(ToolCallbacks.from(toolObjects)));
return this;
}
// ...省略
/**
* 设置用户文本。
*/
public ChatClient.ChatClientRequestSpec user(String text) {
Assert.hasText(text, "text cannot be null or empty");
this.userText = text;
return this;
}
// ...省略
/**
* 触发同步调用:
* 1. buildAdvisorChain() 组装 Advisor 链;
* 2. 将当前 Spec 转换为 ChatClientRequest;
* 3. 创建 DefaultCallResponseSpec 返回。
*/
public ChatClient.CallResponseSpec call() {
BaseAdvisorChain advisorChain = this.buildAdvisorChain();
return new DefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain, this.observationRegistry, this.chatClientObservationConvention);
}
/**
* 触发流式调用:
* 1. buildAdvisorChain() 组装 Advisor 链;
* 2. 将当前 Spec 转换为 ChatClientRequest;
* 3. 创建 DefaultStreamResponseSpec 返回。
*/
public ChatClient.StreamResponseSpec stream() {
BaseAdvisorChain advisorChain = this.buildAdvisorChain();
return new DefaultStreamResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain, this.observationRegistry, this.chatClientObservationConvention);
}
/**
* 构建 Advisor 调用链(核心方法):
* 1. 自动追加 ChatModelCallAdvisor(处理同步调用,内部调用 chatModel.call());
* 2. 自动追加 ChatModelStreamAdvisor(处理流式调用,内部调用 chatModel.stream());
* 3. 使用 DefaultAroundAdvisorChain 将所有 Advisor(含用户自定义 + 自动追加的)组装为链;
* 4. 返回 BaseAdvisorChain,供后续 nextCall() / nextStream() 遍历执行。
*
* 关键点:无论用户是否添加自定义 Advisor,链尾一定会走到模型调用。
*/
private BaseAdvisorChain buildAdvisorChain() {
this.advisors.add(ChatModelCallAdvisor.builder().chatModel(this.chatModel).build());
this.advisors.add(ChatModelStreamAdvisor.builder().chatModel(this.chatModel).build());
return DefaultAroundAdvisorChain.builder(this.observationRegistry).observationConvention(this.advisorObservationConvention).pushAll(this.advisors).build();
}
}
}
总结
前面我们就是对 ChatClient 进行了一个基本的了解,现在我们用例子来看源码的具体实现
第一步,创建 ChatClient
// 创建 ChatClient
ChatClient.create(deepseekChatModel)
ChatClient.create(deepseekChatModel)是快捷入口,内部通过静态方法重载链自动补全默认参数:
没有传观察注册表?默认补一个 NOOP(空操作,不采集监控);
没有传观察约定?默认补 null(使用框架内置约定)。最终这些参数被交给 DefaultChatClientBuilder。
DefaultChatClientBuilder.build() 内部会创建一个 DefaultChatClientRequestSpec,此时所有字段几乎都是空(userText=null、chatOptions=null、advisors=[]),这就是全局默认配置(母版)。然后把它塞进 DefaultChatClient 里。
在ChatClient.create(deepseekChatModel)中走了如下流程
先在 ChatClient 调用 create,通过 create 的静态方法重载自动补全默认参数(NOOP 观察注册表、空观察约定)
public interface ChatClient {
// 通过 create 的静态方法补全默认参数NOOP 观察注册表
static ChatClient create(ChatModel chatModel) {
return create(chatModel, ObservationRegistry.NOOP);
}
// 补全空观察约定
static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry) {
return create(chatModel, observationRegistry, (ChatClientObservationConvention)null, (AdvisorObservationConvention)null);
}
// 将补充好的参数调用 builder 准备构造 ChatClient
static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry, @Nullable ChatClientObservationConvention chatClientObservationConvention, @Nullable AdvisorObservationConvention advisorObservationConvention) {
Assert.notNull(chatModel, "chatModel cannot be null");
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
return builder(chatModel, observationRegistry, chatClientObservationConvention, advisorObservationConvention).build();
}
// ...省略
// 使用 DefaultChatClientBuilder 构建 ChatClient 的建造者 Builder
static Builder builder(ChatModel chatModel, ObservationRegistry observationRegistry, @Nullable ChatClientObservationConvention chatClientObservationConvention, @Nullable AdvisorObservationConvention advisorObservationConvention) {
Assert.notNull(chatModel, "chatModel cannot be null");
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
return new DefaultChatClientBuilder(chatModel, observationRegistry, chatClientObservationConvention, advisorObservationConvention);
}
// ...省略
}
进入到 DefaultChatClientBuilder(...) 中,方法里面调用了 DefaultChatClient.DefaultChatClientRequestSpec() 有参构造方法方法 , 并填写了一些空参数
public class DefaultChatClientBuilder implements ChatClient.Builder {
protected final DefaultChatClient.DefaultChatClientRequestSpec defaultRequest;
// ...省略
// 调用 DefaultChatClient.DefaultChatClientRequestSpec 构建 DefaultChatClient.DefaultChatClientRequestSpec 对象
public DefaultChatClientBuilder(ChatModel chatModel, ObservationRegistry observationRegistry, @Nullable ChatClientObservationConvention chatClientObservationConvention, @Nullable AdvisorObservationConvention advisorObservationConvention) {
Assert.notNull(chatModel, "the " + ChatModel.class.getName() + " must be non-null");
Assert.notNull(observationRegistry, "the " + ObservationRegistry.class.getName() + " must be non-null");
this.defaultRequest = new DefaultChatClient.DefaultChatClientRequestSpec(chatModel, (String)null, Map.of(), Map.of(), (String)null, Map.of(), Map.of(), List.of(), List.of(), List.of(), List.of(), List.of(), (ChatOptions)null, List.of(), Map.of(), observationRegistry, chatClientObservationConvention, Map.of(), (TemplateRenderer)null, advisorObservationConvention);
}
// ...省略
}
继续查看 new DefaultChatClient.DefaultChatClientRequestSpec(...) 的有参构造,可以发现实际是往里面塞一些空的调用模型辅助参数如:chatModel、media、toolCallbacks、messages(消息列表)、userText(用户文本) 等
public class DefaultChatClientBuilder implements ChatClient.Builder {
// ...省略
public static class DefaultChatClientRequestSpec implements ChatClient.ChatClientRequestSpec {
// ...省略
public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userText, Map<String, Object> userParams, Map<String, Object> userMetadata, @Nullable String systemText, Map<String, Object> systemParams, Map<String, Object> systemMetadata, List<ToolCallback> toolCallbacks, List<ToolCallbackProvider> toolCallbackProviders, List<Message> messages, List<String> toolNames, List<Media> media, @Nullable ChatOptions chatOptions, List<Advisor> advisors, Map<String, Object> advisorParams, ObservationRegistry observationRegistry, @Nullable ChatClientObservationConvention chatClientObservationConvention, Map<String, Object> toolContext, @Nullable TemplateRenderer templateRenderer, @Nullable AdvisorObservationConvention advisorObservationConvention) {
// ...参数填入省略部分
this.toolNames.addAll(toolNames);
this.toolCallbacks.addAll(toolCallbacks);
this.toolCallbackProviders.addAll(toolCallbackProviders);
this.messages.addAll(messages);
this.media.addAll(media);
this.advisors.addAll(advisors);
this.advisorParams.putAll(advisorParams);
this.observationRegistry = observationRegistry;
this.chatClientObservationConvention = chatClientObservationConvention != null ? chatClientObservationConvention : DefaultChatClient.DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION;
this.toolContext.putAll(toolContext);
this.templateRenderer = templateRenderer != null ? templateRenderer : DefaultChatClient.DEFAULT_TEMPLATE_RENDERER;
this.advisorObservationConvention = advisorObservationConvention;
}
}
// ...省略
}
这一整条调用链我们可以用图片表示

第二步,生成运行时副本
// 创建 ChatClient
ChatClient.create(deepseekChatModel)
.prompt()
当调用 .prompt() 时,DefaultChatClient 不会直接把DefaultChatClientRequestSpec母版给你(防止污染),而是调用拷贝构造方法,深克隆出一个全新的 DefaultChatClientRequestSpec。这个副本才是你后续 .options()、.user() 操作的对象,也就是接口返回的 ChatClientRequestSpec。
public class DefaultChatClient implements ChatClient {
// ...省略
public ChatClient.ChatClientRequestSpec prompt() {
return new DefaultChatClientRequestSpec(this.defaultChatClientRequest);
}
// ...省略
public static class DefaultChatClientRequestSpec implements ChatClient.ChatClientRequestSpec {
// ...省略
DefaultChatClientRequestSpec(DefaultChatClientRequestSpec ccr) {
this(ccr.chatModel, ccr.userText, ccr.userParams, ccr.userMetadata, ccr.systemText, ccr.systemParams, ccr.systemMetadata, ccr.toolCallbacks, ccr.toolCallbackProviders, ccr.messages, ccr.toolNames, ccr.media, ccr.chatOptions, ccr.advisors, ccr.advisorParams, ccr.observationRegistry, ccr.chatClientObservationConvention, ccr.toolContext, ccr.templateRenderer, ccr.advisorObservationConvention);
}
// ...省略
}
// ...省略
}
第三步,赋值 ChatOptions 和相关内容
// 创建 ChatClient
ChatClient.ChatClientRequestSpec chatClientRequestSpec = ChatClient.create(deepseekChatModel)
.prompt()
// 构建配置
.options(DeepSeekChatOptions.builder()
.model(DeepSeekApi.ChatModel.DEEPSEEK_CHAT)
.build())
.user(message);
// 创建 advisors
List<Advisor> advisors = new ArrayList<>();
// 添加自定义的 ChatMemoryAdvisor
advisors.add(new CustomChatMemoryAdvisor());
// 创建 tools
List<Object> tools = new ArrayList<>();
// 添加 DateTimeTools
tools.add(new DateTimeTools());
// 添加 advisors
chatClientRequestSpec.advisors(advisors);
chatClientRequestSpec.tools(tools);
构建配置则是直接将传入的 ChatOptions 赋值到 ChatClient.ChatClientRequestSpec 的 chatOptions 对象中,
message 则是赋值到 ChatClient.ChatClientRequestSpec 的 userText 中,
advisors 直接添加到 ChatClient.ChatClientRequestSpec 的 advisors,
tools 则会经过 ToolCallbacks.from() 转化 List<ToolCallback> 类型的 toolCallbacks
public class DefaultChatClient implements ChatClient {
// ...省略
public static class DefaultChatClientRequestSpec implements ChatClient.ChatClientRequestSpec
// ...省略
public <T extends ChatOptions> ChatClient.ChatClientRequestSpec options(T options) {
Assert.notNull(options, "options cannot be null");
this.chatOptions = options;
return this;
}
// ...省略
public ChatClient.ChatClientRequestSpec user(String text) {
Assert.hasText(text, "text cannot be null or empty");
this.userText = text;
return this;
}
// ...省略
public ChatClient.ChatClientRequestSpec advisors(List<Advisor> advisors) {
Assert.notNull(advisors, "advisors cannot be null");
Assert.noNullElements(advisors, "advisors cannot contain null elements");
this.advisors.addAll(advisors);
return this;
}
// ...省略
public ChatClient.ChatClientRequestSpec tools(Object... toolObjects) {
Assert.notNull(toolObjects, "toolObjects cannot be null");
Assert.noNullElements(toolObjects, "toolObjects cannot contain null elements");
this.toolCallbacks.addAll(Arrays.asList(ToolCallbacks.from(toolObjects)));
return this;
}
// ...省略
}
// ...省略
}
到了这里我们基本上就完成了调用模型前的一些辅助参数配置了,如果需要对模型的 temperature、topP 等调参则可以直接在 ChatOptions 进行添加,在下一章我们开始讲解 Spring AI 是如何请求到模型中的。
补充
继续深入探究 tool 的装配
在 ChatClient 章节介绍 tools 装配到 chatClientRequestSpec 中只有简单的一句话和一个图
tools则会经过ToolCallbacks.from()转化List<ToolCallback>类型的toolCallbacks
但实际上真有那么简单吗?
chatClientRequestSpec.tools(tools);
在 DefaultChatClient.DefaultChatClientRequestSpec 的 tools 中究竟做了些什么呢?
public ChatClient.ChatClientRequestSpec tools(Object... toolObjects) {
Assert.notNull(toolObjects, "toolObjects cannot be null");
Assert.noNullElements(toolObjects, "toolObjects cannot contain null elements");
this.toolCallbacks.addAll(Arrays.asList(ToolCallbacks.from(toolObjects)));
return this;
}
我们可以看到里面最主要的逻辑就是 ToolCallbacks.from(toolObjects),我们点进去继续往下看 ToolCallbacks.from(...) 方法做了什么。
// 注意,这里只有一段代码
public final class ToolCallbacks {
private ToolCallbacks() {
}
public static ToolCallback[] from(Object... sources) {
// 调用了工具回调的方法代理
return MethodToolCallbackProvider.builder().toolObjects(sources).build().getToolCallbacks();
}
}
他里面又套了一层,构建了 MethodToolCallbackProvider 对象后,继续链式调用 getToolCallbacks() 方法。最后是返回了 ToolCallback[] 数组。
这里先不进行源码查看了,因为我进去后发现这个方法涉及的东西比较多,大家能看到这里显然已经足够有耐心了,但还是为了不消耗大家耐心,并且对 Spring AI 工具回调生成流程的思维更清晰,我们对里面的方法进行一个小小的总结描述。
小总结

正式解读
进行了一波小小的总结后,现在我们就开始啃后面的源码吧!!!
MethodToolCallbackProvider.builder().toolObjects(sources).build().getToolCallbacks();
我们先解读该段代码的前半部分 MethodToolCallbackProvider.builder().toolObjects(sources).build()
进入到 MethodToolCallbackProvider 中,可以看到在 toolObjects(...) 方法里仅仅只是对 List<Object> toolObjects 进行了赋值。在 build() 方法里,将 toolObjects 传参到了自身的有参构造中 。
/**
* 基于普通 Java 对象中标注了 @Tool 的方法来提供 ToolCallback 的实现。
* 它会扫描传入的对象列表,找出所有合法的工具方法,并为每个方法创建一个 MethodToolCallback。
* 这个类在构造时就进行静态校验(例如每个对象必须至少有一个工具方法,方法名不能重复等),
* 确保在应用启动阶段就能发现问题。
*/
public final class MethodToolCallbackProvider implements ToolCallbackProvider {
private static final Logger logger = LoggerFactory.getLogger(MethodToolCallbackProvider.class);
/** 用户传入的、包含 @Tool 方法的普通对象列表 */
private final List<Object> toolObjects;
// ...省略
/**
* 建造者类,用于逐步设置工具对象并最终创建 Provider。
*/
public static final class Builder {
private List<Object> toolObjects;
// 私有构造器,只能通过 builder() 获取
private Builder() {
}
/**
* 设置工具对象列表。
* 参数为可变参数,支持传入多个对象。
*
* @param toolObjects 含有 @Tool 方法的对象
* @return Builder 自身,链式调用
*/
public Builder toolObjects(Object... toolObjects) {
Assert.notNull(toolObjects, "toolObjects cannot be null");
// 将可变参数转换为不可变(或至少固定)的 List 并保存
this.toolObjects = Arrays.asList(toolObjects);
return this;
}
/**
* 构建 MethodToolCallbackProvider 实例,触发构造阶段的各项校验。
*
* @return 构建好的 Provider
*/
public MethodToolCallbackProvider build() {
// 调用私有构造器,完成对象创建与初始化校验
return new MethodToolCallbackProvider(this.toolObjects);
}
}
}
接下来我们对 new MethodToolCallbackProvider(this.toolObjects) 调用有参构造进行解读。同样是在 MethodToolCallbackProvider 中:
public final class MethodToolCallbackProvider implements ToolCallbackProvider {
// ...省略
/**
* 私有构造器
* 完成校验、工具方法存在性检查、提前名称冲突检测等初始化工作。
*
* @param toolObjects 不能为 null,不能包含 null 元素
*/
private MethodToolCallbackProvider(List<Object> toolObjects) {
// 基础断言:列表自身和其元素都不允许为 null
Assert.notNull(toolObjects, "toolObjects cannot be null");
Assert.noNullElements(toolObjects, "toolObjects cannot contain null elements");
// 确保每个工具对象上至少有一个被 @Tool 注解的方法,
// 如果某个对象一个都没有,会直接抛出异常,避免后续无意义的调用。
this.assertToolAnnotatedMethodsPresent(toolObjects);
// 保存工具对象列表
this.toolObjects = toolObjects;
// 提前执行一次工具回调构建和名称重复校验,以便在构造阶段失败快速。
// 注意:这里调用了 getToolCallbacks() 但并未保存结果,只是用于验证。
this.validateToolCallbacks(this.getToolCallbacks());
}
// ...省略
}
里面调用了自身的两个方法分别是 this.assertToolAnnotatedMethodsPresent(...) 和 this.validateToolCallbacks(...) ,单从名字看这两个方法是 assertToolAnnotatedMethodsPresent:断言工具注释方法存在 和 validateToolCallbacks:验证工具回调函数,可以看到他们也不返回任何值,他们有可能仅是进行了校验,我们深入探究下他们实际干了什么。
同样是在当前的 MethodToolCallbackProvider 中,我们先看 assertToolAnnotatedMethodsPresent 方法
this.assertToolAnnotatedMethodsPresent(List<Object>)
public final class MethodToolCallbackProvider implements ToolCallbackProvider {
// ...省略
/**
* 检查列表中的每一个对象是否至少有一个合法的 @Tool 方法。
* 如果没有,则抛出异常并给出提示,防止误将非工具对象传入。
*
* @param toolObjects 待检查的工具对象列表
*/
private void assertToolAnnotatedMethodsPresent(List<Object> toolObjects) {
// 源代码
// for(Object toolObject : toolObjects) {
// List<Method> toolMethods = Stream.of(ReflectionUtils.getDeclaredMethods(AopUtils.isAopProxy(toolObject) ? AopUtils.getTargetClass(toolObject) : toolObject.getClass())).filter(this::isToolAnnotatedMethod).filter((toolMethod) -> !this.isFunctionalType(toolMethod)).toList();
// if (toolMethods.isEmpty()) {
// throw new IllegalStateException("No @Tool annotated methods found in " + String.valueOf(toolObject) + ".Did you mean to pass a ToolCallback or ToolCallbackProvider? If so, you have to use .toolCallbacks() instead of .tool()");
// }
// }
// 优化后
// 遍历每个传入的工具对象
for (Object toolObject : toolObjects) {
// 获取对象的真实 Class:
// 如果是 AOP 代理(JDK 动态代理或 CGLIB),则获取其目标类
// 否则直接取 getClass()
Class<?> targetClass = AopUtils.isAopProxy(toolObject)
? AopUtils.getTargetClass(toolObject)
: toolObject.getClass();
// 通过 ReflectionUtils 获取该类的所有 “声明的方法”:
// 包含类自身定义的方法以及实现的接口中的 default 方法。
Method[] declaredMethods = ReflectionUtils.getDeclaredMethods(targetClass);
// 使用 Stream 对方法进行过滤,只保留:
// 标注了 @Tool 注解的方法
// 返回值不是 Function/Supplier/Consumer 的函数式接口的方法
List<Method> toolMethods = Stream.of(declaredMethods)
// 必须有 @Tool
.filter(this::isToolAnnotatedMethod)
// 排除函数式返回值
.filter(toolMethod -> !this.isFunctionalType(toolMethod))
// 收集成不可变列表(Java 16+)
.toList();
// 如果过滤后没有任何方法,说明该对象可能被错误传入,
// 抛出明确的异常并建议使用 .toolCallbacks() 方式。
if (toolMethods.isEmpty()) {
throw new IllegalStateException(
"No @Tool annotated methods found in " + String.valueOf(toolObject)
+ ". Did you mean to pass a ToolCallback or ToolCallbackProvider? "
+ "If so, you have to use .toolCallbacks() instead of .tool()"
);
}
}
}
// ...省略
}
代码虽然看起来比较少,但是我们分 3 段来看,第一段参数处理,第二段过滤处理,第三段就是返回结果,这里我们重点看前面两段
第一段代码
我们一路向下看可以看到循环中第一段代码
// 获取对象的真实 Class:
// 如果是 AOP 代理(JDK 动态代理或 CGLIB),则获取其目标类
// 否则直接取 getClass()
Class<?> targetClass = AopUtils.isAopProxy(toolObject)
? AopUtils.getTargetClass(toolObject)
: toolObject.getClass();
// 通过 ReflectionUtils 获取该类的所有 “声明的方法”:
// 包含类自身定义的方法以及实现的接口中的 default 方法。
Method[] declaredMethods = ReflectionUtils.getDeclaredMethods(targetClass);
在上面的代码中可以看到用了两个工具类分别为 AopUtils 和 ReflectionUtils,在这里我们就只讨论使用到了的方法,就不花过多的篇幅讨论其他信息了。AopUtils 使用了 isAopProxy()、getTargetClass() 方法,这些方法里面还会有更多的工具类调用的处理,这里我们只用简单的注释说明,有兴趣的同学可以看着源码多学习下
/**
* Spring AOP 工具类,提供代理对象判断、目标类获取等静态方法。
*
* 在 Spring AI 的场景中,如果工具对象被 Spring AOP 增强了(例如添加了 @Transactional),
* 它会被包装成 JDK 动态代理或 CGLIB 代理。代理对象可能隐藏原始类上的 @Tool 注解,
* 因此需要本类的几个方法来"穿透"代理,获取真正的业务类进行反射扫描。
*
*
*/
public abstract class AopUtils {
/**
* 判读当前环境中是否存在 Kotlin 协程的 Reactor 集成库。
* 如果存在,后续反射调用时需要特殊处理挂起函数。
* 这里与工具方法识别无关,可以忽略。
*/
private static final boolean coroutinesReactorPresent = ClassUtils.isPresent("kotlinx.coroutines.reactor.MonoKt", AopUtils.class.getClassLoader());
/**
* 判断一个对象是否是 Spring AOP 代理。
* Spring 的 AOP 代理有两种形式:JDK 动态代理 和 CGLIB 代理,
* 这个方法会同时匹配这两种情况。
*/
public static boolean isAopProxy(@Nullable Object object) {
// 条件一:对象必须实现 SpringProxy 标记接口
// Spring 生成的所有代理对象都会自动实现该接口,它是 Spring 代理的"身份证"
// 条件二:类名满足代理特征
// Proxy.isProxyClass(...):JDK 动态代理类的类名以 "$Proxy" 开头
// 类名包含 "$$":CGLIB 代理类的类名中会插入 "$$",如 DateTimeTools$$EnhancerBySpringCGLIB$$abc123
// 两个条件同时满足,才能确认是 Spring 的 AOP 代理
return object instanceof SpringProxy
&& (Proxy.isProxyClass(object.getClass())
|| object.getClass().getName().contains("$$"));
}
/**
* 穿透 AOP 代理,获取对象的真实目标类。
* 总共有两种获取方式:对象自曝 和 类型反推。
*/
public static Class<?> getTargetClass(Object candidate) {
// 防御性校验:传入对象不能为 null
Assert.notNull(candidate, "Candidate object must not be null");
Class<?> result = null;
// 方式一:对象主动告知自己的目标类
// 某些高级代理类(如 Advised 接口的实现)会直接实现 TargetClassAware 接口,
// 通过 getTargetClass() 主动暴露被代理的原始类
if (candidate instanceof TargetClassAware targetClassAware) {
result = targetClassAware.getTargetClass();
}
// 方式二:如果方式一没拿到结果,根据代理类型反推
if (result == null) {
// isCglibProxy(candidate) —— CGLIB 代理
// → candidate.getClass() 拿到的是子类,如 DateTimeTools$$EnhancerBySpringCGLIB
// → getSuperclass() 返回它的父类,即 DateTimeTools 原始类
//
// 其他情况(普通对象 或 JDK 动态代理)
// → JDK 动态代理本身是基于接口的,getClass() 拿到的类和原始类不直接关联,
// 但 Spring AI 后续会结合接口扫描,这里直接返回 getClass() 作为初始类型
// → 普通对象就更简单了,getClass() 就是它自己的类
result = isCglibProxy(candidate)
? candidate.getClass().getSuperclass()
: candidate.getClass();
}
return result;
}
/**
* 更细粒度的方法:只判断对象是否是 CGLIB 代理。
* CGLIB 通过继承目标类生成子类,因此父类就是原始业务类。
*/
public static boolean isCglibProxy(@Nullable Object object) {
// 条件一:仍然是 Spring 代理,必须先实现 SpringProxy
// 条件二:类名中包含 "$$",这是 CGLIB 生成的子类的命名特征
return object instanceof SpringProxy
&& object.getClass().getName().contains("$$");
}
// ...省略
}
ReflectionUtils 则使用了 getDeclaredMethods 方法:
/**
* Spring 反射工具类,提供方法查找、字段操作、缓存管理等反射相关的静态工具方法。
*
* 在 Spring AI 的场景中,MethodToolCallbackProvider 主要通过本类的
* getDeclaredMethods(Class) 来获取工具对象上的所有声明方法,
* 以便后续从中筛选出带有 @Tool 注解的方法并封装为 ToolCallback。
*
* 本类的方法多数是无状态的,结果会通过 ConcurrentReferenceHashMap 进行缓存,
* 避免重复反射带来的性能开销。对外返回数组时会做防御性拷贝,保护缓存不被外部代码篡改。
*
*/
public abstract class ReflectionUtils {
// ...省略其他变量
/**
* 已解析方法的缓存容器。
*
* Key 为 Class 对象,Value 为该类通过 getDeclaredMethods(Class)
* 解析出的所有声明方法数组(含接口 default 方法)。
*
* 使用 ConcurrentReferenceHashMap 而非普通的 HashMap,原因有二:
* 线程安全:Spring 容器中多线程并发获取方法时不会出现并发问题
* 软引用:当 JVM 内存紧张时,GC 可以回收暂时不用的缓存条目,
* 避免缓存占用过多内存导致 OutOfMemoryError
*
* 初始容量设为 256,足以覆盖大多数应用中的工具类和方法扫描场景,
* 同时避免 Map 频繁扩容带来的性能开销。
*/
private static final Map<Class<?>, Method[]> declaredMethodsCache = new ConcurrentReferenceHashMap(256);
// ...省略其他方法
/**
* 公共入口:获取一个类声明的所有方法(含接口 default 方法)。
* 内部调用私有重载方法,默认开启防御性拷贝。
*/
public static Method[] getDeclaredMethods(Class<?> clazz) {
// 调用私有方法,defensive 传 true,表示返回数组的副本,保护缓存不被外部修改
return getDeclaredMethods(clazz, true);
}
/**
* 私有核心方法:获取一个类的所有声明方法,并支持缓存和防御性拷贝。
*
* @param clazz 要获取方法的类,不能为 null
* @param defensive 是否返回数组的副本(true = 返回克隆,保护缓存;false = 直接返回缓存中的数组)
* @return 该类声明的所有方法(含接口 default 方法),不会为 null
*/
private static Method[] getDeclaredMethods(Class<?> clazz, boolean defensive) {
// 参数校验:确保传入的 Class 对象不为 null
Assert.notNull(clazz, "Class must not be null");
// 尝试从缓存中获取结果
// declaredMethodsCache 是一个 ConcurrentReferenceHashMap(256)
// 已经分析过的类会缓存起来,避免重复反射调用,提升性能
Method[] result = (Method[]) declaredMethodsCache.get(clazz);
// 缓存未命中,需要通过反射来获取方法
if (result == null) {
try {
// 获取类自身直接声明的方法
// Class.getDeclaredMethods() 返回的是:
// - 当前类中声明的所有方法(public/protected/default/private)
// - 不包括构造方法
// - 不包括从父类继承的方法
// - 不包括接口中定义的抽象方法
// 例如 DateTimeTools 中的 getCurrentDateTime()
Method[] declaredMethods = clazz.getDeclaredMethods();
// 查找该类实现的所有接口中的 default 方法
// Java 8 引入的接口 default 方法有方法体,可以被实现类继承,
// 但 getDeclaredMethods() 并不会返回它们,需要单独收集
List<Method> defaultMethods = findDefaultMethodsOnInterfaces(clazz);
// 如果有接口 default 方法,需要合并数组
if (defaultMethods != null) {
// 创建新数组,长度 = 类自身方法数 + 接口 default 方法数
result = new Method[declaredMethods.length + defaultMethods.size()];
// 先把类自身声明的方法拷贝到新数组的前半部分
// 参数含义:源数组, 源起始位置, 目标数组, 目标起始位置, 拷贝数量
System.arraycopy(declaredMethods, 0, result, 0, declaredMethods.length);
// 从类自身方法数量之后的位置开始,逐个追加 default 方法
int index = declaredMethods.length;
for (Method defaultMethod : defaultMethods) {
result[index] = defaultMethod;
// 索引递增,以便按顺序放入后续的 default 方法
++index;
}
} else {
// 没有接口 default 方法,直接使用 getDeclaredMethods() 的结果
result = declaredMethods;
}
// 将最终结果放入缓存
// 如果结果为空数组,放入共享的空数组常量 EMPTY_METHOD_ARRAY,
// 避免每次都为不同的类创建新的空数组对象,节省内存
declaredMethodsCache.put(
clazz,
result.length == 0 ? EMPTY_METHOD_ARRAY : result
);
} catch (Throwable ex) {
// 反射过程中的任何异常(如类加载冲突、安全管理器限制等)
// 包装成 IllegalStateException 并重新抛出,
// 异常信息中附带类名和类加载器信息,方便排查问题
throw new IllegalStateException(
"Failed to introspect Class [" + clazz.getName()
+ "] from ClassLoader [" + clazz.getClassLoader() + "]",
ex
);
}
}
// 返回结果
// 如果开启了防御性拷贝(defensive = true)且数组非空:
// 对数组执行 clone(),返回一个全新的数组副本
// 这样外部代码即使修改了这个副本(排序、清空、替换元素),
// 也不会影响到缓存中的原始数组
// 如果数组为空,或者 defensive = false:
// 直接返回缓存中的数组引用(调用方需保证不修改)
return result.length != 0 && defensive
? (Method[]) ((Method[]) result).clone()
: result;
}
/**
* 查找一个类所实现的所有接口中的 default 方法。
*
* Java 8 引入的接口 default 方法带有方法体,可以被实现类继承使用,
* 但 JDK 的 Class#getDeclaredMethods() 并不会返回这些方法,
* 因此需要本方法单独遍历接口来收集。
*
* 如果类没有实现任何接口,或者实现的接口中没有 default 方法,
* 则返回 null(而非空列表),以减少不必要的数组扩容操作。
*
* @param clazz 要查找的类,不能为 null
* @return 接口中所有 default 方法的列表;如果没有则返回 null
*/
@Nullable
private static List<Method> findDefaultMethodsOnInterfaces(Class<?> clazz) {
// 延迟初始化:只有在真正找到 default 方法时才创建 List
// 大多数类不会实现带有 default 方法的接口,这样可以避免创建无用对象
List<Method> result = null;
// 第一层循环:遍历当前类直接实现的所有接口
// 注意 clazz.getInterfaces() 只返回直接实现的接口,不包括父类实现的接口,
// 但调用方 doWithMethods() 会递归处理父类,所以整体上所有接口都会被覆盖到
for (Class<?> ifc : clazz.getInterfaces()) {
// 第二层循环:遍历当前接口中声明的所有方法
// ifc.getMethods() 返回接口中所有 public 方法(包括从父接口继承的)
for (Method method : ifc.getMethods()) {
// 只保留 default 方法(即有方法体、用 default 关键字修饰的方法)
// method.isDefault() 是 Java 8 引入的方法,用于判断接口方法是否为 default
if (method.isDefault()) {
// 延迟创建 List:只有第一次找到 default 方法时才实例化 ArrayList
// 如果接口中没有任何 default 方法,result 始终保持为 null,方法最终返回 null
if (result == null) {
result = new ArrayList();
}
// 将找到的 default 方法加入结果列表
result.add(method);
}
}
}
// 返回结果:有 default 方法则返回列表,没有则返回 null
// 返回 null 的设计意图是让调用方 getDeclaredMethods() 看到 null 就直接使用
// getDeclaredMethods() 的原始结果,省去数组扩容和拷贝的开销
return result;
}
// ...省略其他方法
}
第二段代码
// 使用 Stream 对方法进行过滤,只保留:
// 标注了 @Tool 注解的方法
// 返回值不是 Function/Supplier/Consumer 的函数式接口的方法
List<Method> toolMethods = Stream.of(declaredMethods)
// 必须有 @Tool
.filter(this::isToolAnnotatedMethod)
// 排除函数式返回值
.filter(toolMethod -> !this.isFunctionalType(toolMethod))
// 收集成不可变列表(Java 16+)
.toList();
可以看到第二段代码调用了 MethodToolCallbackProvider 中的两个方法执行过滤,分别为:isToolAnnotatedMethod,isFunctionalType,我们继续深入探究一下。
public final class MethodToolCallbackProvider implements ToolCallbackProvider {
/**
* 判断给定的方法是否被 @Tool 注解标记。
*
* 这个方法在 MethodToolCallbackProvider 中会被多次调用,
* 分别在构造阶段的预检查和获取工具回调时的过滤链中使用,
* 用于从对象的所有方法中筛选出需要暴露给 AI 的工具方法。
*
* @param method 待检查的反射方法对象,由 ReflectionUtils.getDeclaredMethods(Class) 返回
* @return true 表示方法上存在 Tool 注解,是候选工具方法;
* false 表示不存在该注解,方法会被过滤掉
*/
private boolean isToolAnnotatedMethod(Method method) {
// 使用 Spring 的 AnnotationUtils.findAnnotation 查找方法上的 @Tool 注解
// 与 JDK 原生的 method.getAnnotation(Tool.class) 相比,Spring 的实现更强大:
// 支持元注解:如果 @Tool 被其他注解包裹(如自定义的 @MyTool 上标注了 @Tool),也能被找到
// 支持合并注解:能够递归查找组合注解中的目标注解
// 支持代理方法:如果 method 来自代理类,能追溯到原始方法上的注解
// 返回值是需要强制转换为 Tool 类型,因为 findAnnotation 返回的是泛型 Annotation
Tool annotation = (Tool) AnnotationUtils.findAnnotation(method, Tool.class);
// 如果找到了注解,annotation 不为 null,说明方法被 @Tool 标记
// 使用 Objects.nonNull() 而非 annotation != null,纯属代码风格偏好,语义完全一致
// 返回 true 表示保留该方法,返回 false 表示该方法会被 Stream.filter 过滤掉
return Objects.nonNull(annotation);
}
/**
* 判断工具方法的返回值类型是否为函数式接口。
*
* 函数式接口(Function、Supplier、Consumer 等)通常表达的是"行为"而非"数据",
* AI 模型调用工具后期望获得一个可直接阅读的结果(如字符串、数字、JSON),
* 若返回一个函数对象,AI 既无法理解也无法使用,因此这类方法会被排除。
*
* @param toolMethod 待检查的反射方法对象
* @return true 表示返回值是函数式接口,该方法将被忽略;
* false 表示返回值是普通数据类型,该方法可保留
*/
private boolean isFunctionalType(Method toolMethod) {
// 核心判断:返回值类型是否可赋值给三种常见的函数式接口
// ClassUtils.isAssignable(Function.class, toolMethod.getReturnType())
// 返回值是否为 java.util.function.Function 或其子类型
// 例如:Function<String, Integer> 或 UnaryOperator<String>(继承自 Function)
// ClassUtils.isAssignable(Supplier.class, toolMethod.getReturnType())
// 返回值是否为 java.util.function.Supplier 或其子类型
// 例如:Supplier<String> 或 LongSupplier(函数式接口,但未继承 Supplier)
// ClassUtils.isAssignable(Consumer.class, toolMethod.getReturnType())
// 返回值是否为 java.util.function.Consumer 或其子类型
// 例如:Consumer<String> 或 IntConsumer(类似地接受一个参数无返回值)
// 任一条件满足,isFunction 即为 true
boolean isFunction = ClassUtils.isAssignable(Function.class, toolMethod.getReturnType())
|| ClassUtils.isAssignable(Supplier.class, toolMethod.getReturnType())
|| ClassUtils.isAssignable(Consumer.class, toolMethod.getReturnType());
// 副作用:如果是函数式接口,记录警告日志
// 告知开发者该方法被忽略的原因,方便排查"为什么我的 @Tool 方法没有生效"
if (isFunction) {
logger.warn(
"Method {} is annotated with @Tool but returns a functional type. "
+ "This is not supported and the method will be ignored.",
toolMethod.getName()
);
}
// 返回判断结果
// true 方法会在 Stream.filter 中被排除,不会生成 ToolCallback
// false 方法通过这一层过滤,继续后续流程
return isFunction;
}
}
看完源码,我们一句话总结一下 这个两个方法在进行了什么过滤
isToolAnnotatedMethod : “这个方法是工具方法吗?”
isFunctionalType:“这个方法的返回值 AI 能用吗?”
通过这个两个方法的过滤,获得了可以调用的工具方法列表
this.validateToolCallbacks(ToolCallback[])
this.validateToolCallbacks(this.getToolCallbacks());
我们通过源码可以看到,这里一句代码其实执行了两个方法,分别为 this.getToolCallbacks() 和 this.validateToolCallbacks,我们就查看先调用的方法 this.getToolCallbacks()
public final class MethodToolCallbackProvider implements ToolCallbackProvider {
/**
* 核心方法:扫描所有工具对象,将每个 @Tool 方法封装为 MethodToolCallback,
* 最终合并为一个统一的 ToolCallback[] 数组。
*
* @return 所有工具方法对应的回调数组,已通过名称唯一性校验
*/
public ToolCallback[] getToolCallbacks() {
// 遍历所有工具对象,把每个对象映射为一个 ToolCallback[] 数组
ToolCallback[] toolCallbacks = (ToolCallback[]) this.toolObjects.stream()
// 获取目标类的真实类型(穿透 AOP 代理)
// AopUtils.isAopProxy(toolObject)
// true:对象被 Spring AOP 包装过(例如加了 @Transactional)
// AopUtils.getTargetClass(toolObject) 拆掉代理,拿到原始类
// false:普通对象(如你的 DateTimeTools)
// 直接 toolObject.getClass()
.map((toolObject) -> {
Class<?> targetClass = AopUtils.isAopProxy(toolObject)
? AopUtils.getTargetClass(toolObject)
: toolObject.getClass();
// 获取类的所有声明方法,并做两层过滤
// ReflectionUtils.getDeclaredMethods(targetClass):返回所有声明方法(含接口 default 方法)
// .filter(this::isToolAnnotatedMethod):只保留有 @Tool 注解的方法
// .filter(toolMethod -> !this.isFunctionalType(toolMethod)):排除返回值是函数式接口的方法
Stream<Method> candidateStream = Stream.of(
ReflectionUtils.getDeclaredMethods(targetClass)
)
.filter(this::isToolAnnotatedMethod)
.filter(toolMethod -> !this.isFunctionalType(toolMethod));
// 只保留用户声明的普通方法
// USER_DECLARED_METHODS 的过滤规则:
// 排除桥接方法(编译器为泛型类型擦除生成的方法)
// 排除合成方法(编译器/工具自动生成的方法)
// 排除声明类为 Object 的方法(toString、equals、hashCode 等)
ReflectionUtils.MethodFilter userDeclaredFilter = ReflectionUtils.USER_DECLARED_METHODS;
// 防御性非空检查
Objects.requireNonNull(userDeclaredFilter);
// 对每个通过三层过滤的方法,构建 MethodToolCallback
return (ToolCallback[]) candidateStream
// 过滤
.filter(userDeclaredFilter::matches)
.map((toolMethod) -> {
// 使用 Builder 模式构建一个完整的工具回调对象
return MethodToolCallback.builder()
// 从 @Tool 注解和方法签名生成 ToolDefinition
// 包含:工具名称、描述、参数 schema 等
.toolDefinition(ToolDefinitions.from(toolMethod))
// 从 @Tool 注解生成 ToolMetadata
// 包含:是否 returnDirect 等元数据
.toolMetadata(ToolMetadata.from(toolMethod))
// 反射调用的 Method 对象
.toolMethod(toolMethod)
// 方法所属的实例(调用时作为目标对象)
.toolObject(toolObject)
// 结果转换器:将方法返回值转为 String 返回给 AI
.toolCallResultConverter(
ToolUtils.getToolCallResultConverter(toolMethod)
)
// 创建 MethodToolCallback 实例
.build();
})
// 将当前对象的所有回调收集成一个 ToolCallback[] 数组
.toArray(size -> new ToolCallback[size]);
})
// 此时 Stream 的元素类型为 ToolCallback[](每个工具对象对应一个数组)
// 将所有对象的 ToolCallback[] 数组合并成一个统一的数组
// flatMap 将 Stream<ToolCallback[]> 展平为 Stream<ToolCallback>
// 例如:[回调A1, A2] + [回调B1] → 回调A1, A2, B1
.flatMap(Stream::of)
// 收集到最终的 ToolCallback[] 数组中
.toArray(size -> new ToolCallback[size]);
// 校验工具名称是否重复
this.validateToolCallbacks(toolCallbacks);
// 返回构建完成的工具回调数组
return toolCallbacks;
}
}
可以看到 getToolCallbacks() 这个方法的前半部分。
即构建ReflectionUtils.MethodFilter userDeclaredFilter前,与我们解读的 this.assertToolAnnotatedMethodsPresent(List<Object>) 方法简直一模一样。
但当前方法是多了其他的校验逻辑,以及构建了 MethodToolCallback 列表,最后转化为 ToolCallback[]。在返回构建完成的工具回调数组前还进行了一次校验工具名称是否重复 this.validateToolCallbacks(ToolCallback[])
等等!!
this.validateToolCallbacks(ToolCallback[]),这不就是我们看到getToolCallbacks()作为参数传入的方法吗?

那我们直接看相关的代码
/**
* 校验工具回调数组中是否存在重复的工具名称。
*
* <p>这个方法只做一件事:找出重名工具,找到了就抛异常。
* 如果校验通过(没有重名),方法静默返回,什么也不做。
*
* @param toolCallbacks 待校验的工具回调数组
* @throws IllegalStateException 如果存在重复的工具名称
*/
private void validateToolCallbacks(ToolCallback[] toolCallbacks) {
// 调用 ToolUtils 的静态方法,找出所有出现次数 > 1 的工具名称
// 例如:["getDateTime", "getDateTime"] → duplicateToolNames = ["getDateTime"]
// ["getTime", "getDate"] → duplicateToolNames = [](无重复)
List<String> duplicateToolNames = ToolUtils.getDuplicateToolNames(toolCallbacks);
// 如果有重复名称,抛出异常
if (!duplicateToolNames.isEmpty()) {
// 构建详细的异常信息:
// 重复名称:String.join(", ", duplicateToolNames)
// 例如:"getDateTime, getUserName"
// 来源类名:this.toolObjects.stream().map(o -> o.getClass().getName()).collect(...)
// 例如:"com.example.DateTimeTools, com.example.UserTools"
throw new IllegalStateException(
"Multiple tools with the same name (%s) found in sources: %s".formatted(
String.join(", ", duplicateToolNames), // 重复的工具名称
this.toolObjects.stream()
.map(o -> o.getClass().getName()) // 工具对象来源类名
.collect(Collectors.joining(", ")) // 用逗号拼接
)
);
}
// 如果没有重复 -> 校验通过,方法正常结束,什么都不做
}
validateToolCallbacks()的逻辑相比其他来说就很简单了,仅做了名称是否重复的校验。
回到最开始的代码中,逻辑也很清晰了,也只是再进行了一次校验
this.validateToolCallbacks(this.getToolCallbacks());
总结
经过不停的深入探究 MethodToolCallbackProvider(List<Object>) 构造方法已经很清晰了
private MethodToolCallbackProvider(List<Object> toolObjects) {
Assert.notNull(toolObjects, "toolObjects cannot be null");
Assert.noNullElements(toolObjects, "toolObjects cannot contain null elements");
this.assertToolAnnotatedMethodsPresent(toolObjects);
this.toolObjects = toolObjects;
this.validateToolCallbacks(this.getToolCallbacks());
}
我们可以知道在构建 MethodToolCallbackProvider 对象时在 MethodToolCallbackProvider.builder().toolObjects(sources).build()中没有进行赋值操作,而是进行了多次的校验,构建出MethodToolCallbackProvider 对象,也仅构建出了MethodToolCallbackProvider 对象,没有其他产物,那我们继续看后面代码可以发现
public static ToolCallback[] from(Object... sources) {
return MethodToolCallbackProvider.builder().toolObjects(sources).build().getToolCallbacks();
}
构建完 MethodToolCallbackProvider 对象后,就调用了 getToolCallbacks(),获取到了经过多次校验和过滤的 ToolCallback[]。目的是为了获得一组可以让 AI 合法、唯一、可调用的工具回调
但是还存在一个问题,什么要经过那么多轮复杂的校验才返回 ToolCallback[] 呢?
我们就简单的聊一聊,重复的校验有两个,一个是对传入的工具方法进行校验和过滤,第二个是工具名称校验
第一次在构造时就调用了 MethodToolCallbackProvider(List<Object>) 方法
private MethodToolCallbackProvider(List<Object> toolObjects) {
// ...
// 每个对象有 @Tool 方法吗?
this.assertToolAnnotatedMethodsPresent(toolObjects);
this.toolObjects = toolObjects;
// 工具名称有重复吗?
this.validateToolCallbacks(this.getToolCallbacks());
}
目的很明确就是为了在对象创建的那一刻就验证输入的合法性,快速失败。如果失败,应用无法启动,开发者立刻知道配置有问题。
第二次调用 getToolCallbacks() 又进行了校验
public ToolCallback[] getToolCallbacks() {
// 构建过程中又做了 isToolAnnotatedMethod + isFunctionalType
ToolCallback[] toolCallbacks = this.toolObjects.stream()
.map(toolObject -> {
return Stream.of(getDeclaredMethods(targetClass))
// 注解校验(又执行一遍)
.filter(this::isToolAnnotatedMethod)
// 函数式接口校验(又执行一遍)
.filter(m -> !this.isFunctionalType(m))
// 用户方法校验
.filter(USER_DECLARED_METHODS)
.map(...)
.toArray(...);
})
.flatMap(Stream::of)
.toArray(...);
// 名称唯一性校验
this.validateToolCallbacks(toolCallbacks);
return toolCallbacks;
}
这段代码的目的则是即使对象已成功创建,每次获取回调时仍然保证结果合法。虽然后续调用也可能出错,但至少不会把错误数据返回给调用方。
我们可以看到 MethodToolCallbackProvider(List<Object>) 构造函数是私有的,而 getToolCallbacks() 是公开的,任何人都可以在任何时候调用它。
这时就会出现三种情况:1. 直接调用它,而不经过构造函数、2. 在构造后动态修改了 toolObjects 的内容、3. 多线程并发访问。也就是怕里面内容被修改,导致出现错误
如果只在构造函数中校验一次,后续直接调用 getToolCallbacks() 就没有任何保障了。可以类比一下银行金库的门禁:早上开门前 → 全面安检(构造函数中的校验),每次客户进 → 刷身份证(getToolCallbacks 中的校验)。不能说"早上安检过了,今天再也来不查身份证了"
我们可以总结一下得到:构造时校验确保对象创建时就合法,调用时校验确保每次返回结果都正确。 这是防御性编程的典型实践,宁可多检查一次,也不能让错误数据流出。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)