Spring Aop的應用
AOP的基本概念
-
連線點( Jointpoint) : 表示需要在程式中插入橫切關注點的擴充套件點,連線點可能是類初始化、方法執行、 方法呼叫、欄位呼叫或處理異常等等, Spring 只支援方法執行連線點, 在 AOP 中表示為“在哪裡幹” ;
-
切入點( Pointcut) : 選擇一組相關連線點的模式, 即可以認為連線點的集合,Spring 支援 perl5 正則表示式和 AspectJ 切入點模式, Spring 預設使用 AspectJ 語法, 在 AOP 中表示為“在哪裡乾的集合” ;
-
通知( Advice) : 在連線點上執行的行為, 通知提供了在 AOP 中需要在切入點所選擇的連線點處進行擴充套件現有行為的手段; 包括前置通知( before advice)、後置通知(after advice)、環繞通知( around advice), 在 Spring 中通過代理模式實現AOP,並通過攔截器模式以環繞連線點的攔截器鏈織入通知; 在 AOP 中表示為“幹什麼”;
-
方面/切面( Aspect): 橫切關注點的模組化,可以認為是通知、引入和切入點的組合; 在 Spring 中可以使用 Schema 和@AspectJ 方式進行組織實現; 在 AOP 中表示為“在哪乾和幹什麼集合”
-
引入( inter-type declaration) : 也稱為內部型別宣告, 為已有的類新增額外新的欄位或方法, Spring 允許引入新的介面(必須對應一個實現)到所有被代理物件(目標物件) , 在 AOP 中表示為“幹什麼(引入什麼) ” ;
-
目標物件( Target Object) : 需要被織入橫切關注點的物件,即該物件是切入點選擇的物件,需要被通知的物件,從而也可稱為“被通知物件”;由於 Spring AOP通過代理模式實現,從而這個物件永遠是被代理物件, 在 AOP 中表示為“對誰幹” ;
-
AOP 代理( AOP Proxy) : AOP 框架使用代理模式建立的物件,從而實現在連線點處插入通知(即應用切面) ,就是通過代理來對目標物件應用切面。在 Spring中, AOP 代理可以用 JDK 動態代理或 CGLIB 代理實現,而通過攔截器模型應用切面。
-
織入( Weaving) : 織入是一個過程,是將切面應用到目標物件從而創建出 AOP代理物件的過程, 織入可以在編譯期、類裝載期、執行期進行。
Spring有哪些通知型別呢?
-
前置通知( Before Advice) :在切入點選擇的連線點處的方法之前執行的通知,該通知不影響正常程式執行流程(除非該通知丟擲異常,該異常將中斷當前方法鏈的執行而返回)。
-
後置通知( After Advice) : 在切入點選擇的連線點處的方法之後執行的通知,包括如下型別的後置通知:
-
後置返回通知( After returning Advice) :在切入點選擇的連線點處的方法正常執行完畢時執行的通知, 必須是連線點處的方法沒丟擲任何異常正常返回時才呼叫後置通知。
-
後置異常通知( After throwing Advice) : 在切入點選擇的連線點處的方法丟擲異常返回時執行的通知, 必須是連線點處的方法丟擲任何異常返回時才呼叫異常通知。
-
後置最終通知( After finally Advice) : 在切入點選擇的連線點處的方法返回時執行的通知,不管拋沒丟擲異常都執行,類似於 Java 中的 finally 塊。
-
-
環繞通知( Around Advices): 環繞著在切入點選擇的連線點處的方法所執行的通知,環繞通知可以在方法呼叫之前和之後自定義任何行為,並且可以決定是否執行連線點處的方法、替換返回值、丟擲異常等等。
在 AOP 中,通過切入點選擇目標物件的連線點,然後在目標物件的相應連線點處織入通知,而切入點和通知就是切面(橫切關注點),而在目標物件連線點處應用切面的實現方式是通過 AOP 代理物件。
基於註解的AOP程式設計
基於註解的程式設計,需要依賴AspectJ框架(java中最流行的aop框架)。 第一步:匯入AspectJ的jar包,該框架只有Spring 2.0以上才支援。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.7.3</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.3</version>
<scope>runtime</scope>
</dependency>
第三步:切面類,該類有什麼特點?首先它必須是IOC的bean,還要宣告它是AspectJ切面,最後還可以定義切面的優先順序Order(非必填)
通知有五種註解 @Before :前置通知的註解,在目標方法執行前呼叫
@After:後置通知的註解, 在目標方法執行後呼叫,即使程式丟擲異常都會呼叫
@AfterReturning:返回通知的註解, 在目標方法成功執行後呼叫,如果程式出錯則不會呼叫
@AfterThrowing:異常通知的註解, 在目標方法出現指定異常時呼叫
@Around:環繞通知的註解,很強大(相當於前四個通知的組合),但用的不多。
環繞通知
import java.util.Arrays;
import java.util.List;
/**
* @program: ssm
* @description: 環繞通知
* @author: lee
* @create: 2019-03-14
**/
@Order(2)
@Aspect
@Component
public class AroundAspect {
/**
* 環繞通知,很強大,但用的不多。 用環繞通知測試Order的優先順序看的不明顯(這裡是筆者的失誤)
* 環繞通知需要用ProceedingJoinPoint 型別的引數
*/
@Around("execution(* com.*.aspect.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
Object result = null;
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
try {
System.out.println("@Around 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
result = joinPoint.proceed();
System.out.println("@Around 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
} catch (Throwable e) {
e.printStackTrace();
System.out.println("@Around 異常通知 : 方法名 【 " + methodName + " 】and exception is " + e);
}
System.out.println("@Around 後置通知 : 方法名 【 " + methodName + " 】and args are " + args);
return result;
}
}
攔截物件
package com.plantform.aspect;
import org.springframework.stereotype.Component;
/**
* @program: ssm
* @description: 切面類
* @author: lee
* @create: 2019-03-14
**/
public class AspectMethod {
public int add(int a, int b) {
System.out.println("add 方法執行了 ----> " + (a + b));
return (a + b);
}
public int division(int a, int b) {
System.out.println("division 方法執行了 ----> " + (a / b));
return (a / b);
}
}
攔截通知
package com.plantform.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* @program: ssm
* @Order(n) : 切面的優先順序,n越小,級別越高
* @Aspect:宣告該類是一個切面
* @Component:切面必須是 IOC 中的 bean
* @author: lee
* @create: 2019-03-14
**/
@Order(1)
@Aspect
@Component
public class LogAspect {
/**
* 前置通知的註解,在目標方法執行前呼叫
* execution最基礎的表示式語法。
* 注意點:
* 1. 方法裡面不能有行參,及add(int a, int b) 這是會報錯的。
* 2. int(方法的返回值),add(方法名) 可以用 * 抽象化。甚至可以將類名抽象,指定該包下的類。
* 3. (int, int) 可以用(..)代替,表示匹配任意數量的引數
* 4. 被通知的物件(Target),建議加上包的路徑
*/
@Before(value = "execution(* com.*.aspect.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
/**
* 連線點 joinPoint:add方法就是連線點
* getName獲取的是方法名,是英文的,可以通過國際化轉換對應的中文比較好。
*/
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("@Before 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
}
/**
* 後置通知的註解, 在目標方法執行後呼叫,即使是程式出錯都會呼叫
* 這裡將 方法的返回值 和 CalculatorImp類下所有的方法,以及方法的形參 都抽象了
*/
@After(value = "execution(* com.*.aspect.*.*(..))")
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("@After 後置通知 : 方法名 【 " + methodName + " 】and args are " + args);
}
/**
* 重用切入點定義:宣告切入點表示式。該方法裡面不建議新增其他程式碼
*/
@Pointcut("execution(* com.*.aspect.*.*(..))")
public void declareExecutionExpression(){}
/**
* 返回通知的註解, 在目標方法成功執行後呼叫,如果程式出錯則不會呼叫
* returning="result" 和 形參 result 保持一致
*/
@AfterReturning(value="declareExecutionExpression()", returning="result")
public void afterRunningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("@AfterReturning 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
}
/**
* 異常通知的註解, 在目標方法出現指定異常時呼叫
* throwing="exception" 和 形參 exception 保持一致 , 且目標方法出了Exception(可以是其他異常)異常才會呼叫。
*/
@AfterThrowing(value="declareExecutionExpression()", throwing="exception")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception exception) {
String methodName = joinPoint.getSignature().getName();
System.out.println("@AfterThrowing 異常通知 : 方法名 【 " + methodName + " 】and exception is " + exception);
}
}
執行方法
/**
* @program: ssm
* @description:
* @author: lee
* @create: 2019-03-14
**/
@Controller
public class MainAspect {
@RequestMapping("/static/index")
@ResponseBody
public String main(String[] args) {
AspectMethod aspectMethod=new AspectMethod();
aspectMethod.add(11, 12);
aspectMethod.division(21, 3);
//aspectMethod.division(21, 0);
return "執行完了";
}
}
輸出結果
@Before 前置通知 : 方法名 【 main 】and args are [null]
@Around 前置通知 : 方法名 【 main 】and args are [null]
add 方法執行了 ----> 23
division 方法執行了 ----> 7
@Around 返回通知 : 方法名 【 main 】and args are [null] , result is 執行成功
@Around 後置通知 : 方法名 【 main 】and args are [null]
@After 後置通知 : 方法名 【 main 】and args are [null]
@AfterReturning 返回通知 : 方法名 【 main 】and args are [null] , result is 執行成功
基於xml的AOP程式設計
第一步:核心檔案applicationContext.xml, 首先是配置三個bean,方便是兩個切面類,和一個方法類。 然後配置AOP, aop:config:註明開始配置AOP
aop:pointcut:配置切點重用表示式,expression的值是具體的表示式,id 該aop:pointcut的唯一標識,
aop:aspect:配置切面,ref的值引用相關切面類的bean,order設定優先順序(也可以不設定)。
五種通知的配置:aop:before,aop:after,aop:after-returning,aop:after-throwing,aop:around。method的值就是對應的方法,poincut-ref的值要引用 aop:pointcut 的id。其中有兩個比較特殊:aop:after-returning 要多配置一個returning,其中returning的值要和對應方法的形參保持一致。同理aop:after-throwing 也要多配置一個throwing,其中throwing的值也要和對應方法的形參保持一致。不然執行程式會報錯。
<?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 id="calculator" class="com.atguigu.spring.my.xml.CalculatorImp"></bean>
<bean id="loggerAspect" class="com.atguigu.spring.my.xml.LoggerAspect"></bean>
<bean id="aroundAspect" class="com.atguigu.spring.my.xml.AroundAspect"></bean>
<!-- AOP配置 -->
<aop:config>
<!-- 配置切點表示式 類似註解的重用表示式-->
<aop:pointcut expression="execution(* com.atguigu.spring.my.xml.CalculatorImp.*(..))"
id="pointcut"/>
<!-- 配置切面及通知 method的值就是 loggerAspect類中的值-->
<aop:aspect ref="loggerAspect" order="2">
<aop:before method="beforeAdvice" pointcut-ref="pointcut"/>
<aop:after method="afterAdvice" pointcut-ref="pointcut"/>
<aop:after-returning method="afterRunningAdvice" pointcut-ref="pointcut" returning="result"/>
<aop:after-throwing method="afterThrowingAdvice" pointcut-ref="pointcut" throwing="exception"/>
</aop:aspect>
<aop:aspect ref="aroundAspect" order="1">
<!-- <aop:around method="aroundAdvice" pointcut-ref="pointcut"/> -->
</aop:aspect>
</aop:config>
</beans>
下面幾個類,就是脫去了所有註解的外衣,採用通過配置的xml,實現AOP程式設計。
public interface Calculator {
public int add(int a, int b);
public int division(int a, int b);
}
public class CalculatorImp implements Calculator {
@Override
public int add(int a, int b) {
System.out.println("add 方法執行了 ----> " + (a + b));
return (a + b);
}
@Override
public int division(int a, int b) {
System.out.println("division 方法執行了 ----> " + (a / b));
return (a / b);
}
}
import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.JoinPoint;
public class LoggerAspect {
public void beforeAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("Before 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
}
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("After 後置通知 : 方法名 【 " + methodName + " 】and args are " + args);
}
public void afterRunningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("AfterReturning 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
}
public void afterThrowingAdvice(JoinPoint joinPoint, Exception exception) {
String methodName = joinPoint.getSignature().getName();
System.out.println("AfterThrowing 異常通知 : 方法名 【 " + methodName + " 】and exception is " + exception);
}
}
import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.ProceedingJoinPoint;
public class AroundAspect {
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
Object result = null;
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
try {
System.out.println("@Around 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
result = joinPoint.proceed();
System.out.println("@Around 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
} catch (Throwable e) {
e.printStackTrace();
System.out.println("@Around 異常通知 : 方法名 【 " + methodName + " 】and exception is " + e);
}
System.out.println("@Around 後置通知 : 方法名 【 " + methodName + " 】and args are " + args);
return result;
}
}
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Calculator calculator = (Calculator) ctx.getBean("calculator");
calculator.add(11, 12);
calculator.division(21, 3); // 測試時,將被除數換成0,可以測試AfterReturning ,After 和 AfterThrowing
ctx.close();
}
}
輸出結果
Before 前置通知 : 方法名 【 add 】and args are [11, 12]
add 方法執行了 ----> 23
After 後置通知 : 方法名 【 add 】and args are [11, 12]
AfterReturning 返回通知 : 方法名 【 add 】and args are [11, 12] , result is 23
Before 前置通知 : 方法名 【 division 】and args are [21, 3]
division 方法執行了 ----> 7
After 後置通知 : 方法名 【 division 】and args are [21, 3]
AfterReturning 返回通知 : 方法名 【 division 】and args are [21, 3] , re