
前言
在日常開發(fā)中,會遇到一些對不同的功能進(jìn)行相同的一些增強(qiáng)效果,可以在原有基礎(chǔ)上進(jìn)行相應(yīng)的修改,但是會造成大量的代碼重復(fù),并且對于三方或者一些其他人寫的不能更改的類,無法來實(shí)現(xiàn)。這就提出了新的方法AOP來實(shí)現(xiàn)。
AOP簡介
1. 核心概念
AOP(Aspect Oriented Programming):面向切面編程,一種編程范式,指導(dǎo)開發(fā)者如何組織程序結(jié)構(gòu)。
作用:在不驚動原始設(shè)計(jì)的基礎(chǔ)上為其進(jìn)行功能增強(qiáng)。
Spring理念:無入侵式/無侵入式。
1.1 連接點(diǎn)(JoinPoint)
程序執(zhí)行過程中的任意位置,粒度為執(zhí)行方法、拋出異常、設(shè)置變量等。
在SpringAOP中,理解為方法的執(zhí)行。
1.2 切入點(diǎn)(Pointcut)
匹配連接點(diǎn)的式子。
在SpringAOP中,一個(gè)切入點(diǎn)可以只描述一個(gè)具體方法,也可以匹配多個(gè)方法。
一個(gè)具體方法:com.hao.dao包下的BookDao接口中的無形參無返回值的save()方法。
匹配多個(gè)方法:所有的save()方法,所有的以get開頭的方法,所有以Dao結(jié)尾的接口中的任意方法,所有帶有一個(gè)參數(shù)的方法。
1.3 通知(Advice)
在切入點(diǎn)處執(zhí)行的操作,也就是共性功能。
在SpringAOP中,功能最終以方法的形式呈現(xiàn)。
1.4 通知類
定義通知的類。
1.5 切面(Aspect)
描述通知與切入點(diǎn)的對應(yīng)關(guān)系。
快速入門
1. 快速入門
使用注解開發(fā)模式模擬測定接口開始執(zhí)行時(shí)間。這里直接在方法運(yùn)行前打印當(dāng)前時(shí)間即可。
1.1 導(dǎo)入AOP相關(guān)坐標(biāo)
在pom.xml中導(dǎo)入如下坐標(biāo)。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9</version>
</dependency>
1.2 制作連接點(diǎn)方法(Dao接口與實(shí)現(xiàn)類)
定義一個(gè)模擬接口和實(shí)現(xiàn)類。
public interface BookDao {
public void save();
public void update();
}
@Repository
public class BookDaoImpl implements BookDao {
@Override
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
@Override
public void update() {
System.out.println("book dao update ...");
}
}
1.3 制作共性功能(通知類與通知)
使用@Component和@Aspect注解。
@Component
@Aspect
public class MyAdvice {
}
1.4 定義切入點(diǎn)
使用@Pointcut注解。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.hao.dao.BookDao.update())")
private void pt() {}
}
切入點(diǎn)定義依托一個(gè)不具有實(shí)際意義的方法進(jìn)行,即無參數(shù),無返回值,方法體無實(shí)際邏輯
1.5 綁定切入點(diǎn)與通知的關(guān)系(切面)
由于是測定方法執(zhí)行前的運(yùn)行時(shí)間,這里使用@Before注解。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.hao.dao.BookDao.update())")
private void pt() {}
@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
將方法pt()加入@Before注解注解中即完成綁定。
1.6 開啟Spring對AOP注解驅(qū)動支持
在Spring配置類中添加@EnableAspectJAutoProxy注解即可。
@Configuration
@ComponentScan({"com.hao.dao", "com.hao.aop"})
@EnableAspectJAutoProxy
public class SpringConfig {
}
1.7 測試
public class App {
public static void main(String[] args) throws IOException {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
測試調(diào)用BookDao的update()方法可以看到在方法內(nèi)容前依然打印了當(dāng)前時(shí)間毫秒值。
2. AOP工作流程
2.1 Spring容器啟動
2.2 讀取所有切面配置中的切入點(diǎn)
2.3 初始化bean
判斷bean對應(yīng)的類中的方法是否匹配到任意切入點(diǎn)。
匹配失敗,創(chuàng)建對象。
匹配成功,創(chuàng)建原始對象(目標(biāo)對象)的代理對象。
2.4 獲取bean執(zhí)行方法
獲取bean,調(diào)用方法并執(zhí)行,完成操作。
獲取的bean是代理對象時(shí),根據(jù)代理對象的運(yùn)行模式運(yùn)行原始方法與增強(qiáng)的內(nèi)容,完成操作。
2.5 涉及概念
目標(biāo)對象(Target):原始功能去掉共性功能對應(yīng)的類產(chǎn)生的對象,這種對象是無法直接完成最終工作的。
代理(Proxy):目標(biāo)對象無法直接完成工作,需要對其進(jìn)行功能回填,通過原始對象的代理對象實(shí)現(xiàn)。
AOP切入點(diǎn)表達(dá)式
1. 語法格式
切入點(diǎn):要進(jìn)行增強(qiáng)的方法。
切入點(diǎn)表達(dá)式:要進(jìn)行增強(qiáng)的方法的描述方式。
1.1 示例
描述方式一:執(zhí)行com.hao.dao包下的BookDao接口中的無參數(shù)update()方法
@Pointcut("execution(void com.hao.dao.BookDao.update())")
描述方式二:執(zhí)行com.hao.dao.impl包下的BookDaoImpl類中的無參數(shù)update()方法
@Pointcut("execution(void com.hao.dao.impl.BookDaoImpl.update())")
1.2 標(biāo)準(zhǔn)格式
動作關(guān)鍵字(訪問修飾符 返回值 包名.類/接口名.方法名(參數(shù))異常名)
@Pointcut("execution(public User com.hao.service.UserService.findById(int))")
- 動作關(guān)鍵字:描述切入點(diǎn)的行為動作,例如
execution表示執(zhí)行到指定切入點(diǎn)。 - 訪問修飾符:
public,private等,可以省略。 - 返回值
- 包名
- 類/接口名
- 方法名
- 參數(shù)
- 異常名:方法定義中拋出指定異常,可以省略。
2. 通配符
可以使用通配符描述切入點(diǎn),快速描述。
2.1 *
單個(gè)獨(dú)立的任意符號,可以獨(dú)立出現(xiàn),也可以作為前綴或者后綴匹配符出現(xiàn)。
例:匹配com.hao包下的任意包中的UserService類或接口中所有find開頭的帶有一個(gè)參數(shù)的方法。
@Pointcut("execution(public * com.hao.*.UserService.find* (*))")
2.2 ..
多個(gè)連續(xù)的任意符號,可以獨(dú)立出現(xiàn),常用于簡化包名與參數(shù)的書寫
例:匹配com包下的任意包中的UserService類或接口中所有名稱為findById的方法
@Pointcut("execution(public User com..UserService.findById(..))")
2.3 +
專用于匹配子類類型
@Pointcut("execution(* *..*Service+.* (..))")
3. 書寫技巧
- 所有代碼按照標(biāo)準(zhǔn)規(guī)范開發(fā),否則以下技巧全部失效。
- 描述切入點(diǎn)
通常描述接口,而不描述實(shí)現(xiàn)類。 - 訪問控制修飾符針對接口開發(fā)均采用
public描述(可省略訪問控制修飾符描述)。 - 返回值類型對于增刪改類使用精準(zhǔn)類型加速匹配,對于查詢類使用
*通配快速描述。 -
包名書寫盡量不使用..匹配,效率過低,常用*做單個(gè)包描述匹配,或精準(zhǔn)匹配。 -
接口名/類名書寫名稱與模塊相關(guān)的采用*匹配,例如UserService寫成*Service,綁定業(yè)務(wù)層接口名。 -
方法名書寫以動詞進(jìn)行精準(zhǔn)匹配,名詞采用*匹配,例如getById書寫成getBy*,selectAll書寫成select*。 - 參數(shù)規(guī)則較為復(fù)雜,根據(jù)業(yè)務(wù)方法靈活調(diào)整。
- 通常
不使用異常作為匹配規(guī)則。
AOP通知類型
1. 概述
AOP通知描述了抽取的共性功能,根據(jù)共性功能抽取的位置不同,最終運(yùn)行代碼時(shí)要將其加入到合理的位置。
AOP通知共分為5種類型:
- 前置通知
- 后置通知
- 環(huán)繞通知(重點(diǎn))
- 返回后通知(了解)
- 拋出異常后通知(了解)
2. 類型詳解
2.1 @Before
位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法前運(yùn)行。
相關(guān)屬性:value(默認(rèn)):切入點(diǎn)方法名,格式為類名.方法名()。
例:
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
2.2 @After
位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法后運(yùn)行。
相關(guān)屬性:value(默認(rèn)):切入點(diǎn)方法名,格式為類名.方法名()。
例:
@After("pt()")
public void after() {
System.out.println("after advice ...");
}
2.3 @Around(重點(diǎn),常用)
位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法前后運(yùn)行。
例:
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
// 表示對原始操作的調(diào)用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
注意:
- 環(huán)繞通知必須依賴形參
ProceedingJoinPoint才能實(shí)現(xiàn)對原始方法的調(diào)用,進(jìn)而實(shí)現(xiàn)原始方法調(diào)用前后同時(shí)添加通知。 - 通知中如果未使用
ProceedingJoinPoint對原始方法進(jìn)行調(diào)用將跳過原始方法的執(zhí)行。 - 對原始方法的調(diào)用可以不接收返回值,通知方法設(shè)置成
void即可,如果接收返回值,必須設(shè)定為Object類型。 - 原始方法的返回值如果是
void類型,通知方法的返回值類型可以設(shè)置成void,也可以設(shè)置成Object。 - 由于無法預(yù)知原始方法運(yùn)行后是否會拋出異常,因此環(huán)繞通知方法必須拋出
Throwable對象。
2.4 @AfterReturning(了解)
位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法正常執(zhí)行完畢后運(yùn)行 。
相關(guān)屬性:value(默認(rèn)):切入點(diǎn)方法名,格式為類名.方法名()。
例:
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
2.5 @AfterThrowing(了解)
位置:通知方法定義上方。
作用:設(shè)置當(dāng)前通知方法與切入點(diǎn)之間的綁定關(guān)系,當(dāng)前通知方法在原始切入點(diǎn)方法運(yùn)行拋出異常后執(zhí)行 。
相關(guān)屬性:value(默認(rèn)):切入點(diǎn)方法名,格式為類名.方法名()。
例:
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
AOP通知獲取數(shù)據(jù)
1. AOP通知獲取參數(shù)數(shù)據(jù)
1.1 JoinPoint
適用于前置、后置、返回后、拋出異常后通知。
JoinPoint對象描述了連接點(diǎn)方法的運(yùn)行狀態(tài),可以獲取到原始方法的調(diào)用參數(shù)。
@Before("pt()")
public void before(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
}
1.2 ProceedJointPoint
適用于環(huán)繞通知。
ProceedJointPoint是JoinPoint的子類。
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
// 表示對原始操作的調(diào)用
Object ret = pjp.proceed();
return ret;
}
2. AOP通知獲取返回值數(shù)據(jù)
2.1 返回后通知
返回后通知可以獲取切入點(diǎn)方法中的返回值信息,使用形參可以接收對應(yīng)的返回值對象。
@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(Object ret) {
System.out.println("afterReturning advice ..." + ret);
}
2.2 環(huán)繞通知
環(huán)繞通知中可以手工書寫對原始方法的調(diào)用,得到的結(jié)果即為原始方法的返回值。
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object ret = pjp.proceed();
return ret;
}
3. AOP通知獲取異常數(shù)據(jù)(了解)
3.1 拋出異常后通知
拋出異常后通知可以獲取切入點(diǎn)方法中出現(xiàn)的異常信息,使用形參可以接收對應(yīng)的異常對象。
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..." + t);
}
3.2 環(huán)繞通知
環(huán)繞通知中可以手工書寫對原始方法的調(diào)用,使用try-catch可以獲取對應(yīng)的異常。
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object ret = null;
try {
ret = pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return ret;
}
總結(jié)
以上就是關(guān)于SpringAOP的全部內(nèi)容。
通過上邊的內(nèi)容可以知道環(huán)繞型通知屬于重點(diǎn),在開發(fā)中會經(jīng)常用到。
如果有什么問題,我們可以一起交流討論解決。
最后,希望可以幫助到有需要的碼友。