1. 程式人生 > >Spring事務管理的一次Bug除錯

Spring事務管理的一次Bug除錯

現在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去管理。

每一個坑,都是在幫你學會走路!