代理模式
代理模式是一種結構性設計模式,讓你能夠提供對象的替代品或其占位符。代理控制着對於原對象的訪問,並允許在將請求提交給對象前后進行一些處理。
代理模式結構
- 服務接口(ServiceInterface) 聲明了服務接口提供的功能。代理必須遵循該接口才能偽裝成對象
- 服務(Service)類,提供具體的一些實用的業務邏輯
- 代理(Proxy)類包含一個指向服務對象的引用成員變量,代理完成其交代的任務(例如延遲加載,記錄日志,訪問控制或者緩存等)后會將請求傳遞給服務對象,通常情況下,代理會對其服務對象的整個聲明周期進行管理。
- 客戶端(Client) 能通過同一接口與服務或與代理進行交互,所以你可以在一些需要服務對象的代碼中實用代理。
案例分析
我們有一個常用的數據庫訪問接口,大量的客戶端都是對數據庫進行直接的訪問,對系統資源的消耗特別大,並且有很多的重復查詢操作。
直接訪問數據庫,可能會非常的慢
這時候我們考慮加入緩存,當需要重復的查詢時直接從緩存中獲取數據返回到客戶端,節省系統開銷,並記錄一下每一個客戶端訪問花費的時間。
代理模式建議新建一個與原服務對象接口相同的代理類, 然后更新應用以將代理對象傳遞給所有原始對象客戶端。 代理類接收到客戶端請求后會創建實際的服務對象, 並將所有工作委派給它。
代理將自己偽裝成數據庫對象,可以在客戶端不知道的情況下做緩存查詢操作並記錄其訪問時間或日志
代碼實現
定義查詢數據庫的接口
public interface DataService {
// 通過ID查詢數據
String getById(Integer id);
}
具體的數據庫查詢業務類
public class DataServiceImpl implements DataService{
// 模擬數據
final Map<Integer,String> dataMap = new HashMap<Integer,String>(){{
for (int i = 0; i < 10; i++) {
put(i,"data_"+ i);
}
}};
@Override
public String getById(Integer id) {
// 模擬數據庫查詢的耗時
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return dataMap.get(id);
}
}
創建代理類,偽裝業務類
public class DataServiceProxy implements DataService{
DataService dataService;
// 緩存
Map<Integer,String> cacheMap = new HashMap<>();
public DataServiceProxy(DataService dataService) {
this.dataService = dataService;
}
@Override
public String getById(Integer id) {
// 記錄訪問的開始時間
final long start = System.currentTimeMillis();
String result = null;
// 優先從緩存獲取
String cache = getCache(id);
if (cache == null){
result = dataService.getById(id);
// 放入緩存中
putCache(id,result);
}else {
result = cache;
}
final long end = System.currentTimeMillis();
System.out.println("耗時:" + (end - start) + "ms");
return result;
}
// 緩存信息
private void putCache(Integer id,String value){
cacheMap.put(id,value);
}
// 獲取緩存信息
private String getCache(Integer id){
return cacheMap.get(id);
}
}
客戶端
@Test
public void ProxyTest() {
DataService dataService = new DataServiceImpl();
DataServiceProxy dataServiceProxy = new DataServiceProxy(dataService);
dataServiceProxy.getById(1);
// 第二次查詢
dataServiceProxy.getById(1);
dataServiceProxy.getById(1);
}
這種代理模式的設計方式,我們一般稱之為靜態代理:由編碼人員創建完成或由特定工具生成源代碼,在編譯時就已經將接口、被代理類、代理類等確定類下來,在程序運行之前,代理類的字節碼文件已經生成了。如果由其他的代理內容,可能需要新建很多的代碼來實現。
動態代理
與靜態代理最大的區別在於,動態代理類是在程序運行時創建的代理。例如在上面的例子中DataServiceProxy
代理類是我們自己定義的,在程序運行之前就已經編譯完成。在動態代理中,代理類不是在代碼中定義,而是在程序運行時根據我們的需要在Java
代碼中動態生成的。
在Java
中我們提到動態代理,一般繞不開JDK
動態代理和CGLIB
動態代理。
JDK動態代理
利用JDK
自帶的代理類來完成,相當於利用一個攔截器(需實現接口InvocationHanlder
)配合反射機制生成一個實現代理類的匿名接口,在調用具體的方法前調用InvocationHanlder
來處理。
我們依舊使用DataService
接口和DataServiceImpl
業務類來完成一個動態代理的案例。
- 創建被代理類的接口和業務類(已經有了)
- 創建
InvocationHanlder
接口的實現類,在invoke
方法中實現代理的邏輯 - 通過
Proxy
的靜態方法newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)
創建一個代理對象。
public class JDKProxy implements InvocationHandler {
// 被代理對象
private Object object;
// 緩存
Map<Integer,String> cacheMap = new HashMap<>();
public JDKProxy(Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只代理其中的查詢方法
if (method.getName().equals("getById")){
// 參數
Integer id = (Integer) args[0];
// 記錄訪問的開始時間
final long start = System.currentTimeMillis();
String result = null;
// 優先從緩存獲取
String cache = getCache(id);
if (cache == null){
// 代理執行
result =(String) method.invoke(object,args);
// 放入緩存中
putCache(id,result);
}else {
result = cache;
}
final long end = System.currentTimeMillis();
System.out.println("耗時:" + (end - start) + "ms");
return result;
}else {
return method.invoke(object,args);
}
}
// 緩存信息
private void putCache(Integer id,String value){
cacheMap.put(id,value);
}
// 獲取緩存信息
private String getCache(Integer id){
return cacheMap.get(id);
}
}
InvocationHandler接口詳解
InvocationHandler
接口是proxy
代理實例的調用處理程序實現的一個接口,每一個proxy
代理實例都有一個關聯的調用處理程序;在代理實例調用方法(Method
)時,方法調用被編碼分派到調用處理程序的invoke
方法。
每一個動態代理類的調用處理程序都必須實現InvocationHandler
接口,並且每個代理類的實例都關聯到了實現該接口的動態代理類調用處理程序中,當我們通過動態代理對象調用一個方法時候,這個方法的調用就會被轉發到實現InvocationHandler
接口類的invoke
方法來調用,看如下invoke
方法:
/**
* proxy:代理類代理的真實代理對象com.sun.proxy.$Proxy0(按次序進行,每生成一個 +1)
* method:我們所要調用某個對象真實的方法的Method對象
* args:指代代理對象方法傳遞的參數
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
Client
客戶端在調用時的方式也和靜態代理不一樣,最終是使用代理類$Proxy
來進行方法的調用
@Test
public void JDKProxyTest() {
DataService dataService = new DataServiceImpl();
JDKProxy jdkProxy = new JDKProxy(dataService);
// 獲取代理對象
DataService dataServiceProxy = (DataService) Proxy.newProxyInstance(DataService.class.getClassLoader(), new Class[]{DataService.class}, jdkProxy);
dataServiceProxy.getById(1);
dataServiceProxy.getById(1);
}
其運行的結果是一樣的,都完成了代理內容。
Proxy類詳解
Proxy
類就是用來創建一個代理對象的類,它提供了很多方法,我們最常用的是newProxyInstance
方法。
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
newProxyInstance
就是創建一個代理類對象,它接收三個參數:
loader
:指定代理類的類加載器(我們傳入當前測試類的類加載器)interfaces
:一個interface
對象數組,代理類需要實現的接口(我們傳入被代理類實現的接口,這樣生成的代理類和被代理類就實現了相同的接口)h
:一個InvocationHandler
對象,表示的是當動態代理對象調用方法的時候會關聯到哪一個InvocationHandler
對象上,用來處理方法的調用。這里傳入我們自己實現的handler
CGLIB動態代理
利用asm
開源包,對代理對象類的class
文件加載進來,通過修改其字節碼生成子類來處理。
- 導入
cglib-xxx.jar
包,這里包含了asm
和cglib
- 創建
MethodInterceptor
接口的實現類,在intercept
方法中實現代理的邏輯 - 編寫
getCglibProxy
方法(自定義)返回代理類對象
Pom導入cglb
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
重寫MethodInterceptor
public class CglibProxy implements MethodInterceptor {
// 被代理對象,便於通用,可以寫成Object
private Object object;
// 緩存
Map<Integer,String> cacheMap = new HashMap<>();
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 只代理其中的查詢方法
if (method.getName().equals("getById")) {
// 參數
Integer id = (Integer) args[0];
// 記錄訪問的開始時間
final long start = System.currentTimeMillis();
String result = null;
// 優先從緩存獲取
String cache = getCache(id);
if (cache == null) {
result = (String)method.invoke(object,args);
// 放入緩存中
putCache(id, result);
} else {
result = cache;
}
final long end = System.currentTimeMillis();
System.out.println("耗時:" + (end - start) + "ms");
return result;
} else {
return method.invoke(object, args);
}
}
// 獲取代理對象 這里采用了范型的寫法,更直觀的傳入被代理類,然后返回代理對象
public <T> T getCglibProxy(T t){
this.object = t;//為目標對象target賦值
Enhancer enhancer = new Enhancer();
//設置父類,因為Cglib是針對指定的類生成一個子類,所以需要指定父類
enhancer.setSuperclass(object.getClass());
//設置回調
enhancer.setCallback(this);
//創建並返回代理對象
Object result = enhancer.create();
return (T) result;
}
// 緩存信息
private void putCache(Integer id,String value){
cacheMap.put(id,value);
}
// 獲取緩存信息
private String getCache(Integer id){
return cacheMap.get(id);
}
}
Client
@Test
public void CGLBProxyTest(){
// 被代理類 這里可以不用接口聲明哦
DataService dataService = new DataServiceImpl();
CglibProxy cglibProxy = new CglibProxy();
// 獲取代理對象
DataService proxy = cglibProxy.getCglibProxy(dataService);
proxy.getById(1);
proxy.getById(1);
}
可以發現兩種動態代理的寫法基本差不多,基本的思路都是生成代理類,攔截,反射,獲取真正的代理類方法,執行。那么兩種方式有什么區別和用法呢?
JDK代理和CGLIB代理的區別
JDK
動態代理只能對實現了接口的類生成代理,而不能針對類 ,使用的是Java
反射技術實現,生成類的過程比較高效。CGLIB
是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法 ,使用asm
字節碼框架實現,相關執行的過程比較高效,生成類的過程可以利用緩存彌補,因為是繼承,所以該類或方法最好不要聲明成final
JDK
代理是不需要第三方庫支持,只需要JDK
環境就可以進行代理CGLIB
必須依賴於CGLIB
的類庫,但是它需要類來實現任何接口代理的是指定的類生成一個子類,覆蓋其中的方法,是一種繼承但是針對接口編程的環境下推薦使用JDK
的代理;