1. 别急着搜报错全文先学会给Spring报错分门别类说句不太中听但很实在的话大部分Spring报错之所以难搞不是因为问题本身有多复杂而是因为排查思路一开始就乱了。随便一个NoSuchBeanDefinitionException网上一搜能出来几十种说法有人说加Autowired有人说加Qualifier还有人让你检查包扫描路径——结果你挨个试了一遍问题还在那儿。我不是让你别搜索而是建议你在把报错粘贴进搜索框之前先花三十秒判断一下它属于哪一类。以我带项目的经验看Spring相关的报错基本可以归成六个大类报错类别典型报错信息特征常见发生阶段容器与Bean定义类NoSuchBeanDefinitionException、BeanCurrentlyInCreationException、BeanDefinitionStoreException应用启动阶段依赖注入与装配类UnsatisfiedDependencyException、NoUniqueBeanDefinitionException、Autowired的null指针启动或首次调用时AOP与事务处理类事务不生效、Transactional无提示但回滚失败、代理对象转换异常运行期调用时配置与外部化属性类Could not resolve placeholder、IllegalArgumentException、绑定失败启动阶段Web与MVC请求类404、405、HttpMessageNotReadableException、路径匹配异常接口调用时数据访问与连接类CannotGetJdbcConnectionException、1064语法错、死锁超时运行期数据操作时你可能觉得这个分类太基础了但我想说的是另一层意思绝大多数报错根因往往在系统边界上而不是在报错信息本身指向的那个位置。比如你把一个Service类加了Component注解但忘记它的依赖类并没有被Spring管理启动时报的却是NoSuchBeanDefinitionException——报错信息指向的是那个缺失的Bean但真正的问题出在你对这个对象生命周期的设计上。搞清楚这个逻辑后面所有排查都会顺畅很多。这篇文章不打算做成一个罗列报错和解决方案的字典那东西Spring官方文档里都有比我整理的全。我想换个角度按故障现象→排查过程→根因分析→修复方案→避免方法这个链路把几类高频问题的完整思考过程拆给你看。你能复现的不仅是某一个修复动作更是遇到同类报错时的排查思路。尤其是热词里反复出现的Spring事务失效、三级缓存与循环依赖、Spring Boot 3.0后的配置迁移这几种场景我会把背后的机制讲透——因为只改一行配置不叫解决问题知道为什么改这一行才算真的会。2. 启动即崩的经典现场Bean定义冲突与依赖注入失败的完整排查链路Spring Boot应用启动时直接抛异常是所有人都会遇到的第一道坎。这里的报错花样很多但真正的高频原因其实就是那么几个Bean定义冲突、依赖缺失、循环依赖、包扫描范围错。我一个个拆开讲。2.1 NoSuchBeanDefinitionException先查扫描范围再查Bean定义这个报错的完整信息通常长这样Parameter 0 of method setUserService in com.example.controller.UserController required a bean of type com.example.service.UserService that could not be found.排查链路我建议按下面三步走每一步都有明确的验证手段第一步确认UserService上有没有加Component、Service、Repository这类注解。别觉得这一步多余我见过太多因为类上只写了Autowired就以为Spring会自动管理它的新人。Spring的Bean管理必须显式声明要么注解要么XML或Bean配置类没有第三条路。第二步确认包扫描路径。Spring Boot的主启动类上的SpringBootApplication注解默认只扫描它所在包及其子包。如果UserService放在了com.example.other包下而启动类在com.example.controller包下Spring根本看不到它。我通常会在项目结构上直接约定启动类放在所有业务代码的顶层包下例如com.example子包按controller、service、mapper、config分层。这样扫描范围天然覆盖全部业务类省掉一堆麻烦。第三步也是最容易被忽略的检查这个Bean是不是被条件装配给过滤了。Spring Boot的自动配置大量依赖ConditionalOnProperty、ConditionalOnClass、ConditionalOnMissingBean。如果你的UserService上莫名多了个条件注解或者某个配置类里写了ConditionalOnMissingBean(UserService.class)而恰好别处又定义了一个同类型的Bean那这个缺失其实是条件判断把它排除了。遇到这种报错去IDE里打开搜索全局搜一下UserService类的所有引用看看有没有配置类或条件注解在捣鬼。2.2 NoUniqueBeanDefinitionException同类型多个Bean的三种解法这个报错的本质是选择困难症Spring在注入UserService时发现容器里有不止一个UserService类型的Bean于是它就不知道选谁了。报错信息会清楚列出所有候选者的名字No qualifying bean of type com.example.service.UserService available: expected single matching bean but found 2: userServiceImpl,userServiceProxy三种标准解法按推荐程度从高到低排列解法一Primary。在你想用的那个实现类上加上PrimarySpring就会默认优先选它。这种方式适合有一个主实现其他是辅助实现的场景。但注意如果有两三个实现你都要用Primary只能解决默认选谁的问题解决不了怎么拿到另一个的问题。解法二Qualifier。往注入点上加Qualifier(beanName)beanName是那个类的默认Bean名称通常是首字母小写的类名。这种方法更精确适合明确知道我要哪个实现的场景。解法三重构设计。如果你发现某个接口的实现越来越多而且到处都在用Qualifier指定名字这通常说明接口粒度设计有问题。把职责相近的实现合并或者拆成独立的几个小接口往往比继续在注入点上堆注解更健康。注解只能解决当下的报错重构才能解决未来的混乱。顺带说一个我踩过的坑当你在一个类里用构造器注入多个同类型Bean时一定要在构造器参数上也写Qualifier而不是只写在方法上方或字段上。Spring对构造器参数上注解的识别有时候比你想的更严格稍不留神就会变成这个参数还是不知道选谁的报错。2.3 循环依赖与Spring三级缓存BeanCurrentlyInCreationException背后藏着的机制如果你看到一个报错信息长这样BeanCurrentlyInCreationException: Error creating bean with name a: Requested bean is currently in creation: Is there an unresolvable circular reference?这基本就是A依赖B、B又依赖A形成了闭环。Spring解决不了这个闭环的部分场景就会在单例Bean初始化流程中把它拎出来报错。网上关于循环依赖有大量说法其中Spring三级缓存一定可以解决循环依赖是不准确的。Spring的三级缓存只解决单例、非构造器注入、非代理类场景下的循环依赖其他场景依然会崩。作为工程实践我更推荐的策略不是去想怎么绕开或破解这个报错而是直接把循环依赖消灭在代码层面。常见的重构手法有三种构造器注入改成Lazy延迟注入。在被循环引用的依赖上标注LazySpring会创建一个代理对象等真正调用时才去解析依赖。改动最小但会给代码的可读性带来一定代价——调用方看到的类型没问题实际拿到的却是一个延迟代理。把双向依赖改成单向依赖。A需要BB不需要A问题自然消失。如果你的业务里真的是你中有我、我中有你考虑把其中一个抽出来作为独立的第三方依赖或者引入事件机制解耦。用DependsOn显式指定顺序。这种方式适合Bean之间没有直接注入关系但初始化顺序有依赖的场景。比如某个配置初始化时需要另一个Bean先建好用DependsOn(beanName)就能让Spring按你指定的顺序初始化。我见过不少团队为了让项目跑起来直接给整个应用关掉了循环依赖检测这在Spring Boot 2.6之后的版本会被明确禁用因为官方也认为这个机制带来的心智负担远大于便利。结论很简单循环依赖报错是设计问题不是配置问题不要试图通过关开关来绕。2.4 手写Spring和Autowired注入为null自己的框架才有意思热词里出现手写spring这件事还挺有意思的。我认为手写一个简版Spring确实是理解IoC容器最好的方式——你不用被各种封装迷惑直接面对核心问题注解怎么扫描、Bean怎么定义、依赖怎么注入、单例池怎么管理。动手写的时候比较容易忽略几个点第一Autowired注入的核心是类型驱动。如果只按字段类型去找Bean遇到接口多实现立刻就会踩坑。你要是自己想实现Qualifier思路也不复杂拿到字段上的注解如果存在Qualifier就按注解里的名字去BeanDefinitionMap里查而不是按类型查。第二我建议你在实现IoC时先支持字段注入再做构造器注入。字段注入的实现门槛低写起来直观适合搭建原型的阶段。等你把这个流程跑通了再考虑构造器注入顺便研究一下Spring为什么官方更推荐构造器注入——因为字段注入在依赖不可变性和测试便捷性上有天然劣势。第三手写Spring的过程中三级缓存的实现可以作为进阶课题但说实话如果只是做个给学习用的简易容器单例池加一个正在创建中的标记集合已经够用了。你甚至可以先不做三级缓存等真正理解了为什么单例池不够用再去补。这个过程的价值不在最后的产物在于你已经比90%只会调注解的人更懂容器了。3. 运行期假正常最可怕事务失效与AOP不生效的根因定位这一类问题的特征非常迷惑人——应用不报错接口也正常返回但数据库里该回滚的数据没回滚。这类问题之所以难是因为你面对的不是异常而是沉默的数据错误。等业务方发现数据不对的时候往往已经在错误的路上走了很久。3.1 Transactional不生效的五大根因按出现频率排序根因一Transactional加在非public方法上。Spring的事务管理基于AOP动态代理实现代理只能拦截通过代理对象发起的public方法调用。如果把注解加在private方法上Spring根本不认。IDE通常会在编译期给你一个警告但很多人忽略了它。这是新手最常见的坑但好在修复也最容易——改成public方法即可。根因二同类内部方法调用。这也是高频问题而且隐蔽性强。你写了一个Service类类里有方法A和方法BA方法加了Transactional但在B方法内部直接调用this.a()而不是通过代理调用。诸位这个时候事务是不生效的。因为Spring的AOP代理拦截的是外部对象调用当你用this调用时自己调用自己绕过了代理层。验证方法很清晰在加了事务方法内部打印一下当前对象的Class看它是原始类型还是CGLIB代理类。如果是原始类说明你拿到的不是代理对象事务肯定失效。修复方案有两个一是把自己的代理注入进来ApplicationContext.getBean拿代理对象再调用二是拆一个单独的类出来让事务方法在外部被调用。根因三数据库引擎不支持事务。这里面最经典的就是MySQL的MyISAM引擎。它不支持事务你即便加了注解它也会假装一切正常其实根本没有回滚能力。排查方法很直接执行SHOW TABLE STATUS查看表的Engine字段。解决方案是切到InnoDB。但这个问题在Spring层面很隐蔽因为它什么都不报。根因四异常被捕获后吞掉了。代码里写了try-catch然后catch块里只打了个日志异常根本没往上传。Spring事务默认只对RuntimeException和Error回滚你对Exception类开启回滚的话要先考虑是否值得为它开启rollbackFor。如果你在事务方法内捕获了异常又不想吞掉它正确的姿势是在catch块里手动标记TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()或者在catch之后继续往外抛一个RuntimeException。根因五事务方法所在类没有被Spring管理。如果你new了一个Service对象而非从容器拿Spring的代理根本不存在事务自然无从谈起。注意那些看起来像是Spring管理但其实不是的类——比如某些工具类、某些自己new出来的策略类。3.2 AOP切面不执行的排查顺序AOP不生效和事务失效根因有很大一部分是重合的——代理没创建出来。但AOP还有它自己的独特矛盾点。按我实际的排查顺序来看第一步检查切点表达式。Spring AOP的表达式用的是AspectJ的语法但是否能正确解析是另一回事。比如你写execution(* com.example.service.*.*(..))它只匹配service包下一层的所有方法service.impl包里的类不会命中。这是高频坑。建议表达式的包路径范围先放宽等确认切点能命中后再逐步收紧。第二步确认切面类是不是被Spring管理了。Aspect注解本质上只是告诉Spring这个类是切面你必须同时加上Component或通过Bean把它注册到容器里否则一切白搭。第三步检查pom或构建配置里是否引入了spring-boot-starter-aop。Spring Boot的AOP支持并不是默认就有的缺少依赖时EnableAspectJAutoProxy不会被触发切面自然静默失效。这个错不报任何异常很容易被忽略。第四步确认你是通过容器拿的对象调用方法而不是直接new出来的。如果拿到的是代理对象方法调用才会被切面拦截。有个很实用的验证方法——在切面里打一行日志然后调用目标方法看日志是否输出。如果你确定代码逻辑没错但日志没有那十有八九就是代理对象的问题。3.3 三级缓存、循环依赖与代理对象的矛盾关系前面讲循环依赖时说到三级缓存这里再展开讲一下它和AOP代理之间的微妙关系。为什么Spring要设计三级缓存而不是两级核心原因就是代理对象创建时机。早期的Spring版本中循环依赖和AOP代理撞在一起容易出问题当A需要注入B而B已经被AOP增强过时如果直接从二级缓存里拿出原始B给AA拿到的就是没有切面的假Bean。为了处理这种偏差Spring设计了三级缓存第三层存的是ObjectFactory用来延迟判断这个Bean是否需要改变其最终暴露的形态比如是否需要代理。但在Spring Boot 2.6及之后Spring官方决定默认禁止循环依赖这背后也有这个原因在——循环依赖场景下的代理处理太容易出意外不如直接用报错逼你重构。如果你实在绕不开循环依赖又需要AOP我可以实话告诉你这条路越来越不好走了。把设计改成单向依赖或者事件驱动是唯一能长期稳定运行的方案。4. Spring Boot版本升级与配置迁移报错背后其实是API的变化热词里有一项是spring boot 3中spring security配置迁移这个表面上看是Security的问题本质上却是Spring Boot从2.x升级到3.x之后一大批旧API废弃导致的连锁反应。做升级的人如果只看单个报错会被折腾得怀疑人生如果你先搞清版本升级到底动了哪些核心整个过程会轻松很多。4.1 Spring Boot 3.0之后组件扫描、属性绑定和Security配置的关键变化Spring Boot 3.0是基于Spring Framework 6.0构建的而Spring Framework 6.0的最低要求是JDK 17。因此报错里如果看见UnsupportedClassVersionError、class file version相关字样先检查JDK版本是否达标——这往往不是代码问题。Spring Security在5.x时代用的基于WebSecurityConfigurerAdapter的配置方式在6.0之后被正式删除。之前你可能是这样写的Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/admin/**).hasRole(ADMIN); } }新版本要改成基于SecurityFilterChain的Bean声明Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeRequests() .requestMatchers(/admin/**).hasRole(ADMIN); return http.build(); } }注意几个核心变化antMatchers变成了requestMatchersauthorizeRequests还在但推荐的写法逐渐转向authorizeHttpRequests并且过滤器链的构建方式从继承改成了Bean方法返回。迁移报错时把旧的这几个方法名全局搜一遍替换掉大部分编译错误都能解决。另外Spring MVC里WebMvcConfigurerAdapter也被干掉了现在要直接实现WebMvcConfigurer接口。如果你在迁移时看到cannot find symbol或者class not found之类的报错不要慌先检查是不是还在依赖这些被移除的适配器类。4.2 Caffeine缓存与Spring Cache结合时的配置陷阱热词里出现了spring boot caffeine这个组合本身很常见但配置上有一个很典型的坑你把CacheManager配好了也加了Cacheable注解可是怎么缓存都不命中或者一直报找不到缓存管理器。先确认一个基础问题Spring Boot的spring-boot-starter-cache有没有引入没有它EnableCaching根本不会工作。如果你引入了多个CacheManager的实现依赖CacheProvider比如Ehcache、Redis、Caffeine共存Spring Boot自动配置可能会因为候选多个实现而报错或者随机选一个不一定是你想要的实现。Caffeine的具体配置建议放在application.yml里spring: cache: type: caffeine caffeine: spec: maximumSize500,expireAfterWrite10m这里的spec里的逗号和空格不要随便丢Caffeine配置库对字符串的解析要求很严格格式错了启动时报错还不好看。我自己遇到过的问题是缓存name设置得太随意代码里Cacheable(cacheNames user)配置文件里的cache-names如果没包含user运行时也会报找不到缓存。可以在Cacheable所在的Bean里PostConstruct手动初始化一个CacheManager或者配置CacheManager的setCacheNames提前把要用到的缓存名全部声明齐全。4.3 WebSocket集成时yml配置总是踩的坑Spring Boot集成WebSocketyml配置看起来不复杂但怎么配置都不生效的情况依然很多。这里的深层原因是WebSocket的端点注册方式不同配置的存放位置和解析方式完全不同。如果你用的是基于STOMP协议的WebSocket配置主要在spring.websocket下同时你还需要一个实现WebSocketMessageBrokerConfigurer的配置类。如果你用的是原生WebSocket也就是直接实现WebSocketHandler那种方式yml里能配的东西其实非常少大部分配置要写在Configuration里注册HandlerMapping和HandlerAdapter。常见的报错有这么几类启动时提示WebSocket endpoint无法注册。这种情况一般是ServerEndpoint注解的类没有交给Spring管理或者你漏掉了ServerEndpointExporter这个Bean。在Spring Boot里需要显式声明ServerEndpointExporter才能让ServerEndpoint工作不声明的话你的WebSocket端点就像根本没存在过。握手阶段报403。Spring Security拦截了WebSocket握手请求。你需要把WebSocket握手端点排除在Security过滤链之外或者正确配置permitAll。注意握手路径和消息路径的区别前者在HTTP协议下发生后者在升级后的WebSocket协议下发生。前端连接一直404。优先检查路径拼接问题。我曾经遇到过在yml里配置了context-path导致WebSocket握手路径带上了应用前缀前端没加这个前缀就一直连不上。这种问题排查起来不大吧但挺耗时间。建议WebSocket握手路径统一写成不带context-path的绝对路径或者在前端代码里动态拼接。4.4 Maven依赖冲突导致的ClassNotFoundException和NoClassDefFoundError这种报错属于启动或运行时随机出现的类型。NoClassDefFoundError和ClassNotFoundException虽然长得像但含义完全不同。前者是这个类编译过但现在运行时的类路径里找不到后者是根本就没加载到。Spring项目里最常见的原因就是传递依赖冲突——A依赖了旧版的commons-lang3B依赖了新版的最终打包时某个类就缺失了。排查方法三板斧用mvn dependency:tree查看依赖树找出同一个依赖的不同版本确认最终生效的是哪个。用Maven的dependencyManagement或直接exclusion排除旧版本。如果两个版本必须要共存那就用maven-shade-plugin重定位或者把有冲突的类用自定义类加载器隔离。但这种方案复杂度很高我一般不推荐在Spring项目里用除非你真的无法统一版本。Spring Boot项目里spring-boot-starter-parent已经通过dependencyManagement统一管理了一大批依赖版本所以很多时候你不需要手动指定版本号直接用starter即可。如果你非要手写一个具体版本的依赖反而容易打破这种平衡。5. 数据库报错里最闹心的一种MySQL 1064与数据访问层常见异常手册MySQL 1064错误热度很高它能上榜我一点不意外——这错误几乎人人遇到而且报错信息对新手极不友好。原文长这样SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ... at line 1right syntax to use near后面的那一段SQL就是你语法出错的位置所在。我把排查这个报错的心得总结如下第一步检查关键字冲突。如果你的表名或字段名是order、group、desc、key、index这类MySQL保留字SQL就会语法错。解决办法有两个改名或者用反引号把它包起来。在MyBatis的XML里写动态SQL时这个坑尤其多因为XML里的动态SQL生成的最终SQL往往不容易和预期对齐。第二步检查字符串是不是没加引号。这种情况往往发生在你用应用层代码拼接SQL时传进来的查询参数没被正确包裹成字符串。我把话放这里任何手写拼接SQL的地方都有潜在风险轻则1064重则SQL注入。推荐使用MyBatis的#{}参数占位符而不是直接拼接。第三步注意版本语法差异。MySQL 5.7和8.0在某些SQL语法上有区别比如FOR UPDATE NOWAIT、窗口函数、正则表达式风格等。如果你能确认在Navicat里执行没问题但在Spring应用里就报1064那就要考虑是不是在XML里写了某些转义字符或某些标签如、没做转义。XML里必须写成lt;可以原样出现但建议也转义。除了1064数据访问常见的报错还有连接池耗尽HikariPool-1 - Connection is not available, request timed out after 30000ms。这通常说明你的连接池参数改得不对或者某个事务把连接长时间占用了没释放。我建议先从HikariCP的配置入手调大maximumPoolSize不是首选方案先找出哪个方法慢、哪个事务开的时间长。死锁超时Deadlock found when trying to get lock。报错字面意思是死锁但它其实是并发控制层面的业务问题。排查思路是找到死锁的两条SQL的加锁顺序然后统一加锁顺序或者用更小粒度的事务减少锁的持有时间。主键冲突Duplicate entry 1 for key PRIMARY。这个常见于插入时主键指定了固定值而不是自增生成。看看插入SQL里是否显式传了id字段或者实体里主键字段值被重复赋值了。6. Spring级别的问题排查手段日志、监听器、调试开关到了这一部分我想聊的是排查工具和手段——而不是某一种具体报错的解法。因为到项目后期你会发现报错类型千变万化但排查路径是有规律的。第一件趁手的工具肯定是日志。Spring Boot默认的日志级别是INFO但如果你想定位Bean初始化、自动配置等细节可以把日志级别调成DEBUG。比如在无配置类、无自动配置类、无注册Bean的情况下想确认Spring Boot到底干了什么事可以在application.yml里加logging: level: org.springframework.boot.autoconfigure: DEBUG org.springframework.context: DEBUG调成DEBUG后你能看到ConditionEvaluationReport——也就是自动配置条件的匹配过程哪些条件通过、哪些条件未通过一目了然。这个在排查为什么我加的某个自动配置没有生效时极其好用。第二件趁手的工具是Spring的ApplicationRunner和CommandLineRunner。在应用启动完成后跑一段自定义逻辑可以用来打印容器里某些类型的Bean名称或验证某个Bean到底是不是代理对象。比如Component public class BeanInspector implements ApplicationRunner { private final ApplicationContext context; public BeanInspector(ApplicationContext context) { this.context context; } Override public void run(ApplicationArguments args) { String[] beanNames context.getBeanNamesForType(UserService.class); for (String name : beanNames) { Object bean context.getBean(name); System.out.println(Bean name: name , class: bean.getClass()); } } }这段代码特别适合排查事务不生效AOP切面没执行同类型Bean多了个这类问题——打印出来的Class如果是CGLIB代理类说明代理生成了如果被代理对象没有出现那问题在配置上。第三件工具是Spring Boot的actuator。它不只是监控用的在排查Bean定义的时候同样好用。通过/actuator/beans这个端点你能看到容器里所有Bean的名称、类型、依赖关系还能看到哪些Bean被标记为候选者但最终没被选中。通过/actuator/conditions能看到自动配置条件评估报告。这可比肉眼扫代码高效多了。调试开关方面Spring Framework本身就支持在启动时开启-Dspring.debugtrue打印所有自动配置相关的决策。如果项目连启动都过不去这个参数会帮你把为什么它尝试加载这个类的完整决策链打出来。7. 日志系统本身的坑logback配置、控制台无日志、日志文件打不开有人看到这可能会说日志报错也算Spring问题——算而且很常见。日志系统属于那种平时没人管一出事全抓瞎的组件。一旦配置错误轻则日志文件没有滚动重则应用直接启动失败。最常见的报错之一是NoSuchMethodError: ch.qos.logback.core.util. ...之类。这个根因是logback的版本和slf4j的版本不兼容或者你手动引入了一版logback和Spring Boot管理的版本冲突。Spring Boot的spring-boot-starter-logging里已经默认集成了logback强烈建议你不要自己额外引logback相关的依赖。如果你非要从其他传递依赖里排除logback再重引一个版本那我只能说祝你好运并且准备好mvn dependency:tree排查。另一种常见情况是控制台完全没日志。这多半是你覆盖了logback-spring.xml但配置里没有定义appender nameCONSOLE或者root/logger的level配得太高。比如把level配成了ERROR那平时正常的INFO信息自然都不显示。这个不算报错但每次debug都两眼一抹黑效率太低了。我的习惯是开发环境至少保留INFO级别的控制台日志生产环境才输出WARN及以上级别到文件。还有一种是日志文件写了但打不开。Windows下常见于文件被占用Linux下常见于权限不足。Spring Boot默认的日志路径如果没配好应用会在启动时尝试创建文件但失败报错鬼得很。最好显式地在application.yml里把logging.file.name或logging.file.path配置好别让它在默认目录里猜。8. 日志文件打印不出SQL参数时先查动态SQL映射另外一个高频率又隐蔽的问题是SQL日志没有输出正确内容或者干脆不输出SQL。这倒不是logback的问题而是MyBatis或MyBatis-Plus的日志配置。在Spring Boot里让MyBatis输出SQL的配置通常是mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl或者你可以在日志级别上单独配置logging: level: com.example.mapper: DEBUGcom.example.mapper要换成你自己的Mapper接口包路径。上面两种方法二选一即可重复配置反而会让SQL日志打两遍影响排查效率。实际工作中更折磨人的是日志里能看到SQL语句但是?占位符的绑定参数不显示。这种情况先检查你是不是把Mapper接口的package写错了再确认日志级别的范围是否覆盖到了参数绑定日志所在的类。如果你用的是MyBatis-Plus它有自己的日志级别控制和拦截器配置方式和原生MyBatis略有不同建议不要混着配置。如果你发现某个SQL只有在特定参数下才报错最好的做法不是通过日志一行行核对而是把最终执行的SQL语句复制到数据库客户端里手动执行一遍。很多框架层面的误判在数据库客户端里根本不存在这一步能帮你快速区分是SQL本身问题还是参数绑定问题。9. 记住这个链路顺序比记住一百条报错都值聊了这么多你可能会觉得信息量有点大。我最后想把整套排查逻辑压缩成一张思维快查表以链条形式分享给你以后遇到任何Spring报错都可以先对它做个判断再动手。先看启动还是运行期。启动阶段崩溃优先查Bean定义、配置加载、自动配置条件运行期数据错优先查事务、AOP、数据访问和资源释放。再看是编译还是运行异常。编译期异常优先查依赖版本、API迁移、包名改写运行期异常优先查代理机制、生命周期、并发状态。再看是不是静默失效。不报错但不回滚、不执行切面、不命中缓存这种最费神用前面提到的BeanInspector或actuator做运行时观测。最后才是看报错信息本身。把报错里的类名、方法名、行号、状态码抄下来再对照确定的方向去搜。我自己在项目里带人时经常说一句话Spring报错不是洪水猛兽它只是把设计问题用异常的形式摆在了你面前。这个框架之所以比其他框架复杂是因为它把对象的生命周期、代理机制、依赖关系都抽象了一层又一层。你每解决一个报错实际上是在理解这层抽象下的底层设计。等你经历过十几轮这样的循环再遇到新的报错时你大概率连搜都不用搜心里已经能猜到问题在哪了。这才是Spring报错解决一览最终想带给你的能力。