在 Spring AI 中自定义 Tool 调用返回值——实现 TodoList 提醒注入 依赖版本对应的 SpringAI 版本和 SpringBoot 依赖properties java.version21/java.version project.build.sourceEncodingUTF-8/project.build.sourceEncoding project.reporting.outputEncodingUTF-8/project.reporting.outputEncoding spring-boot.version4.0.1/spring-boot.version spring-ai.version2.0.0-M2/spring-ai.version /properties dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-minimax/artifactId /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring-boot.version}/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement定义 TodoList Tool首先需要定义供 LLM 调用的 Tool。以下是完整实现包含读取和写入两个操作并通过 Caffeine 本地缓存按会话隔离存储/** * author a hrefhttps://github.com/lieeewleikooo/a * date 2025/12/31 */ Component public class TodolistTools extends BaseTools { private static final int MAX_TODOS 20; private static final SetString VALID_STATUSES Set.of(pending, in_progress, completed); private static final MapString, String STATUS_MARKERS Map.of( pending, [ ], in_progress, [], completed, [x] ); private record TodoItem(String id, String text, String status) {} private static final CacheString, ListTodoItem TODOLIST_CACHE Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(Duration.ofMinutes(30)) .build(); Tool(description Update task list. Track progress on multi-step tasks. Pass the full list of items each time (replaces previous list). Each item must have id, text, status. Status: pending, in_progress, completed. Only one item can be in_progress. ) public String todoUpdate( ToolParam(description Full list of todo items. Each item: id (string), text (string), status (pending|in_progress|completed).) ListMapString, Object items, ToolContext toolContext ) { try { String conversationId ConversationUtils.getToolsContext(toolContext).appId(); if (items null || items.isEmpty()) { TODOLIST_CACHE.invalidate(conversationId); return No todos.; } if (items.size() MAX_TODOS) { return Error: Max MAX_TODOS todos allowed; } ListTodoItem validated validateAndConvert(items); TODOLIST_CACHE.put(conversationId, validated); return render(validated); } catch (IllegalArgumentException e) { return Error: e.getMessage(); } } private ListTodoItem validateAndConvert(ListMapString, Object items) { int inProgressCount 0; ListTodoItem result new ArrayList(items.size()); for (int i 0; i items.size(); i) { MapString, Object item items.get(i); String id String.valueOf(item.getOrDefault(id, String.valueOf(i 1))).trim(); String text String.valueOf(item.getOrDefault(text, )).trim(); String status String.valueOf(item.getOrDefault(status, pending)).toLowerCase(); if (StringUtils.isBlank(text)) { throw new IllegalArgumentException(Item id : text required); } if (!VALID_STATUSES.contains(status)) { throw new IllegalArgumentException(Item id : invalid status status ); } if (in_progress.equals(status)) { inProgressCount; } result.add(new TodoItem(id, text, status)); } if (inProgressCount 1) { throw new IllegalArgumentException(Only one task can be in_progress at a time); } return result; } Tool(description Read the current todo list for this conversation. Use this to check progress and see what tasks remain.) public String todoRead(ToolContext toolContext) { String conversationId ConversationUtils.getToolsContext(toolContext).appId(); ListTodoItem items TODOLIST_CACHE.getIfPresent(conversationId); return items null || items.isEmpty() ? No todos. : render(items); } private String render(ListTodoItem items) { if (items null || items.isEmpty()) { return No todos.; } StringBuilder sb new StringBuilder(\n\n); for (TodoItem item : items) { String marker STATUS_MARKERS.getOrDefault(item.status(), [ ]); sb.append(marker).append( #).append(item.id()).append(: ).append(item.text()).append(\n\n); } long done items.stream().filter(t - completed.equals(t.status())).count(); sb.append(\n().append(done).append(/).append(items.size()).append( completed)); return sb.append(\n\n).toString(); } Override String getToolName() { return Todo List Tool; } Override String getToolDes() { return Read and write task todo lists to track progress; } }问题分析为什么不能在普通 Advisor 中拦截工具调用通过阅读源码org.springframework.ai.minimax.MiniMaxChatModel#stream可以发现框架内部会在 ChatModel 层直接执行 Tool 调用而不是将其透传给 Advisor 链。核心执行逻辑如下// Tool 调用的核心逻辑如下 FluxChatResponse flux chatResponse.flatMap(response - { if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(requestPrompt.getOptions(), response)) { // FIXME: bounded elastic needs to be used since tool calling // is currently only synchronous return Flux.deferContextual(ctx - { ToolExecutionResult toolExecutionResult; try { ToolCallReactiveContextHolder.setContext(ctx); toolExecutionResult this.toolCallingManager.executeToolCalls(prompt, response); } finally { ToolCallReactiveContextHolder.clearContext(); } return Flux.just(ChatResponse.builder().from(response) .generations(ToolExecutionResult.buildGenerations(toolExecutionResult)) .build()); }).subscribeOn(Schedulers.boundedElastic()); } return Flux.just(response); }) .doOnError(observation::error) .doFinally(signalType - observation.stop()) .contextWrite(ctx - ctx.put(ObservationThreadLocalAccessor.KEY, observation));这意味着如果我们在外层 Advisor 中尝试拦截 tool_call此时工具已经执行完毕并且无法识别到工具调用。所以我准备使用我自己写的 MiniMaxChatModel 覆盖掉这个源码的逻辑之后再 Advisor 接管这个 Tool 执行。验证思路能否通过 Advisor 接管工具执行我们需要在自己的项目目录创建一个org.springframework.ai.minimax.MiniMaxChatModel具体文件内容可以访问 代码小抄 获取完整的代码。详细代码位置如下图所示这样写好之后就可以让工具调用信号透传到 Advisor 层判断是否有 Tool 调用。验证用的 Advisor 如下Slf4j public class FindToolAdvisor implements StreamAdvisor { Override public FluxChatClientResponse adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) { return Flux.deferContextual(contextView - { log.info(Advising stream); return streamAdvisorChain.nextStream(chatClientRequest).doOnNext(streamResponse - { boolean hasToolCalls streamResponse.chatResponse().hasToolCalls(); log.info(Found tool calls: {}, hasToolCalls); }); }); } Override public String getName() { return FindToolAdvisor; } Override public int getOrder() { return 0; } }Component public class StreamApplication implements CommandLineRunner { Resource private ChatModel chatModel; Override public void run(String... args) throws Exception { ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(FileSystemTools.builder().build()) .defaultAdvisors(new FindToolAdvisor()) .build(); ChatClient.StreamResponseSpec stream chatClient.prompt( 帮我写一个简单的 HTML 页面路径是 E:\\TEMPLATE\\spring-skills 不超过 300 行代码 ).stream(); stream.content().subscribe(System.out::println); } }配置文件spring: ai: minimax: api-key: sk-cp-xxxxx chat: options: model: MiniMax-M2.5测试结果证明工具调用信号可以被成功拦截方案可行改造项目实现 ExecuteToolAdvisor参考 Spring AI 社区中一个尚未合并的 PR#5383我们实现了ExecuteToolAdvisor主要做了两件事工具调用 JSON 格式容错捕获 JSON 解析异常最多重试 3 次再抛出提升大模型调用 Tool 时格式不规范的容错能力。TodoList 提醒注入连续 3 次工具调用均未触发todoUpdate时在ToolResponseMessage的第一个位置注入提醒引导 AI 及时更新任务列表。⚠️注意order顺序由于该 Advisor 接管了工具执行它的order值应尽量大即靠后执行。若order较小可能导致后续 Advisor 的doFinally在每次工具调用时都被触发比如后面的 buildAdvisor、versionAdvisor 只需要执行一次而非在整个对话结束时触发一次。本实现中使用Integer.MAX_VALUE - 100。/** * 手动执行 tool 的 StreamAdvisor关闭框架内部执行自行执行并可在工具返回值中注入提醒如更新 todo。 * * author a hrefhttps://github.com/lieeewleikooo/a * date 2026/3/14 */ Slf4j Component public class ExecuteToolAdvisor implements StreamAdvisor { private static final String TODO_REMINDER reminderUpdate your todos./reminder; private static final String JSON_ERROR_MESSAGE Tool call JSON parse failed. Fix and retry.\n Rules: strict RFC8259 JSON, no trailing commas, no comments, no unescaped control chars in strings (escape newlines as \\n, tabs as \\t), all keys double-quoted.; private static final int MAX_TOOL_RETRY 3; private static final int ORDER Integer.MAX_VALUE - 100; private static final String TODO_METHOD todoUpdate; private static final int REMINDER_THRESHOLD 3; /** * 三次工具没有使用 todoTool 那么就在 tool_result[0] 位置添加 TODO_REMINDER */ private final CacheString, Integer roundsSinceTodo Caffeine.newBuilder() .maximumSize(10_00) .expireAfterWrite(Duration.ofMinutes(30)) .build(); Resource private ToolCallingManager toolCallingManager; Override public FluxChatClientResponse adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) { Assert.notNull(streamAdvisorChain, streamAdvisorChain must not be null); Assert.notNull(chatClientRequest, chatClientRequest must not be null); if (chatClientRequest.prompt().getOptions() null || !(chatClientRequest.prompt().getOptions() instanceof ToolCallingChatOptions)) { throw new IllegalArgumentException( ExecuteToolAdvisor requires ToolCallingChatOptions to be set in the ChatClientRequest options.); } var optionsCopy (ToolCallingChatOptions) chatClientRequest.prompt().getOptions().copy(); optionsCopy.setInternalToolExecutionEnabled(false); return internalStream(streamAdvisorChain, chatClientRequest, optionsCopy, chatClientRequest.prompt().getInstructions(), 0); } private FluxChatClientResponse internalStream( StreamAdvisorChain streamAdvisorChain, ChatClientRequest originalRequest, ToolCallingChatOptions optionsCopy, ListMessage instructions, int jsonRetryCount) { return Flux.deferContextual(contextView - { var processedRequest ChatClientRequest.builder() .prompt(new Prompt(instructions, optionsCopy)) .context(originalRequest.context()) .build(); StreamAdvisorChain chainCopy streamAdvisorChain.copy(this); FluxChatClientResponse responseFlux chainCopy.nextStream(processedRequest); AtomicReferenceChatClientResponse aggregatedResponseRef new AtomicReference(); AtomicReferenceListChatClientResponse chunksRef new AtomicReference(new ArrayList()); return new ChatClientMessageAggregator() .aggregateChatClientResponse(responseFlux, aggregatedResponseRef::set) .doOnNext(chunk - chunksRef.get().add(chunk)) .ignoreElements() .cast(ChatClientResponse.class) .concatWith(Flux.defer(() - processAggregatedResponse( aggregatedResponseRef.get(), chunksRef.get(), processedRequest, streamAdvisorChain, originalRequest, optionsCopy, jsonRetryCount))); }); } private FluxChatClientResponse processAggregatedResponse( ChatClientResponse aggregatedResponse, ListChatClientResponse chunks, ChatClientRequest finalRequest, StreamAdvisorChain streamAdvisorChain, ChatClientRequest originalRequest, ToolCallingChatOptions optionsCopy, int retryCount) { if (aggregatedResponse null) { return Flux.fromIterable(chunks); } ChatResponse chatResponse aggregatedResponse.chatResponse(); boolean isToolCall chatResponse ! null chatResponse.hasToolCalls(); if (isToolCall) { Assert.notNull(chatResponse, chatResponse must not be null when hasToolCalls is true); ChatClientResponse finalAggregatedResponse aggregatedResponse; FluxChatClientResponse toolCallFlux Flux.deferContextual(ctx - { ToolExecutionResult toolExecutionResult; try { ToolCallReactiveContextHolder.setContext(ctx); toolExecutionResult toolCallingManager.executeToolCalls(finalRequest.prompt(), chatResponse); } catch (Exception e) { if (retryCount MAX_TOOL_RETRY) { ListMessage retryInstructions buildRetryInstructions(finalRequest, chatResponse, e); if (retryInstructions ! null) { return internalStream(streamAdvisorChain, originalRequest, optionsCopy, retryInstructions, retryCount 1); } } throw e; } finally { ToolCallReactiveContextHolder.clearContext(); } ListMessage historyWithReminder injectReminderIntoConversationHistory( toolExecutionResult.conversationHistory(), getAppId(finalRequest)); if (toolExecutionResult.returnDirect()) { return Flux.just(buildReturnDirectResponse(finalAggregatedResponse, chatResponse, toolExecutionResult, historyWithReminder)); } return internalStream(streamAdvisorChain, originalRequest, optionsCopy, historyWithReminder, 0); }); return toolCallFlux.subscribeOn(Schedulers.boundedElastic()); } return Flux.fromIterable(chunks); } /** * 获取 AppId */ private String getAppId(ChatClientRequest finalRequest) { if (finalRequest.prompt().getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) { return toolCallingChatOptions.getToolContext().get(CONVERSATION_ID).toString(); } throw new BusinessException(ErrorCode.SYSTEM_ERROR); } private static ListMessage buildRetryInstructions(ChatClientRequest finalRequest, ChatResponse chatResponse, Throwable error) { AssistantMessage assistantMessage extractAssistantMessage(chatResponse); if (assistantMessage null || assistantMessage.getToolCalls() null || assistantMessage.getToolCalls().isEmpty()) { return null; } ListMessage instructions new ArrayList(finalRequest.prompt().getInstructions()); instructions.add(assistantMessage); String errorMessage buildJsonErrorMessage(error); ListToolResponseMessage.ToolResponse responses assistantMessage.getToolCalls().stream() .map(toolCall - new ToolResponseMessage.ToolResponse( toolCall.id(), toolCall.name(), errorMessage)) .toList(); instructions.add(ToolResponseMessage.builder().responses(responses).build()); return instructions; } private static AssistantMessage extractAssistantMessage(ChatResponse chatResponse) { if (chatResponse null) { return null; } Generation result chatResponse.getResult(); if (result ! null result.getOutput() ! null) { return result.getOutput(); } ListGeneration results chatResponse.getResults(); if (results ! null !results.isEmpty() results.get(0).getOutput() ! null) { return results.get(0).getOutput(); } return null; } private static String buildJsonErrorMessage(Throwable error) { String detail ExceptionUtils.getRootCauseMessage(error); if (detail.isBlank()) { return JSON_ERROR_MESSAGE; } return JSON_ERROR_MESSAGE \nError: detail; } /** * 对 conversationHistory 中的 TOOL 类消息在其每个 ToolResponse 的 responseData 后追加提醒。 */ private ListMessage injectReminderIntoConversationHistory(ListMessage conversationHistory, String appId) { if (conversationHistory null || conversationHistory.isEmpty()) { return conversationHistory; } if (!(conversationHistory.getLast() instanceof ToolResponseMessage toolMsg)) { return conversationHistory; } ListToolResponseMessage.ToolResponse responses toolMsg.getResponses(); if (responses.isEmpty()) { return conversationHistory; } ToolResponseMessage.ToolResponse firstResponse responses.getFirst(); if (!updateRoundsAndCheckReminder(appId, firstResponse.name())) { return conversationHistory; } ListToolResponseMessage.ToolResponse newResponses new ArrayList(responses); ToolResponseMessage.ToolResponse actualRes newResponses.removeFirst(); newResponses.add(new ToolResponseMessage.ToolResponse( firstResponse.id(), text, TODO_REMINDER)); newResponses.add(actualRes); ListMessage result new ArrayList( conversationHistory.subList(0, conversationHistory.size() - 1)); result.add(ToolResponseMessage.builder().responses(newResponses).build()); return result; } /** * 构造 returnDirect 时的 ChatClientResponse使用注入提醒后的 conversationHistory 生成 generations。 */ private static ChatClientResponse buildReturnDirectResponse( ChatClientResponse aggregatedResponse, ChatResponse chatResponse, ToolExecutionResult originalResult, ListMessage historyWithReminder) { ToolExecutionResult resultWithReminder ToolExecutionResult.builder() .conversationHistory(historyWithReminder) .returnDirect(originalResult.returnDirect()) .build(); ChatResponse newChatResponse ChatResponse.builder() .from(chatResponse) .generations(ToolExecutionResult.buildGenerations(resultWithReminder)) .build(); return aggregatedResponse.mutate().chatResponse(newChatResponse).build(); } /** * updateRoundsAndCheckReminder * param appId appId * param methodName methodName * return 是否需要更新 */ private boolean updateRoundsAndCheckReminder(String appId, String methodName) { if (TODO_METHOD.equals(methodName)) { roundsSinceTodo.put(appId, 0); return false; } int count roundsSinceTodo.asMap().merge(appId, 1, Integer::sum); return count REMINDER_THRESHOLD; } Override public String getName() { return ExecuteToolAdvisor; } Override public int getOrder() { return ORDER; } }因为这个 Advisor 也使用到了StreamAdvisorChain接口的 copy 所以我们需要覆盖源码的这个StreamAdvisorChain并且实现对应的接口下面的代码包路径是org.springframework.ai.chat.client.advisor.api具体的代码public interface StreamAdvisorChain extends AdvisorChain { /** * Invokes the next {link StreamAdvisor} in the {link StreamAdvisorChain} with the * given request. */ FluxChatClientResponse nextStream(ChatClientRequest chatClientRequest); /** * Returns the list of all the {link StreamAdvisor} instances included in this chain * at the time of its creation. */ ListStreamAdvisor getStreamAdvisors(); /** * Creates a new StreamAdvisorChain copy that contains all advisors after the * specified advisor. * param after the StreamAdvisor after which to copy the chain * return a new StreamAdvisorChain containing all advisors after the specified * advisor * throws IllegalArgumentException if the specified advisor is not part of the chain */ StreamAdvisorChain copy(StreamAdvisor after); }下面的包位置是org.springframework.ai.chat.client.advisor具体的实现代码/** * Default implementation for the {link BaseAdvisorChain}. Used by the {link ChatClient} * to delegate the call to the next {link CallAdvisor} or {link StreamAdvisor} in the * chain. * * author Christian Tzolov * author Dariusz Jedrzejczyk * author Thomas Vitale * since 1.0.0 */ public class DefaultAroundAdvisorChain implements BaseAdvisorChain { public static final AdvisorObservationConvention DEFAULT_OBSERVATION_CONVENTION new DefaultAdvisorObservationConvention(); private static final ChatClientMessageAggregator CHAT_CLIENT_MESSAGE_AGGREGATOR new ChatClientMessageAggregator(); private final ListCallAdvisor originalCallAdvisors; private final ListStreamAdvisor originalStreamAdvisors; private final DequeCallAdvisor callAdvisors; private final DequeStreamAdvisor streamAdvisors; private final ObservationRegistry observationRegistry; private final AdvisorObservationConvention observationConvention; DefaultAroundAdvisorChain(ObservationRegistry observationRegistry, DequeCallAdvisor callAdvisors, DequeStreamAdvisor streamAdvisors, Nullable AdvisorObservationConvention observationConvention) { Assert.notNull(observationRegistry, the observationRegistry must be non-null); Assert.notNull(callAdvisors, the callAdvisors must be non-null); Assert.notNull(streamAdvisors, the streamAdvisors must be non-null); this.observationRegistry observationRegistry; this.callAdvisors callAdvisors; this.streamAdvisors streamAdvisors; this.originalCallAdvisors List.copyOf(callAdvisors); this.originalStreamAdvisors List.copyOf(streamAdvisors); this.observationConvention observationConvention ! null ? observationConvention : DEFAULT_OBSERVATION_CONVENTION; } public static Builder builder(ObservationRegistry observationRegistry) { return new Builder(observationRegistry); } Override public ChatClientResponse nextCall(ChatClientRequest chatClientRequest) { Assert.notNull(chatClientRequest, the chatClientRequest cannot be null); if (this.callAdvisors.isEmpty()) { throw new IllegalStateException(No CallAdvisors available to execute); } var advisor this.callAdvisors.pop(); var observationContext AdvisorObservationContext.builder() .advisorName(advisor.getName()) .chatClientRequest(chatClientRequest) .order(advisor.getOrder()) .build(); return AdvisorObservationDocumentation.AI_ADVISOR .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () - observationContext, this.observationRegistry) .observe(() - { var chatClientResponse advisor.adviseCall(chatClientRequest, this); observationContext.setChatClientResponse(chatClientResponse); return chatClientResponse; }); } Override public FluxChatClientResponse nextStream(ChatClientRequest chatClientRequest) { Assert.notNull(chatClientRequest, the chatClientRequest cannot be null); return Flux.deferContextual(contextView - { if (this.streamAdvisors.isEmpty()) { return Flux.error(new IllegalStateException(No StreamAdvisors available to execute)); } var advisor this.streamAdvisors.pop(); AdvisorObservationContext observationContext AdvisorObservationContext.builder() .advisorName(advisor.getName()) .chatClientRequest(chatClientRequest) .order(advisor.getOrder()) .build(); var observation AdvisorObservationDocumentation.AI_ADVISOR.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () - observationContext, this.observationRegistry); observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start(); // formatter:off FluxChatClientResponse chatClientResponse Flux.defer(() - advisor.adviseStream(chatClientRequest, this) .doOnError(observation::error) .doFinally(s - observation.stop()) .contextWrite(ctx - ctx.put(ObservationThreadLocalAccessor.KEY, observation))); // formatter:on return CHAT_CLIENT_MESSAGE_AGGREGATOR.aggregateChatClientResponse(chatClientResponse, observationContext::setChatClientResponse); }); } Override public CallAdvisorChain copy(CallAdvisor after) { return this.copyAdvisorsAfter(this.getCallAdvisors(), after); } Override public StreamAdvisorChain copy(StreamAdvisor after) { return this.copyAdvisorsAfter(this.getStreamAdvisors(), after); } private DefaultAroundAdvisorChain copyAdvisorsAfter(List? extends Advisor advisors, Advisor after) { Assert.notNull(after, The after advisor must not be null); Assert.notNull(advisors, The advisors must not be null); int afterAdvisorIndex advisors.indexOf(after); if (afterAdvisorIndex 0) { throw new IllegalArgumentException(The specified advisor is not part of the chain: after.getName()); } var remainingStreamAdvisors advisors.subList(afterAdvisorIndex 1, advisors.size()); return DefaultAroundAdvisorChain.builder(this.getObservationRegistry()) .pushAll(remainingStreamAdvisors) .build(); } Override public ListCallAdvisor getCallAdvisors() { return this.originalCallAdvisors; } Override public ListStreamAdvisor getStreamAdvisors() { return this.originalStreamAdvisors; } Override public ObservationRegistry getObservationRegistry() { return this.observationRegistry; } public static final class Builder { private final ObservationRegistry observationRegistry; private final DequeCallAdvisor callAdvisors; private final DequeStreamAdvisor streamAdvisors; private Nullable AdvisorObservationConvention observationConvention; public Builder(ObservationRegistry observationRegistry) { this.observationRegistry observationRegistry; this.callAdvisors new ConcurrentLinkedDeque(); this.streamAdvisors new ConcurrentLinkedDeque(); } public Builder observationConvention(Nullable AdvisorObservationConvention observationConvention) { this.observationConvention observationConvention; return this; } public Builder push(Advisor advisor) { Assert.notNull(advisor, the advisor must be non-null); return this.pushAll(List.of(advisor)); } public Builder pushAll(List? extends Advisor advisors) { Assert.notNull(advisors, the advisors must be non-null); Assert.noNullElements(advisors, the advisors must not contain null elements); if (!CollectionUtils.isEmpty(advisors)) { ListCallAdvisor callAroundAdvisorList advisors.stream() .filter(a - a instanceof CallAdvisor) .map(a - (CallAdvisor) a) .toList(); if (!CollectionUtils.isEmpty(callAroundAdvisorList)) { callAroundAdvisorList.forEach(this.callAdvisors::push); } ListStreamAdvisor streamAroundAdvisorList advisors.stream() .filter(a - a instanceof StreamAdvisor) .map(a - (StreamAdvisor) a) .toList(); if (!CollectionUtils.isEmpty(streamAroundAdvisorList)) { streamAroundAdvisorList.forEach(this.streamAdvisors::push); } this.reOrder(); } return this; } /** * (Re)orders the advisors in priority order based on their Ordered attribute. */ private void reOrder() { ArrayListCallAdvisor callAdvisors new ArrayList(this.callAdvisors); OrderComparator.sort(callAdvisors); this.callAdvisors.clear(); callAdvisors.forEach(this.callAdvisors::addLast); ArrayListStreamAdvisor streamAdvisors new ArrayList(this.streamAdvisors); OrderComparator.sort(streamAdvisors); this.streamAdvisors.clear(); streamAdvisors.forEach(this.streamAdvisors::addLast); } public DefaultAroundAdvisorChain build() { return new DefaultAroundAdvisorChain(this.observationRegistry, this.callAdvisors, this.streamAdvisors, this.observationConvention); } } }效果验证前端展示工具调用 JSON 格式错误时自动重试连续三次未更新 TodoList 时触发提醒注入前端效果