1. 程式人生 > >Srping Transaction rolled back because it has been marked as rollback-only解決方案

Srping Transaction rolled back because it has been marked as rollback-only解決方案

1.異常相關描述

如題,此異常的全部資訊如下:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:718)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:475)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:270)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:91)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
	at com.sun.proxy.$Proxy81.updateProductStockProcess(Unknown Source)
	at fmcgwms.OmsAPIServiceTest.testUpdateProductToProcess(OmsAPIServiceTest.java:113)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:88)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
網上有很多遇到這個問題的先來說說別人的解決方案:
<bean id="transactionManager"
	class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource" />
	<property name="globalRollbackOnParticipationFailure" value="false" /> <!--指定此引數為false-->

</bean>	
這種方案不推薦大家使用,因為會影響到其他事務的管理。

2.解決方案

先來看看Spring事務管理的配置:

<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			<tx:method name="del*" propagation="REQUIRED" read-only="false"
				rollback-for="java.lang.Exception" />
			<tx:method name="insert*" propagation="REQUIRED" read-only="false"
				rollback-for="java.lang.Exception" />
			<tx:method name="update*" propagation="REQUIRED" read-only="false"
				rollback-for="java.lang.Exception" />
			<tx:method name="execute*" propagation="REQUIRED" read-only="false"
				rollback-for="java.lang.Exception" />
			<tx:method name="*" propagation="REQUIRED" read-only="true" />
		</tx:attributes>
</tx:advice>
很明顯,事務為相關方法的Exception進行了事務管理。所以從異常入手的是最簡單的,即定義自己的異常類資訊:
package com.wlyd.fmcgwms.util.exception;
/**
 * 不回滾介面異常(Spring exception被事務管理時只要遇到Exception就會回滾方法內所有操作)
 * 
 * @packge com.wlyd.fmcgwms.util.exception.UnrollbackException
 * @date   2016年7月29日  上午10:07:39
 * @author pengjunlin
 * @comment   
 * @update
 */
public class UnrollbackException extends Exception{

	/**
	 * 
	 */
	private static final long serialVersionUID = -1255980800389665752L;

	@Override
	public String getMessage() {
		return "介面不回滾異常資訊:"+super.getMessage();
	}

	public UnrollbackException(String message, Throwable cause) {
		super(message, cause);
		// TODO Auto-generated constructor stub
	}

	public UnrollbackException(String message) {
		super(message);
		// TODO Auto-generated constructor stub
	}

	public UnrollbackException(Throwable cause) {
		super(cause);
		// TODO Auto-generated constructor stub
	}
	
	

}
遠端介面service實現時定義的事務方法,異常採用自己所定義的異常,Exception繼承實現的異常也會被管理

處理方法:將介面定義的異常內部捕獲而不丟擲。

public String updateProductStock(String token, String requestBody,
			String platformCode, String tenantCode) throws UnrollbackException {
		Log.getLogger(getClass()).info("呼叫九州通達OMS介面.........../updateproductstock REQUEST:"+requestBody); 
		String url = EhcacheUtil.get("JZTD_OMS_API_BASE_URL").toString()	+ "/updateproductstock";
		String result=null;
		try {
			result = RestTemplateUtils.post(url, requestBody, token, platformCode, tenantCode);
			Log.getLogger(getClass()).info("呼叫九州通達OMS介面.........../updateproductstock RESPONSE:"+result); 
		} catch (Exception e) {
			Log.getLogger(getClass()).info("呼叫九州通達OMS介面.........../updateproductstock NO RESPONSE:"+e.getMessage()); 
			// 如需方法內業務操作回滾開啟此註釋程式碼
			//throw new UnrollbackException(">>>>>>呼叫九州通達OMS介面訪問異常!<<<<<");
		}
		return result;
	}
因為是呼叫外部系統的介面,所以事務管理的方法要插入日誌,如果丟擲Exception那麼即使日誌插入成功也最終會回滾。下面程式碼的處理日誌插入不會因為介面呼叫是否異常都會執行下去:
public void updateProductStockProcess(Corporation corporation){
		Map<String, Object> keyMap=null;
		boolean flag=false;//介面呼叫失敗
		int statusCode=102;//失敗
		int interfaceType=3;//介面型別標識
		String requestBody="";
		String tenantCode=corporation.getEsCorAlias();
		// 獲取商品庫存記錄
		List<JZTDOmsProduct> products=wmStockMapper.selectAllItemStockNumber(corporation.getEsCorId()+"");
		if(products==null||products.size()==0){
			Log.getLogger(getClass()).info(">>>>組織"+"("+corporation.getEsCorId()+")"+corporation.getEsCorName()+"沒有庫存記錄!!!");
			return ;
		}
		Map<String,Object> map=new HashMap<String, Object>();
		map.put("Products", products);
		requestBody=JSON.toJSONString(map);
		String platformCode=EhcacheUtil.get("JZTD_OMS_PLATFORMCODE").toString();
		
		// 通過RSA生成金鑰對
		try {
			keyMap = RSAUtils.genKeyPair();
		} catch (Exception e) {
			e.printStackTrace();
			return ;
		}
		// 公鑰(RSA公鑰加密-私鑰解密)
		String publicKey=null;
		try {
			publicKey = RSAUtils.getPublicKey(keyMap);
		} catch (Exception e) {
			e.printStackTrace();
			return ;
		}
		// 模擬生成Token
		String token=SAASTokenManager.generateToken(publicKey, platformCode, tenantCode);
		// 呼叫九州通達OMS庫存同步介面
		try {
			String result=jztdapiService.updateProductStock(token, requestBody, platformCode, tenantCode);
			JSONObject json=JSON.parseObject(result);
			if(json!=null&&json.getBooleanValue("IsSuccess")){
				flag=true;
				statusCode=101;//成功
			}
		} catch (UnrollbackException e) {
			statusCode=104;// 其他異常
			Log.getLogger(getClass()).error(">>>>呼叫九州通達OMS庫存同步介面----訪問異常:"+e.getMessage());
		}
		EbInterfaceLog log=new EbInterfaceLog(new Date(), requestBody, flag==true?0:1, 0, statusCode, interfaceType) ;
		int result=ebInterfaceLogService.insertTable(log,tenantCode );
		Log.getLogger(getClass()).error(">>>>呼叫九州通達OMS庫存同步介面日誌儲存:"+(result>0?true:false));
}
被事務困擾了小半天了,終於解決了,故在此記錄以備查閱。

3.改進方法

實際上Remote介面呼叫時可以不丟擲異常,上面的遠端介面程式碼完全沒有必要做異常丟擲處理,只要內部定義好返回的錯誤資訊就可以了。

業務處理介面定義:

/**
	 * 4.1.3商品庫存同步介面
	 * 
	 * @MethodName: updateProductStock
	 * @Description:
	 * @param token
	 *            RSA加密Token
	 * @param requestBody
	 *            請求訊息體
	 * @param platformCode
	 *            平臺編碼
	 * @param tenantCode
	 *            租戶編碼
	 * @return
	 * @throws UnrollbackException 因為Exception被spring所管理,如介面訪問異常介面日誌插入成功後仍然回滾
	 * @throws
	 */
	public String updateProductStock(String token, String requestBody,
			String platformCode, String tenantCode) ;
這裡不再丟擲異常。

業務介面方法實現:

public void updateProductStockProcess(Corporation corporation){
        Map<String, Object> keyMap=null;
        int interfaceType=3;//介面型別標識
        String requestBody="";
        String tenantCode=corporation.getEsCorAlias();
        // 獲取商品庫存記錄
        List<JZTDOmsProduct> products=wmStockMapper.selectAllItemStockNumber(corporation.getEsCorId()+"");
        if(products==null||products.size()==0){
            Log.getLogger(getClass()).info(">>>>組織"+"("+corporation.getEsCorId()+")"+corporation.getEsCorName()+"沒有庫存記錄!!!");
            return ;
        }
        Map<String,Object> map=new HashMap<String, Object>();
        map.put("Products", products);
        requestBody=JSON.toJSONString(map);
        String platformCode=EhcacheUtil.get("JZTD_OMS_PLATFORMCODE").toString();
        
        // 通過RSA生成金鑰對
        try {
            keyMap = RSAUtils.genKeyPair();
        } catch (Exception e) {
            e.printStackTrace();
            return ;
        }
        // 公鑰(RSA公鑰加密-私鑰解密)
        String publicKey=null;
        try {
            publicKey = RSAUtils.getPublicKey(keyMap);
        } catch (Exception e) {
            e.printStackTrace();
            return ;
        }
        // 模擬生成Token
        String token=SAASTokenManager.generateToken(publicKey, platformCode, tenantCode);
        // 呼叫九州通達OMS庫存同步介面
        String result=jztdapiService.updateProductStock(token, requestBody, platformCode, tenantCode);
        JSONObject json=JSON.parseObject(result);
        boolean flag=json.getBooleanValue("IsSuccess");
        int statusCode=json.getIntValue("statusCode");
        EbInterfaceLog log=new EbInterfaceLog(new Date(), requestBody, flag==true?0:1, 0, statusCode, interfaceType) ;
        int state=ebInterfaceLogService.insertTable(log,tenantCode );
        Log.getLogger(getClass()).error(">>>>呼叫九州通達OMS庫存同步介面日誌儲存:"+(state>0?true:false));
}
此時我們不再關注遠端介面是否異常,所以後面的操作就可以進行下去了。

遠端介面實現方法:

public String updateProductStock(String token, String requestBody,
            String platformCode, String tenantCode)  {
        Log.getLogger(getClass()).info("呼叫九州通達OMS介面.........../updateproductstock REQUEST:"+requestBody); 
        String url = EhcacheUtil.get("JZTD_OMS_API_BASE_URL").toString()    + "/updateproductstock";
        String result=null;
        try {
            result = RestTemplateUtils.post(url, requestBody, token, platformCode, tenantCode);
            Log.getLogger(getClass()).info("呼叫九州通達OMS介面.........../updateproductstock RESPONSE:"+result); 
            JSONObject json=JSON.parseObject(result);
            if(json.getBooleanValue("IsSuccess")){
                json.put("statusCode", 101);//成功
            }else{
                json.put("statusCode", 102);//失敗
            }
            return json.toJSONString();
        } catch (Exception e) {
            Log.getLogger(getClass()).info("呼叫九州通達OMS介面.........../updateproductstock NO RESPONSE:"+e.getMessage()); 
            return requestExceptionInfo(e.getMessage());//異常返回
        }
}
異常訊息返回:
 /**
     * 介面訪問錯誤異常資訊
     * 
     * @MethodName: requestExceptionInfo 
     * @Description: 
     * @param msg
     * @return
     * @throws
     */
    public String requestExceptionInfo(String msg){
        Map<String,Object> map=new HashMap<String, Object>();
        map.put("IsSuccess", false);
        map.put("OperationDesc", "介面呼叫異常:"+msg);
        map.put("statusCode", 104);//其他異常
        return JSON.toJSONString(map);
    }