首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring MVC 層解決 JPA 延遲載入失效:Open Session In View 模式、事務邊界前移、EntityManager 生命週期管理實戰

     編輯:Java 相關知識

問題場景:視圖層觸發 LazyInitializationException


// 領域模型:雙向一對多關聯
@Entity
public class NewsDir {
    @Id @GeneratedValue(strategy = GenerationType.TABLE)
    private Long id;
    
    @Column(unique = true, nullable = false, length = 16)
    private String sn;
    private String title;
    
    @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
    private List children = new ArrayList<>();  // 預設 LAZY
    
    @ManyToOne
    private NewsDir parent;
    // getter/setter...
}

// MVC Action 方法
public Page doList(WebForm form, Module module) {
    NewsDirQueryObject ndqo = new NewsDirQueryObject();
    form.toPo(ndqo);
    ndqo.setDel(true);
    IPageList pageList = service.queryDirsByConditions(ndqo);  // Service 層事務結束
    CommUtilForTeaec.saveIPageList2WebForm(pageList, form);
    form.addResult("dirPath", this.getDirPath(form));
    return module.findPage("list");
}

// 模板文件 (Velocity/FreeMarker/JSP)
#foreach($info in ${dir.children})  // 此處觸發延遲載入
    目錄名稱:${info.title}
#end

錯誤堆疊追蹤


org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.easyjf.cms.domain.NewsDir.children, no session or session was closed
    at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(...)
    at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(...)
    at org.hibernate.collection.AbstractPersistentCollection.readSize(...)

根因分析:事務邊界與 Persistence Context 生命週期不匹配

層級事務狀態EntityManager / Session 狀態
Controller (MVC)無事務關閉 / 脫管
Service (業務邏輯)@Transactional (REQUIRED)開啟 → 提交/回滾 → 關閉
View (模板渲染)無事務已關閉 → 延遲載入失敗

核心衝突:Service 方法返回時事務結束,EntityManager 關閉,持久化上下文失效。View 層遍歷關聯集合時觸發延遲載入,但無可用 Session。

解決方案對比

方案原理優點缺點適用場景
Open Session In View (OSIV)Filter/Interceptor 將 Session/EntityManager 生命週期延伸至整個 HTTP 請求零業務代碼變更、透明解決延遲載入、成熟模式長事務風險、資源佔用久、N+1 問題隱蔽、難以追蹤事務邊界讀多寫少、CRUD 為主、團隊熟悉模式
事務前移至 Controller (本文方案)在 MVC 處理器 (Controller/Action) 開啟事務,Service 方法加入現有事務事務邊界清晰、業務邏輯與事務一致、資源釋放及時需調整事務配置、Controller 耦合事務、寫操作需謹慎讀寫混合、需精確事務控制、團隊接受架構調整
DTO/VO 組裝 (Eager Fetch)Service 層主動載入所需關聯 (JOIN FETCH / EntityGraph),返回離線 DTO零延遲載入風險、明確資料邊界、效能可控開發工作量大、樣板代碼多、靈活性低高效能要求、固定視圖模型、API 介面
@Transactional(readOnly=true) 在 ControllerSpring MVC @Controller 方法加註解開啟唯讀事務配置簡單、語義清晰僅支援 Spring MVC、寫操作需分離Spring MVC 專案、以查詢為主

本文方案實戰:EasyJWeb 處理器層事務配置

原 Service 層事務配置 (失效)


<aop:config>
  <aop:pointcut id="CmsManage" expression="execution(* com.easyjf.cms.service.*.*(..))"/>
  <aop:advisor pointcut-ref="CmsManage" advice-ref="cmsManageAdvice"/>
</aop:config>

<tx:advice id="cmsManageAdvice" transaction-manager="transactionManager">
  <tx:attributes>
    <tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
    <tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
  </tx:attributes>
</tx:advice>

新增 MVC 處理器事務配置 (生效)


<!-- 定義 MVC 核心處理器切點 -->
<aop:pointcut id="easyjwebProcessor" expression="execution(* com.easyjf.web.RequestProcessor.process(..))"/>

<aop:advisor pointcut-ref="easyjwebProcessor" advice-ref="txEasyjwebProcessorAdvice"/>

<tx:advice id="txEasyjwebProcessorAdvice" transaction-manager="transactionManager">
  <tx:attributes>
    <tx:method name="process" propagation="REQUIRED" read-only="false"/>
  </tx:attributes>
</tx:advice>

關鍵點解析

  1. 切點精確攔截:RequestProcessor.process() 是 EasyJWeb 所有請求的統一入口
  2. 傳播行為 REQUIRED:若無事務則新建,若有則加入 (Service 方法 SUPPORTS 將自動加入)
  3. 事務邊界涵蓋:Controller 執行 → Service 執行 → View 渲染 → 事務提交/回滾 → Session 關閉
  4. 唯讀優化:純查詢請求可設 read-only="true",Hibernate 可優化快取/鎖策略

Spring MVC 原生實現:OpenEntityManagerInViewFilter




    openEntityManagerInView
    org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
    
        entityManagerFactoryBeanName
        entityManagerFactory
    


    openEntityManagerInView
    /*

或 Java Config:


@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public OpenEntityManagerInViewFilter openEntityManagerInViewFilter() {
        OpenEntityManagerInViewFilter filter = new OpenEntityManagerInViewFilter();
        filter.setEntityManagerFactoryBeanName("entityManagerFactory");
        return filter;
    }
}

Controller 級 @Transactional 實現 (Spring MVC 4.3+)


@Controller
@Transactional(readOnly = true)  // 類別級預設唯讀
public class NewsDirController {

    @Autowired
    private NewsDirService service;

    @GetMapping("/list")
    public String list(Model model, @RequestParam(defaultValue = "") String keyword) {
        // 此方法在唯讀事務中執行,View 渲染前事務未提交
        List dirs = service.queryByKeyword(keyword);
        model.addAttribute("dirs", dirs);
        return "newsdir/list";
    }

    @PostMapping("/save")
    @Transactional(readOnly = false)  // 方法級覆蓋為讀寫
    public String save(@ModelAttribute NewsDir dir) {
        service.save(dir);
        return "redirect:/list";
    }
}

最佳實踐建議與避坑指南

  1. 優先選擇 DTO/EntityGraph 方案:API 介面、高併發、效能敏感場景,明確資料邊界最可靠
  2. OSIV 作為過渡/補充:傳統 MVC、快速開發、團隊習慣因素,但需監控長事務指標 (連線池佔用、鎖等待)
  3. 事務前移需謹慎:確保 Controller 不包含耗時非資料庫操作 (外部 API 呼叫、大量計算),避免長事務
  4. 區分讀寫事務:查詢請求 read-only=true,修改請求 read-only=false,Hibernate 可啟用唯讀優化
  5. 監控指標:連線池使用率、事務持續時間、Session 關閉異常計數、N+1 查詢檢測 (p6spy/datasource-proxy)
  6. 測試驗證:整合測試覆蓋 View 層關聯遍歷場景,確保無 LazyInitializationException

總結

JPA 延遲載入失效本質是 Persistence Context 生命週期 < HTTP 請求生命週期。Spring 提供三大標準解法:OSIV Filter、Controller @Transactional、DTO 組裝。本文展示的「事務前移至 MVC 處理器」方案通過 AOP 切點攔截統一入口,以最小配置變更解決了 EasyJWeb 框架下的延遲載入問題。**核心洞見**:事務邊界應與「業務邏輯執行 + 視圖渲染」完整對齊,而非僅限於 Service 方法。架構演進方向:明確的資料需求聲明 (EntityGraph/DTO) 取代隱式的延遲載入,將資料邊界從運行時推遲到編譯時/設計時確定。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved