Spring 分层架构解析
为了学习 Java 和 Spring,我让 AI 给我写了一套前后端完善的,适用于教学用途的电商代码。其中的 Spring Boot 单体分层架构如下:
- controller:对外暴露接口,负责请求接收、参数校验入口、响应返回
- service:定义业务能力接口
- service.impl:实现具体业务逻辑
- mapper:数据库访问层
- entity:数据库实体对象
- dto:请求参数对象
- vo:响应对象
- common:统一返回、常量、分页等通用基础能力
- config:全局配置类
- security:认证授权相关能力
- exception:异常体系
- enums:状态与角色枚举
- utils:工具类
这样做的目的,是让项目更容易维护、测试、扩展,也让多人协作时不容易把代码写乱。
“按职责拆分代码,让每一层只做自己该做的事。”
下面详细解释每一类分别负责什么、通常放什么代码、以及彼此之间怎么配合。
整体分层思路
一次典型请求,通常会这样流转:
前端请求 → controller → service → mapper → 数据库
然后结果再一层层返回:
数据库结果 → mapper → service → controller → 返回给前端
其中:
- controller 更靠近“接口层”
- service 更靠近“业务层”
- mapper 更靠近“数据层”
而像 dto、vo、entity,则是不同层之间传递数据时使用的对象。
controller:接口控制层
它是干什么的
controller 是系统对外的入口,负责接收前端或外部系统发来的 HTTP 请求。
它主要做这几件事:
- 定义接口路径,如
/user/login、/order/list - 接收请求参数
- 触发参数校验
- 调用
service - 把结果包装后返回给前端
它应该做什么
适合放:
@RestController@RequestMapping/@GetMapping/@PostMapping@RequestBody/@PathVariable/@RequestParam- 参数校验注解入口,如
@Valid
它不应该做什么
不应该在这里写太多业务逻辑,比如:
- 不要在 controller 里写复杂 if/else 业务判断
- 不要直接操作数据库
- 不要在这里拼大量 SQL
- 不要把认证、库存扣减、订单计算等核心逻辑堆进来
为什么这样设计
因为 controller 的职责应该很单纯:
“把请求交给业务层,把业务结果返回出去。”
示例理解
比如“用户注册”:
- controller 接收用户名、密码、手机号
- 校验参数格式是否正确
- 调用
userService.register() - 返回“注册成功”或者错误信息
service:业务能力接口层
它是干什么的
service 负责定义系统的业务能力,通常写成接口。
例如:
- 用户注册
- 用户登录
- 创建订单
- 查询商品列表
它描述的是:系统能做什么事。
为什么要写接口
写成接口有几个好处:
- 业务定义和业务实现分离
- 方便测试和替换实现
- 代码结构更清晰
- 面向接口编程,扩展性更好
它通常放什么
比如:
1 | public interface UserService { |
这里不一定写具体实现,只定义方法。
核心定位
service 是业务层的“门面”或“抽象定义”。
service.impl:业务实现层
它是干什么的
service.impl 是 service 接口的具体实现,真正写业务逻辑的地方。
它通常负责什么
这里是项目里最核心的部分之一,常做的事有:
- 校验业务规则
- 组合多个 mapper 查询
- 调用其他 service
- 处理事务
- 做状态流转
- 做权限判断中的业务部分
- 生成订单、计算金额、库存扣减等
举例
以注册为例,service.impl 中可能会做:
- 判断用户名是否已存在
- 判断手机号是否已注册
- 密码加密
- 构造用户实体
- 调用 mapper 入库
为什么不放到 controller
因为这些都是业务规则,不是“接口接收”该负责的事。
常见注解
@Service@Transactional
一句话理解
service.impl 决定了:
“这件业务到底怎么做。”
mapper:数据访问层
它是干什么的
mapper 专门负责和数据库打交道。
它做的事情是:
- 查询数据库
- 插入数据
- 更新数据
- 删除数据
常见形式
如果你用的是 MyBatis / MyBatis-Plus,mapper 往往是接口:
1 | public interface UserMapper extends BaseMapper<User> { |
或者自定义一些查询方法。
它应该只关心数据
mapper 只回答这些问题:
- 数据怎么查
- 数据怎么存
它不应该关心:
- 用户能不能注册
- 订单该不该创建
- 库存逻辑是否合法
这些属于业务逻辑,应该放在 service.impl。
一句话理解
mapper 是业务层访问数据库的“通道”。
entity:数据库实体对象
它是干什么的
entity 一般对应数据库表中的一条记录,是数据库实体的 Java 映射。
比如数据库有一张 user 表,那么通常会有一个 User 实体类。
它通常包含什么
- 表字段对应的属性
- ORM 注解
- getter/setter
- 基础字段,如 id、createTime、updateTime
例如:
1 | public class User { |
它的特点
它更偏向“数据库结构”,而不是“接口结构”。
为什么不能直接拿 entity 当接口参数/返回值
因为 entity 往往包含数据库内部字段,比如:
- password
- deleteFlag
- createTime
- updateTime
这些不一定适合直接暴露给前端。
所以通常会再拆出 dto 和 vo。
一句话理解
entity 表示:
“数据库里这张表,在 Java 里的样子。”
dto:请求参数对象
它是干什么的
dto 一般指 Data Transfer Object,在很多 Spring Boot 项目里,常用来表示前端传入的请求参数对象。
比如:
- 注册请求参数
- 登录请求参数
- 新增商品请求参数
- 修改订单状态请求参数
为什么不用 entity 接收请求
因为前端传来的数据,和数据库表字段,不一定是一回事。
例如注册请求:
- 前端传
username - 前端传
password - 前端传
confirmPassword
但数据库表里可能并没有 confirmPassword 这个字段。
所以请求参数应该独立成一个 DTO。
dto 的作用
- 承接请求数据
- 配合参数校验注解使用
- 避免前端直接操作数据库实体
- 让接口语义更清晰
示例
1 | public class RegisterRequest { |
一句话理解
dto 表示:
“前端把什么数据传给我。”
vo:响应对象
它是干什么的
vo 一般指 View Object,用来表示返回给前端的数据结构。
为什么不用 entity 直接返回
因为数据库实体里的字段,往往不适合原样返回:
- 密码不能返回
- 删除标志不一定要返回
- 某些内部字段前端不关心
- 有些字段需要二次加工后再返回
vo 的作用
- 控制返回给前端的数据范围
- 屏蔽敏感字段
- 支持组合字段、展示字段
- 让接口返回更稳定
示例
1 | public class UserVO { |
一句话理解
vo 表示:
“我要把什么数据展示给前端。”
common:通用基础能力
它是干什么的
common 放的是很多模块都会重复使用的公共代码。
常见内容
比如:
- 统一返回结果
Result<T> - 分页对象
PageResult<T> - 常量类
- 通用状态码
- 基础请求/响应父类
- 通用分页参数对象
为什么要单独放
因为这些代码不属于某一个具体业务模块,而是整个项目都会用到的“基础设施”。
示例
统一返回:
1 | public class Result<T> { |
这样所有接口都可以统一返回格式。
一句话理解
common 是项目里的“公共零件库”。
config:全局配置类
它是干什么的
config 用来放项目级别的配置代码。
常见内容
例如:
- Spring MVC 配置
- MyBatis 配置
- 跨域配置
- Jackson 时间格式配置
- Redis 配置
- Swagger/OpenAPI 配置
- 线程池配置
- 拦截器注册
- Bean 注册
举例
比如你希望:
- 全局允许跨域
- 配置接口文档
- 配置密码加密器
PasswordEncoder - 配置统一时间格式
这些一般放在 config。
一句话理解
config 是项目运行规则和组件装配的地方。
security:认证授权相关能力
它是干什么的
security 负责登录认证、权限校验这一套安全相关能力。
常见职责
- 登录认证
- JWT 生成与解析
- 用户身份获取
- 权限校验
- Spring Security 配置
- 过滤器
- 用户上下文
它和 service 的区别
security关注“你是谁、你能不能访问”service.impl关注“这件业务该怎么做”
比如:
用户请求删除订单时:
security先判断用户是否登录、有无权限service.impl再判断订单状态是否允许删除
一句话理解
security 管的是:
“你有没有资格做这件事。”
exception:异常体系
它是干什么的
exception 用来统一管理项目中的异常。
常见内容
- 自定义业务异常
BusinessException - 全局异常处理器
GlobalExceptionHandler - 参数校验异常处理
- 系统异常统一兜底
为什么要统一异常处理
如果没有统一异常处理:
- 接口报错格式会乱
- 前端难以处理
- 错误信息不规范
- 代码里到处 try-catch 很臃肿
典型设计
- 业务错误:抛
BusinessException - 全局异常处理器统一转换成标准返回格式
例如:
1 | throw new BusinessException("用户名已存在"); |
然后全局处理器返回:
1 | { |
一句话理解
exception 负责:
“系统出了错,怎么优雅地告诉前端。”
enums:枚举类
它是干什么的
enums 用来定义一些固定取值的状态、类型、角色等。
常见场景
比如:
- 用户角色:ADMIN、USER
- 订单状态:UNPAID、PAID、CANCELLED
- 删除标记:YES、NO
- 性别:MALE、FEMALE
- 状态码定义
为什么不用魔法值
如果直接在代码里写:
1 | if (user.getRole() == 1) |
别人根本不知道 1 是什么。
而用枚举:
1 | if (UserRoleEnum.ADMIN.getCode().equals(user.getRole())) |
语义就清楚很多。
一句话理解
enums 是把固定状态“起名字”,让代码可读性更强。
utils:工具类
它是干什么的
utils 放一些无状态、通用、和业务弱相关的辅助方法。
常见工具类
比如:
- 日期工具类
- 字符串工具类
- ID 生成工具
- Bean 拷贝工具
- JWT 工具类
- 文件处理工具
- 脱敏工具
注意事项
工具类适合放“通用能力”,不适合塞复杂业务逻辑。
比如:
DateUtils.format()可以放 utils- “用户注册时检查邀请码是否合法” 不能放 utils
因为后者是业务逻辑。
一句话理解
utils 是项目中的“辅助小工具箱”。
dto、entity、vo 的区别
dto:接收请求用
前端传什么,我用什么对象接。
entity:映射数据库用
数据库表长什么样,我用什么对象表示。
vo:返回前端用
我要展示给前端什么,就定义什么对象。
举个完整例子:用户注册后查询用户信息
前端传入:
1 | { |
这对应 RegisterRequest DTO
service 处理后入库:
构造 User entity
1 | User user = new User(); |
最终返回前端:
返回 UserVO
1 | { |
不会把 password 返回出去。
为什么这种架构很常见
职责清晰
每一层知道自己该干什么,不容易乱。
易维护
以后改数据库逻辑,多半改 mapper 或 service;
改接口格式,多半改 controller / dto / vo。
易测试
service 可以单独测业务逻辑。
controller 可以测接口行为。
mapper 可以测数据访问。
易扩展
以后加缓存、加权限、加日志、加事务,都有明确落点。
团队协作友好
前后端、不同后端开发人员,都容易理解项目结构。
你可以把它记成一句话
请求流转
Controller 收请求,Service 讲业务,Mapper 查数据。
对象职责
DTO 接参数,Entity 对表,VO 做返回。
基础支撑
Common 放通用,Config 放配置,Security 管权限,Exception 管异常,Enums 管状态,Utils 放工具。
实际开发中容易犯的错
controller 写太重
把很多业务逻辑堆在 controller 里,导致层次混乱。
entity 直接当返回对象
容易把密码、内部状态等敏感字段暴露出去。
utils 变成“万能垃圾桶”
什么逻辑都往工具类里塞,最后难维护。
mapper 写业务判断
mapper 应该只处理数据,不应该做业务决策。
service 不分接口和实现
小项目可以勉强这么写,但规范项目通常还是建议区分 service 和 service.impl。
简要总结
- controller:接口入口
- service:业务定义接口
- service.impl:业务实现
- mapper:数据库访问
- entity:表对象
- dto:请求对象
- vo:响应对象
- common:通用基础
- config:配置
- security:安全认证
- exception:异常处理
- enums:枚举状态
- utils:工具方法
