轉(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);
}
}
}