需求:假如,一個業務類有四個方法(加減),我們要在每個方法都加上日誌操作。
1、沒有AOP,實現日誌
1.1 定義接口
定義一個數字運算的接口,其中有兩個方法:加法運算和減法運算:
package com.spring.aop;
public interface MathCalculator {
public int add(int a,int b);
public int sub(int a,int b);
}
1.2 定義實現類
定義實現類,除了進行運算,還要打印簡單的日誌功能:
package com.spring.aop;
public class NonAopMathCalculator implements MathCalculator {
@Override
public int add(int a, int b) {
System.out.println("方法執行前參數是:"+a+","+b);
int result = a+b;
System.out.println("方法執行後結果是:"+result);
return result;
}
@Override
public int sub(int a, int b) {
System.out.println("方法執行前參數是:"+a+","+b);
int result = a-b;
System.out.println("方法執行後結果是:"+result);
return result;
}
}
1.3 測試
package com.spring.aop;
public class MathCalculatorTest {
public static void main(String[] args) {
MathCalculator mathCalculator = new NonAopMathCalculator();
int add = mathCalculator.add(9, 6);
System.out.println("加法計算結果:"+add);
int sub = mathCalculator.sub(9, 6);
System.out.println("減法計算結果:"+sub);
}
}
打印結果:
方法執行前參數是:9,6
方法執行後結果是:15
加法計算結果:15
方法執行前參數是:9,6
方法執行後結果是:3
減法計算結果:3
1.4 存在的問題
代碼混亂:越來越多的非業務需求(日誌和驗證等)加入後,原有的業務方法急劇膨脹。 每個方法在處理核心邏輯的同時還必須兼顧其他多個關注點。
代碼分散:以日誌需求爲例,只是爲了滿足這個單一需求, 就不得不在多個模塊(方法)裏多次重複相同的日誌代碼, 如果日誌需求發生變化, 必須修改所有模塊。
2、通過動態代理實現日誌
代理設計模式的原理: 使用一個代理將對象包裝起來,然後用該代理對象取代原始對象。任何對原始對象的調用都要通過代理, 代理對象決定是否以及何時將方法調用轉到原始對象上。這種方式可以解決上述存在的問題:
接口還是上面的,不用改變,現在我們使用動態代理的方式來實現,代碼如下:
2.1 創建接口實現
創建接口的實現類,實現類中只做業務相關的事情,日誌的事情不需要管:
package com.spring.aop;
public class MathCalculatorImpl implements MathCalculator {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public int sub(int a, int b) {
return a-b;
}
}
2.2 創建代理類
創代理類,代理類持有被代理對象的引用:
package com.spring.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class MathCalculatorProxyImpl {
//持有被代理對象的引用
private MathCalculatorImpl target;
//通過構造方法傳入被代理對象
public MathCalculatorProxyImpl(MathCalculatorImpl target) {
this.target = target;
}
//返回代理對象
public MathCalculator getLoggingProxy(){
MathCalculator proxy = null;
ClassLoader classLoader = target.getClass().getClassLoader();//類加載器
Class[] interfaces = new Class[]{MathCalculator.class};//被代理對象實現的接口的數組
//處理器對象,進行日誌操作,被代理對象的所有方法執行都要先經過該類的invoke方法
InvocationHandler handler = new InvocationHandler() {
/**
* proxy: 代理對象。 一般不使用該對象
* method: 正在被調用的方法
* args: 調用方法傳入的參數
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//打印日誌
System.out.println("[before] The method " + methodName + " begins with " + Arrays.asList(args));
//調用目標方法
Object result = null;//目錄方法返回值
try {
//前置通知
result = method.invoke(target, args);
//返回通知, 可以訪問到方法的返回值
} catch (NullPointerException e) {
e.printStackTrace();
//異常通知, 可以訪問到方法出現的異常
}
//後置通知. 因爲方法可以能會出異常, 所以訪問不到方法的返回值
//打印日誌
System.out.println("[after] The method ends with " + result);
return result;
}
};
/**
* classLoader: 代理對象使用的類加載器。
* interfaces: 指定代理對象的類型. 即代理代理對象中可以有哪些方法.
* handler: 當具體調用代理對象的方法時, 應該如何進行響應, 實際上就是調用 InvocationHandler 的 invoke 方法
*/
proxy = (MathCalculator) Proxy.newProxyInstance(classLoader,interfaces,handler);
return proxy;
}
}
2.3 測試
package com.spring.aop;
public class MathCalculatorProxyTest {
public static void main(String[] args) {
//創建被代理對象
MathCalculatorImpl mathCalculatorImpl = new MathCalculatorImpl();
//創建代理對象
MathCalculatorProxyImpl mathCalculatorProxy = new MathCalculatorProxyImpl(mathCalculatorImpl);
MathCalculator proxy = mathCalculatorProxy.getLoggingProxy();
int add = proxy.add(9, 6);
System.out.println("加法計算結果:"+add);
int sub = proxy.sub(9, 6);
System.out.println("減法計算結果:"+sub);
}
}
打印結果:
[before] The method add begins with [9, 6]
[after] The method ends with 15
加法計算結果:15
[before] The method sub begins with [9, 6]
[after] The method ends with 3
減法計算結果:3
2.4 總結
動態代理的方式可以實現日誌操作,這樣實現起來很統一,就算目標類中加了其他方法,不需要任何操作,就能實現日誌功能。但是如果每次這樣手動實現,也會比較麻煩,所以Spring給我們提供了AOP模塊。
3、AOP
3.1 AOP簡介
–業務模塊更簡潔,只包含核心業務代碼。
3.2 AOP術語
3.3 AOP的實現AspectJ使用
3.3.1 註解配置使用AspectJ實現AOP
首先要把jar包導入到工程中
1、創建接口和實現類,並在實現類加上Spring註解
package com.spring.aspectjAop.annotation;
public interface MathCalculator {
public int add(int a, int b);
public int sub(int a, int b);
}
package com.spring.aspectjAop.annotation;
import org.springframework.stereotype.Component;
@Component("mathCalculator")
public class MathCalculatorImpl implements MathCalculator {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public int sub(int a, int b) {
return a-b;
}
}
2、創建AspectJ代理類
package com.spring.aspectjAop.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component//首先切面也是spring的一個Bean
@Aspect//表明這是一個AspectJ切面
public class LoggingAspectJAop {
/**
* 標識這個方法是個前置通知, 切點表達式表示執行任意類的任意方法. 第
* 一個 * 代表匹配任意修飾符及任意返回值, 第二個 * 代表任意類的對象,
* 第三個 * 代表任意方法, 參數列表中的 .. 匹配任意數量的參數
*/
@Before("execution(* com.spring.aspectjAop.annotation.*.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("執行的方法是:"+methodName+",參數是:"+args);
}
}
3、配置文件applicationContext.xml
<?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:aop="http://www.springframework.org/schema/aop"
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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--配置包掃描-->
<context:component-scan base-package="com.spring.aspectjAop.annotation"/>
<!--開啓AspectJ的註解-->
<aop:aspectj-autoproxy/>
</beans>
4、測試
package com.spring.aspectjAop.annotation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
MathCalculator mathCalculator = (MathCalculator) applicationContext.getBean("mathCalculator");
int add = mathCalculator.add(9, 3);
System.out.println("加法結果:"+add);
int sub = mathCalculator.sub(9, 3);
System.out.println("減法結果:"+sub);
}
}
打印結果:
執行的方法是:add,參數是:[9, 3]
加法結果:12
執行的方法是:sub,參數是:[9, 3]
減法結果:6
4、總結
1、要使用AspectJ,就要先導入相關的jar包;
2、applicationContext.xml文件中,要加入aop名稱空間,啓用 AspectJ 註解支持, 只要在 Bean 配置文件中定義一個空的 XML 元素 <aop:aspectj-autoproxy>;Spring IOC 容器偵測到 Bean 配置文件中的 <aop:aspectj-autoproxy> 元素時, 會自動爲與 AspectJ 切面匹配的 Bean 創建代理。
3、切面類要使用註解@Components和@Aspect
4、定義方法,方法上使用註解表明這個方法的執行時機,有如下註解:
5、切面方法的註解中,要編寫AspectJ表達式,告訴該方法要監聽哪些方法;
6、如果要拿到目標方法的方法名稱和參數,就可以在方法形參中直接寫上:JoinPoint joinPoint
5、方法簽名編寫 AspectJ 切入點表達式
–execution public double ArithmeticCalculator.*(double, double): 匹配參數類型爲 double, double 類型的方法
在 AspectJ 中, 切入點表達式可以通過操作符 &&,||, ! 結合起來:
6、後置通知
在剛纔的LoggingAspectJAop加上後置通知 @After:
/**
* 後置通知是在連接點完成之後執行的, 即連接點返回結果或者拋出異常的時候, 下面的後置通知記錄了方法的終止
* @param joinPoint
*/
@After("execution(* com.spring.aspectjAop.annotation.*.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"執行完了");
}
7、返回通知
無論連接點是正常返回還是拋出異常, 後置通知都會執行。 如果只想在連接點返回的時候(方法正常結束)記錄日誌, 應使用返回通知代替後置通知。返回通知可以訪問方法的返回值。
返回通知中, 只要將 returning 屬性添加到 @AfterReturning 註解中,就可以訪問連接點的返回值,該屬性的值即爲用來傳入返回值的參數名稱;必須在通知方法的簽名中添加一個同名參數,在運行時,Spring AOP 會通過這個參數傳遞返回值;原始的切點表達式需要出現在 pointcut 屬性中
在剛纔的LoggingAspectJAop加上返回通知 @AfterReturning:
@AfterReturning(value = "execution(* com.spring.aspectjAop.annotation.*.*(..))",returning = "result")
public void afterRunningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的返回值是:"+result);
}
8、異常通知
只在連接點拋出異常時才執行異常通知;
將throwing 屬性添加到 @AfterThrowing 註解中,也可以訪問連接點拋出的異常。Throwable 是所有錯誤和異常類的超類, 所以在異常通知方法可以捕獲到任何錯誤和異常。
如果只對某種特殊的異常類型感興趣,可以將參數聲明爲其他異常的參數類型,然後通知就只在拋出這個類型及其子類的異常時才被執行。
/**
* 異常通知
*/
@AfterThrowing(value = "execution(* com.spring.aspectjAop.annotation.*.*(..))",throwing = "exception")
public void afterThrowableMethod(JoinPoint joinPoint, Exception exception){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的出現了異常,異常信息是:"+exception.getMessage());
}
9、環繞通知
環繞通知是所有通知類型中功能最爲強大的,能夠全面地控制連接點,甚至可以控制是否執行連接點;
對於環繞通知來說,連接點的參數類型必須是 ProceedingJoinPoint ,它是 JoinPoint 的子接口,允許控制何時執行,是否執行連接點;
在環繞通知中需要明確調用 ProceedingJoinPoint 的 proceed() 方法來執行被代理的方法, 如果忘記這樣做就會導致通知被執行了,但目標方法沒有被執行
注意:環繞通知的方法 需要返回目標方法執行之後的結果,即調用 joinPoint.proceed(); 的返回值,否則會出現空指針異常
/**
* 環繞通知需要攜帶 ProceedingJoinPoint 類型的參數.
* 環繞通知類似於動態代理的全過程: ProceedingJoinPoint 類型的參數可以決定是否執行目標方法.
* 且環繞通知必須有返回值, 返回值即爲目標方法的返回值
*/
@Around(value = "execution(* com.spring.aspectjAop.annotation.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;//定義返回值
String methodName = pjd.getSignature().getName();
List<Object> args = Arrays.asList(pjd.getArgs());
try {
//前置通知
System.out.println(methodName+"方法執行之前,參數是:"+args);
result = pjd.proceed();
//後置通知
System.out.println(methodName+"方法執行完成,結果是:"+result);
} catch (Throwable throwable) {
//異常通知
System.out.println(methodName+"方法執行出現了異常,異常信息:"+throwable.getMessage());
throw new RuntimeException(throwable);
}
//返回通知
System.out.println(methodName+"執行沒有發生異常,一切正常返回,返回結果是:"+result);
return result;
}
10、切面的優先級
11、重(chong)用切入點定義
1、在編寫AspectJ 切面時, 可以直接在通知註解中書寫切入點表達式,但同一個切點表達式可能會在多個通知中重複出現;
2、在AspectJ 切面中, 可以通過 @Pointcut 註解將一個切入點聲明成簡單的方法。切入點的方法體通常是空的, 因爲將切入點定義與應用程序邏輯混在一起是不合理的;
3、切入點方法的訪問控制符同時也控制着這個切入點的可見性,如果切入點要在多個切面中共用, 最好將它們集中在一個公共的類中,在這種情況下,它們必須被聲明爲 public;在引入這個切入點時,必須將類名也包括在內,如果類沒有與這個切面放在同一個包中,還必須包含包名;
4、其他通知可以通過方法名稱引入該切入點。
切面的完整代碼:
package com.spring.aspectjAop.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Order(0)//可以使用 @Order 註解指定切面的優先級, 值越小優先級越高
@Component//首先切面也是spring的一個Bean
@Aspect//表明這是一個AspectJ切面
public class LoggingAspectJAop {
/**
* 定義一個方法, 用於聲明切入點表達式. 一般地, 該方法中再不需要添入其他的代碼.
* 使用 @Pointcut 來聲明切入點表達式.
* 後面的其他通知直接使用方法名來引用當前的切入點表達式.
*/
@Pointcut("execution(* com.spring.aspectjAop.annotation.*.*(..))")
public void JoinPointCut(){}
/**
* 標識這個方法是個前置通知, 切點表達式表示執行任意類的任意方法. 第
* 一個 * 代表匹配任意修飾符及任意返回值, 第二個 * 代表任意類的對象,
* 第三個 * 代表任意方法, 參數列表中的 .. 匹配任意數量的參數
*/
@Before("JoinPointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("執行的方法是:"+methodName+",參數是:"+args);
}
/**
* 後置通知是在連接點完成之後執行的, 即連接點返回結果或者拋出異常的時候, 下面的後置通知記錄了方法的終止
* @param joinPoint
*/
@After("JoinPointCut()")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"執行完了");
}
@AfterReturning(pointcut = "JoinPointCut()",returning = "result")
public void afterRunningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的返回值是:"+result);
}
/**
* 異常通知
*/
@AfterThrowing(pointcut = "JoinPointCut()",throwing = "exception")
public void afterThrowableMethod(JoinPoint joinPoint, Exception exception){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的出現了異常,異常信息是:"+exception.getMessage());
}
/**
* 環繞通知需要攜帶 ProceedingJoinPoint 類型的參數.
* 環繞通知類似於動態代理的全過程: ProceedingJoinPoint 類型的參數可以決定是否執行目標方法.
* 且環繞通知必須有返回值, 返回值即爲目標方法的返回值
*/
@Around(value = "JoinPointCut()")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;//定義返回值
String methodName = pjd.getSignature().getName();
List<Object> args = Arrays.asList(pjd.getArgs());
try {
//前置通知
System.out.println(methodName+"方法執行之前,參數是:"+args);
result = pjd.proceed();
//後置通知
System.out.println(methodName+"方法執行完成,結果是:"+result);
} catch (Throwable throwable) {
//異常通知
System.out.println(methodName+"方法執行出現了異常,異常信息:"+throwable.getMessage());
throw new RuntimeException(throwable);
}
//返回通知
System.out.println(methodName+"執行沒有發生異常,一切正常返回,返回結果是:"+result);
return result;
}
}
12、引入通知
引入通知是一種特殊的通知類型. 它通過爲接口提供實現類, 允許對象動態地實現接口, 就像對象已經在運行時擴展了實現類一樣。
引入通知示例代碼:
3.3.2 基於xml配置文件使用AspectJ實現AOP
1、業務接口和業務實現類
package com.spring.aspectAop.xml;
public interface MathCalculator {
public int add(int a, int b);
public int sub(int a, int b);
}
package com.spring.aspectAop.xml;
import org.springframework.stereotype.Component;
public class MathCalculatorImpl implements MathCalculator {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public int sub(int a, int b) {
return a-b;
}
}
2、切面類
package com.spring.aspectAop.xml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
public class LoggingAspectJAop {
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("執行的方法是:"+methodName+",參數是:"+args);
}
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"執行完了");
}
public void afterRunningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的返回值是:"+result);
}
public void afterThrowableMethod(JoinPoint joinPoint, Exception exception){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName+"方法的出現了異常,異常信息是:"+exception.getMessage());
}
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;//定義返回值
String methodName = pjd.getSignature().getName();
List<Object> args = Arrays.asList(pjd.getArgs());
try {
//前置通知
System.out.println(methodName+"方法執行之前,參數是:"+args);
result = pjd.proceed();
//後置通知
System.out.println(methodName+"方法執行完成,結果是:"+result);
} catch (Throwable throwable) {
//異常通知
System.out.println(methodName+"方法執行出現了異常,異常信息:"+throwable.getMessage());
throw new RuntimeException(throwable);
}
//返回通知
System.out.println(methodName+"執行沒有發生異常,一切正常返回,返回結果是:"+result);
return result;
}
}
3、配置文件
<?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:aop="http://www.springframework.org/schema/aop"
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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--配置業務類Bean-->
<bean id="mathCalculator" class="com.spring.aspectAop.xml.MathCalculatorImpl"/>
<!--配置切面的 bean-->
<bean id="loggingAspectJAop" class="com.spring.aspectAop.xml.LoggingAspectJAop"/>
<!--配置AOP-->
<aop:config>
<!--配置切點表達式-->
<aop:pointcut id="joinPointCut" expression="execution(* com.spring.aspectAop.xml.*.*(..))"/>
<!--配置切面及通知-->
<aop:aspect ref="loggingAspectJAop" order="0">
<!--前置通知-->
<aop:before method="beforeMethod" pointcut-ref="joinPointCut"/>
<!--後置通知-->
<aop:after method="afterMethod" pointcut-ref="joinPointCut"/>
<!--異常通知-->
<aop:after-throwing method="afterThrowableMethod" pointcut-ref="joinPointCut" throwing="exception"/>
<!--返回通知-->
<aop:after-returning method="afterRunningMethod" pointcut-ref="joinPointCut" returning="result"/>
<!-- 環繞通知
<aop:around method="aroundMethod" pointcut-ref="joinPointCut"/>
-->
</aop:aspect>
</aop:config>
</beans>
4、測試
package com.spring.aspectAop.xml;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext-xml.xml");
MathCalculator mathCalculator = (MathCalculator) applicationContext.getBean("mathCalculator");
int add = mathCalculator.add(9, 3);
System.out.println("加法結果:"+add);
int sub = mathCalculator.sub(9, 3);
System.out.println("減法結果:"+sub);
}
}