1. 程式人生 > >spring aop註解失效之謎

spring aop註解失效之謎

問題:

在spring 中使用 @Transactional 、 @Cacheable 或 自定義 AOP 註解時,會發現個問題:

在物件內部的方法中呼叫該物件的其他使用aop機制的方法,被呼叫方法的aop註解失效。

這句話可能說的有點拗口,那麼我們來看幾個 aop 失效的例子吧
  • 事物失效
public class TicketService{
    //買火車票
    @Transactional
    public void buyTrainTicket(Ticket ticket){
        System.out.println("買到了火車票");
        try {
    //在同一個類中的方法,呼叫 aop註解(@Transactional 註解也是aop 註解) 的方法,會使aop 註解失效.
        //此時如果 sendMessage()的傳送訊息動作失敗丟擲異常,“訊息存入資料庫“動作不會回滾。
            sendMessage();
        } catch (Exception  e) {
            logger.warn("傳送訊息異常");
        }
    }

    //買到車票後傳送訊息
    @Transactional
    public void sendMessage(){
        System.out.println("訊息存入資料庫");
        System.out.println("執行傳送訊息動作");
    }
}
  • 快取失效
//使用快取,查詢時先查詢快取,快取中查詢不到時,呼叫資料庫。
@Cacheable(value = "User")
public User getUserById(Integer id){
        System.out.println("查詢資料庫");
    return UserDao.getUserById(id);
}

//在同一個類中的方法,呼叫 aop註解(@Cacheable 註解也是aop 註解) 的方法,會使aop 註解失效  
public User getUser(Integer id){
    //此時註解失效,getUserById 方法不會去快取中查詢資料,會直接查詢資料庫。
    return getUserById(id);
}

還有自定義AOP 註解 在同一個類中的方法級別呼叫也會導致 aop 註解失效。

原因

好了失效的例子已經看過了。那麼為什麼會產生這種情況呢?我門來探究一下原因吧。

spring AOP 使用Java動態代理和 cglib 代理 來建立AOP代理,沒有介面的類 使用cglib 代理。關於 spring aop 的java動態代理原理,請看這片部落格:利用java 的動態代理模擬spring的AOP
熟悉一下 aop 的原理注意看m.invoke(target, args); 部分(我門討論的問題實際上就是m中呼叫同類的其他方法)。

我門知道當方法被代理時,其實是 動態生成了一個代理物件,代理物件去執行 invoke方法,在呼叫被代理物件的方法的時候執行了一些其他的動作。

所以當在被代理物件的方法中呼叫被代理物件的其他方法時。其實是沒有用代理呼叫,是用了被代理物件本身呼叫的。

  • 例如事物的例子:

當我門呼叫buyTrainTicket(Ticket ticket)方法時,spring 的動態代理已經幫我們動態生成了一個代理的物件,暫且我就叫他 $TicketService1。

所以呼叫buyTrainTicket(Ticket ticket) 方法實際上是代理物件 T i c k e t S e r v i c e 1 調 TicketService1呼叫的。 TicketService1.buyTrainTicket(Ticket ticket)

但是在buyTrainTicket 方法內呼叫同一個類的另外一個註解方法sendMessage()時,實際上是this.sendMessage() 這個this 指的是TicketService 物件,並不是$TicketService1 代理物件,沒有走代理。所以 註解失效。

解決

通過分析原因我門知道註解失效是因為 執行方法時沒有走代理,所以在同一個類的方法中呼叫其他註解方法,應該使用代理物件 呼叫。

spring 解決方案

//通過AopContext.currentProxy()獲取當前代理物件。
AopContext.currentProxy();

修改範例

修改XML 新增如下語句;先開啟cglib代理,開啟 exposeProxy = true,暴露代理物件

<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
public class TicketService{
    //買火車票
    @Transactional
    public void buyTrainTicket(Ticket ticket){
        System.out.println("買到了火車票");
        try {

//通過代理物件去呼叫sendMessage()方法          
(TicketService)AopContext.currentProxy().sendMessage();
        } catch (Exception  e) {
            logger.warn("傳送訊息異常");
        }
    }

    @Transactional
    public void sendMessage(){
        System.out.println("訊息存入資料庫");
        System.out.println("執行傳送訊息動作");
    }
}
當然最好的解決方案就是避免在物件內部呼叫

springboot 解決方案

springboot 我用的是1.3.0但是我發現 @EnableAspectJAutoProxy的 expose-proxy=”true” 方法都不存在,在spring4.3 以後註解才有 exposeProxy() 方法(spring 原始碼傳送門 )。所以使用 AopContext 獲取代理物件的方法就流產了。下面是 EnableAspectJAutoProxy 的原始碼。在

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

    /**
     * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
     * to standard Java interface-based proxies. The default is {@code false}.
     */
    boolean proxyTargetClass() default false;

}

既然在AopContext取不到,我門只好去ApplicationContext 中取我門的代理物件了。

  • 新建獲取代理物件的工具類SpringUtil
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        if (SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    //獲取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //通過name獲取 Bean.
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }

    //通過class獲取Bean.
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    //通過name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }

}
  • 修改範例
public class TicketService{
    //買火車票
    @Transactional
    public void buyTrainTicket(Ticket ticket){
        System.out.println("買到了火車票");
        try {

//通過代理物件去呼叫sendMessage()方法          
SpringUtil.getBean(this.getClass()).sendMessage();
        } catch (Exception  e) {
            logger.warn("傳送訊息異常");
        }
    }

    @Transactional
    public void sendMessage(){
        System.out.println("訊息存入資料庫");
        System.out.println("執行傳送訊息動作");
    }
}

原文連線: https://blog.csdn.net/u012373815/article/details/77345655