Spring报错但瞎报

4 阅读5分钟

Spring报错但瞎报

1.1场景说明

我的第一个controller

package com.javasm.controller;
​
import com.javasm.config.demoaitool.CalculatorTools;
import com.javasm.config.demoaitool.DateTimeTools;
import com.javasm.config.demoaitool.WeatherTools;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * 方法内部调用工具类
 */
@RestController
@RequestMapping("/tool")
@RequiredArgsConstructor
public class Tool1Controller {
​
    private final ChatClient chatClient;
​
    private final WeatherTools weatherTools;
    private final DateTimeTools dateTimeTools;
    private final CalculatorTools calculatorTools;
​
    /**
     * 单工具调用
     */
    @GetMapping("/weather")
    public String weather(@RequestParam String msg) {
        return chatClient.prompt()
                .tools(MethodToolCallbackProvider.builder().toolObjects(weatherTools).build())
                .user(msg).call().content();
    }
​
    /**
     * 方法内部多工具调用
     */
    @GetMapping("/multi2")
    public String multiTools(@RequestParam String input) {
        return chatClient.prompt()
                .tools(
                        weatherTools,
                        dateTimeTools,
                        calculatorTools
                )
                .user(input).call().content();
    }
}

我的第二个controller

package com.javasm.controller;
​
import com.javasm.config.demoaitool.CalculatorTools;
import com.javasm.config.demoaitool.DateTimeTools;
import com.javasm.config.demoaitool.WeatherTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
​
import java.util.List;
​
/**
 * 构造时注入 ChatClient,通过 .defaultTools 注册 ToolCallbackProvider
 */
@RestController
@RequestMapping("/tool")
public class ToolController {
​
    private final ChatClient chatClient;
​
    public ToolController(ChatClient.Builder builder,
                          WeatherTools weatherTools,
                          DateTimeTools dateTimeTools,
                          CalculatorTools calculatorTools) {
        //单工具多工具都这样配置
        this.chatClient = builder
                .defaultTools(weatherTools, dateTimeTools, calculatorTools)
                .build();
    }
​
    @GetMapping("/weather")
    public String weather(@RequestParam String msg) {
        return chatClient.prompt().user(msg).call().content();
    }
}

1.2问题说明

因为他俩在一个项目中,并且/tool/weather地址冲突,运行项目的时候本应该报错显示Spring 启动时发现一个地址对应了两个方法,不知道调用哪个,但我在第一次调用的时候显示的却是找不到@tool注解,这让人很费解,并且我询问ai后,ai给我的答案也是胡言乱语:

//甚至ai让我把
                .tools(
                        weatherTools,
                        dateTimeTools,
                        calculatorTools
                )
//改成这个鸟样子
                 .tools(
                         MethodToolCallbackProvider.builder().toolObjects(weatherTools).build(),
                         MethodToolCallbackProvider.builder().toolObjects(dateTimeTools).build(),
                        MethodToolCallbackProvider.builder().toolObjects(calculatorTools).build()
                 )

显然没有解决问题,还是报错,ai依旧胡言乱语。

controller地址冲突依旧没有解决

1.3问题解决

经过问专业java人士,了解到是因为controller中的地址冲突,所以我就把错误改好之后,由于我好奇之前的错误是什么原因是什么,我就又复原错误的代码,运行的栈轨迹和我第一次错误的栈轨迹完全不一致

第一次显示的是

Caused by: java.lang.IllegalStateException: No @Tool annotated methods found in [com.javasm.config.demoaitool.WeatherTools@35cec305, com.javasm.config.demoaitool.DateTimeTools@cbd40c1, com.javasm.config.demoaitool.CalculatorTools@491cafec].Did you mean to pass a ToolCallback or ToolCallbackProvider? If so, you have to use .toolCallbacks() instead of .tool()

我修改完bug后,又恢复到原来的错误前的样子的栈轨迹

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'toolController' method 
com.javasm.controller.ToolController#weather(String)
to {GET [/tool/weather]}: There is already 'tool1Controller' bean method

显然,相同的错误,两次打印的错误信息完全不一致。这让人很疑惑。

1.4问题分析

我不明白,为什么两次相同的错误,反而栈轨迹报错信息不一致,所以我就打开deepseek了

我问:有时第一次报错 的信息找不到错误原因怎么办,显示找不到@tool,最后找到的是controller地址重复和使用的是MethodToolCallbackProvider.builder().toolObjects(weatherTools).build(),反而报错找不到tool
​
deepseek回答:你这个问题问得非常深刻!这恰恰是 Spring 框架异常层层包装 导致的最让人头疼的情况——真正的错误被埋在最底层,表面上看到的异常信息完全指向了另一个方向。我来还原一下你当时的场景,帮你理清楚为什么会这样,以及以后怎么快速找到"真凶"

deepseek分析

第一次启动:
  ① 创建 ToolController → ② 构造函数执行 → ③ 扫描 @Tool 失败 
  → ④ 抛出 "No @Tool annotated methods found" 
  → ⑤ 启动失败(URL 冲突还没被发现)
​
第二次启动(修改完成后,又修改回去,由于缓存的原因):
  ① 创建 ToolController → ② 构造函数执行 → ③ 扫描 @Tool 成功 
  → ④ 继续初始化 → ⑤ 注册 URL 映射 → ⑥ 发现 URL 冲突 
  → ⑦ 抛出 "Ambiguous mapping" 
  → ⑧ 启动失败

地址冲突,导致相同的地址方法两个都无法执行,所以在.tools()扫描的时候根本扫不到工具方法里的@Tool

注意:因为controller没有执行---所以controller构造方法没有执行---所以没有执行.tool()---所以找不到工具类中的@Tool注解的方法---所以报错

chatClient.prompt()
                .tools(
                        weatherTools,
                        dateTimeTools,
                        calculatorTools
                )
                .user(input).call().content();

1.5bug栈轨迹查看思路

首先出现错误,要看最后一个Caused by

如果caused by找不到原因,就要看caused by 下 at 下是自己com.javasm.controller路径下的最后一个信息

Caused by: java.lang.IllegalStateException: No @Tool annotated methods found in [com.javasm.config.demoaitool.WeatherTools@35cec305, com.javasm.config.demoaitool.DateTimeTools@cbd40c1, com.javasm.config.demoaitool.CalculatorTools@491cafec].Did you mean to pass a ToolCallback or ToolCallbackProvider? If so, you have to use .toolCallbacks() instead of .tool()
    at org.springframework.ai.tool.method.MethodToolCallbackProvider.assertToolAnnotatedMethodsPresent(MethodToolCallbackProvider.java:77) ~[spring-ai-model-1.1.2.jar:1.1.2]
    at org.springframework.ai.tool.method.MethodToolCallbackProvider.<init>(MethodToolCallbackProvider.java:61) ~[spring-ai-model-1.1.2.jar:1.1.2]
    at org.springframework.ai.tool.method.MethodToolCallbackProvider$Builder.build(MethodToolCallbackProvider.java:153) ~[spring-ai-model-1.1.2.jar:1.1.2]
    at org.springframework.ai.support.ToolCallbacks.from(ToolCallbacks.java:34) ~[spring-ai-model-1.1.2.jar:1.1.2]
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec.tools(DefaultChatClient.java:923) ~[spring-ai-client-chat-1.1.2.jar:1.1.2]
    at org.springframework.ai.chat.client.DefaultChatClientBuilder.defaultTools(DefaultChatClientBuilder.java:174) ~[spring-ai-client-chat-1.1.2.jar:1.1.2]
    at com.javasm.controller.ToolController.<init>(ToolController.java:30) ~[classes/:na]← 这里!
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na]
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) ~[na:na]
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) ~[na:na]
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:210) ~[spring-beans-6.2.18.jar:6.2.18]
    ... 23 common frames omitted
​
​
Process finished with exit code 1 

虽然说也是指向扫描@tool找不到配置方法的@Tool注解,但是因为自己的业务逻辑和构造逻辑没分清导致的

1.6解决方案

核心原因:不是因为框架在 Controller 层做了扫描,而是因为 你自己的代码调用了扫描方法!,这也是为什么要把业务和构造分开的主要原因,因为这是测试代码,所以没太注重分开写,就出现了这种混乱的错误,在实际的工作项目中,一定要把业务逻辑和构造方法分开写,不仅可以更清晰的找到错误,还可以方便后期的维护

1.不要在controller里做业务逻辑

问题说明
Bean 未完全初始化构造函数执行时,Spring 可能还没完成所有 Bean 的注入和初始化
异常处理复杂构造函数中抛出的异常会导致整个 Bean 创建失败,难以定位
违反单一职责Controller 应该只负责接收请求,不应该负责初始化业务组件
测试困难构造函数里做了很多事情,单元测试时难以 mock

如果一定要在controller中执行,用 @PostConstruct注解

@RestController
@RequestMapping("/tool")
public class ToolController {
    
    private final ChatClient.Builder chatClientBuilder;
    private final WeatherTools weatherTools;
    private ChatClient chatClient;
    
    public ToolController(ChatClient.Builder chatClientBuilder, WeatherTools weatherTools) {
        // ✅ 构造函数只做赋值,不做业务逻辑
        this.chatClientBuilder = chatClientBuilder;
        this.weatherTools = weatherTools;
    }
    
    @PostConstruct  // ← 在 Bean 初始化完成后执行
    public void init() {
        // ✅ 在这里做 Tool 扫描,此时所有 Bean 都已准备好
        var toolProvider = MethodToolCallbackProvider.builder()
            .toolObjects(weatherTools)
            .build();
        
        this.chatClient = chatClientBuilder
            .defaultTools(toolProvider)
            .build();
    }
}