1.解決すべき問題
まず、「事務とは何か?なぜ事務が必要なのか?」ということから始めましょう。
トランザクションは、分割できない一連の操作です。すべての操作が成功するか、すべて失敗します。開発中は、トランザクションを介していくつかの操作を1つのユニットに結合して、プログラムの論理的な正確さを保証する必要があります。たとえば、すべての挿入が成功した、またはロールバックされたが、どれも挿入されていないなどです。プログラマーとしてトランザクション管理に必要なことは、トランザクションを定義することです。つまり、トランザクションの開始と終了を、トランザクションの開始と終了に類似した操作で定義します。
以下は、基本的なJDBCトランザクション管理コードです。
// 开启数据库连接
Connection con = openConnection();try {
// 关闭自动提交
con.setAutoCommit(false);
// 业务处理
// ...
// 提交事务
con.commit();} catch (SQLException | MyException e) {
// 捕获异常,回滚事务
try {
con.rollback(); } catch (SQLException ex) {
ex.printStackTrace(); }} finally {
// 关闭连接
try {
con.setAutoCommit(true);
con.close(); } catch (SQLException e) {
e.printStackTrace(); }}
直感的に、トランザクション管理にJDBCを直接使用するコードには2つの問題があります。
- ビジネス処理コードとトランザクション管理コードが混在しています。
- 多数の例外処理コード(catchでtry-catch)。
また、Hibernate、MyBatis、JPAなどの他のデータアクセステクノロジーを置き換える必要がある場合、トランザクション管理操作は似ていますが、APIは異なりますが、対応するAPIを使用して書き換える必要があります。これは3番目の質問にもつながります。
- 複雑なトランザクション管理API。
解決すべき3つの問題は上記にリストされています。Springトランザクションがどのように解決されるかを見てみましょう。
2.解決方法
2.1複雑なトランザクション管理API
この問題に対応するために、多くのトランザクション管理のAPIの抽象化を簡単に考えることができます。具体的な実装をシールドすることによりインタフェースを定義して、使用戦略モードを特定のAPIを決定します。Springトランザクションで定義された抽象インターフェースを見てみましょう。
Springトランザクションでは、コアインターフェースはPlatformTransactionManagerであり、トランザクションマネージャーとも呼ばれ、次のように定義されています。
public interface PlatformTransactionManager extends TransactionManager {
// 获取事务(新的事务或者已经存在的事务)
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
getTransactionは、TransactionDefinitionを入力することでTransactionStatusを取得します。つまり、定義されたトランザクションメタ情報を通じて対応するトランザクションオブジェクトを作成します。トランザクションのメタ情報は TransactionDefinitionに含まれます。
- PropagationBehavior:伝播動作。
- IsolationLevel:分離レベル。
- タイムアウト:タイムアウト時間。
- ReadOnly:読み取り専用かどうか。
TransactionDefinitionに従って取得されたTransactionStatusは、トランザクションオブジェクトをカプセル化し、トランザクションを操作してトランザクションステータスを表示するメソッドを提供します。次に例を示します。
- setRollbackOnly:トランザクションをロールバック専用としてマークして、ロールバックできるようにします。
- isRollbackOnly:Rollback-onlyとしてマークされているかどうかを確認します。
- isCompleted:トランザクションが完了した(コミットまたはロールバックが完了した)かどうかを確認します。
ネストされたトランザクションの関連メソッドもサポートされています。
- createSavepoint:创建savepoint;
- rollbackToSavepoint:指定したセーブポイントにロールバックします。
- releaseSavePoint:释解放savepoint。
TransactionStatusトランザクションオブジェクトをcommitメソッドまたはrollbackメソッドに渡して、トランザクションのコミットまたはロールバックを完了することができます。
以下では、特定の実装によるTransactionStatusの役割について説明します。例としてcommitメソッドを取り上げ、TransactionStatusを介してトランザクションの送信を完了する方法を説明します。AbstractPlatformTransactionManagerは、PlatformTransactionManagerインターフェースの実装であり、テンプレートクラスとしてのコミット実装は次のとおりです。
public final void commit(TransactionStatus status) throws TransactionException {
// 1.检查事务是否已完成
if (status.isCompleted()) {
throw new IllegalTransactionStateException( "Transaction is already completed - do not call commit or rollback more than once per transaction");
} // 2.检查事务是否需要回滚(局部事务回滚)
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
} processRollback(defStatus, false);
return;
} // 3.检查事务是否需要回滚(全局事务回滚)
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
} processRollback(defStatus, true);
return;
} // 4.提交事务
processCommit(defStatus);}
トランザクション送信の基本的なロジックは、commit テンプレートメソッドで定義されています。トランザクションのステータスを確認することで、例外をスローするか、ロールバックするか、送信するかを決定できます。processRollbackメソッドとprocessCommitメソッドもテンプレートメソッドであり、ロールバックと送信のロジックをさらに定義します。processCommitメソッドを例にとると、特定のコミット操作は、抽象メソッドdoCommitによって完了します。
protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;
doCommitの実現は、特定のデータアクセステクノロジーに依存します。JDBC DataSourceTransactionManagerの対応する具体的な実装クラスでdoCommitの実装を見てみましょう。
protected void doCommit(DefaultTransactionStatus status) {
// 获取status中的事务对象
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
// 通过事务对象获得数据库连接对象 Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
} try { // 执行commit con.commit(); } catch (SQLException ex) { throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}}
commitおよびprocessCommitメソッドでは、入力TransactionStatusによって提供されるトランザクションステータスに従ってトランザクションの動作を決定します。トランザクションのコミットをdoCommitで実行する必要がある場合、データベース接続オブジェクトはTransactionStatusのトランザクションオブジェクトを介して取得され、最後にコミット操作が行われます。 。この例では、トランザクションのステータスと、TransactionStatusによって提供されるトランザクションオブジェクトの役割を理解できます。
以下は、SpringトランザクションAPIで書き直されたトランザクション管理コードです。
// 获得事务管理器
PlatformTransactionManager txManager = getPlatformTransactionManager();DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 指定事务元信息
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);// 获得事务
TransactionStatus status = txManager.getTransaction(def);try {
// 业务处理
}catch (MyException ex) {
// 捕获异常,回滚事务
txManager.rollback(status); throw ex;
}// 提交事务
txManager.commit(status);
JDBC、Hibernate、MyBatisのいずれを使用する場合でも、対応する特定の実装をtxManagerに渡して、複数のデータアクセステクノロジーを切り替えるだけで済みます。
概要:Springトランザクションは、PlatformTransactionManager、TransactionDefinition、およびTransactionStatusインターフェースを介してトランザクション管理APIを統合し、戦略モードとテンプレートメソッドを組み合わせて特定の実装を決定します。
SpringトランザクションAPIコードの別の機能に気づきましたか?SQLExceptionはなくなりました。Springトランザクションが大量の例外処理コードをどのように解決するかを見てみましょう。
2.2たくさんの例外処理コード
JDBCを使用するコードで、例外処理コードをそれほど多く記述する必要があるのはなぜですか。接続の各メソッドは、SQLExceptionをスローし、そしてのSQLExceptionがあるためであるチェック例外、軍そのメソッドを使用しているとき、私たちは、例外処理を実行します。Springトランザクションはこの問題をどのように解決しますか?doCommitメソッドを見てみましょう。
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
} try {
con.commit(); } catch (SQLException ex) {
// 异常转换
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}
Connectionのcommitメソッドは、チェックされた例外SQLExceptionをスローし、SQLExceptionは、catchコードブロックでスローされたTransactionSystemExceptionに変換されます。TransactionSystemExceptionは、チェックされていない例外です。チェック済みの例外をチェックなしの例外に変換することで、例外をキャッチし、例外処理を強制しないかどうかを自分で決定できます。
Springトランザクションは、データベース内のほぼすべてのエラーに対応する例外を定義し、JDBC、Hibernate、MyBatisなどのさまざまな例外APIを統合しました。これにより、特定のデータアクセステクノロジを気にすることなく、例外を処理するときに統一された例外APIインターフェイスを使用できます。
概要:Springトランザクションは、例外変換を通じて強制的な例外処理を回避します。
2.3その他のビジネス処理コードとトランザクション管理コード
2.1節では、SpringトランザクションAPIを使用した書き込み方法、つまりプログラムによるトランザクション管理について説明していますが、「ビジネス処理コードとトランザクション管理コードの混在」の問題はまだ解決されていません。現時点では、Spring AOPを使用して、トランザクション管理コードの横断的関心事をコード、つまり宣言型トランザクション管理から切り離すことができます。例としてアノテーションメソッドを取り上げます。メソッドに@Transactionアノテーションを付けることにより、メソッドにトランザクション管理を提供します。以下の図に原理を示します。
Springトランザクションは、@ Transactionで注釈が付けられたメソッドのクラスのAOP拡張動的プロキシクラスオブジェクトを生成し、TransactionInterceptorをインターセプトチェーンに追加して、ターゲットメソッドを呼び出し、周囲を増やし、トランザクション管理を実現します。
TransactionInterceptorの特定の実装を見てみましょう。そのinvokeメソッドでは、次に示すように、トランザクション管理のためにinvokeWithinTransactionメソッドが呼び出されます。
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable { // 查询目标方法事务属性、确定事务管理器、构造连接点标识(用于确认事务名称)
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass); final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null;
try {
// 通过回调执行目标方法
retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) {
// 目标方法执行抛出异常,根据异常类型执行事务提交或者回滚操作
completeTransactionAfterThrowing(txInfo, ex); throw ex;
} finally {
// 清理当前线程事务信息
cleanupTransactionInfo(txInfo); } // 目标方法执行成功,提交事务
commitTransactionAfterReturning(txInfo); return retVal;
} else {
// 带回调的事务执行处理,一般用于编程式事务
// ...
}}
ターゲットメソッドを呼び出す前後に、トランザクションの作成、例外の処理、トランザクションのコミットなどの操作が追加されます。これにより、トランザクション管理コードを記述する必要がなくなり、@ Transaction属性でトランザクション関連のメタ情報を指定するだけで済みます。
概要:Springトランザクションは、AOPを介して宣言型トランザクションを提供し、ビジネス処理コードとトランザクション管理コードを分離します。
3.問題は何ですか
Springトランザクションは、最初のセクションにリストされた3つの問題を解決しますが、いくつかの新しい問題ももたらします。
3.1非公開メソッドの失敗
@Transactionalは、パブリックレベルのメソッドでマークされている場合にのみ有効になり、非パブリックメソッドには有効になりません。これは、Spring AOPがプライベートメソッドとプロテクトメソッドのインターセプトをサポートしていないためです。原則として、動的プロキシはインターフェースを介して実装されるため、当然、プライベートメソッドや保護メソッドをサポートできません。CGLIBは継承によって実装されます。実際には、保護メソッドのインターセプトをサポートできますが、Spring AOPはそのような使用をサポートしていません。この制限は、プロキシメソッドがパブリックであり、CGLIBとダイナミックプロキシを維持する必要があることを考慮しているためと推測されます一貫しています。保護またはプライベートメソッドをインターセプトする必要がある場合は、AspectJを使用することをお勧めします。
3.2セルフコールの失敗
@Transactionalを含むメソッドがBeanの内部メソッドを介して直接呼び出される場合、@ Transactionalは無効になります。次に例を示します。
public void saveAB(A a, B b)
{ saveA(a); saveB(b);}@Transactional
public void saveA(A a)
{ dao.saveA(a);}@Transactional
public void saveB(B b)
{ dao.saveB(b);}
saveABでsaveAメソッドとsaveBメソッドを呼び出します。両方の@Transactionalは無効になります。これは、Springトランザクションの実装がプロキシクラスに基づいているためです。メソッドが内部で直接呼び出されると、プロキシオブジェクトを経由せず、TransactionInterceptorでインターセプトおよび処理できないターゲットオブジェクトのメソッドを直接呼び出します。解決:
(1)ApplicationContextAware
プロキシオブジェクトは、ApplicationContextAwareによって挿入されたコンテキストを通じて取得されます。
public void saveAB(A a, B b)
{ Test self = (Test) applicationContext.getBean("Test");
self.saveA(a);
self.saveB(b);
}
(2)AopContext
AopContextを介してプロキシオブジェクトを取得します。
public void saveAB(A a, B b)
{ Test self = (Test)AopContext.currentProxy();
self.saveA(a);
self.saveB(b);
}
(3)@Autowired
@Autowiredアノテーションを介してプロキシオブジェクトを挿入します。
@Component
public class Test {
@Autowired
Test self; public void saveAB(A a, B b)
{
self.saveA(a); self.saveB(b); } // ...
}
(4)分割
saveAメソッドとsaveBメソッドを別のクラスに分割します。
public void saveAB(A a, B b)
{ txOperate.saveA(a); txOperate.saveB(b);}
上記の2つの問題は、Springトランザクション実装の制限が原因で発生します。不適切な使用のために間違いを犯しやすい、さらに2つの問題を見てみましょう。
3.3チェック例外はデフォルトではロールバックされない
デフォルトでは、チェックされていない例外をスローするとロールバックがトリガーされますが、チェックされている例外はトリガーされません。
invokeWithinTransactionメソッドによれば、例外処理ロジックが次のように実装されているcompleteTransactionAfterThrowingメソッドにあることがわかります。
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
} if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
// 异常类型为回滚异常,执行事务回滚
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex); throw ex2;
} catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
} } else {
try {
// 异常类型为非回滚异常,仍然执行事务提交
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex); throw ex2;
} catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
} } }}
例外がrollbackOnに基づいてロールバック例外であるかどうかを判別します。RuntimeExceptionおよびErrorのインスタンス、つまり、チェックされていない例外、または@TransactionのrollbackFor属性で指定されたロールバック例外のタイプのみが、トランザクションをロールバックします。それ以外の場合、トランザクションは引き続きコミットされます。したがって、チェックされていない例外をロールバックする必要がある場合は、rollbackFor属性を指定することを忘れないでください。そうしないと、ロールバックが無効になります。
3.4キャッチ例外はロールバックできません
セクション3.3では、チェックされていない例外またはrollbackForで指定された例外をスローすることのみがロールバックをトリガーできると述べました。例外をスローせずにキャッチすると、ロールバックのトリガーに失敗します。これも開発でよくある間違いです。例えば:
@Transactional
public void insert(List<User> users) {
try {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
for (User user : users) {
String insertUserSql = "insert into User (id, name) values (?,?)";
jdbcTemplate.update(insertUserSql, new Object[] { user.getId(),
user.getName() }); } } catch (Exception e) {
e.printStackTrace(); }}
ここでは、すべての例外がキャッチのためにキャッチされ、スローされませんでした。挿入で例外が発生した場合、ロールバックはトリガーされません。
ただし、同時に、このメカニズムを使用して、トランザクションに関与しないデータ操作をラップするためにtry-catchを使用することもできます。たとえば、重要でないログを書き込む場合は、例外をスローしないようにそれらをtry-catchでラップできます。ログの書き込みに失敗すると、トランザクションのコミットに影響します。
著者:ピンチグラスの子
出典:ナゲッツ