1.經典Spring AOP 類圖(主要針對com.springframework.aop包)
AOP Alliance(http://aopalliance.sourceforge.net/) 是個聯合的開源協作組織,在多個項目間進行協作以期提供一套标準的AOP Java接口
Spring AOP就是基于AOP Alliance标準API實作的,如果你打算使用Spring的AOP或基于AOP的任何特性,隻需這個JAR檔案
前置通知【BeforeAdvice】、後置通知【AfterAdvice】、異常通知【ThrowAdvice】都繼承自标準接口 Advice
而環繞通知則直接使用AOP Alliance提供的标準接口【MethodInterceptor】
2.經典Spring AOP使用方式
本質上是基于JDK的動态代理實作的面向切面程式設計,按照JDK動态代理的要求:必須基于接口生成目标對象
(1)定義一個接口 ICalculate,包含幾個簡單方法
public interface ICalculate {
int add(int a, int b);
int sub(int a, int b);
}
(2)定義一個實作類CalculateImpl
public class CalculateImpl implements ICalculate {
/*
* (non-Javadoc)
*
* @see com.yli.sample.spring.exam.ICalculate#add(int, int)
*/
@Override
public int add(int a, int b) {
return a + b;
}
/*
* (non-Javadoc)
*
* @see com.yli.sample.spring.exam.ICalculate#sub(int, int)
*/
@Override
public int sub(int a, int b) {
if (0 == b) {
throw new IllegalArgumentException("發生除0錯誤!");
}
return a / b;
}
}
為什麼要稱為經典的Spring SOP使用方式呢,因為和自行實作JDK的動态代理過程是類似的,屬于基礎程式設計,不能算AOP架構的進階程式設計方式
======================================那麼先介紹自行實作JDK的動态代理==========================================
再定義一個類實作JDK提供的InvocationHandler接口,為委托類【即CalculateImpl】生成代理對象
public class CalculateInvocationHandler implements InvocationHandler {
// 委托對象
private Object target;
public CalculateInvocationHandler(Object target) {
this.target = target;
}
// 為委托對象生成代理對象
public Object newProxyInstance() throws IllegalArgumentException {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass()
.getInterfaces(), this);
}
// 執行委托方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("========開始執行方法========");
System.out.println("方法名稱: " + method.getName());
System.out.println("參數值: " + Arrays.toString(args));
Object result = method.invoke(target, args);
System.out.println("========結束方法執行========");
return result;
}
}
然後使用 main 方法測試
public static void main(String[] args) {
ICalculate ical = new CalculateImpl();
CalculateInvocationHandler handler = new CalculateInvocationHandler(ical);
ICalculate icalProxy = (ICalculate)handler.newProxyInstance();
icalProxy.add(5, 3);
}
列印執行結果
========開始執行方法========
方法名稱: add
參數值: [5, 3]
========結束方法執行========
======================================繼續介紹經典的 Spring AOP 實作方式==========================================
(3)前置通知、後置通知、異常通知、環繞通知
/**
* Spring AOP架構-BeforeAdvice-前置通知
*/
public class CalculateLogBeforeAdvice implements MethodBeforeAdvice {
private static Logger log = LoggerFactory.getLogger(CalculateLogBeforeAdvice.class);
/**
* 實作before方法
* @param method 要執行的方法
* @param args 方法參數
* @param target 執行該方法的目标對象
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
// 列印方法名稱
log.info("the method " + method.getName() + "() begin with");
// 列印參數清單
int index = 0;
for (Object arg : args) {
log.info("param(" + (index++) + "):" + arg.toString());
}
}
}
/**
* 後置通知
*/
public class CalculateLogAfterAdvice implements AfterReturningAdvice {
private static Logger log = LoggerFactory.getLogger(CalculateLogAfterAdvice.class);
/**
* 實作afterReturning方法
* @param returnValue 方法執行傳回值
* @param method 要執行的方法
* @param args 方法參數
* @param target 執行該方法的目标對象
*/
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target)
throws Throwable {
log.info("the method " + method.getName() + "() ends with " + returnValue);
}
}
前置通知和後置通知,直接實作目标接口[MethodBeforeAdvice]和[AfterReturningAdvice]定義的方法
異常通知和環繞通知,其目标接口沒有定義方法(參考最上面的類圖也可以看出來),是以要自行實作方法
/**
* 異常通知,目标接口最終是繼承自AfterAdvice
* 而AfterAdvice是沒有定義方法的,是以目标類要實作自己的方法
*/
public class CalculateLogExceptionAdvice implements ThrowsAdvice {
private static Logger log = LoggerFactory.getLogger(CalculateLogExceptionAdvice.class);
/**
* 方法名稱使用afterThrowing
* 方法參數可以隻使用運作時異常
*/
public void afterThrowing(IllegalArgumentException exp)
throws Throwable {
log.error(exp.getMessage());
}
/**
* 方法簽名可以擴充,比如要列印其他資訊
* method,args,target要麼全部加上,要麼一個都不加
*/
// public void afterThrowing(Method method, Object[] args, Object target,
// IllegalArgumentException exp) throws Throwable {
// log.error(exp.getMessage());
//}
}
/**
* 環繞通知,表現更為直接
* 目标接口直接來自于AOP Alliance包,并非來自Spring架構
*/
public class CalculateLogAroundAdvice implements MethodInterceptor {
private static Logger log = LoggerFactory.getLogger(CalculateLogAroundAdvice.class);
/**
* 方法參數和[前置、後置、異常通知】不同
* 因為目标方法來自于AOP标準架構
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("Around:The method " + invocation.getMethod().getName() + "() begins with "
+ Arrays.toString(invocation.getArguments()));
try {
// 執行代理bean的業務方法
// 相當于 method.invoke()
// 必須要執行
Object result = invocation.proceed();
log.info("Around:The method " + invocation.getMethod().getName() + "() ends with " + result);
// 可以傳回任何值,但建議傳回執行的實際結果
return result;
} catch (IllegalArgumentException exp) {
log.error("Around:Illegal argument " + Arrays.toString(invocation.getArguments())
+ " for method " + invocation.getMethod().getName() + "()");
throw exp;
}
}
}
(4)配置bean
<?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:cxf="http://cxf.apache.org/core"
xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 業務bean -->
<bean id="calculate" class="com.yli.sample.spring.aop.CalculateImpl"></bean>
<!-- 前置通知:記錄業務日志 -->
<bean id="beforeLog" class="com.yli.sample.spring.aop.CalculateLogBeforeAdvice"></bean>
<!-- 後置通知:記錄業務日志 -->
<bean id="afterLog" class="com.yli.sample.spring.aop.CalculateLogAfterAdvice"></bean>
<!-- 異常通知:記錄業務日志 -->
<bean id="exceptionLog" class="com.yli.sample.spring.aop.CalculateLogExceptionAdvice"></bean>
<!-- 環繞通知:記錄業務日志 -->
<bean id="aroundLog" class="com.yli.sample.spring.aop.CalculateLogAroundAdvice"></bean>
<!-- 為業務Bean生成代理Bean -->
<bean id="calculateProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- JDK的動态代理 需要業務類必須實作一個接口,此處可以指定接口 -->
<property name="proxyInterfaces">
<list>
<value>com.yli.sample.spring.aop.ICalculate</value>
</list>
</property>
<!-- 如果不指定要代理的接口,spring會使用cgliab直接代理目标類 -->
<property name="target" ref="calculate"></property>
<!-- 定義代理Bean要使用的通知 -->
<property name="interceptorNames">
<list>
<value>beforeLog</value>
<value>afterLog</value>
<value>exceptionLog</value>
<value>aroundLog</value>
</list>
</property>
</bean>
</beans>
(5)業務接口、實作類、各種通知和bean配置完成了,可以main方法測試
public static void main(String[] args) {
String configLocation = "classpath:/conf/spring/aop-bean.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(configLocation);
// 注意擷取的是代理Bean,不是原生的業務Bean
// 使用經典的Spring AOP,隻有代理Bean才能觸發通知
ICalculate calculateProxy = (ICalculate) context.getBean("calculateProxy");
calculateProxy.sub(15, 5);
calculateProxy.sub(15, 0);
}
執行結果
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogBeforeAdvice - the method sub() begin with
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogBeforeAdvice - param(0):15
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogBeforeAdvice - param(1):5
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogAroundAdvice - Around:The method sub() begins with [15, 5]
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogAroundAdvice - Around:The method sub() ends with 3
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogAfterAdvice - the method sub() ends with 3
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogBeforeAdvice - the method sub() begin with
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogBeforeAdvice - param(0):15
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogBeforeAdvice - param(1):0
11:24:17.126 [main] INFO c.y.s.s.aop.CalculateLogAroundAdvice - Around:The method sub() begins with [15, 0]
Exception in thread "main" 11:24:17.126 [main] ERROR c.y.s.s.aop.CalculateLogAroundAdvice - Around:Illegal argument [15, 0] for method sub()
11:24:17.126 [main] DEBUG o.s.a.f.a.ThrowsAdviceInterceptor - Found handler for exception of type [java.lang.IllegalArgumentException]: public void com.yli.sample.spring.aop.CalculateLogExceptionAdvice.afterThrowing(java.lang.IllegalArgumentException) throws java.lang.Throwable
11:24:17.126 [main] ERROR c.y.s.s.a.CalculateLogExceptionAdvice - 發生除0錯誤!
(6)這種動态代理方式有兩個缺點
業務類的所有方法都被代理,不能根據規則比對方法;隻能使用代理Bean來執行方法才能觸發通知事件!
經典Spring AOP 提供多種方式滿足以上需求,可以【增強部分】,也可以【自動建立代理Bean】
<?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:cxf="http://cxf.apache.org/core"
xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 業務Bean -->
<bean id="autoCalculate" class="com.yli.sample.spring.aop.CalculateImpl"></bean>
<!-- 環繞通知 -->
<bean id="aroundLog" class="com.yli.sample.spring.aop.CalculateLogAroundAdvice"></bean>
<!-- 增強器 支援【部分增強】 -->
<bean id="pointAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<!-- 要增強的方法名稱 -->
<property name="mappedNames">
<list>
<value>add</value>
<value>sub</value>
</list>
</property>
<!-- 增強部分使用的【通知事件】 -->
<property name="advice" ref="aroundLog"></property>
</bean>
<!-- BeanNameAutoProxyCreator 支援【自動代理】 -->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<!-- 通配符比對哪些Bean需要自動建立代理 -->
<value>*Calculate*</value>
</list>
</property>
<property name="interceptorNames">
<list>
<!-- 指定【增強器】 -->
<value>pointAdvisor</value>
<!-- 其實此處可以直接指定【通知】 -->
<!-- <value>aroundLog</value> -->
</list>
</property>
</bean>
</beans>
main方法測試
public static void main(String[] args) {
String location = "classpath:conf/spring/aop2-bean.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(location);
// 使用Spring提供的自動建立代理
// 此處擷取的Bean就可以直接使用業務Bean了
ICalculate calculate1 = (ICalculate) context.getBean("autoCalculate");
calculate1.add(3, 5);
}
除了上面那種方式,Spring還提供一個預設的自動代理,隻需要指定一個【增強器】即可
Spring會自動檢查IOC容器中每個Bean,自動比對增強器
<?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:cxf="http://cxf.apache.org/core"
xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 業務Bean -->
<bean id="autoCalculate" class="com.yli.sample.spring.aop.CalculateImpl"></bean>
<!-- 環繞通知 -->
<bean id="aroundLog" class="com.yli.sample.spring.aop.CalculateLogAroundAdvice"></bean>
<!-- 例如指定一個增強器【正規表達式比對的增強器】
前面已經設定過【根據名稱比對的增強器】
-->
<bean id="regAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="patterns">
<list>
<value>.*sub.*</value>
</list>
</property>
<property name="advice" ref="aroundLog"></property>
</bean>
<!-- DefaultAdvisorAutoProxyCreator 也支援【自動代理】
Spring會檢查IOC中每個增強器與Bean,如果曾強器切入點與Bean相比對,則自動代理
-->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
</beans>