AOP之動態代理
什麼是AOP
在上一章的學習中,我們知道Spring一直致力於簡化我們的Java開發,並且用到了依賴注入(Dependency Injection)與AOP(Aspect-Oriented Programming)這兩項非常重要的技術:
-
DI主要解決了在類和類之間有依賴關係的時候,如何通過注入的方式(屬性注入、構造器注入)形成鬆耦合
-
而今天要學習的AOP則是考慮如何把散落在應用中多處相同的功能剝離出來,使得這些剝離出來的邏輯與業務邏輯相分離的問題。
讓我們先來看一個生活中的案例:
每家每戶都有一個電錶來監控用電量,這樣電力公司就知道應該收取多少費用了。雖然每一臺用電裝置都可以自裝一個用電統計的硬體,但這樣是極不合理,成本上也不划算的,因為每個用電裝置更多關注的是自身的功能是否完善的問題,吸塵器考慮的是清潔效果,微波爐考慮的是加熱效果等等。電力公司只需要在合適的地方,比如用電線路入戶的地方,統一安裝一個電錶,就能很好的解決監控所有用電裝置電量的問題。
軟體系統中的一些功能就像我們家裡的電錶一樣。這些功能需要用到應用程式的多個地方,但是我們又不想在每個點都明確呼叫它們。
在軟體開發中,散佈於應用中多處的功能,被稱為橫切關注點(cross-cutting concern)。通常來講,這些橫切關注點從概念上是與應用的業務邏輯相分離的(但往往會直接嵌入到應用的業務邏輯中)。把這些橫切關注點與業務邏輯相分離正是AOP所要解決的問題。
代理模式
-
代理模式的概念
AOP在實現上採用了設計模式中的動態代理模式,因此,在深入學習SpringAOP之前,我們先來一起了解和學習一下這種強大的設計模式。
代理模式的定義:為其他物件提供一種代理,以控制對這個物件的訪問。換句通俗的話來說,它是一種使用代理物件來執行目標物件的方法,並在代理物件中增強目標物件方法的一種設計模式。
生活中最常見的代理模式就是”中介“。假如說我現在想買一輛二手車,雖然我可以自己去找車源,做質量檢測等一系列的車輛過戶流程,但是這確實太浪費我得時間和精力了。我只是想買一輛車而已為什麼我還要額外做這麼多事呢?於是我就通過中介公司來買車,他們來給我找車源,幫我辦理車輛過戶流程,我只是負責選擇自己喜歡的車,然後付錢就可以了。
代理模式的功能主要是起到增強方法和許可權攔截的作用。
-
代理模式的好處
從上圖我們可以看出,在程式設計中使用代理模式的一些好處:
- 中介隔離:在呼叫方(Caller)不能或不想直接與目標物件(Target)打交道的時候,代理物件(Proxy)可以起到兩者之間中介的作用。
- 開閉原則:我們可以通過給代理物件增加新的功能來擴充套件目標物件的功能,這樣我們只需要修改代理類,而不需要修改目標類,符合程式碼設計的OCP原則(Open Closed Principle,對擴充套件是開放的,對修改是關閉的)。
-
代理模式的種類
根據代理物件建立的不同,分為兩種代理模式:
- 靜態代理:由程式設計師或者特定工具生成原始碼來產生代理物件。在程式執行前,代理類的位元組碼檔案(.class)就已經存在了
- 動態代理:在程式執行期間,運用反射機制、位元組碼生成技術來產生代理類和例項。
靜態代理模式
Proxy—代理
要實現靜態代理,我們首先使用介面的方式來封裝被代理的行為:
介面:
public interface IUserDao {
void save();
}
分別讓目標和代理都來實現這個介面,這樣,對於呼叫方來說,無論和代理還是目標打交道,執行的程式碼都是一致的,都可以執行相同的行為。
public class UserDaoImpl implements IUserDao {
@Override
public void save() {
System.out.println("使用者資料儲存!");
}
}
為目標方編寫對應的代理類,在其中增加新的服務功能:
package com.tuling.dao;
public class UserDaoProxy implements IUserDao{
private IUserDao userDao;
public UserDaoProxy(IUserDao userDao) {
this.userDao = userDao;
}
@Override
public void save() {
System.out.println("開啟事務...");
userDao.save();//執行目標物件方法
System.out.println("提交事務...");
}
}
從上面可以看到,我們最終讓客戶執行了購買行為,除此之外,為了讓客戶享受更為完善的服務,我們還擴充套件了尋找車源、質量檢測、售後諮詢等其它服務。在一個複雜的應用中,我們當然不是僅僅列印幾行字,我們可以封裝單獨的方法來做這些事情,甚至還可以呼叫其它的類來執行這些輔助邏輯。
測試程式碼:
package com.tuling.dao;
public class MainTest {
public static void main(String[] args) {
//目標物件
UserDaoImpl userDao = new UserDaoImpl();
//代理物件
UserDaoProxy proxy = new UserDaoProxy(userDao);
//執行代理物件的方法
proxy.save();
}
/*
* 靜態代理總結:
* 1、可以做到在不修改目標物件的功能前提下,增強目標物件。
* 2、但是由於要和目標物件實現相同的介面,如果目標方法過多,或
者
* 目標物件一旦新增方法,代理物件都要一併維護。
*/
}
動態代理模式
靜態代理模式最大的缺陷就是,我們需要為每一個被代理的目標類都編寫一個代理類。而動態代理可以很好的解決這個問題。JDK的Proxy和開源框架CGLIB可以分別在不同的情況幫助我們生成代理。
當目標的被代理方法抽取了介面時,可以使用JDK Proxy。
介面:
package com.tuling.proxy;
/**
* 介面
* @author fred
*
*/
public interface IUserDao {
void save();
}
目標類:
package com.tuling.proxy;
public class UserDaoImpl implements IUserDao {
@Override
public void save() {
System.out.println("使用者資料儲存!");
}
}
測試類:
package com.tuling.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MainTest {
public static void main(String[] args) {
//目標物件
UserDaoImpl userDao = new UserDaoImpl();
//代理物件
IUserDao proxy = (IUserDao) Proxy.newProxyInstance(
userDao.getClass().getClassLoader(), //目標對
象的類載入器
userDao.getClass().getInterfaces(), //目標對
象的介面陣列
new InvocationHandler() {//事件處理,執行目標對
象的方法時,會觸invoke方法
@Override
public Object invoke(Object proxy, Method
method, Object[] args) throws Throwable {
System.out.println("開啟事務...");
//執行目標物件方法
Object obj = method.invoke(userDao,
args);
System.out.println("提交事務...");
return obj;
}
});
//呼叫代理物件方法
proxy.save();
/*
* JDK動態代理總結:
* 1、目標物件必須要有介面,否則不能實現動態代理。
* 2、代理物件必須強轉為介面型別。
*/
}
}
invoke
方法會在代理物件的被代理方法呼叫的時候觸發。
CGLIB
當目標類沒有實現介面時,我們可以通過開源的CGLIB來實現。
使用maven引入cglib庫:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
也可以引入spring-core庫,該庫中包含了CGLIB。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.2.RELEASE</version>
</dependency>
目標類:
package com.tuling.cglib;
/**
* 目標物件,沒有任何介面
* @author fred
*
*/
public class UserDaoImpl{
public void save() {
System.out.println("使用者資料儲存!");
}
}
代理工廠類:
package com.tuling.cglib;
import java.lang.invoke.MethodHandleInfo;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class ProxyFactory implements MethodInterceptor{
//目標物件
private Object target;
public ProxyFactory(Object target) {
super();
this.target = target;
}
//給目標物件建立一個代理物件
public Object getProxyInstance(){
//1.增強器
Enhancer en = new Enhancer();
//2.設定目標物件的類載入器
en.setClassLoader(target.getClass().getClassLoader());
//3.設定這個動態代理類的父類
en.setSuperclass(target.getClass());
//4.設定要傳入的攔截器
en.setCallback(this);
//5.建立子類(代理物件)
return en.create();
}
@Override
public Object intercept(Object arg0, Method method,
Object[] args, MethodProxy arg3) throws Throwable {
System.out.println("開啟事務...");
//執行目標物件的方法
Object obj = method.invoke(target, args);
System.out.println("提交事務...");
return obj;
}
}
測試類:
package com.tuling.cglib;
public class MainTest {
public static void main(String[] args) {
//目標物件
UserDaoImpl userDao = new UserDaoImpl();
//代理物件
UserDaoImpl proxy =
(UserDaoImpl) new ProxyFactory(userDao).getProxyInstance();
//執行代理物件方法
proxy.save();
}
}
總結
-
靜態代理需要自己手動編寫代理類和目標方法。
-
動態代理就不需要自己手動實現代理類和目標方法,但動態代理的目標類要必須實現介面!
-
Cglib 代理的目標類就不需要實現介面!但目標類不能被final修飾!
Spring AOP 程式設計的實現原理就是 動態代理和Cglib 代理,當目標類實現介面時使用動態代理,沒有則Cglib代理。