在同一個類中調(diào)用另一個方法沒有觸發(fā) Spring AOP 的問題

轉(zhuǎn) https://segmentfault.com/a/1190000008379179

起因

考慮如下一個例子:

@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documentedpublic 
@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, 這個是一個方法注解, 我們的期望是當(dāng)有此注解的方法被調(diào)用時, 需要執(zhí)行指定的切面邏輯, 即執(zhí)行 MyAopAdviseDefine.logMethodInvokeParam 方法.

在 SomeService 類中, 方法 test() 被 MyMonitor 所注解, 因此調(diào)用 test() 方法時, 應(yīng)該會觸發(fā) logMethodInvokeParam 方法的調(diào)用. 不過有一點我們需要注意到, 我們在 MyAopDemo 測試?yán)又? 并沒有直接調(diào)用 SomeService.test() 方法, 而是調(diào)用了 SomeService.hello() 方法, 在 hello 方法中, 調(diào)用了同一個類內(nèi)部的 SomeService.test() 方法. 按理說, test() 方法被調(diào)用時, 會觸發(fā) AOP 邏輯, 但是在這個例子中, 我們并沒有如愿地看到 MyAopAdviseDefine.logMethodInvokeParam 方法的調(diào)用, 這是為什么呢?

這是由于 Spring AOP (包括動態(tài)代理和 CGLIB 的 AOP) 的限制導(dǎo)致的. Spring AOP 并不是擴(kuò)展了一個類(目標(biāo)對象), 而是使用了一個代理對象來包裝目標(biāo)對象, 并攔截目標(biāo)對象的方法調(diào)用. 這樣的實現(xiàn)帶來的影響是: 在目標(biāo)對象中調(diào)用自己類內(nèi)部實現(xiàn)的方法時, 這些調(diào)用并不會轉(zhuǎn)發(fā)到代理對象中, 甚至代理對象都不知道有此調(diào)用的存在.

即考慮到上面的代碼中, 我們在 MyAopDemo.aopTest() 中, 調(diào)用了 someService.hello("abc"), 這里的 someService bean 其實是 Spring AOP 所自動實例化的一個代理對象, 當(dāng)調(diào)用 hello() 方法時, 先進(jìn)入到此代理對象的同名方法中, 然后在代理對象中執(zhí)行 AOP 邏輯(因為 hello 方法并沒有注入 AOP 橫切邏輯, 因此調(diào)用它不會有額外的事情發(fā)生), 當(dāng)代理對象中執(zhí)行完畢橫切邏輯后, 才將調(diào)用請求轉(zhuǎn)發(fā)到目標(biāo)對象的 hello() 方法上. 因此當(dāng)代碼執(zhí)行到 hello() 方法內(nèi)部時, 此時的 this 其實就不是代理對象了, 而是目標(biāo)對象, 因此再調(diào)用 SomeService.test() 自然就沒有 AOP 效果了.

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

解決

弄懂了上面的分析, 那么解決這個問題就十分簡單了. 既然 test() 方法調(diào)用沒有觸發(fā) AOP 邏輯的原因是因為我們以目標(biāo)對象的身份(target object) 來調(diào)用的, 那么解決的關(guān)鍵自然就是以代理對象(proxied object)的身份來調(diào)用 test() 方法.
因此針對于上面的例子, 我們進(jìn)行如下修改即可:

@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 字段中(這里再次強(qiáng)調(diào)的是, SomeService bean 實際上是一個代理對象, 它和 this 引用所指向的對象并不是同一個對象), 因此我們在 hello 方法調(diào)用中, 使用 self.test() 的方式來調(diào)用 test() 方法, 這樣就會觸發(fā) AOP 邏輯了.

Spring AOP 導(dǎo)致的 @Transactional 不生效的問題

這個問題同樣地會影響到 @Transactional 注解的使用, 因為 @Transactional 注解本質(zhì)上也是由 AOP 所實現(xiàn)的.

例如我在 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 來使用事務(wù)功能, 然后他在外部服務(wù)中, 通過調(diào)用 addUsers 方法批量添加用戶. 經(jīng)過了上面的分析后, 現(xiàn)在我們就可知道其實這里添加注解是不會啟動事務(wù)功能的, 因為 AOP 邏輯整個都沒生效嘛.

解決這個問題的方法有兩個, 一個是使用 AspectJ 模式的事務(wù)實現(xiàn):

<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);
        }
    }
 }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末匪凉,一起剝皮案震驚了整個濱河市宏粤,隨后出現(xiàn)的幾起案子跟伏,更是在濱河造成了極大的恐慌,老刑警劉巖销钝,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異琐簇,居然都是意外死亡蒸健,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門婉商,熙熙樓的掌柜王于貴愁眉苦臉地迎上來似忧,“玉大人,你說我怎么就攤上這事丈秩《疲” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵蘑秽,是天一觀的道長饺著。 經(jīng)常有香客問我箫攀,道長,這世上最難降的妖魔是什么幼衰? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任靴跛,我火速辦了婚禮,結(jié)果婚禮上渡嚣,老公的妹妹穿的比我還像新娘汤求。我一直安慰自己,他們只是感情好严拒,可當(dāng)我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布扬绪。 她就那樣靜靜地躺著,像睡著了一般裤唠。 火紅的嫁衣襯著肌膚如雪挤牛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天种蘸,我揣著相機(jī)與錄音墓赴,去河邊找鬼。 笑死航瞭,一個胖子當(dāng)著我的面吹牛诫硕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刊侯,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼章办,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了滨彻?” 一聲冷哼從身側(cè)響起藕届,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎亭饵,沒想到半個月后休偶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡辜羊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年踏兜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片八秃。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡碱妆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出喜德,到底是詐尸還是另有隱情山橄,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站航棱,受9級特大地震影響睡雇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜饮醇,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一它抱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧朴艰,春花似錦观蓄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至毁嗦,卻和暖如春亲茅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狗准。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工克锣, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腔长。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓袭祟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親捞附。 傳聞我的和親對象是個殘疾皇子巾乳,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容