第三章 Spring AOP
第一節 AOP 簡介
1. 概念
AOP全稱為Aspect Oriented Programming,表示面向切面編程。何為切面呢?

由此可以得出,切面是一種將那些與業務無關,但業務模塊都需要使用的功能封裝起來的技術。這樣便于減少系統的重復代碼,降低模塊之間的耦合度。
2. AOP 基本術語
-
連接點( Joinpoint ):
連接點就是被攔截到的程序執行點,因為Spring只支持方法類型的連接點,所以在Spring中連接點就是被攔截到的方法。連接點由兩個信息確定:
- 方法( 表示程序執行點,即在哪個目標方法)
- 相對點(表示方位,即目標方法的什么位置,比如調用前,后等)
-
切入點(Pointcut):
切入點是對連接點進行攔截的條件定義。切入點表達式如何和連接點匹配是AOP的核心,Spring缺省使用AspectJ切入點語法。 一般認為,所有的方法都可以認為是連接點,但是我們并不希望在所有的方法上都添加通知,而切入點的作用就是提供一組規則來匹配連接點,給滿足規則的連接點添加通知。
-
通知、增強(Advice):
可以為切入點添加額外功能,分為:前置通知、后置通知、異常通知、環繞通知、最終通知等。
-
目標對象(Target):
目標對象指將要被增強的對象,即包含主業務邏輯的類對象。或者說是被一個或者多個切面所通知的對象。
-
織入(Weaving):
織入是將切面和業務邏輯對象連接起來, 并創建通知代理的過程。織入可以在編譯時,類加載時和運行時完成。在編譯時進行織入就是靜態代理,而在運行時進行織入則是動態代理
-
代理(Proxy):
被AOP織入通知后,產生的結果類。
-
切面(Aspect):
切面是一個橫切關注點的模塊化,一個切面能夠包含同一個類型的不同增強方法,比如說事務處理和日志處理可以理解為兩個切面。切面由切入點和通知組成,它既包含了橫切邏輯的定義,也包括了切入點的定義。 Spring AOP就是負責實施切面的框架,它將切面所定義的橫切邏輯織入到切面所指定的連接點中。
第二節 AOP 應用
AOP應用場景有許多,最典型的應用場景就是日志和事務。這里以事務實現為例進行講解。
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.11</version>
</dependency>
<!-- 切面相關的包 -->
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.8.9</version>
</dependency>
1. 編寫業務層
public interface UserService {int saveUser(Map<String,Object> params);
}public class UserServiceImpl implements UserService {@Overridepublic int saveUser(Map<String, Object> params) {System.out.println("保存用戶信息" + params);return 0;}
}
2. 配置業務層
AOP 功能的實現是基于 IOC 的,因此,業務層對象應該納入 IOC 容器來管理。
<?xml version="1.0" encoding="UTF-8"?>
<!-- xmlns = xml namespace-->
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!-- 業務層對象--><bean id="userService" class="com.qf.spring.aop.service.impl.UserServiceImpl" />
</beans>
3. 編寫通知類
通知分為前置通知、后置通知、異常拋出通知、環繞通知、最終通知五種。首先實現前置通知。
前置通知接口
public interface MethodBeforeAdvice extends BeforeAdvice {/*** Callback before a given method is invoked.*/void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}
public class BeforeAdvice implements MethodBeforeAdvice {@Overridepublic void before(Method method, Object[] args, Object target) throws Throwable {String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("準備執行方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args));}
}
4. 配置通知
通知的實現也是基于 IOC 容器的,因此需要將通知對象納入 IOC 容器管理。
<?xml version="1.0" encoding="UTF-8"?>
<!-- xmlns = xml namespace-->
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!-- 業務層對象--><bean id="userService" class="com.qf.spring.aop.service.impl.UserServiceImpl" /><!--配置通知對象--><bean id="before" class="com.qf.spring.aop.advice.BeforeAdvice" />
</beans>
5. AOP 配置
當通知對象和業務層對象都納入 IOC 容器管理之后,需要將通知對象作用在業務層對象上。Spring 提供了 aop 標簽來完成這一功能。
<aop:config><!--pointcut表示切點,也就是通知會在哪些位置觸發expression表示切點表達式,切點表達式必須是execution(), execution()方法中的參數必須配置到方法上比如 * com.qf.spring.aop.service..*(..)第一個 * 表示任意訪問修飾符com.qf.spring.aop.service.. 最后的兩個..表示service包下面的所有子包中的類*(..) 表示任意方法, 如果()中沒有..,則表示不帶參數的方法;有,就表示帶任意參數--><aop:pointcut id="切點ID" expression="切點表達式"/><aop:advisor advice-ref="通知ID" pointcut-ref="切點ID" />
</aop:config>
要使用 aop 標簽,必須要在 beans 標簽上添加 aop 命名空間
xmlns:aop="http://www.springframework.org/schema/aop"
然后在 xsi:schemaLocation
屬性值中添加 aop 命名空間和約束文檔
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
<aop:config><aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/><aop:advisor advice-ref="before" pointcut-ref="pc" />
</aop:config>
6. 測試
public class AopTest {@Testpublic void saveUserTest(){ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");UserService userService = context.getBean("userService", UserService.class);Map<String,Object> params = new HashMap<>();params.put("name", "劉德華");params.put("sex", "男");userService.saveUser(params);}
}
7. 后置通知
后置通知接口
public interface AfterReturningAdvice extends AfterAdvice {/*** Callback after a given method successfully returned.*/void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;
}
public class AfterAdvice implements AfterReturningAdvice {@Overridepublic void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("執行完方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",得到返回值:" + returnValue);}
}
<bean id="after" class="com.qf.spring.aop.advice.AfterAdvice" />
<aop:config><!--pointcut表示切點,也就是通知會在哪些位置觸發expression表示切點表達式,切點表達式必須是execution(), execution()方法中的參數必須配置到方法上比如 * com.qf.spring.aop.service..*(..)第一個 * 表示任意訪問修飾符com.qf.spring.aop.service.. 最后的兩個..表示service包下面的所有子包中的類*(..) 表示任意方法, 如果()中沒有..,則表示不帶參數的方法;有,就表示帶任意參數--><aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/><aop:advisor advice-ref="before" pointcut-ref="pc" /><aop:advisor advice-ref="after" pointcut-ref="pc" />
</aop:config>
8. 異常拋出通知
異常拋出通知接口
/*** There are not any methods on this interface, as methods are invoked by* reflection. Implementing classes must implement methods of the form:* void afterThrowing([Method, args, target], ThrowableSubclass);* public void afterThrowing(Exception ex)</pre>* public void afterThrowing(RemoteException)</pre>* public void afterThrowing(Method method, Object[] args, Object target, Exception ex)* public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)*/
public interface ThrowsAdvice extends AfterAdvice {
}
public class ExceptionAdvice implements ThrowsAdvice {public void afterThrowing(Method method, Object[] args, Object target, Exception ex){String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("執行方法時:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",發生了異常:" + ex.getMessage());}
}public class UserServiceImpl implements UserService {@Overridepublic int saveUser(Map<String, Object> params) {System.out.println("保存用戶信息" + params);throw new RuntimeException("異常拋出演示");}
}
<bean id="exception" class="com.qf.spring.aop.advice.ExceptionAdvice" />
<aop:config><!--pointcut表示切點,也就是通知會在哪些位置觸發expression表示切點表達式,切點表達式必須是execution(), execution()方法中的參數必須配置到方法上比如 * com.qf.spring.aop.service..*(..)第一個 * 表示任意訪問修飾符com.qf.spring.aop.service.. 最后的兩個..表示service包下面的所有子包中的類*(..) 表示任意方法, 如果()中沒有..,則表示不帶參數的方法;有,就表示帶任意參數--><aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/><aop:advisor advice-ref="before" pointcut-ref="pc" /><aop:advisor advice-ref="after" pointcut-ref="pc" /><aop:advisor advice-ref="exception" pointcut-ref="pc" />
</aop:config>
9. 環繞通知
環繞通知接口
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {/*** Implement this method to perform extra treatments before and* after the invocation.*/@NullableObject invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
public class AroundAdvice implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {Method method = invocation.getMethod(); //獲取被攔截的方法對象Object[] args = invocation.getArguments();//獲取方法的參數Object target = invocation.getThis(); //獲取代理對象String methodName = method.getName();String className = method.getDeclaringClass().getName();try {System.out.println("準備執行方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args));Object returnValue = method.invoke(target, args);System.out.println("執行完方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",得到返回值:" + returnValue);return returnValue;} catch (Throwable t){System.out.println("執行方法時:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",發生了異常:" + t.getMessage());throw t;}}
}
<!--配置通知對象-->
<!-- <bean id="before" class="com.qf.spring.aop.advice.BeforeAdvice" /><bean id="after" class="com.qf.spring.aop.advice.AfterAdvice" /><bean id="exception" class="com.qf.spring.aop.advice.ExceptionAdvice" />-->
<bean id="around" class="com.qf.spring.aop.advice.AroundAdvice" />
<aop:config><!--pointcut表示切點,也就是通知會在哪些位置觸發expression表示切點表達式,切點表達式必須是execution(), execution()方法中的參數必須配置到方法上比如 * com.qf.spring.aop.service..*(..)第一個 * 表示任意訪問修飾符com.qf.spring.aop.service.. 最后的兩個..表示service包下面的所有子包中的類*(..) 表示任意方法, 如果()中沒有..,則表示不帶參數的方法;有,就表示帶任意參數--><aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/><!-- <aop:advisor advice-ref="before" pointcut-ref="pc" /><aop:advisor advice-ref="after" pointcut-ref="pc" /><aop:advisor advice-ref="exception" pointcut-ref="pc" />--><aop:advisor advice-ref="around" pointcut-ref="pc" />
</aop:config>
第三節 AspectJ
1. AspectJ 簡介
AspectJ是一個面向切面的框架,它擴展了Java語言,定義了AOP 語法,能夠在編譯期提供代碼的織入。Spring通過集成AspectJ實現了以注解的方式定義增強類,大大減少了配置文件中的工作量
2. AspectJ 注解
- @Aspect 切面標識
- @Pointcut 切入點
- @Before 前置通知
- @AfterReturning 后置通知
- @Around 環繞通知
- @AfterThrowing 異常拋出通知
3. AspectJ 應用
3.1 通知類編寫
@Aspect
public class AspectJAdvice {@Before(value = "execution(* com.qf.spring.aop.service..*(..))")public void before(JoinPoint jp){Object[] args = jp.getArgs(); //獲取方法參數Signature signature = jp.getSignature(); //獲取簽名if(signature instanceof MethodSignature){ //如果簽名是方法簽名Method method = ((MethodSignature) signature).getMethod(); //獲取方法String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("準備執行方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args));}}@AfterReturning(value = "execution(* com.qf.spring.aop.service..*(..))", returning = "returnValue")public void after(JoinPoint jp, Object returnValue){Object[] args = jp.getArgs(); //獲取方法參數Signature signature = jp.getSignature(); //獲取簽名if(signature instanceof MethodSignature){ //如果簽名是方法簽名Method method = ((MethodSignature) signature).getMethod(); //獲取方法String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("執行完方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",得到返回值:" + returnValue);}}@AfterThrowing(value = "execution(* com.qf.spring.aop.service..*(..))", throwing = "t")public void exception(JoinPoint jp, Throwable t){Object[] args = jp.getArgs(); //獲取方法參數Signature signature = jp.getSignature(); //獲取簽名if(signature instanceof MethodSignature){ //如果簽名是方法簽名Method method = ((MethodSignature) signature).getMethod(); //獲取方法String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("執行方法時:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",發生了異常:" + t.getMessage());}}@Around("execution(* com.qf.spring.aop.service..*(..))")public Object around(ProceedingJoinPoint pjp) throws Throwable {Object[] args = pjp.getArgs();//獲取方法的參數Object target = pjp.getTarget(); //獲取代理對象Signature signature = pjp.getSignature(); //獲取簽名if(signature instanceof MethodSignature) { //如果簽名是方法簽名Method method = ((MethodSignature) signature).getMethod(); //獲取被攔截的方法對象String methodName = method.getName();String className = method.getDeclaringClass().getName();try {System.out.println("準備執行方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args));Object returnValue = method.invoke(target, args);System.out.println("執行完方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",得到返回值:" + returnValue);return returnValue;} catch (Throwable t){System.out.println("執行方法時:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",發生了異常:" + t.getMessage());throw t;}}return null;}
}
3.2 啟用注解支持
<bean class="com.qf.spring.aop.advice.AspectJAdvice" />
<!--配置通知對象-->
<!-- <bean id="before" class="com.qf.spring.aop.advice.BeforeAdvice" /><bean id="after" class="com.qf.spring.aop.advice.AfterAdvice" /><bean id="exception" class="com.qf.spring.aop.advice.ExceptionAdvice" />-->
<!-- <bean id="around" class="com.qf.spring.aop.advice.AroundAdvice" />-->
<!--<aop:config><!–pointcut表示切點,也就是通知會在哪些位置觸發expression表示切點表達式,切點表達式必須是execution(), execution()方法中的參數必須配置到方法上比如 * com.qf.spring.aop.service..*(..)第一個 * 表示任意訪問修飾符com.qf.spring.aop.service.. 最后的兩個..表示service包下面的所有子包中的類*(..) 表示任意方法, 如果()中沒有..,則表示不帶參數的方法;有,就表示帶任意參數–><aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/><!– <aop:advisor advice-ref="before" pointcut-ref="pc" /><aop:advisor advice-ref="after" pointcut-ref="pc" /><aop:advisor advice-ref="exception" pointcut-ref="pc" />–><aop:advisor advice-ref="around" pointcut-ref="pc" /></aop:config>--><!-- 啟動AspectJ 注解 自動為類生成代理-->
<aop:aspectj-autoproxy proxy-target-class="true" />
第四節 代理模式
代理模式一共分為兩種: 靜態代理和動態代理
1. 靜態代理
靜態代理模式由三個部分構成:
-
一個公共的接口
-
一個代理角色
-
一個被代理角色
其中代理角色和被代理角色均需要實現公共接口。
/*** 人類接口,描述人類購物*/
public interface Person {//購物void shopping();
}
/*** 被代理的角色*/
public class ZhangSan implements Person{@Overridepublic void shopping() {System.out.println("購物");}
}
/*** 代理角色*/
public class PersonProxy implements Person{private ZhangSan zhangSan; //維護一個被代理的角色public PersonProxy(ZhangSan zhangSan) {this.zhangSan = zhangSan;}@Overridepublic void shopping() {System.out.println("代理人準備代理購物");zhangSan.shopping();System.out.println("代理人代理購物完畢");}
}
/*** 靜態代理測試*/
public class StaticProxyTest {public static void main(String[] args) {Person p = new PersonProxy(new ZhangSan());p.shopping();}
}
思考:如果有多人想要代理購物,那么像PersonProxy這樣的類就需要寫多次;從編程的角度出發,這顯然不合理。應該如何解決?
使用泛型進行解決。
public class StaticProxy<T> implements Person {private T t;public StaticProxy(T t) {this.t = t;}@Overridepublic void shopping() {//判斷當前對象是否繼承或者實現了Person接口if(!Person.class.isAssignableFrom(t.getClass()))throw new RuntimeException(t.getClass().getName() + "沒有實現Person接口");System.out.println("代理人準備代理購物");((Person)t).shopping();System.out.println("代理人代理購物完畢");}
}/*** 靜態代理測試*/
public class StaticProxyTest {public static void main(String[] args) {
// Person p = new PersonProxy(new ZhangSan());
// p.shopping();Person p = new StaticProxy<>(new ZhangSan());p.shopping();}
}
思考:如果有多個接口想要代理,應該如何解決?
使用動態代理。
2. 動態代理
動態代理也分為兩種:JDK 動態代理 和 CGLIB 動態代理
2.1 JDK 動態代理
JDK 動態代理是基于接口實現的,因此只能為實現了接口的類做代理。其實現原理是:在內存中生成一個代理類繼承與 Proxy, 同時實現代理接口,然后在內存中進行編譯,編譯后使用類加載器將該代理類加載進來,從而創建一個代理對象。
public class DynamicProxyTest {public static void main(String[] args) {Person p = new ZhangSan();Class<?> clazz = Person.class;Class[] interfaces = { clazz };ClassLoader loader = clazz.getClassLoader();InvocationHandler handler = new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("準備進行代理購物");return method.invoke(p, args);}};Person proxy = (Person) Proxy.newProxyInstance(loader, interfaces, handler);proxy.shopping();}
}
2.2 CGLIB 動態代理
CGLIB 動態代理是基于繼承實現的,因此可以為任何類做代理。如果一個類實現了接口,通常會使用 JDK 動態代理,但也可以強制使用 CGLIB 動態代理。
public class CgLibProxy {public static void main(String[] args) {final Person p = new ZhangSan();//創建字節碼增強對象Enhancer enhancer = new Enhancer();//設置父類,需要繼承enhancer.setSuperclass(p.getClass());//需要注意:這里的InvocationHandler是CGLIB提供的net.sf.cglib.proxy.InvocationHandlerenhancer.setCallback(new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] params) throws Throwable {System.out.println("準備進行代理購物");return method.invoke(p, params);}});//創建動態代理實例Person proxy = (Person) enhancer.create();proxy.shopping();}
}
Spring CGLIB
public class SpringCgLibProxy {public static void main(String[] args) {final Person p = new ZhangSan();//創建字節碼曾強對象Enhancer enhancer = new Enhancer();//設置父類,需要繼承enhancer.setSuperclass(p.getClass());enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object o, Method method, Object[] params, MethodProxy methodProxy) throws Throwable {System.out.println("準備進行代理購物");return method.invoke(p, params);}});//創建動態代理實例Person proxy = (Person) enhancer.create();proxy.shopping();}
}
2.3 區別
JDK 動態代理只能為實現了接口的類做代理, CGLIB 動態代理能夠為所有的類做代理。JDK 動態代理的創建代理從效率上來說要比 CGLIB 動態代理快。Cglib不能對聲明final的方法進行代理,因為final關鍵字修飾的方法不可被重寫。