现在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去管理。
每一个坑,都是在帮你学会走路!