Entity、DTO、Session 不就是三个「数据容器」吗?错!一个储物柜比喻让你彻底搞懂
刚分清 DTO 和 Entity,又来个 Session?这仨到底什么关系?| 黑马点评踩坑实录
前言 / 问题场景
你刚学完 Spring Boot 基础,兴冲冲打开 B 站的黑马点评项目。Controller 里写了第一个接口,Service 里调了第一个 Mapper,感觉一切都在掌握。
然后你看到了这段代码:
User user = ...; // Entity?从数据库查出来的
UserDTO userDTO = ...; // DTO?刚才见过的
session.setAttribute("user", userDTO); // Session???又是什么?
你盯了三秒,脑子里冒出一连串疑问:
Session 不就是存数据的吗?那跟 DTO 有什么区别?Entity 也能存数据啊?为什么非要用 DTO 存进 Session,直接存 Entity 不行吗?
别笑。这是每个 Java 新人第一次同时面对这三个概念时的真实反应——黑马点评项目恰好是让你一口气全撞上的典型场景。
我当时花了一整个下午翻文档、打断点、甚至在 IDE 里按着 Ctrl 点进 HttpSession 看源码,才终于把它们之间的关系理清。这篇文章就是帮你省下那一下午。
环境说明
| 组件 | 版本 / 说明 |
|---|---|
| 项目 | 黑马点评(B 站黑马程序员实战项目) |
| Spring Boot | 2.7.x |
| Servlet | Tomcat 内嵌(Spring Boot 默认) |
| 会话管理 | HttpSession(默认内存存储) |
| 持久层 | MyBatis-Plus + MySQL |
| 依赖管理 | Maven |
问题复现
在黑马点评项目中,用户登录的典型代码路径长这样:
// UserController.java
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
// 1. 校验验证码(从 session 取)
String cacheCode = session.getAttribute("code").toString();
// 2. 查询用户
User user = userService.query().eq("phone", loginForm.getPhone()).one();
// 3. 这里你会看到各种做法...
session.setAttribute("user", user); // 直接存 Entity
session.setAttribute("user", userDTO); // 存 DTO
}
如果你是一个刚接触真实项目开发的新手,你会觉得这三行全都合理——都是「存个对象到 Session 里」,为什么上面那行不行?
更令人困惑的是,你打开这些类的定义:
// 1. Entity —— 跟数据库表长得一模一样
@Data
@TableName("tb_user")
public class User {
@TableId
private Long id;
private String phone;
private String password; // <-- 密码哈希
private String nickName;
private String icon;
private Date createTime;
private Date updateTime;
}
// 2. DTO —— 好像也差不多?
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
// 没有 password,没有 createTime...
}
// 3. Session —— 什么?没有 @Data 注解??
// HttpSession 是一个接口,不是你的 Java Bean!
看着这三个东西,你陷入了沉思:为什么要有三个「容器」?一个不够吗?
排查过程
尝试 1:直接存 Entity 到 Session
你可能会想:反正都能存,存 Entity 不是信息更全吗?前端需要什么我都有。
session.setAttribute("user", user);
// 前端要什么我都给,岂不美哉?
结果:没多久你就发现问题了——
- 密码泄露出去了。你的 /me 接口返回的 User 对象里有 password 字段,虽然前端可能没展示,但浏览器能看见。
- 到处都是 null。你把 User 查到之后修改了几个字段,结果 Session 里的 User 也跟着变了——因为它们是同一个对象引用。
- 序列化问题。如果将来你想把 Session 存到 Redis(Spring Session),User 类里没处理好的字段可能直接报异常。
尝试 2:不用 Session,每次请求都查数据库
既然 Session 这么麻烦,那我不用了。每次请求都从 token 里取 userId,然后重新查一遍数据库不行吗?
@GetMapping("/me")
public Result me(@RequestHeader("Authorization") String token) {
Long userId = parseToken(token);
User user = userService.getById(userId); // 每次都查 DB
return Result.ok(user);
}
结果:2 个问题——
- 性能灾难。首页加载可能触发 5-10 个接口,每个接口都要查一次用户表。并发上来之后数据库直接扛不住。
- 懒加载的坑。如果你 User 关联了其他表(比如订单列表),MyBatis 的懒加载在 Service 层之外随时可能抛 LazyInitializationException。
最终方案:DTO + Session 配合
黑马点评项目的正确做法(也是绝大多数项目的做法):
// 登录成功
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user, userDTO); // 只拷需要的字段
session.setAttribute("user", userDTO);
// 后续请求
UserDTO user = (UserDTO) session.getAttribute("user");
核心思路:Session 存的是「你需要的用户信息」——不是「数据库里的用户」。
| 文件 | 错误做法 | 正确做法 |
|---|---|---|
| UserController.java | session.setAttribute("user", user) | session.setAttribute("user", userDTO) |
| UserDTO.java | 不存在 | 只包含 id, nickName, icon 三个字段 |
| BeanUtils | 没用 | 用 copyProperties 手动拷贝 |
| 序列化 | User 未考虑序列化 | UserDTO 设计时就为 Session 存储做了规划 |
小坑提醒:BeanUtils.copyProperties(source, target) 是浅拷贝。如果 User 里有 List 类型的字段,拷到 UserDTO 后两个对象共享同一个 List 引用,改一个另一个也变。所以在设计 DTO 时尽量避免嵌套集合,或者深拷一遍。
原理分析
三者的本质区别——一句话版
| 概念 | 本质 | 生存周期 | 物理位置 | 对应黑马点评 |
|---|---|---|---|---|
| Entity | 数据库表映射 | 一次查询的生命周期 | Java 堆内存 | User.java @TableName |
| DTO | 数据传输载体 | 一次请求/响应的生命周期 | Java 堆内存 | UserDTO.java |
| Session | 用户的储物柜 | 从登录到退出(或超时) | 服务器内存(或 Redis) | HttpSession |
Entity 和 DTO 是数据模型——它们是「装东西的盒子」。
Session 是存储空间——它是「放盒子的柜子」。
你可以把 DTO 放进 Session 里,但你不能说 DTO 就是 Session。
更形象的类比
想象你去图书馆自习:
- Entity(图书信息表) = 图书馆管理系统的数据库记录。它记录着这本书的所有信息:ISBN、分类号、入库时间、借阅次数。完整但笨重,你不需要知道这些才能看这本书。
- DTO(借书小票) = 你离开时前台给你的小票。上面只有:书名、借书日期、应还日期。轻量、刚好够用。
- Session(你的储物柜) = 图书馆门口的储物柜。你可以把借书小票(DTO)放进去,也可以把水杯、笔记本放进去。柜子本身不是「数据」,它是存放数据的空间。每个人都有自己的柜子(有自己的 Session),互不干扰。
Session 底层工作机制
关键点:
- Session 在服务器端。数据存在服务器内存里(或 Redis 里),不是存在浏览器。
- 浏览器只存了一个 ID(JSESSIONID)。服务器通过这个 ID 找到对应 Session。
- Session 是 KV 结构。
session.setAttribute("user", obj)本质是往一个 Map 里 put 数据。 - Session 有超时时间。默认 30 分钟没人用就自动销毁(可配置)。
- Session 是线程安全的吗? 不是。多个请求可能同时读写同一个 Session,要注意并发问题。
为什么不能把 Entity 直接存 Session?
核心原则:Session 是你的「运行时状态」,不是你的「持久化数据」。
DB 的表结构(Entity)今天加个字段、明天改个长度,是你的领域问题。Session 里存什么,是你接口设计的问题。把 Entity 直接塞进 Session,就是让 DB 的变更直接渗透到你的接口层——这是典型的耦合。
三者协作关系
这段流程告诉你一个最佳实践模式:
数据库 -> Entity(全量数据)
-> copyProperties -> DTO(精简数据)
-> 存入 -> Session(会话存储)
-> 取用 -> Controller -> 响应
每一层都在做职责内的事,不越界。
总结
- Entity 是数据库的镜子——1:1 映射表结构,包含所有字段和 ORM 注解。它的生命周期属于 DAO 层。
- DTO 是接口的契约——只包含你想暴露的字段,与服务/控制器层同生命周期。它是你跟客户端之间的约定。
- Session 是用户的储物柜——它本身不是「数据」而是一个KV 存储空间。可以把 DTO 放进去,用来跨请求保存用户状态。
- 最佳实践:DB 层用 Entity -> Service 层转成 DTO -> 需要跨请求的 DTO 塞进 Session。不要存 Entity,不要混用职责。
- 延伸思考:Session 只是一个方案。在生产环境里,越来越多的项目用 JWT Token + Redis 代替 Session,因为 Session 在分布式环境下有黏性会话的问题。但理解 Session 仍然是理解 Token 基础的前提——它们都在解决同一个问题:HTTP 无状态,怎么记住「你是谁」。
如果你对上面第 5 点感兴趣,下一次我可以聊聊黑马点评项目中 Session 登录 vs Token 登录 的具体实现差异和迁移方案。
本文为原创内容,转载请注明出处。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力!
更多推荐



所有评论(0)