Spring AOP和事務的相關陷阱


  • 1、前言
  • 2、嵌套方法攔截失效
    • 2.1 問題場景
    • 2.2 解決方案
    • 2.3 原因分析
      • 2.3.1 原理
      • 2.3.2 源代碼分析
  • 3、Spring事務在多線程環境下失效
    • 3.1 問題場景
    • 3.2 解決方案
    • 3.3 原因分析
  • 4、總結

1、前言

Spring AOP在使用過程中需要注意一些問題,也就是平時我們說的陷阱,這些陷阱的出現是由於Spring AOP的實現方式造成的。對於這些缺陷本人堅持的觀點是:一是每一樣技術都或多或少有它的局限性,很難稱得上完美,只要掌握其實現原理,在使用時不要掉進陷阱就行,也就是進行規避;二是更進一步講,我們應該接受這就是技術本身的特點,也說不上什么缺陷,它本身就在“那里”,只是我們要的結果是“這樣”,而它表現的是“那樣”,恰好不是我們想要的而已。

對於Spring AOP的陷阱,我總結了以下兩個方面,現在分別進行介紹。

2、嵌套方法攔截失效

2.1 問題場景

通過例子來講解這樣更好,首先加上注解配置:

<!-- 啟用注解式AOP -->
<aop:aspectj-autoproxy/>

然后定義一個切面,代碼如下:

@Aspect
@Component
public class AnnotationAspectTest {
 
    @Pointcut("execution(* *.action(*))")
    public void action() {
    }
 
    @Pointcut("execution(* *.work(*))")
    public void work() {
    }
 
    @Pointcut("action() || work())")
    public void compositePointcut() {
    }
 
    //前置通知
    @Before("compositePointcut()")
    public void beforeAdvice() {
        System.out.println("before advice.................");
    }
 
    //后置通知
    @After("compositePointcut()")
    public void doAfter() {
        System.out.println("after advice..................");
    }
}
View Code

測試代碼:

//定義接口
public interface IPersonService {
    String action(String msg);
 
    String work(String msg);
}
  
//編寫實現類
@Service
public class PersonServiceImpl implements IPersonService {
 
    public String action(String msg) {
        System.out.println("FooService, method doing.");
 
        this.work(msg);                                           // *** 代碼 1 ***
 
        return "[" + msg + "]";
    }
 
    @Override
    public String work(String msg) {
        System.out.println("work: * " + msg + " *");
        return "* " + msg + " *";
    }
}
  
//單元測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class FooServiceTest {
 
    @Autowired
    private IPersonService personService;
 
    @Test
    public void testAction() {
        personService.action("hello world.");
    }
}
View Code

測試結果:

說明嵌套在action方法內部的work方法沒有被進行切面增強,它沒有被“切中”。

2.2 解決方案

在實現類中,如果注釋掉代碼1,將代碼1改為:

((IPersonService) AopContext.currentProxy()).work(msg);   // *** 代碼 2 ***

並且在XML配置中加上expose-proxy="true",變為:<aop:aspectj-autoproxy expose-proxy="true"/>

運行結果為:

嵌套在action方法內部的work方法被進行了切面增強,它被“切中”。

2.3 原因分析

2.3.1 原理

以上結果的出現與Spring AOP的實現原理息息相關,由於Spring AOP采用了動態代理實現AOP,在Spring容器中的bean(也就是目標對象)會被代理對象代替,代理對象里加入了我們需要的增強邏輯,當調用代理對象的方法時,目標對象的方法就會被攔截。而上文中問題出現的症結也就是在這里,為了進一步說明這個問題,用圖片說明最好:

通過調用代理對象的action方法,在其內部會經過切面增強,然后方法被發射到目標對象,在目標對象上執行原有邏輯,如果在原有邏輯中嵌套調用了work方法,則此時work方法並沒有被進行切面增強,因為此時它已經在目標對象內部。

而解決方案很好地說明了,將嵌套方法發射到代理對象,這樣就完成了切面增強。

2.3.2 源代碼分析

接下來我們簡單看一下源代碼,Spring AOP的代碼邏輯相當清晰:

/**
 * Implementation of {@code InvocationHandler.invoke}.
 * <p>Callers will see exactly the exception thrown by the target,
 * unless a hook method throws an exception.
 */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      ... ...
 
      Object retVal;
 
      //*** 代碼3 ***
      if (this.advised.exposeProxy) {
         // Make invocation available if necessary.
         oldProxy = AopContext.setCurrentProxy(proxy);
         setProxyContext = true;
      }
 
      ... ...
 
      // Get the interception chain for this method.
      List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
 
      // Check whether we have any advice. If we don't, we can fallback on direct
      // reflective invocation of the target, and avoid creating a MethodInvocation.
      if (chain.isEmpty()) {
         // We can skip creating a MethodInvocation: just invoke the target directly
         // Note that the final invoker must be an InvokerInterceptor so we know it does
         // nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
         retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args);
      }
      else {
         // We need to create a method invocation...
         invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
         // Proceed to the joinpoint through the interceptor chain.
         retVal = invocation.proceed();
      }
 
      ... ...
}
View Code

 

在代碼3處,如果配置了exposeProxy開關,則會將代理對象暴露在當前線程中,以供其它需要的地方使用。那么是怎么暴露的呢?答案很簡單,通過使用靜態的全局ThreadLocal變量就解決了問題。

3、Spring事務在多線程環境下失效

3.1 問題場景

沿用上面的代碼稍作修改,加上事務配置:

<!-- 數據庫的事務管理器配置 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="meilvDataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>

代碼如下所示:

@Service
@Transactional(propagation = Propagation.REQUIRED, timeout = 10000000)
public class PersonServiceImpl implements IPersonService {
 
    @Autowired
    IUserDAO userDAO;
 
    @Override
    public String action(final String msg) {
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                (getThis()).work(msg);
            }
        }).start();
 
        UserDO userDO = new UserDO();
        userDO.setName("lanlan");
        userDAO.insert(userDO);
 
        return "[" + msg + "]";
    }
 
    @Override
    public String work(String msg) {
        System.out.println("work: * " + msg + " *");
        UserDO userDO = new UserDO();
        userDO.setName("yanyan");
        userDAO.insert(userDO);
 
        throw new RuntimeException();
    }
 
    private IPersonService getThis() {
        try {
            return (IPersonService) AopContext.currentProxy();
        } catch (IllegalStateException e) {
            return this;
        }
    }
}
View Code

結果:work方法中拋出異常,但是沒有影響事務的回滾,說明事務在子線程中失效了。

3.2 解決方案

只需要將多線程中的方法提出來,或者作為另一個Service類中的方法即可。

 
@Service
@Transactional(propagation = Propagation.REQUIRED, timeout = 10000000)
public class PersonServiceImpl implements IPersonService {
 
    @Autowired
    IUserDAO userDAO;
 
    @Override
    public String action(final String msg) {
 
       (getThis()).work(msg);
 
        UserDO userDO = new UserDO();
        userDO.setName("lanlan");
        userDAO.insert(userDO);
 
        return "[" + msg + "]";
    }
 
    @Override
    public String work(String msg) {
        System.out.println("work: * " + msg + " *");
        UserDO userDO = new UserDO();
        userDO.setName("yanyan");
        userDAO.insert(userDO);
 
        throw new RuntimeException();
    }
 
    private IPersonService getThis() {
        try {
            return (IPersonService) AopContext.currentProxy();
        } catch (IllegalStateException e) {
            return this;
        }
    }
}
View Code

 

上面只是一個簡單的例子,用於進行問題說明。

a、如果去掉多線程,將方法放在同一個類里,Spring則會根據事務的傳播配置參數,是否重新啟用新的事務。

b、如果將方法獨立出來放在新的類里,並且該方法也配置了事務,則會重新啟用新的事務。

3.3 原因分析

Spring的事務處理為了與數據訪問解耦,它提供了一套處理數據資源的機制,而這個機制與上文中的原理相差無幾,也是采用的ThreadLocal的方式。

在編程中,Service實例都是單例的無狀態的,事務管理則需要加入事務控制的相關狀態變量,使得Service實例不再是無狀態線程安全的,解決這個問題的方式就是使用ThreadLocal。

通過使用ThreadLocal將數據源綁定在當前線程上,在當前線程的事務中,從設定的地方去取連接就會是同一個數據庫連接,這樣操作事務就會在同一個連接上進行。

如下圖所示:

但是,ThreadLocal的特性是,綁定在當前線程中的變量不會自動傳遞到其它線程中(當然,InheritableThreadLocal可以在父子線程中間傳遞變量值,但是這需要特殊的使用場景),所以當開啟子線程時,子線程並沒有父線程的數據庫連接資源。

對於上文提到的陷阱:如果另外開啟線程,那么在新線程中將獲取不到父線程的連接,事務要么失效,要么重新開啟一個新的。

源代碼如下:

 

public abstract class DataSourceUtils {
 
   public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
      try {
         return doGetConnection(dataSource);
      }
      catch (SQLException ex) {
         throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
      }
   }
    
   public static Connection doGetConnection(DataSource dataSource) throws SQLException {
       ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
       if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
          conHolder.requested();
          if (!conHolder.hasConnection()) {
             logger.debug("Fetching resumed JDBC Connection from DataSource");
             conHolder.setConnection(dataSource.getConnection());
          }
          return conHolder.getConnection();
       }
    
       Connection con = dataSource.getConnection();
 
       ......
 
       return con;
    }
}
  
  
public abstract class TransactionSynchronizationManager {
 
   private static final ThreadLocal<Map<Object, Object>> resources =
         new NamedThreadLocal<Map<Object, Object>>("Transactional resources");
 
   /**
    * Retrieve a resource for the given key that is bound to the current thread.
    * @param key the key to check (usually the resource factory)
    * @return a value bound to the current thread (usually the active
    * resource object), or {@code null} if none
    * @see ResourceTransactionManager#getResourceFactory()
    */
   public static Object getResource(Object key) {
      Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
      Object value = doGetResource(actualKey);
      if (value != null && logger.isTraceEnabled()) {
         logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
               Thread.currentThread().getName() + "]");
      }
      return value;
   }
 
   /**
    * Actually check the value of the resource that is bound for the given key.
    */
   private static Object doGetResource(Object actualKey) {
      Map<Object, Object> map = resources.get();
      if (map == null) {
         return null;
      }
      Object value = map.get(actualKey);
      // Transparently remove ResourceHolder that was marked as void...
      if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
         map.remove(actualKey);
         // Remove entire ThreadLocal if empty...
         if (map.isEmpty()) {
            resources.remove();
         }
         value = null;
      }
      return value;
   }
}
View Code

4、總結

本文總結了Spring AOP和事務的兩個陷阱,在平時的實際開發中經常與遇到,只有深入了解了其中的原理,才會在工作中能夠有效應對。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM