Spring學習總結(八):AOP介紹
一、AOP簡介
1、AOP基本介紹
Spring中的另一個重點是AOP。AOP 的全稱是“Aspect Oriented Programming”,即面向切面程式設計,它將業務邏輯的各個部分進行隔離,使開發人員在編寫業務邏輯時可以專心於核心業務,從而提高了開發效率。簡單來說就是不通過修改原始碼方式,在主幹功能裡面新增新功能。
在傳統的業務處理程式碼中,我們一般都會進行事務處理、日誌記錄等操作。以往通過使用OOP的思想可以以組合或者繼承的方式實現程式碼的重用,但如果要實現某個功能(如日誌記錄),此時同樣的程式碼就會分散到各個方法中。當想要關閉某個功能或者對功能進行修改,就必須要修改所有的相關方法。這不但增加了開發人員的工作量,而且提高了程式碼的出錯率。
AOP 採取橫向抽取機制,取代了傳統縱向繼承體系的重複性程式碼。這種橫向抽取機制能夠將影響到多個類的公共行為封裝到一個可重用的模組,並將其命名為“Aspect”,也就是我們所說的切面。切面指的是那些與業務無關,但被業務模組所共同呼叫的邏輯或責任,將它們封裝起來,從而減少系統中的重複程式碼,降低了模組之間的耦合度,有利於後續的可操作和可維護。
舉個例子:在某個系統中,日誌記錄是基礎功能,系統中的業務流程都需要進行日誌記錄。如果將日誌記錄功能寫到每一個業務流程中,就會造成程式碼冗餘,也增加了維護的難度。假設此時日誌記錄邏輯發生變化,那每個業務流程中的日誌記錄程式碼都需要進行修改,這種方式顯然不可取。相反,如果將日誌記錄功能抽取出來,作為獨立的模組,當業務流程需要的時候,系統自動將日誌記錄功能切入到業務流程中,這樣維護起來就方便多了。而這正式AOP所能實現的。如下圖:
2、AOP的相關術語
術語 | 描述 |
---|---|
橫切關注點 | 對哪些方法進行攔截,攔截後怎麼處理,這些關注點稱之為橫切關注點 |
Aspect(切面) | 通常是一個類,裡面可以定義切入點和通知。一個應用程式可以擁有任意數量的切面 |
JointPoint(連線點) | 程式執行過程中明確的點,即:業務流程在執行過程中需要插入切面的具體位置,一般是方法的呼叫。因為Spring只支援方法型別的連線點,所以在Spring中連線點指的就是被攔截到的方法,實際上連線點還可以是欄位或者構造器 |
Advice(通知) | 切面的具體實現方法,在攔截到 Joinpoint 之後要做的事情,即對切入點增強的內容 |
Pointcut(切入點) | 就是帶有通知的連線點,用於定義通知應該切入到哪些連線點上,不同的通知通常需要切入到不同的連線點上。在程式中主要體現為書寫切入點表示式。 |
Weave(織入) | 表示切入,也稱為織入。將切面應用到目標物件從而建立一個新的代理物件的過程。這個過程可以發生在編譯期、類裝載期及執行期 |
Introduction(引入) | 在不修改程式碼的前提下,引入可以在執行期為類動態地新增一些方法或欄位 |
代理(Proxy) | 表示代理物件。將通知應用到目標物件之後被動態建立的物件。可以簡單地理解為,代理物件為目標物件的業務邏輯功能加上被切入的切面所形成的物件。Spring中的AOP代理可以使JDK動態代理,也可以是CGLIB代理,前者基於介面,後者基於子類 |
目標物件(Target Object) | 包含連線點的物件,即被一個或者多個切面所通知的物件。也被稱作被通知或被代理物件 |
3、AOP中的通知型別
通知型別 | 描述 |
---|---|
前置通知(Before) | 在目標方法被呼叫之前做增強處理,@Before只需要指定切入點表示式即可 |
返回後通知(AfterReturning) | 在目標方法正常完成後做增強,@AfterReturning除了指定切入點表示式後,還可以指定一個返回值形參名returning,代表目標方法的返回值 |
丟擲異常後通知(AfterThrowing) | 主要用來處理程式中未處理的異常,@AfterThrowing除了指定切入點表示式後,還可以指定一個throwing的返回值形參名,可以通過該形參名來訪問目標方法中所丟擲的異常物件 |
後置通知(After) | 在目標方法完成之後做增強,無論目標方法時候成功完成。@After可以指定一個切入點表示式 |
環繞通知(Around) | 在目標方法完成前後做增強處理,環繞通知是最重要的通知型別,像事務、日誌等都是環繞通知 |
4、切入點表示式
切入點指示符用來指示切入點表示式目的,在Spring AOP中目前只有執行方法這一個連線點。切入點表示式的格式如下:
execution([可見性] 返回型別 [宣告型別].方法名(引數) [異常]),其中[]中的為可選,其他的還支援萬用字元的使用:
*:匹配所有字元
..:一般用於匹配多個包,多個引數
+:表示類及其子類
切入點表示式中還可以使用運算子,如:&&、||、!
下面舉個例子,來說明切入點表示式的使用:
(1)標準的表示式寫法:public void com.yht.controller.UserController.findUser()
(2)全通配寫法:*..*.*(..)
(3)省略訪問修飾符:voidcom.yht.controller.UserController.findUser()
(4)返回值使用萬用字元*:*com.yht.controller.UserController.findUser()
(5)包名使用萬用字元*:* *.*.*.UserController.findUser() //有幾級包就寫幾個*
(6)類名和方法名使用萬用字元*:*..*.*()
(7)引數列表
如果有引數,在方法的()加入*——(*)
有無引數均可:在方法的()加入..——(..)
(8)通常寫法:* com.yht.controller.UserController.*(..)
關於切入點表示式的其他介紹可參考一下部落格:
https://blog.51cto.com/lavasoft/172292
https://www.cnblogs.com/sjqq/p/10241781.html
二、Spring中的代理模式
前面介紹了:Spring中的AOP代理可以使JDK動態代理,也可以是CGLIB代理,前者基於介面,後者基於子類 。接下來就對這兩種代理方式進行實現。
1、JDK動態代理
(1)建立IOderDao介面及其子類OrderDaoImpl。
public interface IOrderDao {
void addOrder();
void deleteOrder();
void updateOrder();
void searchOrder();
}
public class OrderDaoImpl implements IOrderDao {
@Override
public void addOrder() {
System.out.println("新增訂單");
}
@Override
public void deleteOrder() {
System.out.println("刪除訂單");
}
@Override
public void updateOrder() {
System.out.println("更新訂單");
}
@Override
public void searchOrder() {
System.out.println("查詢訂單");
}
}
(2)建立切面類 MyAspect
public class MyAspect {
public void before(){
System.out.println("方法執行之前");
}
public void after(){
System.out.println("方法執行之後");
}
}
(3)建立代理類 MyBeanFactory
public class MyBeanFactory {
public static IOrderDao getBean() {
// 準備目標類
final IOrderDao customerDao = new OrderDaoImpl();
// 建立切面類例項
final MyAspect myAspect = new MyAspect();
// 使用代理類,進行增強
//Proxy 的 newProxyInstance() 方法的第一個引數是當前類的類載入器,第二引數是所建立例項的實現類的介面,第三個引數就是需要增強的方法
return (IOrderDao) Proxy.newProxyInstance(
MyBeanFactory.class.getClassLoader(),
new Class[] { IOrderDao.class }, new InvocationHandler() {
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
myAspect.before(); // 前增強
Object obj = method.invoke(customerDao, args);
myAspect.after(); // 後增強
return obj;
}
});
}
}
(4)進行測試。
@Test
public void test() {
// 從工廠獲得指定的內容(相當於spring獲得,但此內容時代理物件)
IOrderDao orderDao = MyBeanFactory.getBean();
// 執行方法
orderDao.updateOrder();
System.out.println("=====================");
orderDao.searchOrder();
}
結果如下:
2、cglib動態代理
(1)引入jar包
<!-- https://mvnrepository.com/artifact/cglib/cglib-nodep -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.1</version>
</dependency>
(2)建立目標類 Goods
public class Goods{
public void addGoods() {
System.out.println("新增貨物");
}
public void deleteGoods() {
System.out.println("刪除貨物");
}
public void updateGoods() {
System.out.println("更新貨物");
}
public void searchGoods() {
System.out.println("查詢貨物");
}
}
(3)建立代理類 GoodsBeanFactory
public class GoodsBeanFactory {
public static Goods getBean() {
// 準備目標類
final Goods goodsDao = new Goods();
// 建立切面類例項
final MyAspect myAspect = new MyAspect();
// 生成代理類,CGLIB在執行時,生成指定物件的子類,增強
Enhancer enhancer = new Enhancer();
// 確定需要增強的類
enhancer.setSuperclass(goodsDao.getClass());
// 添加回調函式
enhancer.setCallback(new MethodInterceptor() {
// intercept 相當於 jdk invoke,前三個引數與 jdk invoke—致
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
myAspect.before(); // 前增強
Object obj = method.invoke(goodsDao, args); // 目標方法執行
myAspect.after(); // 後增強
return obj;
}
});
// 建立代理類
Goods goodsDaoProxy = (Goods) enhancer.create();
return goodsDaoProxy;
}
}
(4)進行測試
@Test
public void testGoods() {
// 從工廠獲得指定的內容(相當於spring獲得,但此內容時代理物件)
Goods goods = GoodsBeanFactory.getBean();
// 執行方法
goods.deleteGoods();
System.out.println("=====================");
goods.searchGoods();
}
結果如下: