在同一個類中呼叫另一個方法沒有觸發 Spring AOP 的問題

永順發表於2017-02-17

起因

考慮如下一個例子:

@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyMonitor {
}
@Component
@Aspect
public class MyAopAdviseDefine {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(com.xys.demo4.MyMonitor)")
    public void pointcut() {
    }

    // 定義 advise
    @Before("pointcut()")
    public void logMethodInvokeParam(JoinPoint joinPoint) {
        logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
    }
}
@Service
public class SomeService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    public void hello(String someParam) {
        logger.info("---SomeService: hello invoked, param: {}---", someParam);
        test();
    }

    @MyMonitor
    public void test() {
        logger.info("---SomeService: test invoked---");
    }
}
@EnableAspectJAutoProxy(proxyTargetClass = true)
@SpringBootAppliMyion
public class MyAopDemo {
    @Autowired
    SomeService someService;

    public static void main(String[] args) {
        SpringAppliMyion.run(MyAopDemo.class, args);
    }

    @PostConstruct
    public void aopTest() {
        someService.hello("abc");
    }
}

在這個例子中, 我們定義了一個註解 MyMonitor, 這個是一個方法註解, 我們的期望是當有此註解的方法被呼叫時, 需要執行指定的切面邏輯, 即執行 MyAopAdviseDefine.logMethodInvokeParam 方法.

在 SomeService 類中, 方法 test() 被 MyMonitor 所註解, 因此呼叫 test() 方法時, 應該會觸發 logMethodInvokeParam 方法的呼叫. 不過有一點我們需要注意到, 我們在 MyAopDemo 測試例子中, 並沒有直接呼叫 SomeService.test() 方法, 而是呼叫了 SomeService.hello() 方法, 在 hello 方法中, 呼叫了同一個類內部的 SomeService.test() 方法. 按理說, test() 方法被呼叫時, 會觸發 AOP 邏輯, 但是在這個例子中, 我們並沒有如願地看到 MyAopAdviseDefine.logMethodInvokeParam 方法的呼叫, 這是為什麼呢?

這是由於 Spring AOP (包括動態代理和 CGLIB 的 AOP) 的限制導致的. Spring AOP 並不是擴充套件了一個類(目標物件), 而是使用了一個代理物件來包裝目標物件, 並攔截目標物件的方法呼叫. 這樣的實現帶來的影響是: 在目標物件中呼叫自己類內部實現的方法時, 這些呼叫並不會轉發到代理物件中, 甚至代理物件都不知道有此呼叫的存在.

即考慮到上面的程式碼中, 我們在 MyAopDemo.aopTest() 中, 呼叫了 someService.hello("abc"), 這裡的 someService bean 其實是 Spring AOP 所自動例項化的一個代理物件, 當呼叫 hello() 方法時, 先進入到此代理物件的同名方法中, 然後在代理物件中執行 AOP 邏輯(因為 hello 方法並沒有注入 AOP 橫切邏輯, 因此呼叫它不會有額外的事情發生), 當代理物件中執行完畢橫切邏輯後, 才將呼叫請求轉發到目標物件的 hello() 方法上. 因此當程式碼執行到 hello() 方法內部時, 此時的 this 其實就不是代理物件了, 而是目標物件, 因此再呼叫 SomeService.test() 自然就沒有 AOP 效果了.

簡單來說, 在 MyAopDemo 中所看到的 someService 這個 bean 和在 SomeService.hello() 方法內部上下文中的 this 其實代表的不是同一個物件(可以通過分別列印兩者的 hashCode 以驗證), 前者是 Spring AOP 所生成的代理物件, 而後者才是真正的目標物件(SomeService 例項).

解決

弄懂了上面的分析, 那麼解決這個問題就十分簡單了. 既然 test() 方法呼叫沒有觸發 AOP 邏輯的原因是因為我們以目標物件的身份(target object) 來呼叫的, 那麼解決的關鍵自然就是以代理物件(proxied object)的身份來呼叫 test() 方法.
因此針對於上面的例子, 我們進行如下修改即可:

@Service
public class SomeService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private SomeService self;

    public void hello(String someParam) {
        logger.info("---SomeService: hello invoked, param: {}---", someParam);
        self.test();
    }

    @CatMonitor
    public void test() {
        logger.info("---SomeService: test invoked---");
    }
}

上面展示的程式碼中, 我們使用了一種很 subtle 的方式, 即將 SomeService bean 注入到 self 欄位中(這裡再次強調的是, SomeService bean 實際上是一個代理物件, 它和 this 引用所指向的物件並不是同一個物件), 因此我們在 hello 方法呼叫中, 使用 self.test() 的方式來呼叫 test() 方法, 這樣就會觸發 AOP 邏輯了.

Spring AOP 導致的 @Transactional 不生效的問題

這個問題同樣地會影響到 @Transactional 註解的使用, 因為 @Transactional 註解本質上也是由 AOP 所實現的.

例如我在 stackoverflow 上看到的一個類似的問題: Spring @Transaction method call by the method within the same class, does not work?
這裡也記錄下來以作參考.

那個哥們遇到的問題如下:

public class UserService {

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
            // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                    .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            addUser(user.getUserName, user.getPassword);
        }
    } 
}

他在 addUser 方法上使用 @Transactional 來使用事務功能, 然後他在外部服務中, 通過呼叫 addUsers 方法批量新增使用者. 經過了上面的分析後, 現在我們就可知道其實這裡新增註解是不會啟動事務功能的, 因為 AOP 邏輯整個都沒生效嘛.

解決這個問題的方法有兩個, 一個是使用 AspectJ 模式的事務實現:

<tx:annotation-driven mode="aspectj"/>

另一個就是和我們剛才在上面的例子中的解決方式一樣:

public class UserService {
    private UserService self;

    public void setSelf(UserService self) {
        this.self = self;
    }

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
        // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            self.addUser(user.getUserName, user.getPassword);
        }
    } 
}

相關文章