淺談java代理模式
講解java代理模式
目錄何謂代理模式
代理模式
,即Proxy Pattern,23種java常用設計模式之一。代理模式提供了對目標物件額外的訪問方式,即通過代理物件訪問目標物件,這樣可以在不修改原目標物件的前提下,提供額外的功能操作,擴充套件目標物件的功能。
代理模式的主要作用是為其他物件提供一種代理以控制對這個物件的訪問。在某些情況下,一個物件不想或者不能直接引用另一個物件,而代理物件可以在客戶端和目標物件之間起到中介的作用。
通俗來說,代理模式就相當於我們現實生活中的中介。舉個例子,比如我們想要購買某件東西,比如衣服,電腦等,我們一般都不會選擇直接去練習廠家購買,而是從廠家的代理商那裡去買。比如我們在淘寶上買東西,淘寶肯定是不生產衣服的,他只是在我們和廠商之間搭橋,這裡就可以認為淘寶起的就是代理的作用。
那麼在我們軟體開發中,如何應用代理模式呢?他又具體分為哪些呢?
代理模式
是面向物件程式設計中比較常見的一種設計模式,這是常見代理模式的UML示意圖:
在代理模式中:
- 使用者只關心具體的功能,而並不關心到底是誰提供的,即被代理的介面
Subject
- 上圖得知,真正實現者是
RealSubject
,但是他並不會與使用者直接接觸,而是交給Proxy
代理 - 代理就是上圖中的
Proxy
,它也實現了Subject 介面
,所以它能夠直接與使用者接觸 - 使用者呼叫
Proxy
的時候,其實內部呼叫了RealSubject
,所以Proxy
相當於中介,它可以增強RealSubject
操作
代理模式的作用:
- 功能增強:原有功能新增額外功能
- 控制訪問:代理類不讓你訪問目標
在java中,實現代理模式主要有兩種方式:靜態代理,動態代理,下面逐一進行講解。
這裡我先宣告幾個例類,方便下面演示:
-
建立一個介面類
UserDao
:package com.soberw.example.dao; /** * @author soberw * @Classname UserDao * @Description * @Date 2022-02-13 13:50 */ public interface UserDao { void show(); }
-
建立一個實現類
UserDaoImpl
,即被代理的目標物件package com.soberw.example.dao; /** * @author soberw * @Classname UserDaoImpl * @Description * @Date 2022-02-13 13:51 */ public class UserDaoImpl implements UserDao{ //實現了某一功能 @Override public void show() { System.out.println(" show something ...... "); } }
靜態代理
之所以是靜態代理,是因為他是在事先預定好的,即程式在執行之前,我們就確定了委託物件,通過代理類,我們可以實現在不改變原來功能的基礎上,對原有介面功能的功能進行再拓展。
以上面例子說明:
-
此時如果呼叫show()方法自然會正常執行方法流程
建立測試方法執行:
@Test public void testProxy1(){ UserDao userDaoImpl = new UserDaoImpl(); userDaoImpl.show(); }
-
但是現在想在方法中新增新的實現功能,我們這時候當然可以直接去修改實現類UserDaoImpl的原始碼,但是會導致:
-
- 原始碼可能改動比較多
-
- 重複程式碼比較多
-
- 程式碼維護性低
-
-
因此我們可以使用代理完成對功能的拓展,那麼如何通過靜態代理實現呢?
-
使用
靜態代理
,需要我們代理物件和目標物件都實現同樣的介面 -
因此,這裡我宣告一個代理類
UserProxyStatic
package com.soberw.example.proxy; import com.soberw.example.dao.UserDao; /** * @author soberw * @Classname UserProxy * @Description * @Date 2022-02-13 13:54 */ public class UserProxyStatic implements UserDao { private UserDao userDao; public UserProxyStatic(UserDao userDao){ this.userDao = userDao; } @Override public void show() { //加入額外的拓展功能 System.out.println("something in show() before..."); userDao.show(); System.out.println("something in show() after..."); } }
-
測試執行:
@Test public void testProxy2(){ UserDao userDaoImpl = new UserDaoImpl(); UserDao userProxy = new UserProxyStatic(userDaoImpl); userProxy.show(); }
這就實現了對原有功能的拓展,並且不改變原始碼。
總結一下:使用靜態代理可以在不修改目標物件的前提下拓展目標物件的功能
但是其確定也非常明顯:
- 冗餘。由於代理物件要實現與目標物件一致的介面,會產生過多的代理類。
- 不易維護。一旦原介面增加方法,目標物件與代理物件都要進行修改。
動態代理
於是為了解決靜態代理的一些弊端,就有了動態代理
,將程式的執行抉擇放到了執行時,動態地建立代理物件,從而實現對目標物件的代理功能。
靜態代理與動態代理的主要區別在於:
- 靜態代理在編譯時已經實現,編譯後會生成對應的實際的class檔案
- 動態代理是在執行時產生的,是在執行時動態的生成類位元組碼,並載入到了JVM中
- 動態代理實際上用的就是反射的機制
而實現動態代理的方式又可分為兩種:
- JDK動態代理
- CGLIB動態代理
JDK動態代理
JDK動態代理,實際上就是使用java官方給我們提供的一些API去實現的,涉及到的類都存在於java.lang.reflect包下
Proxy 核心類,通過呼叫此類的newInstance()
靜態方法生成代理類
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
引數說明:
ClassLoader loader
:類載入器,通過反射獲取物件並向記憶體中載入Class<?>[] interfaces
:介面集合,增強方法所在的類,即目標物件所實現的介面InvocationHandler h
:具體要完成哪些功能,即代理物件返回值說明:
返回指定的介面的一個代理類例項物件
InvocationJandler 呼叫處理器,內部就一個方法invoke()
,表示代理物件要執行的具體的方法,相當於中介類,呼叫目標方法,可在invoke()內部寫入我們的增強功能:
public Object invoke(Object proxy, Method method, Object[] args)
引數說明:
Object proxy
:代理的物件Method method
:代理物件呼叫的方法Object[] args
:方法引數返回代理的物件
Method 協助代理類完成方法的呼叫操作,主要用的是內部的invoke()
方法,注意,該invoke方法時具體的方法,而上面的invoke()是一個介面方法:
public Object invoke(Object obj, Object... args)
引數說明:
Object obj
:呼叫的物件Object... args
:方法引數返回方法的返回值
實際上:
如果我們的目標物件中有多個方法都需要增強業務,我們可以通過
method.getName()
方法獲取方法名,然後根據不同的方法完成不同的操作邏輯。
具體實現如下:
-
建立一個類作為代理類
UserDaoProxy
:package com.soberw.example.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * @author soberw * @Classname UserDaoProxy * @Description * @Date 2022-02-13 17:13 */ public class UserDaoProxy { private UserDaoProxy(){ } //傳入目標物件,返回代理後的物件 public static Object getProxyInstance(Object obj) { //呼叫代理類,傳入引數,其中包括一個內部匿名類 return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在方法之前加入額外功能 System.out.println("something in show() before..."); //呼叫原本的方法,並接收返回值 Object result = method.invoke(obj, args); //在方法之後加入額外功能 System.out.println("something in show() after..."); //返回新方法的返回值 return result; } }); } }
-
測試起來也很簡單:
@Test public void testProxy3(){ //建立目標物件例項並傳入即得到代理物件 UserDao userDao = new UserDaoImpl(); UserDao dao = (UserDao) UserDaoProxy.getProxyInstance(userDao); dao.show(); }
-
但是實際上我們一般也不會去建立內部匿名類,下面提供另外一種寫法:
-
建立一個
InvocationJandler
的實現類UserProxyDynamic
:package com.soberw.example.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * @author soberw * @Classname UserProxyDynamic * @Description * @Date 2022-02-13 15:56 */ public class UserProxyDynamic implements InvocationHandler { //建立的是誰的代理物件,就把誰傳遞進來 Object obj; //有引數構造傳遞 public UserProxyDynamic(Object obj){ this.obj = obj; } //增強的邏輯 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在方法之前加入額外功能 System.out.println("something in show() before..."); //呼叫原本的方法,並接收返回值 Object result = method.invoke(obj, args); //在方法之後加入額外功能 System.out.println("something in show() after..."); //返回新方法的返回值 return result; } }
-
進行測試:
@Test public void testProxy4(){ //建立目標物件例項並傳入即得到代理物件 UserDao userDao = new UserDaoImpl(); UserDao dao = (UserDao) Proxy.newProxyInstance(userDao.getClass().getClassLoader(), userDao.getClass().getInterfaces(), new UserProxyDynamic(userDao)); dao.show(); }
動態代理物件不需要實現介面,但是要求目標物件必須實現介面InvocationJandler,否則不能使用動態代理。
CGLIB動態代理
CGLIB
(Code Generation Library )是一個第三方程式碼生成類庫,執行時在記憶體中動態生成一個子類物件從而實現對目標物件功能的擴充套件。
前面說到限制動態代理的物件必須實現一個或多個介面,而使用CGLIB
就不用,真正的達到了代理類無侵入
。
使用CGLIB需要引入CGLIB的jar包,或者如果你是maven專案,引入依賴:
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
還是以實現上面例子為目標:
-
建立一個代理物件類
UserDaoCGLIB
,實現CGLIB提供的類MethodInterceptor
:package com.soberw.example.proxy; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * @author soberw * @Classname UserDaoCGLIB * @Description * @Date 2022-02-13 18:42 */ public class UserDaoCGLIB implements MethodInterceptor { private Object obj; public UserDaoCGLIB(Object obj) { this.obj = obj; } public Object getProxyInstance() { //工具類 Enhancer en = new Enhancer(); //設定父類 en.setSuperclass(obj.getClass()); //設定回撥函式 en.setCallback(this); //建立子類物件代理 return en.create(); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { //在方法之前加入額外功能 System.out.println("something in show() before..."); //呼叫原本的方法,並接收返回值 Object result = method.invoke(obj, objects); //在方法之後加入額外功能 System.out.println("something in show() after..."); //返回新方法的返回值 return result; } }
-
測試:
@Test public void testProxy5(){ UserDaoImpl userDao = new UserDaoImpl(); UserDao dao = (UserDao) new UserDaoCGLIB(userDao).getProxyInstance(); dao.show(); }
CGLIB代理無需實現介面,通過生成類位元組碼實現代理,比反射稍快,不存在效能問題,但CGLIB會繼承目標物件,需要重寫方法,所以目標物件不能為final類。
但是毋庸置疑的是,CGLIB是一個強大的高效能的程式碼生成包,它可以在執行期擴充套件Java類與實現Java介面。
它廣泛的被許多AOP的框架使用,例如Spring AOP和dynaop,為他們提供方法的interception(攔截)。