ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot三层架构详解:职责边界、事务失效与循环依赖排查

Spring Boot三层架构详解:职责边界、事务失效与循环依赖排查 刚入行那几年我一直在想一个问题为什么Java圈子的项目翻来覆去都是Controller、Service、Mapper这老三层直到自己动手写过几个烂摊子又啃过一些开源项目之后才明白三层架构不是什么高深理论它就是Java生态里最皮实、最能扛住业务变化的一套组织代码的方式。尤其是配上Spring Boot之后三层架构几乎是零成本落地你新建一个工程天然就是按这个路子来的。这篇文章我就从实战角度把Spring Boot三层架构的来龙去脉、每个层的职责边界、以及那些文档里不会明说的坑一次讲透。后面要聊的东西适合正在学Spring Boot的初学者也适合那些写了几年代码但没细想过为什么要这么分层的朋友。我会尽量用大白话配合真实项目里会遇到的场景把每一层该干什么、不该干什么、层与层之间怎么协作都掰开揉碎讲清楚。1. 三层架构的职责边界与依赖方向1.1 Controller层真的只是接请求这么简单吗很多人对Controller层的理解就是接收前端请求调一下Service把结果返回这句话大体没错但实际操作里Controller承担的事情远比这个多。我见过不少项目把Controller写成万能入口里面塞了参数校验、权限判断、数据拼装、甚至直接写SQL这种写法短期看是爽了但后面每一次需求变更都是一场灾难。Controller层的核心职责是协议适配。HTTP请求进来可能是JSON、表单、甚至是XMLController要做的是把外部请求转换成内部方法调用能识别的参数再调用Service层获取结果最后把结果序列化成前端需要的格式返回。它不该关心这个订单能不能创建这种业务规则也不该关心数据到底存在哪张表。我在实际项目里给团队定的规矩很简单Controller里只允许做三件事——取参、调Service、包装返回。任何多余的逻辑都属于越界。有人会问参数校验算不算多余逻辑我的建议是简单的参数格式校验比如非空、长度限制可以放在Controller层用Spring的Valid注解就能搞定但涉及业务规则的校验比如这个用户是否有权限下单必须下沉到Service层。还有一个容易被忽略的点Controller层的命名和URL设计其实也属于职责的一部分。一个规范的项目Controller的类名应该跟业务模块强相关比如OrderController、UserController方法名要能让人一眼看出是在干什么避免那种doAction、handleData这类毫无信息量的命名。1.2 Service层是业务核心但不是万能中转站Service层是整个三层架构里最值钱的一层因为业务规则都在这层沉淀。但我也踩过一个大坑把Service做成Controller的照搬Controller收什么参Service就照单全收然后直接丢给Mapper中间没有任何业务逻辑。这种两头受气的写法本质上等于没分层。真正合格的Service层应该具备三个特征。第一个特征是业务规则内聚比如下单这个动作要检查库存、要扣减余额、要生成订单号、要记录日志这些步骤必须在一个事务里串起来这就是Service层存在的意义。第二个特征是对上层屏蔽实现细节Controller不需要知道数据是存在MySQL还是Redis里它只需要调用orderService.createOrder(...)拿到结果。第三个特征是能够跨层协调比如某个操作需要同时调用数据库和第三方接口这个编排逻辑只能在Service层做。关于事务我想多说一句。很多初学者喜欢在Controller方法上加Transactional这其实是错误的姿势。事务应该开在Service层因为事务的粒度必须跟业务边界对齐Controller是协议层它天然不属于业务事务的范畴。我自己定过一个简单标准一个Service方法就是一个完整业务动作要么全部成功要么全部回滚。Service层的接口设计也值得细说。有些团队喜欢先定义接口再写实现我觉得小项目完全没必要直接写实现类就够了等到确实出现多实现场景比如订单有普通订单和秒杀订单两种处理策略再抽出接口不迟。过早抽象是浪费过晚抽象是重构成本这个度要靠经验拿捏。1.3 DAO层与数据访问的边界感DAO层在Spring Boot里通常是Mapper接口是离数据库最近的一层它的职责只有一个完成对象与数据库记录之间的映射。这个映射包括SQL的编写、参数的绑定、结果集的处理。但很多人把DAO层理解得太窄觉得它就是写SQL的地方。实际上DAO层还要负责处理数据库的方言差异、分页逻辑、主键生成策略、以及一些简单的数据组装。举个例子用MyBatis的时候一个复杂查询可能需要关联四五张表这时候返回的VO对象应该由谁组装我的习惯是简单的一对一、一对多关联直接在SQL里用ResultMap映射复杂的多表聚合查询单独建一个查询专用的Mapper方法而不是在Service里用代码for循环拼接。还有个实操经验DAO层的方法命名应该跟SQL语义强相关比如selectByUserId、deleteByOrderId、updateStatusById这样见到方法名就能猜到SQL大概长什么样。我见过有些项目Mapper方法是query1、query2这种命名维护起来简直让人抓狂。2. 为什么Java生态偏爱三层架构2.1 三层架构和MVC的关系很多人把三层架构和MVC混为一谈其实它们是从两个维度描述同一件事。MVC关注的是表现层的职责划分——Model、View、Controller三者如何协作三层架构关注的是整个应用的纵向切分——表现层、业务层、数据访问层各管一段。在Spring Boot的Web场景里Controller同时扮演了MVC里的C和三层架构里表现层的角色Service承担业务层Mapper/Repository承担数据访问层。理解这个区别的意义在于如果你用Spring Boot写接口你其实同时在使用MVC和三层架构但如果你写的是一个批处理任务或者消息消费者那就只有三层架构在起作用没有MVC的View和Controller。换句话说三层架构是比MVC更底层的组织思想。2.2 为什么不用六边形架构——现实世界的答案热词里有人在问为什么Java大部分用三层架构不用六边形架构这个问题我在团队内部也讨论过。六边形架构也叫端口与适配器架构确实更优雅它强调领域层独立于一切外部依赖通过端口反向依赖外部系统。但现实是绝大多数业务项目根本用不上这种级别的抽象。原因很简单。第一团队协作成本六边形架构需要严格的依赖倒置设计每一层都要定义端口接口项目成员如果没有统一的架构认知很容易写歪代码review成本极高。第二中小型项目的投入产出比一个CRUD占七成的管理后台用六边形架构意味着每个用例都要多写接口、多写适配器收益主要体现在未来可能替换外部依赖但这个未来多数时候根本不会来。第三Spring Boot的生态惯性Spring Boot的自动配置、Starter机制本身就是面向三层架构设计的Controller、Service、Repository这些注解已经把三层语义定死了。我不是说六边形架构不好它在DDD领域建模、中台建设这类复杂场景下确实有价值。但在用一个Spring Boot项目快速交付一个可维护的业务系统这件事上三层架构是经过几十年验证的最优解。架构不是越高级越好而是越匹配团队认知和业务复杂度越好。2.3 Spring Boot让三层架构落地更顺滑Spring Boot对三层架构最大的贡献是消灭了配置文件的零碎感。在传统SSH整合时代配一个三层结构需要写一堆XML定义Bean、声明事务代理、配数据源每一步都可能出错。Spring Boot把这一切变成了约定你只要在类上标注对应的注解组件就会被自动扫描注册。我曾经带过一个项目从SSH框架迁移到Spring Boot最大的感触不是代码量变少了而是开发的心智负担变低了。不需要再关心Bean是怎么装配的、事务代理是怎么拦截的只需专注于业务逻辑本身。Spring Boot的依赖注入DI机制让三层之间的解耦更彻底Controller声明依赖Service接口Service声明依赖Mapper接口具体实现由Spring容器在运行时注入这也是三层架构能保持稳定最核心的机制。3. 在Spring Boot中落地三层架构的实操细节3.1 包结构与依赖注入的正确姿势先给一个我惯用的包结构模板这个结构在中小型项目里非常顺手com.example.project ├── controller │ └── OrderController.java ├── service │ ├── OrderService.java │ └── impl │ └── OrderServiceImpl.java ├── mapper │ └── OrderMapper.java ├── entity │ └── Order.java ├── dto │ ├── CreateOrderRequest.java │ └── OrderResponse.java ├── vo │ └── Result.java ├── config │ └── WebConfig.java └── common ├── exception └── utils包结构的核心原则是按技术层次分包按业务模块分包。上面这个模板是按技术层次分的每个包下面再根据业务建子包。还有一种流派是按业务模块分先分user、order再在模块内分controller、service等适合业务边界特别清晰的场景。两种流派没有绝对优劣但在一个项目里必须只选一种混用就是灾难。依赖注入的正确姿势是面向接口编程。Controller注入Service接口而不是实现类这样将来替换业务实现的时候Controller的代码可以完全不动。Spring Boot默认是单例模式所以Controller里注入的Service实例是线程安全的但这依赖一个前提Service实现类里不能定义可变的共享状态。我见过有同事在Service里定义了一个HashMap做缓存但没加锁结果并发下数据错乱排查了好久。Service层必须是无状态bean这是铁律。3.2 DTO、VO、Entity到底怎么分三层架构落地过程中最容易产生分歧的就是Entity、DTO、VO这些对象怎么用。我的经验是Entity对应数据库表结构只在DAO层和Service层之间传递VO是给前端展示用的对象只在Controller层出现DTO是业务参数对象承担跨层数据传递。这么说有点抽象拿订单来举例。数据库表t_order有字段id、user_id、total_amount、status。前端创建订单时提交的JSON可能只有userId和items列表这时候CreateOrderRequestDTO里就不需要有totalAmount字段金额是Service层算出来的。下单完成后返回给前端的OrderResponseVO可能包含orderId、statusDesc这种经过业务加工的信息直接拿Entity返回是搞不定的。很多初学者图省事直接把Entity层往上捅Controller和Service到处都在用Entity这个做法在小项目里还能忍但一旦涉及敏感字段就会出问题。比如User实体里有password字段如果直接把Entity序列化返回密码就泄露给前端了。文章开头提到的Spring Boot全局过滤器处理XSS攻击的场景如果实体没有清晰的分层你都不知道该在哪里做脱敏、在哪里做过滤。我的建议是Entity绝不直接返回给前端至少得经过一层转换。转换的代码可以放在Service层也可以单独建一个Assembler类别在Controller里手动set属性那样业务代码会被脏死。3.3 事务、异常和通用返回——隐藏的第四层虽然没有明文规定但成熟的三层架构项目通常会在三层之上加几个基础设施层比如异常层、通用返回层。这些基础设施处理得好不好直接决定项目后期好不好维护。先说话异常。Controller方法里try-catch包住所有逻辑再抛Exception的做法我实在看不下去——异常处理应该遵循向上抛、统一接的原则。Service层抛业务异常如OrderNotExistExceptionController层不处理由全局异常处理器RestControllerAdvice统一捕获并转成规范的错误码返回。这样Controller里的代码都是干干净净的业务调用可读性会好很多。再说通用返回。我习惯定义一个ResultT结构包含code、message、data三个字段所有接口都返回这个结构。这样做有三个好处一是前后端约定统一前端只看code就能判断成功失败二是异常情况下也能返回统一的JSON结构三是给可能的微服务化留后路。事务刚才提过这里再强调一点如果Service实现类里有两个方法互相调用同一个类内部Transactional会失效因为Spring的事务是通过AOP代理实现的内部调用的this.method()不会经过代理。解决方法很简单把需要独立事务的方法放到另一个Service类里或者用AopContext.currentProxy()获取代理对象再调用。这个坑非常隐蔽不踩一次很难记住。4. 常见问题与排查技巧实录4.1 循环依赖与Service层设计Spring Boot中循环依赖是一个让人头疼的问题。ClassA依赖ClassBClassB又依赖ClassA两个Bean互相引用启动直接报错。常见场景出现在Service层和事务管理器、或两个Service互相调用的时候。前面说的防止事务失效这个点也容易引发循环依赖。有同事说我用this调用事务方法失效那我干脆在ClassA里注入ClassB让ClassB调用ClassA的事务方法结果绕成了循环依赖。这个问题有十几个解决姿势但我的建议是重构设计而不是硬解依赖。比如把事务方法抽到独立的TransactionService里或者把相互调用的逻辑拆成三个类从根上避免循环依赖。靠Spring的setter循环依赖可以解决但那是在给未来埋雷。排查循环依赖的几个常用手段启动日志会明确提示The dependencies of some of the beans in the application context form a cycle按照日志里给出的Bean名称链条可以快速定位是哪两个类互相引用。有些循环依赖是间接的A依赖B、B依赖C、C依赖A需要展开日志里的Bean链去追。4.2 三层架构最容易被写歪的三种病态项目做久了你会发现三层架构最大的敌人不是架构本身而是开发者在细节上的变通。我这里总结三种最典型的病态各位可以对号入座有则改之无则加勉。第一种是**直通式DAO化**Controller直接注入Mapper跳过Service层。比如一个简单的字典查询有些开发觉得就这么点逻辑还要写个Service多麻烦于是直接在Controller里查库。这种代码一多Service层名存实亡后续想加缓存、加权限都无从下手。第二种是**上帝Service**一个Service类几百行几十个方法什么业务都往里塞。这种类看着功能齐全实际上一旦要处理并发事务、要梳理依赖关系就寸步难行。我的建议是一个Service类只负责一个业务域比如订单域就OrderService用户域就UserService如果一个域太大就按子域继续拆绝不心软。第三种是**Model实体乱飞**刚才提过的Entity往外捅只是其一还有人会把业务参数做成Map传递整个链路全靠魔法字符串沟通。这种写法最要命它彻底摧毁了三层架构的类型安全边界。我记得有次排查一个线上问题最后发现是有人往Map里放了同名key后写的覆盖了先写的值查了一个晚上。你说这时间花得冤不冤。4.3 面试时怎么回答三层架构问题很多人在面试Spring Boot岗位时会被问到三层架构普通回答就是Controller、Service、DAO各层职责是什么这种回答不会扣分但也不出彩。我建议按三个关键词来组织答案。关键词一关注点分离。三层架构的本质是把协议解析、业务处理、数据访问三种不同变化频率的关注点分开让每一层可以独立演进。协议变化不影响业务业务变化不影响数据库数据库换掉不影响上层。关键词二依赖倒置。虽然叫三层架构但Spring Boot里的依赖方向严格说是上层依赖下层的抽象即Controller依赖Service接口、Service依赖Mapper接口。真正稳定的是这一层抽象关系而不是具体实现类。关键词三成本权衡。如果面试官追问为什么不用六边形架构你可以把第二节里讲的成本逻辑讲一遍强调架构选型必须匹配团队规模和业务复杂度。有限的时间里与其追求理论上的优雅不如追求可交付、可维护和团队共识。最后补一句可以加分的经验三层架构不是银弹它解决的是业务逻辑与基础设施解耦的问题不解决微服务怎么拆分的问题。当项目复杂度继续上升自然演化出DDD分层、模块化、微服务等更精细的结构那时候三层架构会退化为某一模块内部的实现细节。5. 顺手把Spring Boot反编译和配置这些周边问题聊透热词里有人问怎么将springboot jar反编译成项目这个问题经常出现在接手老系统的场景。说句实话Spring Boot的可执行jar本质上是一个包含所有依赖和配置的fat jar反编译思路分两步走先用解压工具解出BOOT-INF/classes目录里面是项目自己的类再用工具如JD-GUI或IntelliJ自带的反编译器把.class转成.java。但反编译出来的代码只能用于排查问题想直接还原成能编译的工程基本不现实因为注解信息、资源文件结构都会丢失。至于Spring Boot版本太高的问题我的建议是能用稳定版绝不用最新版。Spring Boot的版本迭代速度很快新版本往往伴随Spring Framework新特性、依赖升级和配置项调整对老项目意味着潜在的不兼容。如果项目在2020年前后锁定在2.3.x或2.4.x完全没问题如果跟着Java 17或21走建议用2.7.x或3.x系列。升级版本的时候重点检查三个地方javax.*到jakarta.*的包名变化、spring.factories到AutoConfiguration.imports的自动装配文件变化、以及安全配置的写法变化。热词里提到的Spring Boot自动装配原理我在这里简单点一句Spring Boot的starter包通过META-INF/spring.factories老版本或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports新版本注册自动配置类自动配置类上标注了ConditionalOnClass、ConditionalOnMissingBean等一系列条件注解Spring容器启动时逐个判断条件满足条件的配置类才会生效并注入Bean。理解了这个原理你就明白为什么引入一个starter无需任何配置就能获得一堆默认Bean——这就是Spring Boot能成为三层架构最佳土壤的根本原因。关于如何排查这些问题也是一套固定流程调整完版本先编译再启动启动完看控制台有没有BeanDefinitionStoreException或者NoSuchBeanDefinitionException这种异常八成是包扫描路径不对或自动配置没生效。用--debug参数启动应用控制台会打印自动配置报告哪个配置类生效、哪个被排除一目了然。我在升级过几个老项目之后已经养成习惯只要动Spring Boot版本就先开--debug跑一遍比翻文档快多了。写到这里我这个人实操中的体会是三层架构是Spring Boot项目的基本盘它不炫目但足够可靠。真正的高手不是把架构写得天花乱坠而是能把Controller、Service、DAO三层保持得干干净净让任何一个新接手的人都能在半小时内找到业务代码。至于那些更高级的架构思想当你把基础层的功夫磨练到位了需要的时候自然能顺理成章地引入。先把这三层拿捏明白比什么架构书都管用。
返回列表