Spring AOP中自我呼叫的問題
前幾天在做專案的時候同事說,在使用AOP進行攔截的時候發現有些方法有時候能輸出攔截的日誌有時候不輸出攔截的日誌。發現在單獨呼叫這些方法的時候是有日誌輸出,在被同一個類中的方法呼叫的時候沒有日誌輸出。我記得之前看過一篇文章是講Spring事務自我呼叫不起作用的問題,應該是同樣的問題(如果要觀看那篇文章請點選這裡http://jinnianshilongnian.iteye.com/blog/1487235)。這裡先說一下AOP攔截不到自我呼叫方法的原因:假設我們有一個類是ServiceA,這個類中有一個A方法,A方法中又呼叫了B方法。當我們使用AOP進行攔截的時候,首先會建立一個ServiceA的代理類,其實在我們的系統中是存在兩個ServiceA的物件的,一個是目標ServiceA物件,一個是生成的代理ServiceA物件,如果在代理類的A方法中呼叫代理類的B方法,這個時候AOP攔截是可以生效的,但是如果在代理類的A方法中呼叫目標類的B方法,這個時候AOP攔截是不生效的,大多數情況下我們都是在代理類的A方法中直接呼叫目標類的B方法
先前準備:
Service類:
package com.zkn.spring.miscellaneous.service;
/**
* 自我呼叫的服務類
* @author zkn
*/
public interface SelfCallService {
/**
* 方法A
*/
void selfCallA();
/**
* 方法B
*/
void selfCallB();
}
Service的實現類:
package com.zkn.spring.miscellaneous.service.impl; import com.zkn.spring.miscellaneous.service.SelfCallService; import org.springframework.aop.support.AopUtils; import org.springframework.stereotype.Service; /** * @author zkn */ @Service public class SelfCallServiceImpl implements SelfCallService{ /** * 方法A */ public void selfCallA() { System.out.println("我是方法A"); System.out.println("是否是AOP攔截:" + AopUtils.isAopProxy(this)); this.selfCallB(); } /** * 方法B */ public void selfCallB() { System.out.println("我是方法B"); } }
AOP配置類:
package com.zkn.spring.miscellaneous.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; /** * @author zkn */ @Component @Aspect public class SelfCallAOP { @Pointcut("execution(* com.zkn.spring.miscellaneous.service.SelfCallService.*(..))") public void pointCut(){ } @Around("pointCut()") public void aroundAdvice(ProceedingJoinPoint pjp){ //獲取簽名的資訊 Signature signature = pjp.getSignature(); System.out.println("被攔截的方法名為:"+signature.getName()); try { pjp.proceed(); System.out.println("方法執行完成:"+signature.getName()); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
Controller類:
package com.zkn.spring.miscellaneous.controller;
import com.zkn.spring.miscellaneous.service.SelfCallService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
* @author zkn
* @date 2017/05/16
*/
@RestController
public class ProcessSelfCallController {
@Autowired
private SelfCallService selfCallService;
@RequestMapping("processSelfCallA")
public String processSelfCallA() {
selfCallService.selfCallA();
return "處理自我呼叫!";
}
@RequestMapping("processSelfCallB")
public String processSelfCallB() {
selfCallService.selfCallB();
return "直接呼叫方法B!";
}
}
Spring的配置檔案:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd"
default-autowire="byName">
<!--開啟註解-->
<context:annotation-config/>
<!--掃描基礎包 這裡要注意的是如果SpringMVC和Spring掃描的包是一樣的話,AOP的配置可能會失效-->
<context:component-scan base-package="com.zkn.spring.miscellaneous.service"/>
<context:component-scan base-package="com.zkn.spring.miscellaneous.aop"/>
<!--配置AOP proxy-target-class為true的時候是用Cglib動態代理,false的時候啟用JDK動態代理-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
SpringMVC的配置檔案:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd"
default-autowire="byName">
<!--請求解析器-->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=utf-8</value>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
</list>
</property>
</bean>
<!--先開啟MVC的註解掃描-->
<mvc:annotation-driven/>
<!--開啟註解掃描-->
<context:component-scan base-package="com.zkn.spring.miscellaneous.controller"/>
</beans>
這裡需要注意的是:網上有很多人說自己在SpringMVC配置的AOP不起作用。原因是在SpringMVC的配置檔案中開啟自動掃描的包和在Spring的配置檔案中開啟自動掃描的包一樣,而SpringMVC的自動掃描覆蓋了Spring的自動掃描(子父容器)。所以這裡最好SpringMVC只掃描Controller這一層的包,其他的包交給Spring來掃描。OK我們的準備動作已經完成了下面看我們的解決辦法:
通過ThreadLocal暴露代理物件
第一種解決方式是通過ThreadLocal暴露代理物件的方式。我們只需要做這兩步就行了。第一步:Spring的配置檔案中將<aop:aspectj-autoproxy proxy-target-class="true" />改為: <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
第二步:修改Service的實現類,修改為如下:@Service
public class SelfCallServiceImpl implements SelfCallService {
/**
* 方法A
*/
public void selfCallA() {
//通過暴露ThreadLocal的方式獲取代理物件
((SelfCallService)AopContext.currentProxy()).selfCallB();
}
/**
* 方法B
*/
public void selfCallB() {
System.out.println("我是方法B");
}
}
下面我們訪問以下看看效果如何:http://localhost:8080/processSelfCallA,結果如下所示:從上圖中可以看到selfCallA和selfCallB兩個方法都被攔截到了,說明我們的配置生效了。
通過初始化方法的方式:
如果我們使用這一種方式的話,那麼我們需要做的是需要注入ApplicationContext物件,然後從ApplicationContext物件中獲取被代理的類。注意:配置檔案不做變動。具體做法如下:
@Service
public class SelfCallServiceImpl implements SelfCallService{
//注入ApplicationContext物件
@Autowired
//(1)
private ApplicationContext applicationContext;
//(2)
private SelfCallService selfCallService;
//(3)
//在所有屬性被設定完值之後被呼叫(在Spring容器的宣告週期中也只會被呼叫一次)
//也可以通過實現InitializingBean介面,實現afterPropertiesSet方法 如果是使用XML配置的話,也可以通過指定init-method的方式
//執行順序PostConstruct-->afterPropertiesSet-->init-method
@PostConstruct
public void setSelfCallService(){
selfCallService = applicationContext.getBean(SelfCallServiceImpl.class);
}
/**
* 方法A
*/
public void selfCallA() {
//第二種方式 從上下文中獲取被代理的物件 標號為(1)、(2)、(3)、(4)的就是第二種實現自我呼叫的方式
//這種方式的缺點是:不能解決scope為prototype的bean。
//(4)
selfCallService.selfCallB();
}
/**
* 方法B
*/
public void selfCallB() {
System.out.println("我是方法B");
}
}
在指定初始化方法這裡我們使用了註解的方式,即指定了@PostConstruct這個註解,注意這個註解是JDK提供的,不是Spring提供的。PS:指定初始化方法我們最少有這樣三種方式可以達到這樣的效果。一:使用@PostConstruct註解;二:實現InitializingBean介面,實現afterPropertiesSet方法(不推薦,對程式碼的侵入性較強);三:通過xml配置檔案指定init-method的方式。這部分的內容屬於Spring Bean生命週期的範圍,會在下一篇文章中詳細介紹。效果和上面使用ThreadLocal暴露代理物件是一樣的。
通過BeanPostProcessor的方式:
這一種方式需要我們定義一個介面,這個介面用來區分和設定被代理物件。具體的做法如下:
1、定義一個專門用來處理自我呼叫的Service:
public interface SelfCallWrapperService {
/**
* 設定自我呼叫的物件
* @param obj
*/
void setSelfObj(Object obj);
}
2、定義一個類用實現BeanPostProcessor介面:
public class BeanPostProcessorSelfCall implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String s) throws BeansException {
return bean;
}
public Object postProcessAfterInitialization(Object bean, String s) throws BeansException {
if (bean instanceof SelfCallWrapperService) {
((SelfCallWrapperService)bean).setSelfObj(bean);
}
return bean;
}
}
3、讓自我呼叫的Service實現1中的介面:
@Service
public class SelfCallServiceImpl implements SelfCallService, SelfCallWrapperService {
private SelfCallService selfCall;
/**
* 方法A
*/
public void selfCallA() {
selfCall.selfCallB();
}
/**
* 方法B
*/
public void selfCallB() {
System.out.println("我是方法B");
}
/**
* 設定自我呼叫的物件
*
* @param obj
*/
public void setSelfObj(Object obj) {
selfCall = (SelfCallService)obj;
}
}
BeanPostProcessor也是Spring Bean生命週期中的內容。同樣在下一章會做介紹。它的效果也是和是用ThreadLocal的方式一樣的。
這三種方式各有特點:使用ThreadLocal的方式最為簡單,並且各種情況都能適用;使用初始化方法的方式不能解決Bean為prototype的情景(或許配合lookup-method可能解決這個問題),對於迴圈依賴的支援也可能會有問題;使用BeanPostProcessor的方式對迴圈依賴注入的支援會有問題。不過我們在專案中配置迴圈依賴的情況可能會比較少一些,Bean配置為prototype的情景可能會更少(我只在電票的專案中用過。。。。。)。正常情況下這三種方式我們都是可以正常使用的。
參考: