Transaction rolled back because it has been marked as rollback-onlyエラー探究

48072 ワード

背景:一つのdaoがデータをデータベースに挿入した時に異常が発生し、キャプチャした後にログを印刷し、もう一つのSQL異常類をservice層に投げ直し、service層が捕獲して処理した後に、もう一つのカスタムメッセージを持ってcontroler層に異常を投げました。その後、カスタマイズしたmessageをフロントに投げて展示します。しかし、フロントにはTransaction rolled back because it has been marked as rollback-onlyの情報が展示されています。業務コードは以下の通りです。一つのモデル類Sync ExamData Cient Template.java
public 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を実行できます。