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) 方法實際上是代理物件 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