Transaction rolled back because it has been marked as rollback-onlyエラー探究
48072 ワード
背景:一つのdaoがデータをデータベースに挿入した時に異常が発生し、キャプチャした後にログを印刷し、もう一つのSQL異常類をservice層に投げ直し、service層が捕獲して処理した後に、もう一つのカスタムメッセージを持ってcontroler層に異常を投げました。その後、カスタマイズしたmessageをフロントに投げて展示します。しかし、フロントには
サービスクラス:
controlerクラス:
Springソースの追跡:serviceから異常を投げ出してcontrollerまでいったい何を経験しましたか?
1.MethodProxy.java
2.
3.
4.
上記の
ここで、エラーの原因を知るべきです。データベースは異常に検査異常ではないです。しかし、消化しました。捨てられませんでした。SQLExceptionをservice層に捨てました。サービス層でもExceptionを捨てました。検診の異常ですので、comitメソッドを実行します。前述のトランザクションはグローバルrollback-onlyに設定されていますので、comitは正常なcomitではなく、rollbackを行い、事務境界に到達した時にUexpected Rollback Exceptionの異常を投げました。
解決策を知ることができます。1.
総括:この異常を解決する方法:まず、捕獲された非検査異常を事務境界外に投げ出し、消化することではない。第二種類:自分で消化するなら、新しい非受検異常をカスタマイズしてください。これで正確にrollbackを実行できます。異常時にcomit操作を実行するのではなく、rollbackを実行できます。
Transaction rolled back because it has been marked as rollback-only
の情報が展示されています。業務コードは以下の通りです。一つのモデル類Sync ExamData Cient Template.javapublic final Map<String, Object> syncData(T examData, List<V> examSyncInfoList) throws SyncException,SyncSQLException {
Map<String, Object> result;
try {
result = syncExamData(examData);
}catch (Exception e){
throw new SyncException(e);
}
if ((boolean)result.get("status")){
try {
updateSyncInfo(examSyncInfoList, result, examData);
}catch (Exception e){
e.printStackTrace();
throw new SQLException();
// throw new SyncSQLException(e);
}
}
return result;
}
コード:updateSyncInfo(examSyncInfoList, result, examData);
は、insert動作を実行し、トランザクションを有する。コードに注意してください。throw new SQLException();
はキャプチャされた異常eを投げ出すのではなく、新しい異常を投げました。サービスクラス:
try {
List<ExamSyncInfo> planList = getExamSyncInfos(syncType);
result = syncExamPlanDataClient.syncData(examPlan, planList);
} catch (SyncException e1) {
logger.error(" 【" + examPlan.getId() + "】 ", e1);
throw new Exception(" , : !");
} catch (SQLException e2) {
// , ( ),
planDataRollBack(examPlan, syncType);
throw new Exception(" , : !");
}
if (!(boolean)result.get("status"))
throw new Exception(" , : !");
上のコードcatchはSQLException異常に達しました。そしてExceptionを一つ投げ出してcontroler層に異常です。controlerクラス:
try {
examPlanService.syncExamPlan(examPlan, ADD_PLAN);
} catch (Exception e) {
e.printStackTrace();
addMessage(redirectAttributes, e.getMessage());
return "redirect:"+Global.getAdminPath()+"/examplan/examPlan/?repage";
}
serviceは複数の異常をcatchする可能性があるので、サービス層で異常をコントロール層に変換し、フロントエンドに異常情報を返す場合は、e.getMessage()
でservice層の異常情報を抽出すれば良いと考えられています。しかし、上記のような問題が発生しました。データベースに異常が発生したら、フロントエンドは「試験計画の保存に失敗した。原因:試験能力プラットフォームに同期して失敗した」というヒントを得るべきですが、実際にはタイトルの提示を受けました。一体なぜですか?下を見て分解してください。Springソースの追跡:serviceから異常を投げ出してcontrollerまでいったい何を経験しましたか?
1.MethodProxy.java
public Object invoke(Object obj, Object[] args) throws Throwable {
try {
this.init();
MethodProxy.FastClassInfo fci = this.fastClassInfo;
return fci.f1.invoke(fci.i1, obj, args);
} catch (InvocationTargetException var4) {
throw var4.getTargetException();
} catch (IllegalArgumentException var5) {
if (this.fastClassInfo.i1 < 0) {
throw new IllegalArgumentException("Protected method: " + this.sig1);
} else {
throw var5;
}
}
}
このクラスは反射でserviceを呼び出す方法です。この文は次に実行されます。throw var4.getTargetException();
は2にジャンプします。2.
TransactionAspectSupport#invokeWithinTransaction
方法に入ると、以下はこの方法の一部コードである。if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
このときcompleteTransactionAfterThrowing(txInfo, ex);
コードに入ります。*この方法からは、例外が出たら先に事務を完了するということです。すなわち、異常が事務定義の境界を打ち出す前に、まず事務を完了するということです。*この方法に入ってみましょう。3.
TransactionAspectSupport#completeTransactionAfterThrowing
方法は、以下のコードである。protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.hasTransaction()) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
if (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 ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
catch (Error err) {
logger.error("Application exception overridden by rollback error", ex);
throw err;
}
}
else {
// We don’t roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
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 ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
catch (Error err) {
logger.error("Application exception overridden by commit error", ex);
throw err;
}
}
}
}
コードは次にif (txInfo.transactionAttribute.rollbackOn(ex))
のコードに実行され、txInfo.transactionAttribute
はトランザクション構成の属性であるべきであり、次いでrollbackOn
の方法に進む。4.
rollbackOn
方法はTransactionAttribute
インターフェースで定義されています。ソースは以下の通りです。/**
* Should we roll back on the given exception?
* @param ex the exception to evaluate
* @return whether to perform a rollback or not
*/
boolean rollbackOn(Throwable ex);
コメントからこの方法は主にこの異常に対してロールバックを行うかどうかを判断することが分かります。この方法に進んでコードを実現します。@Override
public boolean rollbackOn(Throwable ex) {
return this.targetAttribute.rollbackOn(ex);
}
継続ログイン:RuleBasedTransactionAttribute#rollbackOn
方法:/**
* Winning rule is the shallowest rule (that is, the closest in the
* inheritance hierarchy to the exception). If no rule applies (-1),
* return false.
* @see TransactionAttribute#rollbackOn(java.lang.Throwable)
*/
@Override
public boolean rollbackOn(Throwable ex) {
if (logger.isTraceEnabled()) {
logger.trace("Applying rules to determine whether transaction should rollback on " + ex);
}
RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;
if (this.rollbackRules != null) {
for (RollbackRuleAttribute rule : this.rollbackRules) {
int depth = rule.getDepth(ex);
if (depth >= 0 && depth < deepest) {
deepest = depth;
winner = rule;
}
}
}
if (logger.isTraceEnabled()) {
logger.trace("Winning rollback rule is: " + winner);
}
// User superclass behavior (rollback on unchecked) if no rule matches.
if (winner == null) {
logger.trace("No relevant rollback rule found: applying default rules");
return super.rollbackOn(ex);
}
return !(winner instanceof NoRollbackRuleAttribute);
}
転覆するかどうかを判定する勝負の法則は、原則に近いということです。this.rollbackRules
はいくつかのロールバックのルールを表しています。rule.getDepth(ex);
は異常なルールの深さを発見し、ソースコードを通じて:public int getDepth(Throwable ex) {
return getDepth(ex.getClass(), 0);
}
private int getDepth(Class<?> exceptionClass, int depth) {
if (exceptionClass.getName().contains(this.exceptionName)) {
// Found it!
return depth;
}
// If we’ve gone as far as we can go and haven’t found it…
if (exceptionClass == Throwable.class) {
return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}
異常の深さを求めるということは、ルールで定義されている異常な名前文字列を含んでいる場合、例えばSyncSQLExceptionという異常な名前文字列を抛り出すということです。さもないと例外を投げ出した父親に再帰して探します。見つけられなければ戻ります。上記の
getDepth
方法のコードを呼び出したので、勝負規則は、投げられた異常に最も近い規則を見つけることである。その後ロールバックが必要かどうかを判断します。winner
がnullである場合、親の中に入って規則を探す:DefaultTransactionAttribute#rollbackOn
方法:/**
* The default behavior is as with EJB: rollback on unchecked exception.
* Additionally attempt to rollback on Error.
* This is consistent with TransactionTemplate’s default behavior.
*/
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
デフォルト規則は、投げられた異常がRuntimeException
またはError
の例であれば、trueに戻り、つまりロールバックします。じゃ、私達はロールバックが必要ですか?答えは肯定的です。上から3ステップ目の方法で、ロールバックすれば、一般的なルールに合致します。データベースは異常ロールバックします。さもなくば:comitの操作を実行するのはcomit時報の上の誤りであるべきで、今comitのソースコードに入ります:AbstractPlatformTransactionManager#commit
方法:/**
* This implementation of commit handles participating in existing
* transactions and programmatic rollback requests.
* Delegates to {@code isRollbackOnly}, {@code doCommit}
* and {@code rollback}.
* @see org.springframework.transaction.TransactionStatus#isRollbackOnly()
* @see #doCommit
* @see #rollback
*/
@Override
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus);
return;
}
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus);
// Throw UnexpectedRollbackException only at outermost transaction boundary
// or if explicitly asked to.
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
return;
}
processCommit(defStatus);
}
次にdefStatus.isGlobalRollbackOnly()
方法に進む:DefaultTransactionStatus#isGlobalRollbackOnly
方法:/**
* Determine the rollback-only flag via checking both the transaction object,
* provided that the latter implements the {@link SmartTransactionObject} interface.
* Will return "true" if the transaction itself has been marked rollback-only
* by the transaction coordinator, for example in case of a timeout.
* @see SmartTransactionObject#isRollbackOnly
*/
@Override
public boolean isGlobalRollbackOnly() {
return ((this.transaction instanceof SmartTransactionObject) &&
((SmartTransactionObject) this.transaction).isRollbackOnly());
}
this.transaction
は、ビジネスマネージャのオブジェクトであり、データベース接続を表しています。ここで私たちのプロジェクトはDataSourceTransactionObject
で、SmartTransactionObject
クラスを継承しています。isRollbackOnly
の方法に入ると、ResourceHolderSupport#isRollbackOnly
の方法があります。なぜtrueですか?Spring事務で非受検異常が発生した場合、全体の事務はrollback-onlyとしてマークされます。isGlobalRollbackOnly()
方法を呼び出したところに戻ります。つまり、comitメソッド内でprocessRollback(defStatus);
方法を継続してロールバックして、次のコードを実行します。// Throw UnexpectedRollbackException only at outermost transaction boundary
// or if explicitly asked to.
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
注釈によって分かることができます。一番外側の事務境界にのみUexpectedRollbackExceptionを投げ出したり、明確な要求があれば。status.isNewTransaction()
がtrueであれば、事務境界に達すると、UexpectedRollbackExceptionを投げ出します。つまり先端で得られた異常です。ここで、エラーの原因を知るべきです。データベースは異常に検査異常ではないです。しかし、消化しました。捨てられませんでした。SQLExceptionをservice層に捨てました。サービス層でもExceptionを捨てました。検診の異常ですので、comitメソッドを実行します。前述のトランザクションはグローバルrollback-onlyに設定されていますので、comitは正常なcomitではなく、rollbackを行い、事務境界に到達した時にUexpected Rollback Exceptionの異常を投げました。
解決策を知ることができます。1.
RuntimeException
類を継承する業務の異常をカスタマイズします。2.サービスをservice層に異常投げます。3.カスタムエラー情報サービスを持っている新しいサービスを異常にビジネス境界を投げ続けます。controller層4.controller層に行ったら、カスタムエラーメッセージをフロントエンドに投げて表示します。総括:この異常を解決する方法:まず、捕獲された非検査異常を事務境界外に投げ出し、消化することではない。第二種類:自分で消化するなら、新しい非受検異常をカスタマイズしてください。これで正確にrollbackを実行できます。異常時にcomit操作を実行するのではなく、rollbackを実行できます。