公司动态

Spring 分层架构注入乱象分析

📅 2026/8/19 12:04:47
Spring 分层架构注入乱象分析
引言最近在一个老项目中开发一个新的功能使用AI工具生成代码。但是生成的代码中有一部分代码出现了 Controller/Service 注入Mapper 的情况其原因是AI参考的代码就有注入乱象。查看这个代码的提交日期已经是三年前了并且开发者已经离职很久了。有可能当时这个开发者为了图一时之方便违背了三层架构的核心职责也有可能他的代码风格就是这样。总而言之这种注入乱象的代码会出现职责混乱、耦合度高、测试困难、AOP失效等诸多问题这是不可取的并且随着AI开发逐步成为主流AI读取到这些低质量代码后继续生成低质量的代码。举例说明在典型的 SpringBoot 项目中我们通常采用Controller - Service - Mapper的分层架构。本文以 Article主表、ArticleRemark子表 为例一个Article 对应多个 ArticleRemark列举常见的实现方案分析每种方案的不合理之处、潜在隐患以及违反的面向对象设计原则并给出推荐做法。假设现在有一个需求获取文章信息。返回 ArticleVO 有评论列表属性开发接口有 6 个方案1 ArticleController 注入 IArticleRemarkService2 ArticleController 注入 ArticleRemarkServiceImpl 实现类3 ArticleController 注入 ArticleRemarkMapper4 ArticleServiceImpl 注入 IArticleRemarkService5 ArticleServiceImpl 注入 ArticleRemarkServiceImpl 实现类6 ArticleServiceImpl 注入 ArticleRemarkMapper。结论先行在传统Controller - Service - Mapper分层结构中方案 4 是最推荐的ArticleServiceImpl注入IArticleRemarkService由文章服务在 Service 层完成文章与评论列表的组装。方案 1能跑但职责放错了层方案 2、5错在依赖具体实现类方案 3、6最差跨层直接依赖Mapper破坏了分层和业务封装。一、先看依赖方向和分层原则正常依赖链应该是Controller ↓ 依赖接口 Service 接口 ↑ 实现 ServiceImpl ↓ 依赖自己模块的 Mapper Mapper也就是说ArticleController应该只依赖IArticleServiceArticleServiceImpl可以依赖ArticleMapperArticleRemarkController应该只依赖IArticleRemarkServiceArticleRemarkServiceImpl可以依赖ArticleRemarkMapper需要跨模块协作时应该由 Service 层依赖对方的 Service 接口而不是Controller去组合多个 Service更不是直接依赖对方的 Mapper。二、6 种方案对比表方案注入位置依赖类型主要问题违反原则1ArticleController 注入 IArticleRemarkService接口Controller 负责业务组装职责不清单一职责原则、迪米特法则2ArticleController 注入 ArticleRemarkServiceImpl具体实现类Controller 依赖实现类且 Controller 组装业务依赖倒置原则、开闭原则、单一职责原则、迪米特法则3ArticleController 注入 ArticleRemarkMapperMapper 接口Controller 跨层访问持久层绕过 Service分层架构、迪米特法则、单一职责原则、开闭原则4ArticleServiceImpl 注入 IArticleRemarkService接口基本合理注意循环依赖和接口粒度基本不违反核心原则5ArticleServiceImpl 注入 ArticleRemarkServiceImpl具体实现类Service 依赖实现类替换和测试困难依赖倒置原则、开闭原则6ArticleServiceImpl 注入 ArticleRemarkMapperMapper 接口Service 跨层访问持久层破坏评论业务封装单一职责原则、迪米特法则、分层架构、开闭原则三、逐个方案详细分析方案 1ArticleController 注入 IArticleRemarkServiceRestControllerpublicclassArticleController{privatefinalIArticleServicearticleService;privatefinalIArticleRemarkServicearticleRemarkService;GetMapping(/article/{id})publicResultArticleVOdetail(PathVariableLongid){ArticleVOvoarticleService.getArticle(id);ListArticleRemarkVOremarksarticleRemarkService.listRemarksByArticleId(id);vo.setRemarkList(remarks);returnResult.ok(vo);}}不正确的地方Controller 开始承担业务组装职责。它不应该知道“文章详情 文章信息 评论列表”这个业务规则。隐患如果 App 端、管理后台、定时任务也需要“文章 评论”的数据这段组装逻辑会重复。Controller 变厚以后接口逻辑膨胀后很难维护。Controller 直接依赖两个 Service知道太多细节。如果未来需要统一事务、缓存、权限控制Controller 层很难处理。违反原则单一职责原则Controller 既负责 HTTP 参数处理又负责业务组装。迪米特法则Controller 不应该了解评论服务的存在和调用方式。方案 1 不是完全不能跑但它不是好的分层设计。方案 2ArticleController 注入 ArticleRemarkServiceImpl 实现类RestControllerpublicclassArticleController{privatefinalIArticleServicearticleService;privatefinalArticleRemarkServiceImplarticleRemarkService;}不正确的地方Controller 直接依赖ArticleRemarkServiceImpl具体实现类。隐患Spring 默认可能使用 JDK 动态代理如果IArticleRemarkService存在代理对象类型是接口不是实现类按实现类注入甚至可能启动失败。即使开启 CGLIB能注入成功也已经和具体实现绑定无法替换。如果ArticleRemarkServiceImpl有Transactional、缓存等 AOP 增强依赖具体实现类可能导致代理失效或行为不可控。单元测试时很难 mock 一个具体类。违反原则依赖倒置原则高层模块 Controller 不应该依赖低层模块的具体实现。开闭原则替换评论服务实现时需要修改 Controller。单一职责原则、迪米特法则同方案 1。方案 3ArticleController 注入 ArticleRemarkMapperRestControllerpublicclassArticleController{privatefinalIArticleServicearticleService;privatefinalArticleRemarkMapperarticleRemarkMapper;GetMapping(/article/{id})publicResultArticleVOdetail(PathVariableLongid){ArticleVOvoarticleService.getArticle(id);ListArticleRemarkremarksarticleRemarkMapper.selectByArticleId(id);vo.setRemarkList(BeanUtil.copyToList(remarks,ArticleRemarkVO.class));returnResult.ok(vo);}}不正确的地方Controller 直接访问评论模块的 Mapper跨越了 Service 层完全破坏了分层架构。隐患Controller 直接接触数据库查询SQL 逻辑散落到 Controller。绕过ArticleRemarkService可能丢失评论业务规则例如只查询未删除评论过滤审核未通过的评论黑名单用户评论不展示敏感词屏蔽分页、缓存、权限控制等。数据库表结构变化时Controller 可能需要跟着改。Controller 变得和数据库字段强耦合。违反原则分层架构原则Controller 不能直接访问 Mapper。单一职责原则Controller 承担了评论查询和组装职责。迪米特法则Controller 知道了 Mapper 层的存在。开闭原则评论查询逻辑变化时需要修改 Controller。虽然ArticleRemarkMapper是接口但它属于 DAL 层抽象不是业务层抽象所以仍然破坏了依赖方向。方案 4ArticleServiceImpl 注入 IArticleRemarkServiceServicepublicclassArticleServiceImplimplementsIArticleService{privatefinalArticleMapperarticleMapper;privatefinalIArticleRemarkServicearticleRemarkService;OverridepublicArticleVOgetArticleDetail(LongarticleId){ArticlearticlearticleMapper.selectById(articleId);ListArticleRemarkVOremarksarticleRemarkService.listRemarkVOsByArticleId(articleId);returnArticleVO.from(article,remarks);}}优点Controller 只依赖IArticleService保持薄控制器。业务组装逻辑在 Service 层符合分层。ArticleServiceImpl依赖的是IArticleRemarkService接口符合依赖倒置原则。评论业务规则仍然封装在ArticleRemarkService中。可复用其他 Service、定时任务需要文章详情时可以直接调用IArticleService.getArticleDetail()。测试方便mockIArticleRemarkService即可。需要注意的隐患循环依赖如果将来ArticleRemarkServiceImpl也注入IArticleService会出现ArticleService - ArticleRemarkService - ArticleService的循环依赖。解决方式避免双向依赖拆分查询服务使用事件或轻量接口。接口粒度问题如果IArticleRemarkService中有大量评论增删改查方法而文章服务只用到listRemarkVOsByArticleId()则文章服务被迫依赖了很多不需要的方法。可以考虑拆分一个IArticleRemarkQueryService接口只提供查询能力这符合接口隔离原则。违反原则基本不违反核心面向对象原则。如果接口设计不合理可能轻微违反接口隔离原则。方案 5ArticleServiceImpl 注入 ArticleRemarkServiceImpl 实现类ServicepublicclassArticleServiceImplimplementsIArticleService{privatefinalArticleRemarkServiceImplarticleRemarkService;}不正确的地方Service 层直接依赖另一个模块的 Service 实现类而不是接口。隐患与方案 2 类似可能因 Spring 代理方式不同导致注入失败或 AOP 失效。文章服务与评论服务实现类强耦合替换实现极其困难。单元测试无法方便 mock 具体实现类。如果ArticleRemarkServiceImpl发生修改文章服务可能被迫重新编译和修改。违反原则依赖倒置原则Service 应该依赖抽象而不是具体实现。开闭原则替换评论服务实现时需要修改文章服务代码。方案 6ArticleServiceImpl 注入 ArticleRemarkMapperServicepublicclassArticleServiceImplimplementsIArticleService{privatefinalArticleMapperarticleMapper;privatefinalArticleRemarkMapperarticleRemarkMapper;OverridepublicArticleVOgetArticleDetail(LongarticleId){ArticlearticlearticleMapper.selectById(articleId);ListArticleRemarkremarksarticleRemarkMapper.selectByArticleId(articleId);returnArticleVO.from(article,remarks);}}不正确的地方文章服务跨模块直接访问评论模块的 Mapper绕过了评论 Service。隐患评论业务规则会被文章服务绕过例如评论逻辑删除评论审核状态评论可见范围用户黑名单过滤。这些规则原本封装在ArticleRemarkServiceImpl中一旦文章服务直接查 Mapper就会导致两套评论查询逻辑并存以后容易出现数据不一致。文章服务与评论表结构强耦合评论表字段变更会影响文章服务。SQL 分散维护成本上升。如果文章服务需要评论 VO而不是实体还需要在文章服务中处理实体转 VO 的逻辑进一步增加耦合。违反原则单一职责原则文章服务承担了评论持久化查询职责。迪米特法则文章服务知道了评论 Mapper 的存在。分层架构原则Service 不应该直接访问其他模块的 Mapper。开闭原则评论查询逻辑或表结构变化时需要修改文章服务。封装性破坏了评论模块的业务封装。四、推荐写法ControllerRestControllerRequiredArgsConstructorRequestMapping(/article)publicclassArticleController{privatefinalIArticleServicearticleService;GetMapping(/{id})publicResultArticleVOdetail(PathVariableLongid){returnResult.ok(articleService.getArticleDetail(id));}}ArticleServiceImplServiceRequiredArgsConstructorpublicclassArticleServiceImplimplementsIArticleService{privatefinalArticleMapperarticleMapper;privatefinalIArticleRemarkServicearticleRemarkService;OverridepublicArticleVOgetArticleDetail(LongarticleId){ArticlearticlearticleMapper.selectById(articleId);ListArticleRemarkVOremarkVOsarticleRemarkService.listRemarkVOsByArticleId(articleId);returnArticleVO.from(article,remarkVOs);}}这样做的好处Controller 薄文章服务负责文章详情这个业务用例评论模块继续通过IArticleRemarkService对外提供能力评论的持久化、过滤、状态规则仍然封装在评论服务内部后续如果文章详情要增加其他信息例如作者信息、分类信息可以继续在ArticleServiceImpl中注入对应 Service 接口进行组装。五、最终排序推荐程度从高到低方案 4 方案 1 方案 5 ≈ 方案 2 方案 6 ≈ 方案 3更直观地说推荐方案 4 勉强可接受但不推荐方案 1 不推荐方案 2、方案 5 禁止方案 3、方案 6核心原因方案 4Service 层依赖接口职责正确扩展性好。方案 1接口依赖对了但 Controller 承担了业务组装。方案 2、5依赖具体实现类破坏依赖倒置和可替换性。方案 3、6跨层依赖 Mapper破坏分层、封装和单一职责。