🎯 Java Web 架构全组件详解(终极汇总版)
本文涵盖:Entity、DTO、VO、Controller、Service 的核心定义、职责边界、使用场景、反模式,以及大厂规范。
一、Entity(实体)
📌 定义
数据库表的 Java 映射,与持久层(Repository/DAO)直接交互。
🎯 核心职责
- 1:1 映射数据库表结构
- 承载 ORM(JPA/Hibernate/MyBatis)注解
- 代表业务领域的核心对象
✅ 什么时候用?
- 必须用:所有与数据库交互的层(Repository、DAO)
- 必须用:使用 JPA/Hibernate 时,每个表对应一个 Entity
- 必须用:MyBatis 的结果集映射
❌ 什么时候用不上?
- 严禁:直接返回给前端
- 严禁:作为 Controller 的入参
- 严禁:跨服务传递(除非是 RPC 的共享领域对象)
📦 典型写法
java
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", unique = true)
private String username;
@Column(name = "password")
private String password; // 加密存储
@Column(name = "created_at")
private LocalDateTime createdAt;
@OneToMany(mappedBy = "user")
private List<Order> orders; // 对象导航
}
💥 反模式(千万别做)
❌ @Entity public class UserDTO ------ 职责混淆
❌ Controller 直接返回 List<User> ------ 暴露敏感字段
❌ Entity 里写 @NotBlank 校验注解 ------ 校验应在 DTO
二、DTO(数据传输对象)
📌 定义
接口的契约,定义 API 的请求参数和响应数据。
🎯 核心职责
- 定义前后端/服务间的通信协议
- 数据校验(请求)
- 按需裁剪字段(响应)
- 接口版本管理
✅ 什么时候用?
- 必须用:所有 Controller 的入参(POST/PUT/GET 参数)
- 必须用:所有 Controller 的返回体
- 必须用:微服务间的 RPC 调用
- 建议用:Service 层与 Controller 层的数据传输
❌ 什么时候用不上?
- 内部方法调用(private 方法)
- 同层传输(Service → Service 尽量用 Entity 或 领域对象)
- 纯查询项目(但依然建议用 DTO 封装查询参数)
📦 典型写法(请求 vs 响应)
请求 DTO
java
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 4, max = 20)
private String username;
@NotBlank
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z]).{6,20}$")
private String password; // 明文传输
@Email
private String email;
// 没有 id!没有 createTime!
}
响应 DTO
java
@Data
public class UserDetailDTO {
private Long id;
private String username;
private String email;
private String avatar; // 完整 URL
private String createTime; // "2024-01-01 12:00:00"
private List<String> roles; // 聚合数据
// 没有 password!没有 salt!
}
💥 反模式
❌ 一个 DTO 通吃所有接口(UserDTO 被 5 个接口共用)
❌ DTO 里有 password 字段但不小心返回了
❌ DTO 字段名用下划线(user_name)而不是驼峰(userName)
❌ 请求 DTO 用基本类型(int pageNum → 默认 0 不是 1)
三、VO(视图对象)
📌 定义
视图层的模型 ,专门为页面渲染组装的数据结构。
🎯 核心职责
- 数据格式化(时间、金额、枚举)
- 多表聚合(页面需要的数据一次性组装好)
- UI 状态标识(是否选中、是否可编辑)
✅ 什么时候用?
- 必须用:服务端渲染(Thymeleaf、JSP、Freemarker)
- 必须用:页面需要的数据和接口 DTO 差异很大
- 可选用:复杂报表、PDF 导出、Excel 导出
- 不适用:纯前后端分离项目(前端自己渲染)
❌ 什么时候用不上?
- 90% 的项目用不上:Vue/React + Spring Boot = 只用 DTO
- 简单的 CRUD 查询(DTO 直接给前端)
📦 典型写法
java
@Data
public class UserProfileVO {
private String username;
private String nickname;
private String avatarUrl; // 已拼接 CDN 域名
private String memberLevel; // "黄金会员"(由积分计算)
private String registerDate; // "2024-01-01"
private String statusDesc; // "正常"
private Boolean canEdit; // 是否有编辑权限(权限计算)
private Integer orderCount; // 订单数量(统计)
// 全都是加工后的数据,没有原始类型
}
💥 反模式
❌ 前后端分离项目硬造 VO(多此一举)
❌ VO 和 DTO 字段完全一样(那就别分)
❌ VO 里还有 LocalDateTime(没格式化)
四、Controller(控制器)
📌 定义
HTTP 接口的门面,负责请求接收、参数校验、响应返回。
🎯 核心职责
- URL 路由映射
- 请求参数 -> DTO 转换
- 基础校验(@Valid)
- 调用 Service 层
- DTO -> 响应/ VO -> 视图
- 异常处理
✅ 什么时候用?
- 必须用:所有对外暴露的 HTTP 接口
- 必须用:接收前端传参
- 必须用:返回 JSON/XML/HTML
❌ 什么时候用不上?
- 内部定时任务(Schedule)
- 消息消费者(MQ Listener)
- RPC 服务提供者(用 @Service 暴露)
📦 典型写法
java
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
@Autowired
private UserService userService;
// 请求 -> DTO -> Service -> DTO -> 响应
@PostMapping
public Result<UserDetailDTO> create(@Valid @RequestBody UserCreateDTO createDTO) {
UserDetailDTO detail = userService.create(createDTO);
return Result.success(detail);
}
// 查询 -> 分页 DTO -> 响应
@GetMapping
public Result<PageResult<UserListDTO>> list(@Valid UserQueryDTO queryDTO) {
PageResult<UserListDTO> page = userService.list(queryDTO);
return Result.success(page);
}
// 服务端渲染场景
@GetMapping("/profile")
public String profile(Model model) {
UserVO vo = userService.getProfileForView();
model.addAttribute("user", vo);
return "user/profile"; // 返回视图
}
}
💥 反模式
❌ Controller 里写业务逻辑(应该在 Service)
❌ Controller 直接操作 Entity(Repository 都不经过)
❌ 一个方法几百行(超过 50 行就该拆)
❌ 返回 Entity 给前端
五、Service(服务层)
📌 定义
业务逻辑的核心,处理领域规则、事务、协调。
🎯 核心职责
- 业务逻辑实现
- 事务管理(@Transactional)
- Entity 与 DTO 的转换
- 多资源协调(DB + Redis + MQ)
- 权限校验
✅ 什么时候用?
- 必须用:所有业务逻辑处理
- 必须用:事务操作
- 必须用:复杂的数据组装
- 必须用:调用外部服务/第三方 API
❌ 什么时候用不上?
- 纯查询且无业务逻辑(但建议保留)
- 简单的 CRUD(但依然建议 Service 做一层包装)
📦 典型写法
java
@Service
@Transactional(rollbackFor = Exception.class)
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private DeptRepository deptRepository;
@Autowired
private UserConverter converter; // MapStruct
// 接收 DTO,返回 DTO
public UserDetailDTO create(UserCreateDTO createDTO) {
// 1. DTO -> Entity
User user = converter.toEntity(createDTO);
// 2. 业务逻辑
user.setPassword(passwordEncoder.encode(createDTO.getPassword()));
user.setCreateTime(LocalDateTime.now());
user.setStatus(1);
// 3. 保存
userRepository.save(user);
// 4. Entity -> DTO 返回
return converter.toDetailDTO(user);
}
// 复杂查询:聚合多表
public UserDetailDTO getDetail(Long id) {
User user = userRepository.findById(id).orElseThrow();
Dept dept = deptRepository.findById(user.getDeptId()).orElse(null);
List<Role> roles = roleRepository.findByUserId(id);
// Entity 组合 -> DTO
return converter.toDetailDTO(user, dept, roles);
}
}
💥 反模式
❌ Service 互相调用形成循环依赖
❌ Service 层过厚(1000 行一个类)
❌ Service 返回 Entity 给 Controller
❌ 事务粒度太大(一个方法操作 10 张表)
📊 六、完整数据流转全景图
场景 A:前后端分离(Vue/React + API)
【前端】
↓ JSON (请求DTO)
【Controller】参数校验
↓ 请求DTO
【Service】业务逻辑、事务
↓ Entity ←→ Repository ←→ 【DB】
↓ 响应DTO
【Controller】
↓ JSON (响应DTO)
【前端】
典型链路:
前端 JSON → Controller(@RequestBody UserCreateDTO)
→ Service(create)
→ Repository(save)
→ DB
← Repository(find)
← Service(convert to UserDetailDTO)
← Controller(@ResponseBody)
← 前端
场景 B:服务端渲染(Thymeleaf/JSP)
【浏览器】
↓ HTTP请求
【Controller】接收参数
↓ 请求DTO/Param
【Service】业务逻辑
↓ Entity ←→ Repository ←→ 【DB】
↓ DTO(原始数据)
【Controller】DTO → VO(组装)
↓ Model.addAttribute("user", VO)
【ViewResolver】
↓ HTML
【浏览器】
典型链路:
浏览器 GET /user/1
→ Controller(@PathVariable)
→ Service(getDetail)
→ Repository
→ DB
← Service(return UserDTO)
← Controller(convert to UserVO)
← Model
← Thymeleaf
← HTML
🎯 七、终极总结(面试/复盘用)
| 组件 | 一句话定义 | 数据方向 | 依赖层级 | 必备场景 |
|---|---|---|---|---|
| Entity | 数据库的影子 | 双向(Repository) | Repository | 所有 ORM 项目 |
| DTO | 接口的契约 | 双向(Controller) | Controller/Service | 所有 API 项目 |
| VO | 页面的模型 | 单向(后端→前端) | Controller | 服务端渲染 |
| Controller | HTTP 门面 | 接收/响应 | Web 层 | 所有 Web 项目 |
| Service | 业务核心 | 内部流转 | 核心层 | 所有业务系统 |
✅ 什么时候必须用 Entity?
只要用数据库,就必须用 Entity
✅ 什么时候必须用 DTO?
只要有 API,就必须用 DTO
✅ 什么时候必须用 VO?
只要后端渲染 HTML,就必须用 VO
✅ 什么时候必须用 Controller?
只要暴露 HTTP 接口,就必须用 Controller
✅ 什么时候必须用 Service?
只要有业务逻辑,就必须用 Service
❌ 什么时候可以不用?
| 组件 | 可以省略的场景 | 是否推荐省略 |
|---|---|---|
| VO | 前后端分离项目 | ✅ 强烈推荐省略 |
| DTO | 无 API 的内部模块 | ⚠️ 可以,但罕见 |
| Entity | 无数据库的项目 | ✅ 可以 |
| Service | 极简单的 CRUD | ❌ 不推荐 |
| Controller | 非 Web 项目(Job/MQ) | ✅ 可以 |
📌 八、大厂规范速查表(直接背)
✅ 正确架构:
└── controller/
├── UserController.java # 只做参数接收和返回
└── vo/ # 仅服务端渲染需要
└── service/
├── UserService.java # 业务逻辑
└── impl/
└── repository/
├── UserRepository.java # 数据访问
└── entity/
├── User.java # 数据库映射
└── dto/
├── request/ # 请求DTO
│ ├── UserCreateRequest.java
│ └── UserQueryRequest.java
└── response/ # 响应DTO
├── UserDetailResponse.java
└── UserListResponse.java
核心原则:
- Entity 永不出口(不返回给前端,不作为接口入参)
- DTO 永不存库(不交给 Repository,不写 @Entity)
- VO 永不跨层(只在 Controller 组装,Service 不返回 VO)
- Controller 零逻辑(只做校验和转换)
- Service 无 HTTP(不知道是 API 还是 RPC 还是 Job)