
1. 从一次线上 500 说起NO session 到底在报什么先说结论org.hibernate.LazyInitializationException: could not initialize proxy - no Session这个异常本质不是数据库连不上也不是 SQL 写错了而是你访问懒加载属性的那一刻Hibernate 的 SessionJPA 里叫 EntityManager已经关了。代理对象还在但它背后的“取数通道”没了于是只能抛异常。我先把场景还原一下你大概率遇到过类似代码Entity public class Order { Id private Long id; ManyToOne(fetch FetchType.LAZY) private User user; // getter / setter }Controller 里这么写GetMapping(/order/{id}) public OrderVO getOrder(PathVariable Long id) { Order order orderRepository.findById(id).orElseThrow(); OrderVO vo new OrderVO(); vo.setUserName(order.getUser().getName()); // 这里炸 return vo; }findById本身没问题它返回的是Order实体user字段是一个 Hibernate 代理。问题出在order.getUser().getName()这一行——此时findById所在的事务或者说EntityManager已经提交并关闭代理无法再发起 SQL于是no Session。这里有个容易混淆的点很多人以为Transactional加在 Service 上就万事大吉但如果你的 Controller 直接调 Repository或者 Service 方法没有事务、事务提前结束Session 照样会关。Session 的生命周期和事务边界是绑定的这是理解整个问题的钥匙。那为什么有时候在单元测试里不报错因为测试方法本身可能被Transactional包住整个测试跑在一个事务里Session 一直开着自然看不到问题。一上生产、一进 Web 请求事务边界变短异常就冒出来了。这个异常在 Spring Data JPA 项目里出现频率极高尤其是做 DTO 转换、JSON 序列化、模板渲染的时候。它属于那种“本地跑得好好的一联调就翻车”的典型。接下来我会从三条路径切入OpenEntityManagerInView、Transactional边界、DTO 投影并给出可复制的application.yml、实体映射和验证步骤。顺带说一句如果你在排查过程中需要频繁切换不同模型来对比生成的修复代码可以用 TaoToken 的统一 Key 通道一个 Key 走多家模型省得来回改配置这个后面会讲怎么接。先明确一点NO session 不是靠“加大事务范围”无脑解决的那样容易把事务拖长、连接池打满。正确做法是先定位 Session 在哪一刻关闭再决定用哪种方案。下面进入实操。2. 前置用 TaoToken 统一 Key 通道准备排查环境排查这类异常时我经常需要让模型帮我读一段堆栈、生成对比代码或者解释某个配置项。不同模型各有擅长但每换一个就要改一次 Base URL 和 Key很烦。TaoToken 的思路是给你一个统一的 API 入口和一把 Key背后路由到不同模型配置只写一份。它的 API 地址是https://taotoken.net/api兼容 OpenAI 风格的调用格式。也就是说你原来用 OpenAI SDK 写的代码只要把base_url和api_key换掉就能用。对 Java 项目来说如果你在写一些辅助脚本、或者用 Spring AI 之类的框架做本地调试这个统一入口能省不少事。先拿 Key。打开控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session在 API Keys 页面创建一个 Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session拿到形如sk-xxxx的 Key 后先别急着写 Java用 curl 验证通道是否通curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: gpt-4o-mini, messages: [ {role: user, content: 用一句话解释 Hibernate LazyInitializationException 的成因} ] }如果返回里有choices数组和正常内容说明通道没问题。这一步很重要因为后面排查时你会反复用它来生成和验证代码片段通道不通会浪费大量时间。如果你更习惯在编辑器里直接对话可以打开模型对话页面https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session把堆栈粘进去让它帮你定位是哪一行触发了懒加载。对于长期做编码、Agent 类任务的同学Coding Plan 会更划算适合高频调用https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session接入文档在这里里面有各语言 SDK 的配置示例https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session需要说明的是TaoToken 在这里扮演的是“统一调用入口”的角色帮你把模型调用这件事的配置收敛成一份它不替代你的 IDE也不替代 Spring 本身。排查 JPA 问题的主力仍然是日志、断点和正确的配置。下面进入正题。3. 可复制配置application.yml 与实体映射三件套这一节给你可以直接抄的配置。先看application.yml重点是 JPA 和连接池部分spring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo?useSSLfalseserverTimezoneAsia/Shanghai username: root password: root hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 jpa: hibernate: ddl-auto: none open-in-view: false show-sql: true properties: hibernate: format_sql: true generate_statistics: true logging: level: org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: INFO org.springframework.orm.jpa: DEBUG org.hibernate.SQL: DEBUG这里有两个关键点。第一open-in-view: false。Spring Boot 默认是true也就是开启OpenEntityManagerInView它会把 EntityManager 的生命周期延长到整个 Web 请求结束。很多人靠这个“掩盖”了 NO session但代价是数据库连接被占用到视图渲染完高并发下连接池容易被打满。我建议显式设为false让问题暴露出来再用正确的方式解决。第二generate_statistics: true配合统计日志能帮你看到 Session 的开启和关闭时机。再看实体映射。假设Order和User是一对多/多对一关系Entity Table(name t_order) public class Order { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; ManyToOne(fetch FetchType.LAZY) JoinColumn(name user_id) private User user; OneToMany(mappedBy order, fetch FetchType.LAZY, cascade CascadeType.ALL) private ListOrderItem items new ArrayList(); // getter / setter }Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; // getter / setter }如果你确实需要在一定范围内保持 Session 打开可以在 Service 方法上加事务并明确readOnlyService public class OrderService { private final OrderRepository orderRepository; public OrderService(OrderRepository orderRepository) { this.orderRepository orderRepository; } Transactional(readOnly true) public OrderVO getOrderVO(Long id) { Order order orderRepository.findById(id).orElseThrow(); OrderVO vo new OrderVO(); vo.setUserName(order.getUser().getName()); return vo; } }注意Transactional必须加在被外部调用的 public 方法上且调用方不能是同一个类内部的自调用否则代理不生效事务不会开启Session 依然会提前关闭。这是最常见的坑之一。如果你用的是 Spring Boot 3 Jakarta 命名空间记得把javax.persistence换成jakarta.persistence否则实体根本不会被扫描到报错会更迷惑。最后给一个 DTO 投影的写法这是我最推荐的方案因为它从根上避免了懒加载public interface OrderProjection { Long getId(); String getUserName(); }public interface OrderRepository extends JpaRepositoryOrder, Long { Query(select o.id as id, u.name as userName from Order o join o.user u where o.id :id) OrderProjection findProjectionById(Param(id) Long id); }投影查询直接返回需要的字段不产生代理对象自然不会有 NO session。代价是你要为每种视图写一个投影接口但换来的是清晰的边界和更好的性能。4. 验证请求用日志和断点确认 Session 是否存活配置写完了怎么确认 Session 到底在哪一刻关闭我给你一套可跟做的验证流程。第一步打开 SQL 和 Session 日志。上面application.yml里已经配了show-sql和StatisticalLoggingSessionEventListener。启动应用后发一个请求curl http://127.0.0.1:8080/order/1观察控制台。如果看到类似下面的输出说明 Session 在事务提交后关闭了Session Metrics { 12345 nanoseconds spent acquiring 1 JDBC connections; 0 nanoseconds spent releasing 0 JDBC connections; 67890 nanoseconds spent preparing 1 JDBC statements; ... }这个Session Metrics日志出现的位置就是 Session 关闭的时刻。如果它出现在你访问order.getUser().getName()之前那异常必然发生。第二步在 Service 方法里打断点。用 IDEA 的 Debug 模式在order.getUser().getName()这一行停下然后在 Evaluate Expression 里执行org.hibernate.Session session entityManager.unwrap(org.hibernate.Session.class); session.isOpen();如果返回false说明 Session 已关。如果返回true说明还在事务内问题可能出在别处比如对象是 detached 状态。第三步验证open-in-view的影响。把spring.jpa.open-in-view改成true重启再发同样的请求。如果异常消失那就坐实了是 Session 生命周期问题。但我不建议长期开着它原因前面说过。第四步验证投影方案。把 Controller 改成调用findProjectionById再发请求。这次应该直接返回数据日志里只有一条 join 查询没有额外的懒加载 SQL。如果你在验证过程中需要模型帮你解读某段日志可以把日志贴到模型对话里https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session让它帮你判断 Session 关闭的时机。这一步不是必须的但对不熟悉 Hibernate 内部机制的同学很有帮助。补充一个细节如果你用的是EntityGraph来预加载关联验证方式类似。在 Repository 方法上加EntityGraph(attributePaths {user}) OptionalOrder findWithUserById(Long id);这样一次查询就把user带出来了Session 关闭后访问也不会报错。日志里会看到 join 查询而不是两条独立 SQL。5. 常见报错排查401、local proxy failed、reading choices、OAuth排查过程中除了 NO session 本身你还可能撞上一些“外围”报错。这一节把它们列出来对照。401 Unauthorized如果你在用 TaoToken 的 API 做辅助调试返回 401 通常是 Key 没带对或过期了。检查请求头-H Authorization: Bearer sk-你的Key注意Bearer后面有一个空格Key 不要有多余换行。如果还不行去 API Keys 页面重新生成一个https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_sessionlocal proxy failed这个报错一般出现在你本地配了代理但代理没起来或者端口不对。检查你的环境变量HTTP_PROXY/HTTPS_PROXY或者 IDE 里的代理设置。如果你根本没配代理那可能是某些库默认读了系统代理。把它清掉再试。reading choices 相关报错比如Error reading choices或choices is null通常是响应体不是预期的 JSON 结构。先用 curl 直接打一次确认返回格式curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d {model:gpt-4o-mini,messages:[{role:user,content:ping}]}如果 curl 正常但代码报错多半是 SDK 版本或解析逻辑问题。OAuth 相关报错如果你在接入某些需要 OAuth 的工具链报错通常和 token 刷新、回调地址有关。检查回调 URL 是否和注册时一致token 是否过期。这类问题在接入文档里有说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session回到 NO session 本身再补几个高频坑一是Transactional加在 private 方法上代理不生效。二是同类内部方法调用事务不传播。三是Transactional和Async混用异步线程里没有事务上下文Session 直接没有。四是用了Transactional(readOnly true)却执行了写操作某些数据库会报错但和 NO session 不是一回事别混淆。如果你在排查时发现堆栈里出现ByteBuddyInterceptor或JavassistLazyInitializer那基本可以确认是懒加载代理在无 Session 环境下被访问。这时候回到第 3 节的投影方案或者用JOIN FETCHQuery(select o from Order o join fetch o.user where o.id :id) OptionalOrder findWithUserJoinFetch(Param(id) Long id);JOIN FETCH一次查询把关联带出Session 关闭后访问也没问题。注意它和分页一起用时要小心可能触发内存分页警告。6. 收尾把 Session 边界当成设计问题而不是配置问题写到这里我想强调一个观点NO session 表面是配置问题根子上是边界设计问题。你让实体跨越了 Session 边界就必然要面对代理失效。与其到处加Transactional把事务拖长不如在设计阶段就想清楚这个视图到底需要哪些字段需要关联数据就用投影或JOIN FETCH一次取回不需要就别让实体流出 Service 层。我自己的习惯是Repository 只返回实体或投影Service 负责组装 DTOController 只碰 DTO。实体永远不越过 Service 边界这样 NO session 基本不会出现。open-in-view保持false让问题在开发阶段就暴露而不是留到生产。如果你在团队里推行这套规范可以把它写进代码评审清单任何返回实体的 Controller 方法都要被质疑。时间长了这类异常会显著减少。最后如果你在排查时需要快速对比不同模型给出的修复方案或者想让模型帮你审查一段实体映射可以用 TaoToken 的统一通道一个 Key 切换模型配置只维护一份。模型对话入口https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session需要长期做编码辅助的看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session接入细节都在文档里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentjpa_no_session把 Session 边界管好NO session 就不再是玄学。