Spring事务管理的一次Bug调试

在JavaWeb开发中,作者遇到一个Spring事务管理失效的Bug。问题源于错误的@Transactional注解、事务配置的重复以及Spring配置与SpringMVC配置的不匹配。经过调试,发现将Service包扫描从SpringMVC配置文件移回Spring配置文件中解决了问题,强调了理解配置和注解的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

现在JavaWeb使用了各种框架,而各种框架对程序产生作用是通过配置+注解的方式来实现的。这就出现了一个问题:调试困难!
前几天做SQLite数据导入MySQL的时候,出现了一个事务失效的Bug,费尽周折才解决。最终总结了一下,这个Bug是由三四个不同方面的原因导致的,有配置方面的,注解方面的,程序方面的种种。而调试过程现在回头来看也确实是经验帮了很大的忙,入手不久的新人可能再看半天也是干瞪眼。
不过有一个不一定靠谱的结论:首先一定要相信是你自己的程序出了问题,不要轻易怀疑是框架出了问题。

Bug描述:

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = {java.sql.SQLException.class})
    public String transferData(String realurl, String dburl) throws SQLException {
        Connection connection = openSQLiteConnection(realurl);
        if (connection == null) {
            return ResultBuilder.getResult(false, "", "无法打开数据库!").toString();
        }
        try {
            DBInfo dbInfo = transferDBInfo(connection, dburl);
            Map<Integer, NormItemSection> sectionMap = new HashMap<Integer, NormItemSection>();
            transferNormItemSection(connection, dbInfo, sectionMap);
            transferNormItem(connection, dbInfo, sectionMap);
            Map<Integer, ResourceCatalog> catalogMap = new HashMap<Integer, ResourceCatalog>();
            transferResourceCatalog(connection, dbInfo, catalogMap);
            transferResource(connection, dbInfo, catalogMap);
            return ResultBuilder.getResult(true, "", "保存成功!").toString();
        } catch (SQLException e) {
            e.printStackTrace();
            throw e;
        } finally {
            closeSQLiteConnection(connection);
        }
    }

以上是主要的业务逻辑代码,主要做数据迁移的事情!
- 打开SQLite数据库连接;
- 迁移DBInfo数据;
- 迁移NormItemSection数据;
- 迁移ResourceCatalog数据;
- 迁移Resource数据;
- 关于SQLite数据库连接;
为了方便调试,使用了一个仅有几条数据的SQLite文件。在迁移DBInfo完成后,由于NormItemSection表名写错,在迁移Section的时候抛出了一个SQLException异常。这时候查看MySQL数据库,却发现DBInfo的数据完成了迁移,且没有被回退。
而整个的迁移过程属于同一个事务,一荣俱荣,一损俱损。很明显,事务机制没有起到作用。

发现的第一个问题——注解
这里写图片描述
如上图所示,@Transactional这个注解有两个,一个来自javax包,一个是spring的包。
之前加注解的时候没有注意,直接回车把第一个注解加了进来。
发现方式:要给@Transactional注解加配置参数的时候,发现其没有propagation这个配置参数,然后才发现使用了错误的@Transactional注解。
本以为问题已经找到,结果发现事务机制仍然不起作用。

第二个尝试
这里写图片描述
如上是在applicationContext中配置的关于事务的切面配置,使用了Spring的AOP核心机制。在Service层上增加了事务配置(由于Service层做主要的业务处理,有可能调用多个DAO或者JPA进行数据库操作,而每一次的业务处理都应该属于同一个事务,因此需要将事务配置在Service层)。
由于已经在NormDBServiceImpl类上增加了@Transactional配置,而applicationContext中的AOP配置起到的是同样的作用,因此怀疑是否由于重复事务导致了问题。
将AOP配置注释掉以后,发现问题依旧。
PS:这里虽然没有发现Bug的问题所在,但是了解了一下事务配置的两种方式。配置和注解各有优势:
- 配置可以集中管理事务,但需要对Service层的相关方法命名有所规范,因为不同的读写方法需要配置不同的事务属性(read-only,propagation,rollback-for等)。这些方法通过通配符的方式进行配置,但需要有相应的关键字支持(如果命名不规范,就属于比较坑的问题,可能导致不好调试);
- 注解是直接配置在Service代码中的,有侵入性,同时比较分散。需要对各个ServiceImpl类分别进行注解配置,同时各个ServiceImpl类的不同方法也需要进行不同的配置处理。但是@Transactional注解方式比较明了,可以清晰定位事务的配置,不需要从配置文件脑补事务是否起作用;

发现的第二个问题——异常
关于这个问题定位,是有待考量的,需要再测试一下。
这里写图片描述
从@Transactional的注释来看,缺省情况下,事务会在RuntimeException的情况下回滚。但是从代码来看,@Transactional注解的rollbackFor这个属性的缺省配置是为空的,也就是没有配置回滚所对应的异常(有可能内部实现是RuntimeException回滚的)。
而回到我们自己的ServiceImpl代码,可以看到抛出的异常是SQLException,而且这个异常是SQLite抛出的,不是MySQL抛出的(可以认为是一个业务异常)。SQLException继承自Exception,与RuntimeException一样(Exception继承自Throwable)。
处理办法是将@Transactional的propagation属性配置为SQLException,发现事务仍然失效。(后面没有验证这个地方的相关疑惑,需要抽时间验证一下:是否RuntimeException是缺省配置可以导致回滚;另外AOP切面配置的时候使用了Exception,应该是可行的)。

终极问题——Spring配置&SpringMVC配置
applicationContext配置如下,可以看到主要包括如下内容:
- 数据库配置(url,drivername,username,password等);
- entityManagerFactory配置,JPA的实体管理工厂(内部配置了实体包扫描路径,packagesToScan);
- transactionManager事务管理配置;
- JPA的repository包扫描路径;
- 事务管理配置注解驱动;

<!-- 数据库相关配置 -->
    <import resource="datasource.xml"/>

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
        <property name="packagesToScan" value="com.hdzbk.*.entity"/>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
                <prop key="hibernate.format_sql">true</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.enable_lazy_load_no_trans">true</prop>
                <prop key="hibernate.max_fetch_depth">5</prop>
                <prop key="hibernate.jdbc.fetch_size">50</prop>
                <prop key="hibernate.jdbc.batch_size">50</prop>
            </props>
        </property>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>

    <!-- 配置Spring Data JPA扫描目录 entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"-->
    <jpa:repositories base-package="com.hdzbk.template.repository" enable-default-transactions="false" />

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

dispatcher-servlet配置摘选:
- SpringMVC的注解驱动;
- 相关包扫描;
- 静态资源的mapping配置;

<!-- 用来解决ResponseBody返回中文乱码问题 默认字符集是iso-8859-1,现在改成utf8 -->
    <mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="supportedMediaTypes" value = "text/html;charset=UTF-8" />
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <context:component-scan base-package="com.hdzbk.template.controller"/>
    <context:component-scan base-package="com.hdzbk.template.service"/>

    <mvc:resources mapping="/static/**" location="/WEB-INF/static/"/>
    <mvc:resources mapping="/plugins/**" location="/WEB-INF/plugins/"/>
    <mvc:resources mapping="/normdb/**" location="/WEB-INF/normdb/"/>

之前在刚开始启动项目的时候,对于配置部分的内容大部分属于拿来主义,直接搬过来的。后来才逐渐了解各个配置的作用和意义。中间由于解决别的一些问题,将包扫描的相关内容全部从applicationContext中挪到了dispatcher-servlet中(一个是Spring的配置,一个是SpringMVC的配置)。
问题就在这里:
- 事务相关的配置属于Spring,放在applicationContext中;
- 事务配置作用于Service层,注解或者配置都是作用于ServiceImpl的类(属于Service包);
- Service的包扫描放在了SpringMVC的配置文件dispatcher-servlet中;
这就导致,事务配置想要起作用的时候,就去找自己应该起作用的地方,结果找不到。
解决办法:将Service的包扫描从SpringMVC挪回到Spring中。
PS:applicationContext和dispatcher-servlet这两个配置文件各有作用,分别进行Spring的配置和SpringMVC的配置。SpringMVC配置的主要是Controller,所以要将Controller的包扫描放到dispatcher-servlet中;而事务配置属于Spring,作用于Service,所以要将Service的包扫描放到applicationContext中。
可以认为,SpringMVC管理Controller即可,Service、DAO/JPA、Entity等内容交给Spring去管理。


每一个坑,都是在帮你学会走路!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值