概述
在项目中,我们使用不同的对象模型来处理不同场景的数据,这是分层架构的重要体现。
为什么需要多种对象?
- 🔐安全性:防止敏感数据泄露
- 🎯职责分离:每个对象只关注自己的职责
- 🔄灵活性:不同层可以独立演化
- 🛡️解耦:数据库变动不影响前端
1️⃣ Entity(实体类)- 数据库映射
定义
Entity是与数据库表一一对应的 Java 对象,也叫持久化对象。
特点:
- 🗄️ 对应数据库表结构
- 📦 包含所有字段(包括敏感字段)
- 🔗 包含数据库注解(如 @Table、@Column)
- 💾 只在 DAO/Mapper 层使用
实战案例 - User 实体
package com.MiniBlog.entity; import lombok.Data; import javax.persistence.*; import java.util.Date; /** * 用户实体类 - 对应数据库表 tb_user * * 【注解说明】 * @Entity:JPA注解,表示这是一个实体类 * @Table:指定对应的数据库表名 * @Data:Lombok注解,自动生成getter/setter/toString等 */ @Data @Entity @Table(name = "tb_user") public class User { // ========== 【主键】 ========== /** * @Id:主键标识 * @GeneratedValue:主键生成策略(自增) */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // ========== 【基本信息】 ========== /** * 用户编号(业务主键) */ @Column(name = "no", length = 50) private String no; /** * 真实姓名 */ @Column(name = "realname", length = 100) private String realname; /** * 手机号 */ @Column(name = "mobile", length = 20) private String mobile; /** * 邮箱 */ @Column(name = "email", length = 100) private String email; // ========== 【敏感信息】⚠️ ========== /** * 密码(加密后的) * 【注意】这个字段不应该返回给前端! */ @Column(name = "password", length = 200) private String password; /** * 密码盐值 * 【注意】这个字段不应该返回给前端! */ @Column(name = "salt", length = 50) private String salt; /** * 身份证号 * 【注意】需要脱敏后才能返回给前端! */ @Column(name = "cardno", length = 50) private String cardno; // ========== 【状态字段】 ========== /** * 账号状态:1-正常,2-冻结,3-注销 */ @Column(name = "status") private Integer status; /** * 是否删除:0-否,1-是 */ @Column(name = "deleted") private Integer deleted; // ========== 【微信相关】 ========== @Column(name = "openid", length = 100) private String openid; @Column(name = "unionid", length = 100) private String unionid; @Column(name = "mp_openid", length = 100) private String mpOpenid; // ========== 【人脸识别】 ========== @Column(name = "faceid", length = 100) private String faceid; // ========== 【时间戳】 ========== /** * 创建时间 */ @Column(name = "create_time") @Temporal(TemporalType.TIMESTAMP) private Date createTime; /** * 更新时间 */ @Column(name = "update_time") @Temporal(TemporalType.TIMESTAMP) private Date updateTime; /** * 创建人 */ @Column(name = "create_user") private Integer createUser; /** * 更新人 */ @Column(name = "update_user") private Integer updateUser; }数据库表结构(对应):
CREATE TABLE `tb_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `no` varchar(50) DEFAULT NULL COMMENT '用户编号', `realname` varchar(100) DEFAULT NULL COMMENT '真实姓名', `mobile` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `password` varchar(200) DEFAULT NULL COMMENT '密码', `salt` varchar(50) DEFAULT NULL COMMENT '盐值', `cardno` varchar(50) DEFAULT NULL COMMENT '身份证号', `status` int(11) DEFAULT '1' COMMENT '状态', `deleted` int(11) DEFAULT '0' COMMENT '是否删除', `openid` varchar(100) DEFAULT NULL COMMENT '微信openid', `unionid` varchar(100) DEFAULT NULL COMMENT '微信unionid', `mp_openid` varchar(100) DEFAULT NULL COMMENT '公众号openid', `faceid` varchar(100) DEFAULT NULL COMMENT '人脸ID', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `create_user` int(11) DEFAULT NULL COMMENT '创建人', `update_user` int(11) DEFAULT NULL COMMENT '更新人', PRIMARY KEY (`id`), UNIQUE KEY `uk_mobile` (`mobile`), KEY `idx_no` (`no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';2️⃣ VO(View Object)- 视图对象
定义
VO是返回给前端的视图对象,只包含前端需要展示的数据。
特点:
- 👁️ 只包含前端需要的字段
- 🔒不包含敏感字段(密码、盐值等)
- 🎨 可能包含计算字段(如年龄、格式化日期)
- 📤 只在 Controller 层返回给前端
实战案例 - UserVo
package com.MiniBlog.vo.user; import lombok.Data; import com.payslip.entity.UserCard; import java.util.List; /** * 用户视图对象 - 返回给前端 * * 【设计原则】 * 1. 只包含前端需要的字段 * 2. 敏感字段不包含(如password、salt) * 3. 需要脱敏的字段已处理(如cardno) * 4. 可以包含关联对象(如银行卡列表) */ @Data public class UserVo { // ========== 【基本信息】 ========== private Integer id; private String no; private String realname; /** * 手机号(脱敏) * 例如:138****5678 */ private String mobile; /** * 邮箱(可能脱敏) * 例如:abc***@qq.com */ private String email; /** * 身份证号(脱敏) * 例如:320***********1234 */ private String cardno; // ========== 【注意】❌ 不包含这些字段 ========== // private String password; // 密码不返回 // private String salt; // 盐值不返回 // ========== 【状态】 ========== private Integer status; /** * 状态文本(前端显示用) * 计算属性,根据 status 值生成 */ private String statusText; // ========== 【头像】 ========== private String avatar; // ========== 【微信信息】 ========== /** * 是否绑定微信 * 计算属性:openid 不为空则已绑定 */ private Boolean hasWechat; /** * 是否绑定公众号 * 计算属性:mpOpenid 不为空则已绑定 */ private Boolean hasMpWechat; // ========== 【关联信息】 ========== /** * 用户的银行卡列表 * 这是关联查询的结果,Entity 中没有这个字段 */ private List<UserCard> cards; // ========== 【时间】 ========== /** * 创建时间(格式化后) * 例如:2024-01-15 10:30:00 */ private String createTime; // ========== 【计算字段】 ========== /** * 账号年龄(天数) * 根据创建时间计算,Entity 中没有这个字段 */ private Integer accountAge; /** * 是否实名认证 * 根据 cardno 是否为空判断 */ private Boolean isRealAuth; }3️⃣ DTO(Data Transfer Object)- 数据传输对象
定义
DTO是服务间传输数据的对象,用于跨层或跨服务传递数据。
特点:
- 📡 用于 Service 层之间传递数据
- 🔄 用于微服务之间传递数据
- 📦 可能包含多个实体的数据
- 🎯 职责单一,只负责数据传输
实战案例 - UserTokenDTO
package com.MiniBlog.dto.user; import lombok.Data; import lombok.Builder; /** * 用户登录成功后返回的数据传输对象 * * 【使用场景】 * - 用户注册成功 * - 用户登录成功 * - Token刷新成功 */ @Data @Builder public class UserTokenDTO { /** * 用户ID */ private Integer userId; /** * 用户编号 */ private String userNo; /** * 用户姓名 */ private String userName; /** * 访问令牌(JWT Token) * 前端需要保存,每次请求携带 */ private String token; /** * Token过期时间(秒) */ private Long expiresIn; /** * 刷新令牌 * 用于Token过期后刷新 */ private String refreshToken; /** * 是否首次登录 * 首次登录需要引导用户完善信息 */ private Boolean firstLogin; /** * 用户头像 */ private String avatar; }4️⃣ Form(表单对象)- 接收前端数据
定义
Form是接收前端提交数据的对象,包含参数校验规则。
特点:
- 📥 只在 Controller 层接收前端数据
- ✅ 包含参数校验注解(@NotNull、@Size等)
- 🎯 一个接口一个Form,职责明确
- 🔒 校验规则集中管理
实战案例 - UserMobileRegisterDTO
package com.MiniBlog.form.user; import lombok.Data; import javax.validation.constraints.*; /** * 用户手机号注册表单 * * 【使用场景】 * POST /user/mobile-register * * 【校验规则】 * - 手机号必填,格式正确 * - 验证码必填,6位数字 * - 密码必填,6-20位 */ @Data public class UserMobileRegisterDTO { /** * 手机号 * * @NotBlank:不能为空(会自动trim) * @Pattern:正则校验 */ @NotBlank(message = "手机号不能为空") @Pattern( regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确" ) private String mobile; /** * 短信验证码 */ @NotBlank(message = "验证码不能为空") @Size(min = 4, max = 6, message = "验证码长度为4-6位") private String code; /** * 密码 */ @NotBlank(message = "密码不能为空") @Size(min = 6, max = 20, message = "密码长度为6-20位") private String password; /** * 邀请码(可选) */ private String inviteCode; }5️⃣ 对象之间的转换流程
完整数据流转图
┌─────────────────────────────────────────────────────────────────┐ │ 前端(浏览器/APP) │ └────────────┬────────────────────────────────────────┬────────────┘ │ │ │ ① 发送请求 │ ⑥ 接收响应 │ JSON: {mobile, code, password} │ JSON: {userId, token, userName} ▼ ▲ ┌─────────────────────────────────────────────────────────────────┐ │ Controller 层(控制器) │ │ @PostMapping("/register") │ │ public ApiResponse<UserTokenDTO> register( │ │ @Valid @RequestBody UserMobileRegisterDTO form) { │ │ │ │ ② Form 接收参数 ⑤ VO 返回给前端 │ │ └─> 自动校验(@Valid) └─> Entity 转 VO │ │ │ │ UserTokenDTO dto = userService.register(form); │ │ return ApiResponse.ok(dto); │ │ } │ └────────────┬────────────────────────────────────────┬────────────┘ │ │ │ ③ Form 传给 Service │ ④ DTO 返回 ▼ ▲ ┌─────────────────────────────────────────────────────────────────┐ │ Service 层(业务逻辑) │ │ public UserTokenDTO register(UserMobileRegisterDTO form) { │ │ │ │ // 1. 校验验证码 │ │ validateCode(form.getMobile(), form.getCode()); │ │ │ │ // 2. 创建 Entity 对象 │ │ User user = new User(); │ │ user.setMobile(form.getMobile()); │ │ user.setPassword(encryptPassword(form.getPassword())); │ │ user.setCreateTime(new Date()); │ │ │ │ // 3. 保存到数据库 │ │ userDao.save(user); ────┐ │ │ │ │ │ // 4. 生成 Token │ │ String token = generateToken(user.getId()); │ │ │ │ // 5. 构建 DTO 返回 │ │ return UserTokenDTO.builder() │ │ .userId(user.getId()) │ │ .token(token) │ │ .userName(user.getRealname()) │ │ .build(); │ │ } │ │ └───────────────────────────────┼─────────────────────────────────┘ │ │ Entity 保存/查询 ▼ ┌─────────────────────────────────────────────────────────────────┐ │ DAO/Mapper 层(数据访问) │ │ │ │ public interface UserRepository { │ │ User save(User user); // 保存 │ │ User findById(Integer id); // 查询 │ │ } │ └────────────┬────────────────────────────────────────┬────────────┘ │ │ │ SQL 语句 │ 查询结果 ▼ ▲ ┌─────────────────────────────────────────────────────────────────┐ │ 数据库(MySQL) │ │ │ │ tb_user 表 │ │ ┌────┬──────┬──────────┬──────────┬──────────┐ │ │ │ id │ no │ mobile │ password │ salt │ │ │ ├────┼──────┼──────────┼──────────┼──────────┤ │ │ │ 1 │ U001 │ 13800000 │ ******* │ ******* │ │ │ └────┴──────┴──────────┴──────────┴──────────┘ │ └─────────────────────────────────────────────────────────────────┘6️⃣ 对象转换代码示例
实际项目中的转换
@RestController @RequestMapping("/user") @Slf4j public class UserController extends BaseController { @Autowired private UserService userService; @Autowired private UserCardService userCardService; /** * 根据Token获取用户信息 * * 【数据流转】 * 1. 从数据库查询 Entity(包含所有字段) * 2. Entity 转换为 VO(只包含安全字段) * 3. 关联查询银行卡(补充数据) * 4. 返回 VO 给前端 */ @GetMapping("/findByToken") public ApiResponse<UserVo> findByToken() { // ========== ① 查询 Entity ========== Integer userId = LoginContext.getUserId(); User user = userService.findById(userId); // Entity 对象 // 校验用户存在 Asserts.notNull(user, -10001, "登录失效"); // ========== ② Entity 转 VO ========== // 使用 BeanUtil 复制属性(只复制同名字段) UserVo userVo = BeanUtil.copyProperties(user, UserVo.class); // ========== ③ 补充数据 ========== // 查询关联的银行卡列表 List<UserCard> cards = userCardService.findByUserId(user.getId()); userVo.setCards(cards); // 设置计算字段 userVo.setHasWechat(user.getOpenid() != null); userVo.setIsRealAuth(user.getCardno() != null); // 脱敏处理 if (user.getMobile() != null) { userVo.setMobile(desensitizeMobile(user.getMobile())); } if (user.getCardno() != null) { userVo.setCardno(desensitizeCardNo(user.getCardno())); } // ========== ④ 返回 VO ========== return ApiResponse.ok(userVo); } /** * 手机号脱敏 * 13800138000 -> 138****8000 */ private String desensitizeMobile(String mobile) { if (mobile == null || mobile.length() != 11) { return mobile; } return mobile.substring(0, 3) + "****" + mobile.substring(7); } /** * 身份证号脱敏 * 320102199001011234 -> 320***********1234 */ private String desensitizeCardNo(String cardNo) { if (cardNo == null || cardNo.length() < 8) { return cardNo; } return cardNo.substring(0, 3) + "***********" + cardNo.substring(cardNo.length() - 4); } }使用工具类转换(推荐)
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.copier.CopyOptions; /** * 对象转换工具类 */ public class BeanConverter { /** * Entity 转 VO * 自动忽略 null 值 */ public static <T> T toVo(Object source, Class<T> targetClass) { if (source == null) { return null; } return BeanUtil.copyProperties(source, targetClass, CopyOptions.create().ignoreNullValue()); } /** * Entity List 转 VO List */ public static <S, T> List<T> toVoList(List<S> sourceList, Class<T> targetClass) { if (sourceList == null || sourceList.isEmpty()) { return Collections.emptyList(); } return sourceList.stream() .map(source -> toVo(source, targetClass)) .collect(Collectors.toList()); } /** * Form 转 Entity */ public static <T> T toEntity(Object form, Class<T> entityClass) { if (form == null) { return null; } T entity = BeanUtil.copyProperties(form, entityClass); // 设置创建时间等默认字段 if (entity instanceof BaseEntity) { ((BaseEntity) entity).setCreateTime(new Date()); } return entity; } } // 使用示例 @Service public class UserServiceImpl implements UserService { @Override public UserVo getUserInfo(Integer userId) { // 1. 查询 Entity User user = userDao.findById(userId); // 2. 转换为 VO UserVo vo = BeanConverter.toVo(user, UserVo.class); // 3. 补充额外数据 vo.setCards(userCardService.findByUserId(userId)); return vo; } @Override public List<UserVo> getUserList(List<Integer> userIds) { // 1. 批量查询 Entity List<User> users = userDao.findByIds(userIds); // 2. 批量转换为 VO return BeanConverter.toVoList(users, UserVo.class); } }7️⃣ 为什么要分这么多对象?
核心原因
1. 安全性
// ❌ 错误:直接返回 Entity @GetMapping("/user/{id}") public ApiResponse<User> getUser(@PathVariable Integer id) { User user = userService.findById(id); return ApiResponse.success(user); // 密码、盐值都返回了! } // 前端收到的数据(危险!) { "id": 123, "mobile": "13800138000", "password": "e10adc3949ba59abbe56e057f20f883e", // MD5密码 "salt": "a1b2c3d4", // 盐值暴露 "cardno": "320102199001011234" // 身份证明文 } // ✅ 正确:返回 VO @GetMapping("/user/{id}") public ApiResponse<UserVo> getUser(@PathVariable Integer id) { User user = userService.findById(id); UserVo vo = BeanUtil.copyProperties(user, UserVo.class); // VO 中没有 password、salt 字段 return ApiResponse.success(vo); } // 前端收到的数据(安全) { "id": 123, "mobile": "138****8000", // 脱敏 "cardno": "320***********1234" // 脱敏 }2. 解耦
// 数据库表结构变更,不影响前端 // 数据库改了字段名:user_name -> real_name @Entity public class User { @Column(name = "real_name") // 数据库字段改了 private String realname; // Java字段不变 } // VO 不变,前端不受影响 public class UserVo { private String realname; // 前端继续用原来的字段 }3. 灵活性
// VO 可以包含 Entity 没有的计算字段 public class UserVo { private Integer id; private String realname; // ========== 【计算字段】 ========== // Entity 中没有这些字段 private Integer age; // 根据生日计算 private String statusText; // 根据status转换(1->正常,2->冻结) private Boolean isVip; // 根据会员等级判断 private String createTimeText; // 格式化日期(2024-01-15) // ========== 【关联数据】 ========== // Entity 中没有这些字段 private List<UserCard> cards; // 关联的银行卡 private Integer orderCount; // 订单数量(统计) private BigDecimal totalAmount; // 总消费金额(统计) }4. 职责分离
// 每个对象都有明确的职责 Entity: 负责与数据库交互 ↓ DTO: 负责在Service层传递数据 ↓ VO: 负责返回给前端展示 Form: 负责接收前端提交的数据 ↓ DTO: 负责在Service层传递数据 ↓ Entity: 负责保存到数据库8️⃣ 实战完整流程示例
用户注册完整流程
// ========== ① 前端提交表单 ========== // POST /user/register // Body: { "mobile": "13800138000", "code": "1234", "password": "abc123" } // ========== ② Controller 接收 Form ========== @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/register") public ApiResponse<UserTokenDTO> register( @Valid @RequestBody UserMobileRegisterDTO form) { // Form 对象 log.info("用户注册: mobile={}", form.getMobile()); // 调用 Service,传递 Form,接收 DTO UserTokenDTO dto = userService.register(form); // 返回 DTO 给前端 return ApiResponse.ok(dto); } } // ========== ③ Service 处理业务逻辑 ========== @Service @Slf4j public class UserServiceImpl implements UserService { @Autowired private UserRepository userDao; @Autowired private SmsService smsService; @Override @Transactional(rollbackFor = Exception.class) public UserTokenDTO register(UserMobileRegisterDTO form) { // 1. 校验验证码 boolean valid = smsService.validateCode(form.getMobile(), form.getCode()); Asserts.isTrue(valid, "验证码错误"); // 2. 检查手机号是否已注册 User existUser = userDao.findByMobile(form.getMobile()); Asserts.isNull(existUser, "手机号已注册"); // 3. Form 转 Entity User user = new User(); user.setNo(generateUserNo()); // 生成用户编号 user.setMobile(form.getMobile()); // 手机号 user.setRealname("用户" + user.getNo()); // 默认昵称 // 4. 密码加密 String salt = UUID.randomUUID().toString(); String encryptedPwd = Secure.encryptPassword(form.getPassword(), salt); user.setSalt(salt); user.setPassword(encryptedPwd); // 5. 设置默认值 user.setStatus(1); // 正常 user.setDeleted(0); // 未删除 user.setCreateTime(new Date()); user.setUpdateTime(new Date()); // 6. 保存到数据库(Entity) userDao.save(user); log.info("用户注册成功: userId={}", user.getId()); // 7. 生成 Token String token = JwtUtil.generateToken(user.getId(), user.getNo()); // 8. 构建 DTO 返回 return UserTokenDTO.builder() .userId(user.getId()) .userNo(user.getNo()) .userName(user.getRealname()) .token(token) .expiresIn(7200L) // 2小时 .firstLogin(true) .build(); } private String generateUserNo() { return "U" + System.currentTimeMillis(); } } // ========== ④ DAO 保存 Entity ========== @Repository public interface UserRepository extends JpaRepository<User, Integer> { /** * 根据手机号查询用户 */ User findByMobile(String mobile); /** * 根据用户编号查询 */ User findByNo(String no); } // ========== ⑤ 数据库存储 ========== // INSERT INTO tb_user // (no, mobile, realname, password, salt, status, deleted, create_time, update_time) // VALUES // ('U1704528000123', '13800138000', '用户U1704528000123', // 'e10adc3949ba59abbe56e057f20f883e', 'a1b2c3d4', 1, 0, NOW(), NOW()); // ========== ⑥ 返回给前端 ========== // Response: { "code": 0, "message": "成功", "data": { "userId": 123, "userNo": "U1704528000123", "userName": "用户U1704528000123", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expiresIn": 7200, "firstLogin": true } }9️⃣ 最佳实践与规范
推荐做法
1. Entity 使用规范
✅ DO: - 只在 DAO/Mapper 层使用 - 字段名与数据库列名对应 - 包含完整的数据库注解 - 不要有业务逻辑代码 ❌ DON'T: - 不要在 Controller 中直接返回 Entity - 不要在 Entity 中写复杂的方法 - 不要让前端知道 Entity 的结构2. VO 使用规范
✅ DO: - 只包含前端需要的字段 - 敏感字段要脱敏 - 可以包含计算字段 - 可以包含关联数据 - 只在 Controller 返回时使用 ❌ DON'T: - 不要包含密码、盐值等敏感字段 - 不要包含数据库注解 - 不要在 Service 层使用3. DTO 使用规范
✅ DO: - 在 Service 层之间传递数据 - 在微服务之间传递数据 - 可以包含多个 Entity 的数据 - 可以包含业务状态信息 ❌ DON'T: - 不要直接暴露给前端 - 不要包含数据库操作4. Form 使用规范
✅ DO: - 只在 Controller 接收前端数据 - 包含完整的校验注解 - 一个接口一个 Form - 字段命名清晰 ❌ DON'T: - 不要在 Service 层定义 Form - 不要在 Form 中写业务逻辑🔟 对比总结表
对象类型 | 作用 | 使用层级 | 主要特点 | 是否包含敏感字段 |
Entity | 数据库映射 | DAO/Mapper | 与表一一对应,包含数据库注解 | ✅ 是 |
VO | 返回给前端 | Controller | 只包含展示字段,脱敏处理 | ❌ 否 |
DTO | 服务间传输 | Service | 跨层传输数据,业务对象 | 🔸 视情况而定 |
Form | 接收前端数据 | Controller | 包含校验注解,参数验证 | 🔸 可能包含 |