刚分清 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 Boot2.7.x
ServletTomcat 内嵌(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);
// 前端要什么我都给,岂不美哉?

结果:没多久你就发现问题了——

  1. 密码泄露出去了。你的 /me 接口返回的 User 对象里有 password 字段,虽然前端可能没展示,但浏览器能看见。
  2. 到处都是 null。你把 User 查到之后修改了几个字段,结果 Session 里的 User 也跟着变了——因为它们是同一个对象引用
  3. 序列化问题。如果将来你想把 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 个问题——

  1. 性能灾难。首页加载可能触发 5-10 个接口,每个接口都要查一次用户表。并发上来之后数据库直接扛不住。
  2. 懒加载的坑。如果你 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.javasession.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 Map服务器 (Tomcat)浏览器Session Map服务器 (Tomcat)浏览器浏览器保存 Cookie第一次访问(无 Cookie)创建 HttpSession 对象分配唯一 JSESSIONIDSet-Cookie: JSESSIONID=abc123后续请求(携带 Cookie)根据 JSESSIONID 找到 Sessionsession.setAttribute("user", dto)存入服务器内存另一个接口请求session.getAttribute("user")返回之前存的 DTO返回用户数据

关键点:

  1. Session 在服务器端。数据存在服务器内存里(或 Redis 里),不是存在浏览器。
  2. 浏览器只存了一个 ID(JSESSIONID)。服务器通过这个 ID 找到对应 Session。
  3. Session 是 KV 结构session.setAttribute("user", obj) 本质是往一个 Map 里 put 数据。
  4. Session 有超时时间。默认 30 分钟没人用就自动销毁(可配置)。
  5. Session 是线程安全的吗? 不是。多个请求可能同时读写同一个 Session,要注意并发问题。

为什么不能把 Entity 直接存 Session?

正确做法

存 Entity 到 Session

有什么问题?

密码/敏感字段暴露

DB 结构变更波及 Session 数据

序列化体积大,浪费内存

Entity 关联了懒加载字段

在 Controller 层访问关联对象
抛 LazyInitializationException

拷贝到 DTO 再存

只保留需要的字段

与 DB 解耦

轻量、安全、可序列化

核心原则:Session 是你的「运行时状态」,不是你的「持久化数据」。

DB 的表结构(Entity)今天加个字段、明天改个长度,是你的领域问题。Session 里存什么,是你接口设计的问题。把 Entity 直接塞进 Session,就是让 DB 的变更直接渗透到你的接口层——这是典型的耦合。

三者协作关系

表现层

会话层

业务层

持久层

查询

拷贝

存入

读取

响应

MySQL

Entity

Service

DTO

HttpSession

Controller

客户端

这段流程告诉你一个最佳实践模式

数据库 -> Entity(全量数据)
    -> copyProperties -> DTO(精简数据)
    -> 存入 -> Session(会话存储)
    -> 取用 -> Controller -> 响应

每一层都在做职责内的事,不越界。

总结

  1. Entity 是数据库的镜子——1:1 映射表结构,包含所有字段和 ORM 注解。它的生命周期属于 DAO 层。
  2. DTO 是接口的契约——只包含你想暴露的字段,与服务/控制器层同生命周期。它是你跟客户端之间的约定。
  3. Session 是用户的储物柜——它本身不是「数据」而是一个KV 存储空间。可以把 DTO 放进去,用来跨请求保存用户状态。
  4. 最佳实践:DB 层用 Entity -> Service 层转成 DTO -> 需要跨请求的 DTO 塞进 Session。不要存 Entity,不要混用职责。
  5. 延伸思考:Session 只是一个方案。在生产环境里,越来越多的项目用 JWT Token + Redis 代替 Session,因为 Session 在分布式环境下有黏性会话的问题。但理解 Session 仍然是理解 Token 基础的前提——它们都在解决同一个问题:HTTP 无状态,怎么记住「你是谁」

如果你对上面第 5 点感兴趣,下一次我可以聊聊黑马点评项目中 Session 登录 vs Token 登录 的具体实现差异和迁移方案。


本文为原创内容,转载请注明出处。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力!

更多推荐