Spring4 AOP詳解
第一章Spring 快速入門並沒有對Spring4 的 AOP 做太多的描述,是因為AOP切面編程概念不好理解。所以這章主要從三個方面詳解AOP:AOP簡介(了解),基於注解的AOP編程(重點)和基於xml的AOP編程。
AOP簡介
什么是AOP
AOP(Aspect Oriented Programming)面向切面編程,是對傳統的OOP(ObjectOriented Programming)面向對象編程的補充。
AOP的作用
如果A,B,C三個方法都要在執行前做驗證操作,執行后做日志打印操作。腫么辦?
排版好丑。。。。。。
AOP專業術語
** 切面(Aspect) ** : A,B,C,方法執行前都要調用的驗證邏輯和執行后都要調用的日志邏輯,這兩層業務邏輯就是切面。
** 通知(Advice) ** : 有五種通知,執行前,執行后,執行成功后,執行拋出異常后,環繞通知。就是切面執行的方法。
** 目標(Target) ** : 被通知的對象,這里就是A,B,C三個方法。
** 連接點(Joinpoint) **:連接點是一個應用執行過程中能夠插入一個切面的點。
** 切點(pointcut) **:每個類都擁有多個連接點,即連接點是程序類中客觀存在的事務。AOP 通過切點定位到特定的連接點
打個比方:一天,三位俠客(被通知的對象Target)來我府上做客,被大門(切面Aspect)攔住,門前有五個保安(負責通知的Advice),因為其中一位俠客會降龍十八掌(滿足被通知的一個條件Joinpoint),其中一位保安告知他:"你可以進去了"。另外兩個俠客因為武藝超群(滿足被通知的統一標准poincut)也都進去了。
基於注解的AOP編程
基於注解的編程,需要依賴AspectJ框架(java中最流行的aop框架)。
第一步:導入AspectJ的jar包,該框架只有Spring 2.0以上才支持。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.2.2.RELEASE</version>
</dependency>
第二步:核心文件applicationContext.xml,里面需要配置自動掃描包(用於IOC注解)和配置啟用AspectJ注解
<?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.itdragon.spring.*" ></context:component-scan>
<!-- 使 AspectJ 的注解起作用 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
第三步:切面類,該類有什么特點?首先它必須是IOC的bean,還要聲明它是AspectJ切面,最后還可以定義切面的優先級Order(非必填)
通知有五種注解
** @Before ** :前置通知的注解,在目標方法執行前調用
** @After **:后置通知的注解, 在目標方法執行后調用,即使程序拋出異常都會調用
** @AfterReturning **:返回通知的注解, 在目標方法成功執行后調用,如果程序出錯則不會調用
** @AfterThrowing **:異常通知的注解, 在目標方法出現指定異常時調用
** @Around **:環繞通知的注解,很強大(相當於前四個通知的組合),但用的不多,
還有為了簡化開發的重用切入點@Pointcut,以及抽象表達"*"
public interface Calculator {
public int add(int a, int b);
public int division(int a, int b);
}
import org.springframework.stereotype.Repository;
@Repository("calculator")
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;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @Order(n) : 切面的優先級,n越小,級別越高
* @Aspect:聲明該類是一個切面
* @Component:切面必須是 IOC 中的 bean
*/
@Order(2)
@Aspect
@Component
public class LoggerAspect {
/**
* 前置通知的注解,在目標方法執行前調用
* execution最基礎的表達式語法。
* 注意點:
* 1. 方法里面不能有行參,及add(int a, int b) 這是會報錯的。
* 2. int(方法的返回值),add(方法名) 可以用 * 抽象化。甚至可以將類名抽象,指定該包下的類。
* 3. (int, int) 可以用(..)代替,表示匹配任意數量的參數
* 4. 被通知的對象(Target),建議加上包的路徑
*/
@Before("execution(int com.atguigu.spring.my.aop.CalculatorImp.add(int , int))")
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("execution(* com.atguigu.spring.my.aop.CalculatorImp.*(..))")
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.atguigu.spring.my.aop.CalculatorImp.*(..))")
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);
}
/**
* 公用多個切面
*/
@AfterReturning("execution(* com.xxx.xx.baserver.operation.service.WorkOrderService.createWorkOrder(..))" +
" || execution(* com.xxx.xx.baserver.operation.service.WorkOrderService.updateWorkOrder(..))")
}
import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(1)
@Aspect
@Component
public class AroundAspect {
/**
* 環繞通知,很強大,但用的不多。 用環繞通知測試Order的優先級看的不明顯(這里是筆者的失誤)
* 環繞通知需要用ProceedingJoinPoint 類型的參數
*/
@Around("execution(* com.atguigu.spring.my.aop.CalculatorImp.*(..))")
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
division 方法執行了 ----> 7
@After 后置通知 : 方法名 【 division 】and args are [21, 3]
@AfterReturning 返回通知 : 方法名 【 division 】and args are [21, 3] , result is 7
很簡單對吧,用到的注解其實並不是很多。
以上代碼有一個不足之處,就是測試Order優先級的時候,效果不明顯。AroundAspect的優先級高於LoggerAspect,從打印的日志中發現,只有AroundAspect的前置通知在LoggerAspect前面打印,其他通知均在后面。
因為博客和課堂不同,如果把每個知識點都單獨寫出來,篇幅可能太長。筆者盡可能將所有知識點都寫在一起,學A知識的同時將B,C,D的知識一起學習。但難免會有一些不聽話的知識點。所以請各位讀者見諒。
基於xml的AOP編程
上一篇文章講到了基於xml的IOC設置bean,篇幅較長,內容較復雜。但配置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] , result is 7
到這里,基於xml文件的AOP編程也講完了。4不4很簡單。