Controller 请求一直 404?但是我明明有路由啊 记一次诡异的 Spring Boot 路由消失排查


 问题背景
 
 最近在维护一个 Spring Boot + Shiro + MyBatis-Plus 的后台管理系统时,遇到了一个诡异的 Bug:给 Controller 方法加上 @Transactional 注解后,该接口直接返回 404,去掉就恢复正常。更诡异的是,同类中的其他接口不受影响。

 问题复现

 @RestController
 @RequestMapping("/api/admin/tools")
 @RequiredArgsConstructor
 public class AdminToolController {

     private final ToolMapper toolMapper;

     // 这个接口一直正常
     @PostMapping("/list")
     public Result<PageResult<Tool>> list(@RequestBody PageQuery q) {
         // ...
     }

     // 加上 @Transactional 后,POST /api/admin/tools/create → 404
     @PostMapping("/create")
     @RequiresPermissions("content:tool:add")
     @Transactional  // ← 罪魁祸首
     public Result<?> create(@RequestBody Map<String, Object> body) {
         Tool tool = buildTool(body);
         toolMapper.insert(tool);
         return Result.ok();
     }
 }

 去掉 @Transactional,重启,POST /api/admin/tools/create 正常响应。加上,重启,404。

 排查过程

 第一步:确认不是路由冲突

 检查了所有 Controller,没有任何 URL 路径冲突。/api/admin/tools/create 在全局是唯一的。

 第二步:确认不是 Shiro 拦截

 Shiro 配置中 /api/admin/** 走 JWT 过滤器。如果认证失败,返回的是 401,不是 404。而且同一个类里其他方法都能正常访问,排除 Shiro 问题。

 第三步:定位到 @Transactional

 逐个对比能正常访问的接口和 404 的接口,唯一区别就是 @Transactional。去掉注解就正常,加上就 404,百发百中。

 根因分析

 Spring AOP 代理机制

 @Transactional 基于 Spring AOP 实现。当 Spring 发现 Bean 上有 @Transactional 方法时,会为该 Bean 创建一个 CGLIB 动态代理对象。

 // 你写的
 @RestController
 public class AdminToolController {
     @PostMapping("/create")
     @Transactional
     public Result<?> create(...) { ... }
 }

 // Spring 实际放到容器里的
 AdminToolController$$EnhancerBySpringCGLIB$$xxxxx  // CGLIB 子类代理

 关键矛盾

 Spring MVC 的 RequestMappingHandlerMapping 在启动时扫描 @RestController 的 Bean,从中提取 @PostMapping、@GetMapping 等注解来构建路由映射表。

 当 Bean 被 CGLIB 代理后,RequestMappingHandlerMapping 拿到的可能是代理对象而不是原始对象。在特定条件下(尤其是项目同时存在多个 AOP 切面,如 Shiro 权限注解 + 事务注解),代理链可能导致 @PostMapping 注解无法被正确解析,路由注册失败。

 请求过来时,DispatcherServlet 在映射表中找不到匹配的 Handler → 404。

 为什么有的项目没问题?

 ┌──────────────────────────────────────────┬───────────────────────────┐
 │                   条件                   │         是否触发          │
 ├──────────────────────────────────────────┼───────────────────────────┤
 │ @Transactional 在 Service 层             │ ❌ 不触发,这是标准做法   │
 ├──────────────────────────────────────────┼───────────────────────────┤
 │ Spring Boot 使用原生 AspectJ LTW         │ ❌ 不触发,LTW 不创建代理 │
 ├──────────────────────────────────────────┼───────────────────────────┤
 │ Controller 上只有一个 AOP 注解           │ ⚠️ 低概率触发             │
 ├──────────────────────────────────────────┼───────────────────────────┤
 │ Controller 上混用 Shiro + @Transactional │ ✅ 高概率触发             │
 ├──────────────────────────────────────────┼───────────────────────────┤
 │ Spring 5.3.x + CGLIB 默认代理            │ ✅ 你的情况               │
 └──────────────────────────────────────────┴───────────────────────────┘

 简单说:不是你写错了,是 AOP 代理链的组合碰巧踩中了这个坑。

 解决方案

 方案一:去掉 Controller 上的 @Transactional(最简单)

 @PostMapping("/create")
 @RequiresPermissions("content:tool:add")
 // @Transactional ← 删掉,单条 insert 本来就不需要事务
 public Result<?> create(@RequestBody Map<String, Object> body) {
     Tool tool = buildTool(body);
     toolMapper.insert(tool);
     return Result.ok();
 }

 方案二:事务逻辑下沉到 Service 层(推荐)

 // Controller
 @PostMapping("/create")
 @RequiresPermissions("content:tool:add")
 public Result<?> create(@RequestBody Map<String, Object> body) {
     toolService.createTool(body);
     return Result.ok();
 }

 // Service
 @Service
 public class ToolService {
     @Transactional
     public void createTool(Map<String, Object> body) {
         Tool tool = buildTool(body);
         toolMapper.insert(tool);
     }
 }

 方案三:如需在 Controller 批量操作,用编程式事务

 @PostMapping("/plans/{toolId}")
 public Result<?> savePlans(@PathVariable Long toolId, @RequestBody List<PricingPlan> plans) {
     transactionTemplate.execute(status -> {
         pricingPlanMapper.delete(...);
         for (PricingPlan p : plans) {
             pricingPlanMapper.insert(p);
         }
         return null;
     });
     return Result.ok();
 }

 总结

 1. @Transactional 不要放 Controller 层,即使别的项目没出问题,这也是反模式
 2. 事务最好放在 Service 层,职责清晰、代理稳定
 3. Controller 的单条 insert / update / delete 不需要 @Transactional,MyBatis / JPA 自带原子操作
 4. 遇到 "加了注解就 404" 这种诡异问题,优先怀疑 AOP 代理干扰了路由映射

 ---

 ▎ 一句话记住:Controller 做路由分发的,Service 做业务逻辑的,事务是业务逻辑的一部分,放 Service。

0 条评论

当前评论已经关闭


登录用户头像