[Java EE] 企业级Java Web应用的概念辨析: POJO(PO / DTO / VO) | BO/DO | DAO
序
概念不清,会很影响开发中的逻辑性和条理性,进而影响接口设计,代码编写的质量。
网络上大家对这些个概念的探究很多,但终究没有一个统一的说法。
不论哪家解释,我觉得最重要的是: 1)词汇之间的解释统一; 2)词汇之间的联系不冲突,能自圆其说,自成一体。
在自己初步探索后,个人更认同这样的理念(如下)。
若读者凑巧阅读到本文,欢迎一起探讨。
1 POJO := Plain Ordinary Java Object
POJO := Plain Ordinary Java Object
- 定义:
- 简单、朴素的Java对象。它是一个简单的 Java Bean,通常只包含属性、getter/setter 方法以及一些基本的逻辑。
POJO
是一个普通的 Java 对象,不依赖于任何框架或接口。
- POJO 与 PO / DTO / VO 的关系?
一般的,POJO 默认指 PO(数据实体类)
POJO ⊇ { PO, DTO, VO }
- 特点
- 不依赖于任何框架(如 Spring、Hibernate)。
但一般会使用 lombok (
@Data
/@Builder
/ ...) 这类代码开发的提效工具库
- 不实现任何特定的接口或不继承特定的类。
- 通常用于表示简单的数据结构。
- Demo
public class User {
private String name;
private int age;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
PO := Persistent Object
- 定义:
- 持久化对象(Persistent Object)
用于表示数据库中的一条记录,映射成Java对象
PO
是与数据库表直接映射的对象
PO
仅用于表示数据库数据,无任何其它的数据操作
- 通常用于
ORM
(对象关系映射)框架中,如 Hibernate、MyBatis 等。PO
的每个属性通常对应数据库表中的字段。
- 特点:
- 与数据库表结构一一对应。
- 通常包含与数据库交互的注解或配置。
- Demo:
@Entity
@Table(name = "user")
public class UserEntity {//或 User
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
// Getters and Setters
}
DTO := Data Transfer Object
- 定义
- 数据传输对象(Data Transfer Object)
DTO
是用于在系统内不同微服务或不同系统之间传输数据的对象,通常用于减少网络传输的数据量或简化数据传递。
DTO
通常用于【不同Web服务之间(RestFul / RPC调用 / WebService调用等)】DTO
通常不包含业务逻辑。
- 特点
- 用于在系统内不同微服务或不同系统之间传输数据的对象。
- 通常只包含数据字段和 getter/setter 方法。
- 可以组合多个
POJO
/PO
的数据。
一个DTO
可以对应多个从存储层返回的PO
(Persistent Object)的json数组
这里可以使用AutoMapper
来进行自适配,减少两种对象适配的代码量。
- Demo
public class UserDto {
private String name;
private String email;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
VO := View Object(个人更认同) := Value Object
- 定义
- 视图对象/值对象
- 通常用于业务层之间的数据传递,用于封装业务逻辑中的数据。
VO
对象通常是不可变的,且不包含业务逻辑。
- VO for request and response of users or controller layers
用于表示一个与前端界面交互的Java对象;用于展示层,它的作用是把某个指定页面(或页内组件)的所有数据封装起来。
- 举例:
- 展示层将
用户信息Dto
传送过来男性显示成帅哥(客户端1),或者显示成靓仔(客户端2);将帅哥或者靓仔,转换成男性,以DTO形式请求服务端。- 视图模型
VO
可以对应客户端的网页显示,同样的DTO
比如性别男,可以对应多个VO
进行显示,即可以对应多个客户端,比如VO1把性别男显示成帅哥,VO2把性别男显示成靓仔等等。
- VO VS PO:
- PO: 纯数据库表记录的完全映射;
- VO: 仅包含前端需要展示的数据/属性;(需要对前端/用户屏蔽部分字段;减小数据传输量)
- 特点
- 通常用于展示层与服务层之间的数据传输。
- 不可变性,通常不包含业务逻辑。
- Demo
public class UserVO {
private String username;
private String email;
// Getters and Setters
}
QO := Query Object
- 定义:
QO
是用于封装查询条件的对象,通常用于复杂查询场景。QO
可以包含分页信息、排序规则、过滤条件等。
- 特点:
- 用于封装查询条件。
- 通常用于 DAO 层或 Service 层。
- Demo
public class UserQuery {
private String username;
private int page;
private int size;
private String sortBy;
// Getters and Setters
}
2 BO/DO := Business Object / Domain Object
BO/DO := Business Object / Domain Object
- 定义:
- 业务对象(Business Object) / 业务对象/领域对象
BO
是封装业务逻辑的对象,通常包含业务规则和逻辑。- BO 可以组合多个 PO 或 DTO,并对其进行操作。
BO 包含了业务逻辑,常封装了对DAO,RPC等的调用;
可进行PO,VO,DTO之间的转换;
通常属于业务层,但要区别于直接对外提供服务的服务层;
BO提供基本业务单元的基本业务操作,在设计上属于被服务层业务流程调用的对象,一个业务流程可能需要调用多个BO来完成
- 特点:
- 包含业务逻辑。
- 可以组合多个 PO 或 DTO。
- 通常用于 Service 层。
- Demo
public class UserBO {
private UserPO userPO;
private List<OrderPO> orders;
public double calculateTotalOrderAmount() {
return orders.stream().mapToDouble(OrderPO::getAmount).sum();
}
// Getters and Setters
}
3 DAO
Database Access Object
- 定义:
- 数据库访问对象(Database Access Object)
DAO
是用于封装对数据源的访问的对象,通常用于抽象和封装对数据库的 CRUD 操作。DAO 层是数据访问层,负责与数据库交互。
用DAO
对象访问数据库,包括增删查改等操作;同POJO一起使用
- 特点:
- 面向 Service层,提供对数据库的访问接口。
- 隐藏数据库操作的细节。
- 通常与 PO 或 Entity 一起使用。
- Demo
public interface UserDAO {
UserPO getUserById(Long id);
void saveUser(UserPO user);
void updateUser(UserPO user);
void deleteUser(Long id);
}
// 实现类
public class UserDAOImpl implements UserDAO {
@Override
public UserPO getUserById(Long id) {
// 数据库查询逻辑
}
@Override
public void saveUser(UserPO user) {
// 数据库保存逻辑
}
@Override
public void updateUser(UserPO user) {
// 数据库更新逻辑
}
@Override
public void deleteUser(Long id) {
// 数据库删除逻辑
}
}
J 提问环节
- 假定: 我们有一个面试系统,数据库中存储了很多面试题,通过 web 和 API 提供服务。可能会做如下的设计:
- 数据库表:面试题
- PO:
- VO:
- DTO:
- DAO:
- BO:
Y 最佳实践 for Java Web 应用


基础 Java Bean
- BaseObject
Controller 层 : 与 前端视图层(VO) 或 面向第三方应用的传输层(DTO)
- 本层操纵 Java Bean 的类一般命名: XxxController (主流/推荐)
- BasePage
- BaseRequest
- BaseResponse
- PageRequest
- PageResponse
- XxxRequest 及 XxxPageRequest
如: GetConfigRequest(根据API进行一一定义) / SetConfigRequest
API 命名规范 (仅使用1种)
增 : createXxx [√] / addXxx [x] / appendXxx [√]
删 : deleteXxx [√]
查 : exportXxx / getXxx [√] / findXxx [x] / queryXxx[x]
改 : setXxx[] / updateXxx[]
复数/批量 : ...List[√] / ...s[x]
-
XxxPageResponse(XxxDto/XxxVo) 及 XxxResponse(XxxDto/XxxVo)
-
场景总结
- BaseRequest
(非分页请求场景) - PageRequest
(分页请求场景) - XResponse<PageResponse
> (分页响应场景 / 非分页响应场景) 【通用响应/推荐】 - XResponse<BaseResponse
>(非分页响应场景)
Service 层 : 处理业务逻辑、将 PO 与 VO / BO / DO / DTO 互转
- 本层操纵 Java Bean 的类一般命名: XxxService / XxxServiceImpl (主流/推荐)
Service 层的协作模式
- 模式1: Service + DAO
DAO
中只做CRUD及类似的简单操作(称之为功能点,不包含业务逻辑)
Service中通过调用一个或多个DAO中的功能点来组合成为业务逻辑.
Service的数量应该由功能模块来决定。
在这种模型中业务逻辑是放在Service中的,事务的边界也应该在Service中控制.
当然,直接在Service中控制事务会引入非业务逻辑的代码,幸好Spring的AOP可以解决这个问题,这也是引入Spring的原因之一.
如果说到缺点,就在于对某些对象的操作就是简单的CRUD,Service层显得累赘
- 模式2: Service + BO, 而 BO = DAO + 业务方法
在原先
DAO
的基础上添加业务方法,形成BO
对象。
需要注意的是:BO
中的业务方法往往是针对一个实体对象的,如果需要跨越多个实体对象,则方法应该放在Service中。
当然小规模的应用中,没有Service,完全是DAO或BO也是可以接受的。
- 参考文献
场景:PO 转 VO / DTO / BO (必读)
方案1:PO 作为 VO 的构造器入参(组合式)
UML关系:组合关系
案例
// 实体类(PO类)
public class Goods {
private Long id;
private String name;
private BigDecimal price;
private String description;
// 其他属性和方法...
}
// VO类通过组合实现
public class GoodsVO {
private Long id;
private String name;
private BigDecimal price;
// 构造方法或工厂方法,将实体类转换为VO类
public GoodsVO(Goods goods) {
this.id = goods.getId();
this.name = goods.getName();
this.price = goods.getPrice();
}
// Getter 和 Setter 方法...
}
通过组合,我们实现了
VO
类对实体类的定制化展示,同时保留了灵活性,使得VO
类的设计不受实体类的限制。
但会重复编写大量的 set 和 get 代码,对象之间频繁复制。
方案2:继承方式的VO设计(继承式)【不推荐】
VO
类继承PO
实体类,转而调用实体类的部分属性,可以达到复用相同属性的效果。
如果你决定使用继承,下面是一个简单的示例。
在这个例子中,GoodsVO 类继承自 Goods 类,通过继承,GoodsVO 类拥有了 Goods 类的所有属性和方法。
// VO类通过继承实现
public class GoodsVO extends Goods {
// 新增或覆盖需要展示的属性
private String displayInfo;
// 构造方法或工厂方法,将实体类转换为VO类
public GoodsVO(Goods goods) {
// 调用父类构造方法,复制基本属性
super.setId(goods.getId());
super.setName(goods.getName());
super.setPrice(goods.getPrice());
super.setDescription(goods.getDescription());
// 初始化VO类特有的属性
this.displayInfo = "Additional information for display";
}
// Getter 和 Setter 方法...
}
- 通过继承,我们可以在
GoodsVO
类中新增或覆盖需要展示的属性,实现对特定场景的定制化。但需要注意的是,继承关系通常带来类之间的紧密耦合,可能会限制类的扩展性和灵活性。
- 这种方式生成的文档很不清晰,每个接口都有一堆没返回给前端的字段;业务中的额外字段会污染
VO
;POJO(PO)
承担的责任太重了,
方案3:开源的Bean转换工具 MapStruct 【推荐】
参见文档:
方案4:开源的Bean转换工具 MapStruct-Plus 【推荐】
参见文档
方案5:开源的Bean转换工具 Spring BeanUtils
参见文档
原则上,不推荐用于做Java数据对象的转换工具;
但其工具类不仅限于做数据对象的转换功能,还有一些其他不错的方法(按需使用即可)
方案5:开源的Bean转换工具 Apache Commons BeanUtils
参见文档
Repository(DAO) 层 : 定义、访问、操纵数据实体(PO/Entity)
-
本层的 Java Bean,主要通过包路径(
xx.entity
) 和 ORM的注解(如:@Entity
)区分。 -
本层操纵 Java Bean 的类一般命名: XxxRepository (主流/推荐) / XxxMapper / XxxDAO
R FAQ
Q:由“为什么VO不能继承PO?” 引出的为什么组合优于继承?
Q:为什么组合优先于继承?DTO / VO / DO 的真正意义?
- 不少工程师在个人项目中喜欢直接使用各种
model
来继承po
,单纯的因为可以直接省去写各种重复getter
和setter
的步骤,并且不需要使用各种对象拷贝工具(如:MapStruct / MapStruct-Plus / Spring 的 BeanUtils.copyProperties等)。
但是,搞vo
、do
、po
,还有其他各种Xo
,是正为了解藕它们之间的联系,而继承却是建立它们之间的耦合关系,确实是会限制类的扩展性和灵活性。
比如:当表结构做了增减,
do
变了vo
也变了,你的接口返回项也会变化,线上的项目容易翻车(虽然我觉得表结构不会那么轻易变化),也不利于节约流量和信息安全,会返回很多不必要的字段给前端,容易被不怀好意的人推测出数据库的字段。
- 面向对象编程中,有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承。
同样地,在《阿里巴巴Java开发手册》中有一条规定:谨慎使用继承的方式进行扩展,优先使用组合的方式实现。
X 参考文献

本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!