SpringMVC+Spring+Hibernate集成问题

    |     2017年2月17日   |   JavaWeb   |     0 条评论   |    2081

用 SpringMVC + Spring + Hibernate 搭 SSH 时,最常见的一类坑是:HibernateTemplate 一执行 save / update 就抛 InvalidDataAccessApiUsageException,提示写操作在只读模式(FlushMode.MANUAL)下不被允许。

根因通常不是 Hibernate 本身坏了,而是 OpenSessionInViewFilter 把 Session 的 flush 模式钉死了,而业务方法又没被声明式事务保护。下面按「现象 → 原因 → 三种改法 → 事务与扫描」把这条链路走完。

一、报错现象

项目启动后,只要走到写库操作,堆栈会落到 HibernateTemplate.checkWriteOperationAllowed:

org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.MANUAL): Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition.
	at org.springframework.orm.hibernate4.HibernateTemplate.checkWriteOperationAllowed(HibernateTemplate.java:1128)
	at org.springframework.orm.hibernate4.HibernateTemplate$12.doInHibernate(HibernateTemplate.java:621)
	at org.springframework.orm.hibernate4.HibernateTemplate$12.doInHibernate(HibernateTemplate.java:618)

中文意思就是:当前 Session 处于只读 / FlushMode.MANUAL。要么把 Session 改成 FlushMode.COMMIT / AUTO,要么去掉事务定义上的 readOnly 标记。

二、原因:OpenSessionInView 把 Session 绑成只读

OpenSessionInViewFilter 在 getSession 时,会把拿到的 Session 的 flush mode 设为 FlushMode.NEVER(新版本对应 MANUAL),再把 sessionFactory 绑到 TransactionSynchronizationManager。这样整个 request 共用同一个 Session,请求结束后再解绑,并按是否已与 transaction 绑定来决定要不要 closeSession。

HibernateTemplate 只有发现当前 Session 上存在非 readOnly 的事务 时,才会把 flush 切到 FlushMode.AUTO,从而允许 insert / update / delete。没有事务、也没有人为改 flush mode,整个 doFilter 过程就一直是 Flush.NEVER。

所以:受声明式事务保护的方法有写权限,没被保护的方法没有。SSH 集成时最容易踩的,是 Controller 和 Service 被同一套 component-scan 扫进去,事务切面绑错层。

三、三种解决办法

办法 做什么 适用
方法一:拆开扫描 spring-servlet.xml 只扫 @Controller;applicationContext.xml 排除 @Controller 推荐,职责清晰
方法二:execute 回调 不用 HibernateTemplate.save,改成 session.save 临时绕过 flush 检查
方法三:合并配置 把 spring-servlet.xml 和 applicationContext.xml 合成一份 小项目能用,不推荐长期

方法一:父子容器各扫各的

SpringMVC 的 DispatcherServlet 和根容器 ContextLoaderListener 是两套容器。父容器管 Service / Dao / 事务,子容器管 Controller。扫描范围重叠时,Controller 会在子容器里再 new 一份,事务切面绑不上。

spring-servlet.xml 只扫控制器:

<context:component-scan base-package="com.ittx.spring002.controller" >
    <!-- 只扫描 base-package 指定包下 @Controller 注解 -->
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

applicationContext.xml 扫业务层,排除控制器:

<context:component-scan base-package="com.ittx.spring002">
    <!-- 除了 base-package 包下 @Controller 注解全都扫描 -->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

方法二:HibernateCallback 自己拿 Session

原来是 getHibernateTemplate().save(user),改成 execute 回调:

getHibernateTemplate().execute(new HibernateCallback<User>() {
    @Override
    public User doInHibernate(Session session) throws HibernateException {
        session.save(user);
        return null;
    }
});

方法三:合并两个 xml

把 spring-servlet.xml 和 applicationContext.xml 合成一份,父子容器问题会消失,但配置职责会糊在一起。

四、事务怎么配

1. 代码开事务

原文写法里的 beginTrasaction / getTrasaction 是拼写错误,正确 API 是:

session.beginTransaction();
session.save(user);
session.getTransaction().commit();

2. 声明式事务(tx:advice)

Hibernate4 下 getCurrentSession() 必须处在事务中。查询方法即便只读,也要给 REQUIRED,同时可标 read-only:

<tx:annotation-driven transaction-manager="txManager" />

<!-- 声明式容器事务管理 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
        <tx:method name="save*" propagation="REQUIRED" />
        <tx:method name="add*" propagation="REQUIRED" />
        <!-- hibernate4 必须开启事务,否则 getCurrentSession() 获取不到 -->
        <tx:method name="get*" propagation="REQUIRED" read-only="true" />
    </tx:attributes>
</tx:advice>
<aop:config expose-proxy="true">
    <!-- 只对业务逻辑层实施事务 -->
    <aop:pointcut id="txPointcut"
        expression="execution(* com.ittx.spring002.server..*.*(..))" />
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" />
</aop:config>

3. 注解事务

<bean id="txManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

<!-- 用注解来实现事务管理 -->
<tx:annotation-driven transaction-manager="txManager" />

五、component-scan 怎么用

xml 里配了 <context:component-scan base-package="com.ittx.spring002" /> 之后,Spring 会扫该包及子包,遇到 @Component、@Controller、@Service 等就注册成 bean。

配了 component-scan 就不必再写 <context:annotation-config/>,前者已经包含后者。它还有两个子标签:

<context:include-filter />
<context:exclude-filter />

use-default-filters 默认是 true,会扫描指定包下全部带 @Component 及其派生注解(@Service、@Repository 等)的类。如果只想按 include-filter 精确扫,把它改成 false。

一句话总结:SSH 写库报 FlushMode.MANUAL,优先检查 OpenSessionInView 叠加事务切面是否绑到 Service;父子容器扫描重叠时,用 include/exclude 把 Controller 和业务层拆开。

转载请注明来源:SpringMVC+Spring+Hibernate集成问题
本文链接地址:https://ai.zhousir.top/?p=1931
回复 取消