关键词VO、DO、DTO、Java 分层架构、领域模型、数据传输对象刚入行的时候最怕看到项目里一堆XxxVO、XxxDO、XxxDTO飞来飞去。明明都是装数据的类为什么还要分这么多种能不能干脆统一用一个Xxx搞定答案先放这短期能跑长期必乱。这篇文章就把这三个O讲清楚顺便给你一套能直接抄的项目分层范本。一、先给结论它们各管一段对象全称中文活动区域关注点DOData Object / Domain Object数据对象 / 领域对象数据库 ↔ DAO 层表结构、持久化DTOData Transfer Object数据传输对象服务层 ↔ 控制层跨进程/跨层精简、安全、按需组装VOView Object / Value Object视图对象控制层 ↔ 前端页面展示、格式化一句话记忆DO 贴近数据库DTO 贴近传输VO 贴近屏幕。二、DO和数据库表同呼吸共命运DO 是数据持久层的核心字段基本和数据库表一一对应。它的唯一职责就是忠实地映射表结构。// 用户表 user 对应的 DOTableName(user)publicclassUserDO{privateLongid;privateStringusername;privateStringpassword;// 注意敏感字段privateStringphone;privateIntegerstatus;privateDatecreateTime;privateDateupdateTime;// getter / setter 省略}要点字段名、类型尽量和表保持一致别在这层做展示优化。密码、盐值这类敏感字段可以存在于 DO但绝不能一路传到前端。在 MyBatis / JPA 的 Entity 里DO 和 Entity 经常是同一类东西的不同叫法。三、DTO跨层传输的瘦身快递盒当 Controller 要调 Service或者微服务之间要传数据你不会把整个 DO 丢过去里面还带着密码呢。DTO 就是为了按需组装、减少冗余、屏蔽敏感字段而存在的。// 创建用户的请求 DTOpublicclassUserCreateDTO{NotBlank(message用户名不能为空)privateStringusername;Pattern(regexp^1[3-9]\\d{9}$,message手机号格式不正确)privateStringphone;// 没有 password、没有 id、没有时间戳}// 服务间返回的 DTO聚合了用户和订单概要publicclassUserProfileDTO{privateLonguserId;privateStringnickname;privateIntegerorderCount;privateBigDecimaltotalSpent;}要点DTO 里通常带参数校验注解NotBlank、Pattern它是入参契约。一个 DO 可以拆成多个 DTO列表页 DTO、详情页 DTO反向也可以多表聚合成一个 DTO。跨服务调用时DTO 就是接口的对外承诺改字段要慎重涉及兼容性。四、VO只给前端看得到的东西VO 是最后一道关专门给前端页面用。它做的是格式化 裁剪日期转成字符串、枚举转成中文、数字加上单位、把嵌套结构摊平。// 前端用户列表展示用的 VOpublicclassUserListVO{privateLongid;privateStringusername;privateStringphoneMasked;// 138****8888privateStringstatusText;// 1 - 正常0 - 禁用privateStringcreateTimeText;// 2026-07-17 10:30}// 转换过程典型在 Controller 或 Converter 里UserListVOvonewUserListVO();vo.setId(do.getId());vo.setUsername(do.getUsername());vo.setPhoneMasked(maskPhone(do.getPhone()));vo.setStatusText(do.getStatus()1?正常:禁用);vo.setCreateTimeText(format(do.getCreateTime()));要点VO 里不要出现业务计算逻辑只做展示所需的数据搬运。前端要什么格式VO 就长什么样——它是以页面为中心的。敏感字段在 VO 层面必须彻底消失。五、三者怎么串起来一张流程图看明白[ 数据库 ] │ (MyBatis 查询) ▼ UserDO ──► Service 层做业务处理 │ │ (DO → DTO跨层/跨服务传) ▼ UserDTO ──► Controller 接收 / 转发 │ │ (DTO/DO → VO按页面需求组装) ▼ UserVO ──► 返回给前端渲染简化记忆DO 进来DTO 跑腿VO 出去。六、新手最容易踩的 3 个坑一个类打天下前端要改个字段展示你直接改了 DO结果把数据库映射也搞崩了。分层就是为了改展示不动存储。DTO 当 VO 用把带密码的 DTO 直接序列化返回前端——这是真实发生过的严重事故。过度设计只有 3 张表的小项目硬凑 VO/DO/DTO 三套反而拖慢开发。中小项目可以DO VO两件套起步等业务复杂了再补 DTO。七、什么时候该用什么时候可以省项目规模推荐组合说明内部小工具 / DemoEntity 直接用怎么快怎么来普通单体后台DO VOController 直接转 VO 返回标准分层后台DO DTO VOService 出 DTOController 转 VO微服务DTO强约束 VO跨服务必须用 DTO 当契约八、总结DO忠于数据库DTO忠于传输VO忠于页面。分层的本质不是多写几个类而是隔离变化存储结构、接口契约、展示需求三者各自演进互不干扰。别为了分层而分层但一旦项目超过玩具级这套三件套就是你的安全带。如果你现在项目里还是一个 User 类走天下不妨从今天起先把返回前端的对象单独抽成 VO这一步成本最低、收益最明显。觉得有用点个赞 关于 DTO 和 VO 的互相转换MapStruct / 手写 Converter下篇可以展开讲。评论区说说你们项目用的是几件套
VO、DO、DTO 到底怎么分?一篇讲透后端分层的数据对象
关键词VO、DO、DTO、Java 分层架构、领域模型、数据传输对象刚入行的时候最怕看到项目里一堆XxxVO、XxxDO、XxxDTO飞来飞去。明明都是装数据的类为什么还要分这么多种能不能干脆统一用一个Xxx搞定答案先放这短期能跑长期必乱。这篇文章就把这三个O讲清楚顺便给你一套能直接抄的项目分层范本。一、先给结论它们各管一段对象全称中文活动区域关注点DOData Object / Domain Object数据对象 / 领域对象数据库 ↔ DAO 层表结构、持久化DTOData Transfer Object数据传输对象服务层 ↔ 控制层跨进程/跨层精简、安全、按需组装VOView Object / Value Object视图对象控制层 ↔ 前端页面展示、格式化一句话记忆DO 贴近数据库DTO 贴近传输VO 贴近屏幕。二、DO和数据库表同呼吸共命运DO 是数据持久层的核心字段基本和数据库表一一对应。它的唯一职责就是忠实地映射表结构。// 用户表 user 对应的 DOTableName(user)publicclassUserDO{privateLongid;privateStringusername;privateStringpassword;// 注意敏感字段privateStringphone;privateIntegerstatus;privateDatecreateTime;privateDateupdateTime;// getter / setter 省略}要点字段名、类型尽量和表保持一致别在这层做展示优化。密码、盐值这类敏感字段可以存在于 DO但绝不能一路传到前端。在 MyBatis / JPA 的 Entity 里DO 和 Entity 经常是同一类东西的不同叫法。三、DTO跨层传输的瘦身快递盒当 Controller 要调 Service或者微服务之间要传数据你不会把整个 DO 丢过去里面还带着密码呢。DTO 就是为了按需组装、减少冗余、屏蔽敏感字段而存在的。// 创建用户的请求 DTOpublicclassUserCreateDTO{NotBlank(message用户名不能为空)privateStringusername;Pattern(regexp^1[3-9]\\d{9}$,message手机号格式不正确)privateStringphone;// 没有 password、没有 id、没有时间戳}// 服务间返回的 DTO聚合了用户和订单概要publicclassUserProfileDTO{privateLonguserId;privateStringnickname;privateIntegerorderCount;privateBigDecimaltotalSpent;}要点DTO 里通常带参数校验注解NotBlank、Pattern它是入参契约。一个 DO 可以拆成多个 DTO列表页 DTO、详情页 DTO反向也可以多表聚合成一个 DTO。跨服务调用时DTO 就是接口的对外承诺改字段要慎重涉及兼容性。四、VO只给前端看得到的东西VO 是最后一道关专门给前端页面用。它做的是格式化 裁剪日期转成字符串、枚举转成中文、数字加上单位、把嵌套结构摊平。// 前端用户列表展示用的 VOpublicclassUserListVO{privateLongid;privateStringusername;privateStringphoneMasked;// 138****8888privateStringstatusText;// 1 - 正常0 - 禁用privateStringcreateTimeText;// 2026-07-17 10:30}// 转换过程典型在 Controller 或 Converter 里UserListVOvonewUserListVO();vo.setId(do.getId());vo.setUsername(do.getUsername());vo.setPhoneMasked(maskPhone(do.getPhone()));vo.setStatusText(do.getStatus()1?正常:禁用);vo.setCreateTimeText(format(do.getCreateTime()));要点VO 里不要出现业务计算逻辑只做展示所需的数据搬运。前端要什么格式VO 就长什么样——它是以页面为中心的。敏感字段在 VO 层面必须彻底消失。五、三者怎么串起来一张流程图看明白[ 数据库 ] │ (MyBatis 查询) ▼ UserDO ──► Service 层做业务处理 │ │ (DO → DTO跨层/跨服务传) ▼ UserDTO ──► Controller 接收 / 转发 │ │ (DTO/DO → VO按页面需求组装) ▼ UserVO ──► 返回给前端渲染简化记忆DO 进来DTO 跑腿VO 出去。六、新手最容易踩的 3 个坑一个类打天下前端要改个字段展示你直接改了 DO结果把数据库映射也搞崩了。分层就是为了改展示不动存储。DTO 当 VO 用把带密码的 DTO 直接序列化返回前端——这是真实发生过的严重事故。过度设计只有 3 张表的小项目硬凑 VO/DO/DTO 三套反而拖慢开发。中小项目可以DO VO两件套起步等业务复杂了再补 DTO。七、什么时候该用什么时候可以省项目规模推荐组合说明内部小工具 / DemoEntity 直接用怎么快怎么来普通单体后台DO VOController 直接转 VO 返回标准分层后台DO DTO VOService 出 DTOController 转 VO微服务DTO强约束 VO跨服务必须用 DTO 当契约八、总结DO忠于数据库DTO忠于传输VO忠于页面。分层的本质不是多写几个类而是隔离变化存储结构、接口契约、展示需求三者各自演进互不干扰。别为了分层而分层但一旦项目超过玩具级这套三件套就是你的安全带。如果你现在项目里还是一个 User 类走天下不妨从今天起先把返回前端的对象单独抽成 VO这一步成本最低、收益最明显。觉得有用点个赞 关于 DTO 和 VO 的互相转换MapStruct / 手写 Converter下篇可以展开讲。评论区说说你们项目用的是几件套