一、什么是AOP
全稱Aspect Oriented Programming,即面向切面編程,AOP是Spring框架的第二大核心,第一大為IOC。什么是面向切面編程?切面就是指某一類特定的問題,所以AOP也可以稱為面向特定方法編程。例如對異常的統一處理,簡單來說,AOP是一種思想,是對某一類問題的集中處理。AOP的優勢在于程序運行期間在不修改源碼的基礎上對已有的方法進行增強(無侵入性)
二、什么是Spring AOP
AOP是一種思想,它的實現方法有很多,有Spring AOP,也有AspectJ,CGLIB等,Spring AOP只是其中的一種實現方式。
三、Spring AOP入門
首先在pom文件中引入依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
編寫AOP程序
@Slf4j
@Aspect
@Component
public class TimeAspect {@Around("execution(* com.example.bookManagement.Controller.*.*(..))")public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {//記錄開始時間long startTime = System.currentTimeMillis();//執行原始方法Object result = joinPoint.proceed();//記錄方法執行時間long endTime = System.currentTimeMillis();long elapsedTime = endTime - startTime;log.info(joinPoint.getSignature()+"耗時: {} ms", elapsedTime);return result;}
}
運行程序,觀察Controller方法運行時間:
對程序進行簡單的講解:
@Aspect:標識這是一個切面類。
@Around:環繞通知,在目標方法的前后都會被執行,后面的表達式表示對哪些方法進行增強。
ProceedingJoinPoint.proceed()讓原方法執行。
四、Spring AOP詳解
1.Spring AOP核心
? ? ? ? 1>切點(Pointcut)
? ? ? ? 切點的作用是提供一組規則,告訴程序對哪些方法進行功能增強
????????
@Around("execution(* com.example.bookManagement.Controller.*.*(..))")
什么表達式中的"execution(* com.example.bookManagement.Controller.*.*(..))"就是切點表達式
? ? ? ? 2>.連接點(Join Point)
????????滿足切點表達式規則的方法,就是連接點,也就是可以被AOP控制的方法 。
????????例如:"execution(* com.example.bookManagement.Controller路徑下的方法都是連接點。
? ? ? ? 3>.通知(Advice)
? ? ? ? 通知就是具體要做的工作,指哪些重復的邏輯,也就是共性功能。
? ? ? ? 比如例子中的記錄業務方法的耗時時間,就是通知。
long startTime = System.currentTimeMillis();//執行原始方法Object result = joinPoint.proceed();//記錄方法執行時間long endTime = System.currentTimeMillis();long elapsedTime = endTime - startTime;log.info(joinPoint.getSignature()+"耗時: {} ms", elapsedTime);
? ? ? ? 4>.切面(Aspect)
? ? ? ? 切面=切點+通知
? ? ? ? 通過切面能夠描述當前AOP程序需要針對哪些方法,在什么時候執行什么樣的操作,切面即包含了通知邏輯的定義,也包含了連接點的定義。即例子中的如下代碼:
@Around("execution(* com.example.bookManagement.Controller.*.*(..))")public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {//記錄開始時間long startTime = System.currentTimeMillis();//執行原始方法Object result = joinPoint.proceed();//記錄方法執行時間long endTime = System.currentTimeMillis();long elapsedTime = endTime - startTime;log.info(joinPoint.getSignature()+"耗時: {} ms", elapsedTime);return result;}
? ? ? ? 切面所在的類,我們一般稱為切面類(被@Aspect標注的類)
2.通知類型
上述的@Around就是其中一類的通知類型。Spring中的AOP通知類型有以下幾類:
@Around:環繞通知,此注解標注的通知方法在目標方法前后都被執行
@Before:前置通知,此注解標注的方法在目標方法前被執行。
@After:后置通知,此注解標注的方法在目標方法后被執行,無論是否有異常都會執行。
@AfterReturning:返回后通知,此注解標注的通知方法在目標方法后被執行,有異常不會執行。
@AfterThrowing:異常后通知,此注解的通知方法發生異常后執行。
下面是代碼案例:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Slf4j
@Aspect
@Component
public class AspectDemo {//前置通知@Before("execution(* com.example.aop.Controller.*.*(..))")public void before() {log.info("執行Before方法");}//后置通知@After("execution(* com.example.aop.Controller.*.*(..))")public void after() {log.info("執行After方法");}//返回后通知@AfterReturning("execution(* com.example.aop.Controller.*.*(..))")public void afterReturning() {log.info("執行AfterReturn 方法");}//拋出異常后通知@AfterThrowing("execution(* com.example.aop.Controller.*.*(..))")public void afterThrowing() {log.info("執行AfterThrowing方法");}//環繞通知@Around("execution(* com.example.aop.Controller.*.*(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("Around方法開始執行");Object result = joinPoint.proceed();log.info("Around方法執行");return result;}
}
添加以下測試程序:
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("/t1")public String test1() {return "test1";}@RequestMapping("/t2")public String test2() {int a = 10/0;return "test2";}
}
運行t1:
可以看出執行順序如下:
運行t2,觀察異常情況:
程序發生異常情況下:
@AfterReturning標識的通知方法不會執行,@AfterThrowing標識的通知方法執行了
@Around第一個方法執行了,第二個沒有執行。
3.@PointCut
什么代碼存在一個問題,就是存在大量的切點表達式execution(* com.example.aop.Controller.*.*(..)),Spring提供了@PointCut注解,把公共的切點表達式提取出來,需要時引入該切點表達式即可。
上述代碼可以改為:
@Slf4j
@Aspect
@Component
public class AspectDemo {@Pointcut("execution(* com.example.aop.Controller.*.*(..))")public void pointcut() {}//前置通知@Before("pointcut()")public void before() {log.info("執行Before方法");}//后置通知@After("pointcut()")public void after() {log.info("執行After方法");}//返回后通知@AfterReturning("pointcut()")public void afterReturning() {log.info("執行AfterReturn 方法");}//拋出異常后通知@AfterThrowing("pointcut()")public void afterThrowing() {log.info("執行AfterThrowing方法");}//環繞通知@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("Around方法開始執行");Object result = joinPoint.proceed();log.info("Around方法執行");return result;}
}
但是這種方式只適用于當前切面,如果當其他切面要使用時,就需要將private改為public,而且引用方式改為:全限定類名.方法名()
例如:
@Slf4j
@Aspect
@Component
public class AspectDemo2 {@Before("com.example.aop.AspectDemo.AspectDemo.pointcut()")public void before() {log.info("執行before方法:Aspect Demo2");}
}
4.切面優先級@Order
當我們在一個項目中,定義了多個切面類,而且這些切面類的多個切入點都匹配了同一個目標方法,當目標方法運行時,這些切面類中的通知方法都會執行,那么這幾個通知方法的執行順序是什么樣子的呢。
這里直接給出結果:
存在多個切面類時,默認按照切面類的類名字母排序:
@Before:字母排名靠前的先執行。
@After:字母排名靠前的后執行。
這種方式并不方便管理,因為類名往往是不考慮首字母的。
Spring提供了一個全新的注解,來控制這些切面通知的執行順序:@Order
@Order(數字):
@Before:數字小的先執行;
@After:數字大的先執行。
5.切點表達式
切點表達式常見的有兩種表達式
1.execution(......):根據方法的簽名來匹配。
2.@annotation(.....):根據注解匹配。
1.execution表達式
最常見的切點表達式,用來匹配方法,語法為:
execution(<訪問修飾符> <返回類型> <包名.類名.方法(方法參數)> <異常>)
其中訪問修飾符和異常可以省略。
其中:*表示任意一層/一個包/參數,..表示任意多個包或者參數。
2.@annotation注解
相較于execution表達式,@annotation注解適用于多個無規則的方法,比如TestController中的t1和User Controller中的t2.。
實現步驟:
1.編寫自定義注解。
2.使用@annotation表達式來描述切點。
3.在連接點的方法上添加自定義注解。
例子:
先定義Controller:
@RequestMapping("/user")
public class UserController {@RequestMapping("/u1")public String u1() {return "u1";}@RequestMapping("/u2")public String u2() {return "u2";}
}
@RequestMapping("/test")
@RestController
public class TestController {@RequestMapping("/t1")public String test1() {return "test1";}@RequestMapping("/t2")public String test2() {int a = 10/0;return "test2";}
}
創建自定義注解類@MyAspect
package com.example.aop.MyAspect;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
解釋:@Target標識了Annotation所修飾的對象范圍,即注解可以用在什么地方
@Retention標明注解的生命周期。
定義切面類
使用@annotation切點表達式定義切點,只對@MyAspect生效
@Slf4j
@Component
@Aspect
public class MyAspectDemo {@Before("@annotation(com.example.aop.MyAspect.MyAspect)")public void before() {log.info("MyAspect->before method");}@After("@annotation(com.example.aop.MyAspect.MyAspect)")public void after() {log.info("MyAspect->after method");}
}
添加自定義注解
在TestController中的t1和UserController中的u1這兩個方法上添加自定義注解@MyAspect,其他的不添加。
@RequestMapping("/user")
public class UserController {@MyAspect@RequestMapping("/u1")public String u1() {return "u1";}@MyAspect@RequestMapping("/t1")public String test1() {return "test1";}
運行程序進行測試:
可以看出切面通知被執行了
運行其他的方法:
可以看出并沒有執行切面通知
經典面試題:
Spring AOP的實現方式:
1.基于注解@Aspect
2.基于自定義注解(參考@annotation部分的內容)
3.基于Spring API(提供xml配置的方式,但是自從SpringBoot廣泛使用后,這種方式幾乎就看不見了)
4.基于代理來實現(更加久遠的方式,寫法繁瑣)
五、Spring AOP原理
Spring AOP是基于動態代理來實現AOP的,首先先了解代理模式:
1.代理模式
也叫委托模式,為其他對象提供一種代理以控制對這個對象的訪問,它的作用就是提供一個代理類,讓我們在調用目標方法的時候,不再是直接對目標方法進行調用,就是提供代理類簡介訪問。
根據代理的創建時期,代理模式可以分為靜態代理和動態代理
靜態代理:由程序員創建代理類或特定工具自動生成源代碼再對其進行編譯,在程序運行前代理類的.class文件就已經存在了。
動態代理:在程序運行時,運用反射機制動態創建而成。
靜態代理
靜態代理由于代碼冗余度高,靈活性低,無法在沒有接口的情況下使用,所以使用的頻率較低。
下面是一個簡單的例子:
就用戶注冊的例子:
// 公共接口:用戶服務
public interface UserService {void register(String username, String password); // 注冊方法
}
// 真實目標對象:具體用戶服務實現
public class RealUserService implements UserService {@Overridepublic void register(String username, String password) {System.out.println("真實注冊邏輯:用戶 " + username + " 注冊成功(密碼已加密)");// 這里可以是復雜的數據庫操作、校驗等真實邏輯}
}
/ 靜態代理類:增強用戶服務
public class UserServiceProxy implements UserService {private final UserService realUserService; // 持有真實對象的引用// 構造方法傳入真實對象public UserServiceProxy(UserService realUserService) {this.realUserService = realUserService;}// 重寫接口方法,織入增強邏輯@Overridepublic void register(String username, String password) {// 前置增強:注冊前的日志記錄System.out.println("【代理前置】開始處理注冊請求:用戶 " + username);// 調用真實對象的方法(核心業務)realUserService.register(username, password);// 后置增強:注冊后的日志記錄System.out.println("【代理后置】注冊流程結束,已記錄操作日志");}
}
運行一個例子:
public class Client {public static void main(String[] args) {// 1. 創建真實對象UserService realService = new RealUserService();// 2. 創建代理對象,傳入真實對象UserService proxyService = new UserServiceProxy(realService);// 3. 通過代理對象調用方法(觸發代理邏輯)proxyService.register("張三", "123456");}
}
結果:
【代理前置】開始處理注冊請求:用戶 張三
真實注冊邏輯:用戶 張三 注冊成功(密碼已加密)
【代理后置】注冊流程結束,已記錄操作日志
動態代理
相比于靜態代理,動態代理就更加靈活,我們不需要針對每一個目標對象都單獨創建一個代理
對象,而是把這個創建對象的工作推遲到程序運行時由JVM來實現,也就是說動態代理在程序運行時,根據需要動態創建生成。
常見的實現方式有兩種:
1.JDK動態代理
2.CGLIB動態代理
JDK動態代理
實現步驟:
1.定義一個接口及其實現類
2.自定義InvocationHandler并重寫invoke方法,在invoke方法中調用目標方法并自定義一些處理邏輯。
3.通過Proxy.newProxyInstance(ClassLoder loder,Class<?>[] interfaces, InvaocationHandler h)方法創建代理對象。
定義JDK動態代理類
實現InvocationHandler接口
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class JDKInvocationHandler implements InvocationHandler {//目標對象private Object target;public JDKInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//增強代理內容System.out.println("我是代理者");Object reVal = method.invoke(target, args);//代理增強內容System.out.println("代理結束");return reVal;}
}
創建一個代理對象并使用
import com.example.aop.HouseSubject.HouseSubject;
import com.example.aop.HouseSubject.RealHouseSubject;
import com.example.aop.JDK.JDKInvocationHandler;
import org.springframework.cglib.proxy.Proxy;public class Main {public static void main(String[] args) {HouseSubject target = new RealHouseSubject();//創建一個代理類HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(target.getClass().getClassLoader(),new Class[]{HouseSubject.class},new JDKInvocationHandler(target));proxy.rentHouse();}
}
HouseSubject和RealHouseSubject的代碼:
public interface HouseSubject {void rentHouse();void saleHouse();}
public class RealHouseSubject implements HouseSubject {@Overridepublic void rentHouse() {System.out.println("房屋擁有者出租房子");}@Overridepublic void saleHouse() {System.out.println("房屋擁有者出售房子");}
}
運行結果:
代碼講解:
1.InvocationHandler
InvocationHandler接口是java動態代理的關鍵接口之一,它定義了一個單一方法invoke(),用于處理被代理對象的方法調用。
public interface InvocationHandler
extends Callback
{/*** @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])*/public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;}
參數說明:proxy:代理對象;method:代理對象需要實現的方法,即其中需要重寫的方法;
args:mothed所對應方法的參數。
2.Proxy
Proxy類中使用頻率最高的方法是:newProxyInstance(),這個方法主要用來生成一個代理對象。
public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) {try {Class clazz = getProxyClass(loader, interfaces);return clazz.getConstructor(new Class[]{ InvocationHandler.class }).newInstance(new Object[]{ h });} catch (RuntimeException e) {throw e;} catch (Exception e) {throw new CodeGenerationException(e);}}
參數含義:
loder:類加載器,用于加載代理對象。
interfaces:被代理類實現的一些接口。
h:實現了InvocationHandler接口的對象。
CGLIB動態代理
JDK有一個很致命的缺點就是只能代理實現了接口的類,在沒有實現接口的情況下,我們就可以使用CGLIB來解決問題。
實現步驟:
1.定義一個被代理類;
2.自定義MethodInterceptor并重寫intercept方法,intercept用于增強目標方法,和JDK中的invoke方法類似。
3.通過Enhancer類中的create()創建代理類。
添加依賴:
CGLIB是一個開源項目,使用的話需要引入依賴
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version></dependency>
自定義MethodInterceptor(方法攔截器)
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;public class CGLIBInterceptor implements MethodInterceptor {private Object target;public CGLIBInterceptor(Object target) {this.target = target;}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("代理者開始代理");Object result = methodProxy.invoke(target, objects);System.out.println("代理者結束代理");return result;}
}
注意導入的包為import org.springframework.cglib.proxy.MethodProxy;
創建代理類并使用:
public class Main {public static void main(String[] args) {HouseSubject target = new RealHouseSubject();HouseSubject proxy = (HouseSubject) Enhancer.create(target.getClass(), new CGLIBInterceptor(target));proxy.rentHouse();}
}
運行結果:
代碼講解:
1.MethodInterceptor
和JDK方式中的InvocationHandler類似,它只是定義了一個方式Intercept,用于增強目標方法。
2.Enhancer.create()
用來生成一個代理對象。
面試題:
Spring使用的哪種方式:
默認proxyTargetClass (源碼中的一個重要參數)?false,如果實現了接口,使用JDK;普通類,使用CGLIB
Spring Boot使用哪種方式:
在SpringBoot2.0以后,proxyTargetClass默認為true,默認使用CGLIB。