Spring AOP 概念及實戰
說在前面
- 本章相關代碼及筆記位址:飛機票🚀
- 🌍Github:🚀Java超神之路:【🍔Java全生态技術學習筆記,一起超神吧🍔】
- 🪐CSDN:🚀Java超神之路:【🍔Java全生态技術學習筆記,一起超神吧🍔】
目錄
- Spring AOP 概念及實戰
- 說在前面
- 目錄
- 一. AOP基礎概念
- 1.1 AOP?
- 1.2 AOP中的一些常用概念
- 1.3 通知類型
- 二. AOP底層實作
- 2.1 AOP底層原理
- 2.2 代理概述
- 2.3 靜态代理實作
- 2.3 JDK動态代理實作
- 三. Spring AOP的操作
- 3.1 什麼是AspectJ?
- 3.1.1 在Spring架構中,一般都是基于AspectJ來實作AOP的相關操作
- 3.1.2 基于AspectJ實作AOP操作
- 3.2 準備工作
- 3.2.1 引入Spring AOP相關依賴
- 3.2.2 切入點表達式了解
- 3.3 AOP操作 - 基于AspectJ注解
- 3.3.1 建立被增強類及方法
- 3.3.2 建立切面類(寫方法增強邏輯的地方)
- 3.3.3 加載bean到Spring容器并開啟Aspect
- 3.3.4 測試機結果
- 3.3.5 通知的執行順序
- 3.3.6 設定增強類的加載優先級
- 3.1 什麼是AspectJ?
一. AOP基礎概念
1.1 AOP?
AOP面向切面程式設計,可以
不修改源代碼進行方法增強
,AOP是OOP(面向對象程式設計)的延續,主要用于日志記錄、性能統計、安全控制、事務處理等方面。它是基于
代理設計模式
,而代理設計模式又分為靜态代理和
動态代理
,靜态代理比較簡單就是一個接口,分别由一個真實實作和一個代理實作,而動态代理分為基于接口的
JDK動态代理
和基于類的
cglib的動态代理
,咱們正常都是面向接口開發,是以AOP使用的是基于接口的JDK動态代理。
1.2 AOP中的一些常用概念
- 切面(Aspect):AOP核心就是切面,它将多個類的通用行為封裝成可重用的子產品,該子產品含有一組API提供橫切功能。比如,一個日志子產品可以被稱作日志的AOP切面。根據需求的不同,一個應用程式可以有若幹切面。在Spring AOP中,切面通過帶有@Aspect注解的類實作。
- 連接配接點(Join Point):哪些方法需要被AOP增強,這些方法就叫做連接配接點。
- 通知(Advice):AOP在特定的切入點上執行的增強處理,有
,before
,after
,afterReturning
,afterThrowing
around
- 切入點(Pointcut):實際真正被增強的方法,稱為切入點。
1.3 通知類型
通知(advice)是你在你的程式中想要應用在其他子產品中的橫切關注點的實作。Advice主要有以下5種類型:
- 前置通知(Before Advice):在連接配接點之前執行的Advice,不過除非它抛出異常,否則沒有能力中斷執行流。使用
注解使用這個Advice。@Before
- 傳回之後通知(After Retuning Advice):在連接配接點正常結束之後執行的Advice。例如,如果一個方法沒有抛出異常正常傳回。通過
關注使用它。@AfterReturning
- 抛出(異常)後執行通知(After Throwing Advice):如果一個方法通過抛出異常來退出的話,這個Advice就會被執行。通過
注解來使用。@AfterThrowing
- 後置通知(After Advice):無論連接配接點是通過什麼方式退出的(正常傳回或者抛出異常)都會執行在結束後執行這些Advice。通過
注解使用。@After
- 圍繞通知(Around Advice):圍繞連接配接點執行的Advice,就你一個方法調用。這是最強大的Advice。通過
注解使用。@Around
二. AOP底層實作
2.1 AOP底層原理
它是基于代理設計模式,而代理設計模式又分為靜态代理和動态代理,靜态代理比較簡單就是一個接口,分别由一個真實實作和一個代理實作,而動态代理分為基于接口的JDK動态代理和基于類的CGLIB的動态代理。
第一種 有接口情況,使用 JDK 動态代理
建立接口實作類代理對象,增強類的方法
JDK動态代理是去建立一個UserDao接口的實作類的代理對象,該接口實作類的代理對象會調用該接口的真實實作,并且在代理對象中調用真實實作類的前後做方法增強
第二種 沒有接口情況,使用 CGLIB 動态代理
建立子類的代理對象,增強類的方法
CGLIB動态代理是去建立一個User類子類的代理對象,該子類的代理對象會去調用父類User中的方法,并且在子類代理對象調用其父類方法簽後去做增強
2.2 代理概述
代理就是在不修改源代碼的情況下使得原本不具備某種行為能力的類、對象具有該種行為能力,實作對目标對象的功能擴充
代理的應用場景
- 事務處理
- 權限管理
- 日志收集
- AOP切面
- …
Java的代理分為靜态代理和動态代理
靜态代理的局限性:隻能代理某一類型接口的執行個體,不能代理任意接口任意方法的操作。
靜态代理隻能代理固定或單一接口的方法,也就是說不能做到任何類任何方法的代理。
2.3 靜态代理實作
Movie 接口的實作
/**
* 委托類的父接口
*/
public interface Movie {
void player();
}
實作了Movie 接口的 真實實作類(委托類):
public class RealMovie implements Movie {
@Override
public void player() {
System.out.println(">>>>>>>> 您正在觀看《士兵突擊》");
}
}
實作了Movie 接口的 代理實作類:
public class Cinema implements Movie {
RealMovie realMovie;
public Cinema(RealMovie realmovie) {
this.realMovie = realmovie;
}
public void player() {
//對目标方法進行方法增強
System.out.println("|||||||||||||||||||||||電影開始前,賣爆米花");
//執行真實實作的目标方法
realMovie.player();
//對目标方法進行方法增強
System.out.println("----------------------電影結束了,打掃衛生");
}
}
具體的調用如下:
public class ProxyTest {
public static void main(String[] args) {
//建立電影院(靜态代理)
Cinema cinema = new Cinema(new RealMovie());
cinema.player();
}
}
使用靜态代理的好處:
使得真實角色處理的業務更加純粹,不再去關注一些公共的事情。
公共的業務由代理來完成—實作業務的分工。
公共業務發生擴充時變得更加集中和友善。
缺點:每一個代理類都必須實作一遍真實 實作類(也就是realMovie)的接口,如果接口增加方法,則代理類也必須跟着修改。其次,每一個代理類對應一個真實實作類(委托類),如果真實實作(委托類)非常多,則靜态代理類就非常臃腫,難以勝任。
2.3 JDK動态代理實作
動态代理有别于靜态代理,是根據代理的對象,動态建立代理類。這樣,就可以避免靜态代理中代理類接口過多的問題。動态代理是通過反射來實作的,借助Java自帶的java.lang.reflect.Proxy,通過固定的規則生成。
其步驟如下:
- 建立一個需要動态代理的接口,即Movie接口
- 建立一個需要動态代理接口的真實實作,即RealMovie類
- 建立一個動态代理處理器,實作
,并重寫invoke方法去增強真實實作中的目标方法InvocationHandler接口
- 在測試類中,生成動态代理的對象。
第一二步驟,和靜态代理一樣,不過說了。第三步,代碼如下:
/**
* 動态代理處理類
*/
public class MyInvocationHandler implements InvocationHandler {
//需要動态代理接口的真實實作類
private Object object;
//通過構造方法去給需要動态代理接口的真實實作類指派
public MyInvocationHandler(Object object) {
this.object = object;
}
/**
* 對真實實作(被代理對象)的目标方法進行增強
* 當代理對象調用真實實作類的方法時,就會執行動态代理處理器中的該invoke方法
*
* @param proxy 生成的代理對象
* @param method 代理對象調用的方法
* @param args 調用的方法中的參數
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//方法增強
System.out.println("賣爆米花");
//object是真實實作,args是調用方法的參數
//當代理對象調用真實實作的方法,那麼這裡就會将真實實作和方法參數傳遞過去,去調用真實實作的方法
method.invoke(object,args);
//方法增強
System.out.println("掃地");
return null;
}
}
第四步,建立動态代理的對象
public class DynamicProxyTest {
public static void main(String[] args) {
// 儲存生成的代理類的位元組碼檔案
//由于設定sun.misc.ProxyGenerator.saveGeneratedFiles 的值為true,是以代理類的位元組碼内容儲存在了項目根目錄下,檔案名為$Proxy0.class
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//需要動态代理接口的真實實作
RealMovie realMovie = new RealMovie();
//動态代理處理類
MyInvocationHandler handler = new MyInvocationHandler(realMovie);
//擷取動态代理對象
//第一個參數:真實實作(被代理對象)的類加載器
//第二個參數:真實實作類(被代理對象)它所實作的所有接口的數組
//第三個參數:動态代理處理器
Movie movie = (Movie) Proxy.newProxyInstance(realMovie.getClass().getClassLoader(),
realMovie.getClass().getInterfaces(),
handler);
movie.player();
}
}
結果
由于設定 sun.misc.ProxyGenerator.saveGeneratedFiles 的值為true,是以代理類的位元組碼内容儲存在了項目根目錄下,檔案名為$Proxy0.class
三. Spring AOP的操作
3.1 什麼是AspectJ?
3.1.1 在Spring架構中,一般都是基于AspectJ來實作AOP的相關操作
AspectJ并不是Spring的組成部分,它是獨立的AOP架構(不需要Spring也能獨立使用),
是以我們一般把AspectJ和Spring架構一起使用,去進行一些AOP操作。
3.1.2 基于AspectJ實作AOP操作
- 基于XML配置檔案實作
- 基于注解方式實作(常用、友善)
3.2 準備工作
3.2.1 引入Spring AOP相關依賴
在原有的依賴的基礎上添加jar包(包括AspectJ)
3.2.2 切入點表達式了解
切入點表達式的作用:用于表達對哪個類裡面的哪個方法(切入點)進行增強
文法結構:
舉例 1:對 com.eayon.dao.BookDao 類裡面的 add 進行增強 execution(* com.dao.dao.BookDao.add(..))
舉例 2:對 com.eayon.dao.BookDao 類裡面的所有的方法進行增強execution(* com.dao.dao.BookDao.* (..))
舉例 3:對 com.eayon.dao 包裡面所有類,類裡面所有方法進行增強execution(* com.dao.dao.*.* (..))
PS:上面舉例三個都是去切具體的某個方法、類。我們也可以去切到某個包下所有的方法,也可以去切某包下帶有某注解的方法等等。
常見的切入點表達式示例
- 所有方法執行
- 名稱以"set"開頭的所有方法執行
- AccountService接口中的所有方法執行
- service包下所有方法執行
- service包下的所有連接配接點(僅在Spring AOP中執行方法)
- 代理實作AccountService接口的任何連接配接點(僅在Spring AOP中執行方法)
- 所有帶有@checkLogin注解的方法或類
3.3 AOP操作 - 基于AspectJ注解
3.3.1 建立被增強類及方法
package com.eayon.aop;
import org.springframework.stereotype.Component;
/**
* @Description AOP被增強類
*/
@Component//通過IOC中的注解将該類執行個體化到Spring容器
public class Car {
//汽車前進方法
public void forward(String carName){
System.out.println(carName + "牌汽車前進了");
}
//汽車後退方法
public void backoff(String carName){
System.out.println(carName + "牌汽車後退了");
}
}
3.3.2 建立切面類(寫方法增強邏輯的地方)
package com.eayon.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component// 通過IOC中的注解将該類執行個體化到Spring容器
@Aspect// 聲明切面類,并為本類生成代理對象
public class CarAspect {
/**
* 相同切入點抽取
*/
@Pointcut(value = "execution(* com.eayon.aop.Car.*(..))")
public void pointCut(){
}
/**
* 前置通知
* @Before注解表示作為前置通知
*/
//@@Before(value = "execution(* com.eayon.aop.Car.*(..))")
@Before(value = "pointCut()")//抽取切入點
public void before(){
System.out.println("Before...");
}
/**
* 後置通知(傳回通知)
*/
//@AfterReturning
@AfterReturning(value = "pointCut()")
public void afterReturning(){
System.out.println("AfterReturning...");
}
/**
* 異常通知
*/
//
@AfterThrowing(value = "pointCut()")
public void afterThrowing(){
System.out.println("AfterThrowing...");
}
/**
* 最終通知
*/
//@After
@After(value = "pointCut()")
public void after(){
System.out.println("After...");
}
/**
* @Around 代表環繞通知 value代表切入點,即Car類中的所有方法
* 同理 你想用其他通知隻需要變更注解就可以了 value都是一個意思
* @Before 前置通知注解
* @After 後置通知注解
* @AfterThrowing 抛出(異常)後執行通知注解
* @AfterReturning 傳回之後通知注解
*
* @return
*/
//@Around
@Around(value = "pointCut()")
public Object before(ProceedingJoinPoint point) throws Throwable {
//擷取切點方法上的參數
Object[] args = point.getArgs();
//進行方法增強 修改參數值
if(null != args && args.length > 0){
//原來的參數值
Object carName = args[0];
System.out.println("Around 原來的參數值" + carName);
//更換參數
args[0] = "奔馳";
}
//繼續執行切點方法 并使用更換後的參數
return point.proceed(args);
}
}
3.3.3 加載bean到Spring容器并開啟Aspect
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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--開啟元件掃描 掃描com.eayon包下所有帶有注解(@Component @Service等)的類 然後去執行個體化-->
<context:component-scan base-package="com.eayon"></context:component-scan>
<!-- 開啟Aspect生成代理對象 也就是掃描帶有@Aspect注解的類并生成代理對象-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
配置類方式
package com.eayon.conf;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @Description Spring配置類 用于掃描帶有注解的類将其執行個體化到IOC容器
* 開啟Aspect生成代理對象 也就是掃描帶有@Aspect注解的類并生成代理對象
*/
@Configuration//聲明目前類是配置類并加載到IOC容器
@ComponentScan(basePackages = {"com.eayon"})//開啟元件掃描 掃描com.eayon包下所有帶有注解(@Component @Service等)的類 然後去執行個體化
@EnableAspectJAutoProxy(proxyTargetClass = true)//開啟Aspect生成代理對象 也就是掃描帶有@Aspect注解的類并生成代理對象
public class SpringConf {
}
3.3.4 測試機結果
package com.eayon.demo;
public class AopTest {
@Test
public void test_car_aop() {
// 加載配置類執行個體化所有bean、并開啟Aspect生成代理對象 也就是掃描帶有@Aspect注解的類并生成代理對象
// ApplicationContext context = new ClassPathXmlApplicationContext("spring_conf.xml");//加載配置檔案,效果一樣
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConf.class);//加載配置類,效果一樣
Car car = context.getBean("car", Car.class);
car.forward("奧迪");
}
}
3.3.5 通知的執行順序
環繞通知 -> 前置通知 -> 目标方法 -> 後置通知 -> 最終通知
抛出異常通知随時可能執行,根據異常觸發決定
3.3.6 設定增強類的加載優先級
有多個增強類對同一個方法進行增強,可設定增強類的加載優先級。
舉例:比如上面有一個CarAspect增強類對User類中的方法進行增強,現在有一個CarAspect2增強類也對User類中的方法進行增強,那麼哪個肯定是哪個增強類先被加載,則先執行哪個增強類。是以我們可以通過在增強類上面添加注解
@Order(數字類型值)
進行設定類的加載優先級,數字類型值越小優先級越高
@Order(1)//加載優先級
@Component // 通過IOC中的注解将該類執行個體化到Spring容器
@Aspect // 聲明切面類,并為本類生成代理對象
public class CarAspect {
.....省略.....
}