微信原文:Java 動態代理詳解
博客原文:Java 動態代理詳解php
動態代理在Java中有着普遍的應用,好比Spring AOP、Hibernate數據查詢、測試框架的後端mock、RPC遠程調用、Java註解對象獲取、日誌、用戶鑑權、全局性異常處理、性能監控,甚至事務處理等。html
本文主要介紹Java中兩種常見的動態代理方式:JDK原生動態代理和CGLIB動態代理。java
因爲Java動態代理與java反射機制關係緊密,請讀者確保已經瞭解了Java反射機制,可參考上一篇文章《Java反射機制詳解》面試
本文將介紹的Java動態代理與設計模式中的代理模式有關,什麼是代理模式呢?數據庫
代理模式:給某一個對象提供一個代理,並由代理對象來控制對真實對象的訪問。代理模式是一種結構型設計模式。segmentfault
代理模式角色分爲 3 種:後端
Subject(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;設計模式
RealSubject(真實主題角色):真正實現業務邏輯的類;api
Proxy(代理主題角色):用來代理和封裝真實主題;數組
代理模式的結構比較簡單,其核心是代理類,爲了讓客戶端可以一致性地對待真實對象和代理對象,在代理模式中引入了抽象層
代理模式按照職責(使用場景)來分類,至少能夠分爲如下幾類:一、遠程代理。 二、虛擬代理。 三、Copy-on-Write 代理。 四、保護(Protect or Access)代理。 五、Cache代理。 六、防火牆(Firewall)代理。 七、同步化(Synchronization)代理。 八、智能引用(Smart Reference)代理等等。
若是根據字節碼的建立時機來分類,能夠分爲靜態代理和動態代理:
咱們先經過實例來學習靜態代理,而後理解靜態代理的缺點,再來學習本文的主角:動態代理
編寫一個接口 UserService ,以及該接口的一個實現類 UserServiceImpl
public interface UserService {
public void select();
public void update();
}
public class UserServiceImpl implements UserService {
public void select() {
System.out.println("查詢 selectById");
}
public void update() {
System.out.println("更新 update");
}
}
複製代碼
咱們將經過靜態代理對 UserServiceImpl 進行功能加強,在調用 select
和 update
以前記錄一些日誌。寫一個代理類 UserServiceProxy,代理類須要實現 UserService
public class UserServiceProxy implements UserService {
private UserService target; // 被代理的對象
public UserServiceProxy(UserService target) {
this.target = target;
}
public void select() {
before();
target.select(); // 這裏才實際調用真實主題角色的方法
after();
}
public void update() {
before();
target.update(); // 這裏才實際調用真實主題角色的方法
after();
}
private void before() { // 在執行方法以前執行
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() { // 在執行方法以後執行
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
複製代碼
客戶端測試
public class Client1 {
public static void main(String[] args) {
UserService userServiceImpl = new UserServiceImpl();
UserService proxy = new UserServiceProxy(userServiceImpl);
proxy.select();
proxy.update();
}
}
複製代碼
輸出
log start time [Thu Dec 20 14:13:25 CST 2018]
查詢 selectById
log end time [Thu Dec 20 14:13:25 CST 2018]
log start time [Thu Dec 20 14:13:25 CST 2018]
更新 update
log end time [Thu Dec 20 14:13:25 CST 2018]
複製代碼
經過靜態代理,咱們達到了功能加強的目的,並且沒有侵入原代碼,這是靜態代理的一個優勢。
雖然靜態代理實現簡單,且不侵入原代碼,可是,當場景稍微複雜一些的時候,靜態代理的缺點也會暴露出來。
一、 當須要代理多個類的時候,因爲代理對象要實現與目標對象一致的接口,有兩種方式:
二、 當接口須要增長、刪除、修改方法的時候,目標對象與代理類都要同時修改,不易維護。
固然是讓代理類動態的生成啦,也就是動態代理。
爲何類能夠動態的生成?
這就涉及到Java虛擬機的類加載機制了,推薦翻看《深刻理解Java虛擬機》7.3節 類加載的過程。
Java虛擬機類加載過程主要分爲五個階段:加載、驗證、準備、解析、初始化。其中加載階段須要完成如下3件事情:
java.lang.Class
對象,做爲方法區這個類的各類數據訪問入口因爲虛擬機規範對這3點要求並不具體,因此實際的實現是很是靈活的,關於第1點,獲取類的二進制字節流(class字節碼)就有不少途徑:
*$Proxy
的代理類的二進制字節流因此,動態代理就是想辦法,根據接口或目標對象,計算出代理類的字節碼,而後再加載到JVM中使用。可是如何計算?如何生成?狀況也許比想象的複雜得多,咱們須要藉助現有的方案。
這裏有一些介紹:java-source.net/open-source…
爲了讓生成的代理類與目標對象(真實主題角色)保持一致性,從如今開始將介紹如下兩種最多見的方式:
注:使用ASM對使用者要求比較高,使用Javassist會比較麻煩
JDK動態代理主要涉及兩個類:java.lang.reflect.Proxy
和 java.lang.reflect.InvocationHandler
,咱們仍然經過案例來學習
編寫一個調用邏輯處理器 LogHandler 類,提供日誌加強功能,並實現 InvocationHandler 接口;在 LogHandler 中維護一個目標對象,這個對象是被代理的對象(真實主題角色);在 invoke
方法中編寫方法調用的邏輯處理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;
public class LogHandler implements InvocationHandler {
Object target; // 被代理的對象,實際的方法執行者
public LogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args); // 調用 target 的 method 方法
after();
return result; // 返回方法的執行結果
}
// 調用invoke方法以前執行
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
// 調用invoke方法以後執行
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
複製代碼
編寫客戶端,獲取動態生成的代理類的對象須藉助 Proxy 類的 newProxyInstance 方法,具體步驟可見代碼和註釋
import proxy.UserService;
import proxy.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Client2 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
// 設置變量能夠保存動態代理類,默認名稱以 $Proxy0 格式命名
// System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 1. 建立被代理的對象,UserService接口的實現類
UserServiceImpl userServiceImpl = new UserServiceImpl();
// 2. 獲取對應的 ClassLoader
ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
// 3. 獲取全部接口的Class,這裏的UserServiceImpl只實現了一個接口UserService,
Class[] interfaces = userServiceImpl.getClass().getInterfaces();
// 4. 建立一個將傳給代理類的調用請求處理器,處理全部的代理對象上的方法調用
// 這裏建立的是一個自定義的日誌處理器,須傳入實際的執行對象 userServiceImpl
InvocationHandler logHandler = new LogHandler(userServiceImpl);
/* 5.根據上面提供的信息,建立代理對象 在這個過程當中, a.JDK會經過根據傳入的參數信息動態地在內存中建立和.class 文件等同的字節碼 b.而後根據相應的字節碼轉換成對應的class, c.而後調用newInstance()建立代理實例 */
UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
// 調用代理的方法
proxy.select();
proxy.update();
// 保存JDK動態代理生成的代理類,類名保存爲 UserServiceProxy
// ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");
}
}
複製代碼
運行結果
log start time [Thu Dec 20 16:55:19 CST 2018]
查詢 selectById
log end time [Thu Dec 20 16:55:19 CST 2018]
log start time [Thu Dec 20 16:55:19 CST 2018]
更新 update
log end time [Thu Dec 20 16:55:19 CST 2018]
複製代碼
InvocationHandler 和 Proxy 的主要方法介紹以下:
java.lang.reflect.InvocationHandler
Object invoke(Object proxy, Method method, Object[] args)
定義了代理對象調用方法時但願執行的動做,用於集中處理在動態代理類對象上的方法調用
java.lang.reflect.Proxy
static InvocationHandler getInvocationHandler(Object proxy)
用於獲取指定代理對象所關聯的調用處理器
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
返回指定接口的代理類
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
構造實現指定接口的代理類的一個新實例,全部方法會調用給定處理器對象的 invoke 方法
static boolean isProxyClass(Class<?> cl)
返回 cl 是否爲一個代理類
生成的代理類到底長什麼樣子呢?藉助下面的工具類,把代理類保存下來再探個究竟
(經過設置環境變量sun.misc.ProxyGenerator.saveGeneratedFiles=true也能夠保存代理類)
import sun.misc.ProxyGenerator;
import java.io.FileOutputStream;
import java.io.IOException;
public class ProxyUtils {
/** * 將根據類信息動態生成的二進制字節碼保存到硬盤中,默認的是clazz目錄下 * params: clazz 須要生成動態代理類的類 * proxyName: 爲動態生成的代理類的名稱 */
public static void generateClassFile(Class clazz, String proxyName) {
// 根據類信息和提供的代理類名稱,生成字節碼
byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
String paths = clazz.getResource(".").getPath();
System.out.println(paths);
FileOutputStream out = null;
try {
//保留到硬盤中
out = new FileOutputStream(paths + proxyName + ".class");
out.write(classFile);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
複製代碼
而後在 Client2 測試類的main的最後面加入一行代碼
// 保存JDK動態代理生成的代理類,類名保存爲 UserServiceProxy
ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");
複製代碼
IDEA 再次運行以後就能夠在 target 的類路徑下找到 UserServiceProxy.class,雙擊後IDEA的反編譯插件會將該二進制class文件
UserServiceProxy 的代碼以下所示:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.UserService;
public final class UserServiceProxy extends Proxy implements UserService {
private static Method m1;
private static Method m2;
private static Method m4;
private static Method m0;
private static Method m3;
public UserServiceProxy(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
// 省略...
}
public final String toString() throws {
// 省略...
}
public final void select() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
// 省略...
}
public final void update() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("proxy.UserService").getMethod("select");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m3 = Class.forName("proxy.UserService").getMethod("update");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
複製代碼
從 UserServiceProxy 的代碼中咱們能夠發現:
public final
修飾,因此代理類只可被使用,不能夠再被繼承m + 數字
的格式命名super.h.invoke(this, m1, (Object[])null);
調用,其中的 super.h.invoke
其實是在建立代理的時候傳遞給 Proxy.newProxyInstance
的 LogHandler 對象,它繼承 InvocationHandler 類,負責實際的調用處理邏輯而 LogHandler 的 invoke 方法接收到 method、args 等參數後,進行一些處理,而後經過反射讓被代理的對象 target 執行方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args); // 調用 target 的 method 方法
after();
return result; // 返回方法的執行結果
}
複製代碼
JDK動態代理執行方法調用的過程簡圖以下:
代理類的調用過程相信你們都明瞭了,而關於Proxy的源碼解析,還請你們另外查閱其餘文章或者直接看源碼
maven引入CGLIB包,而後編寫一個UserDao類,它沒有接口,只有兩個方法,select() 和 update()
public class UserDao {
public void select() {
System.out.println("UserDao 查詢 selectById");
}
public void update() {
System.out.println("UserDao 更新 update");
}
}
複製代碼
編寫一個 LogInterceptor ,繼承了 MethodInterceptor,用於方法的攔截回調
import java.lang.reflect.Method;
import java.util.Date;
public class LogInterceptor implements MethodInterceptor {
/** * @param object 表示要進行加強的對象 * @param method 表示攔截的方法 * @param objects 數組表示參數列表,基本數據類型須要傳入其包裝類型,如int-->Integer、long-Long、double-->Double * @param methodProxy 表示對方法的代理,invokeSuper方法表示對被代理對象方法的調用 * @return 執行結果 * @throws Throwable */
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(object, objects); // 注意這裏是調用 invokeSuper 而不是 invoke,不然死循環,methodProxy.invokesuper執行的是原始類的方法,method.invoke執行的是子類的方法
after();
return result;
}
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
複製代碼
測試
import net.sf.cglib.proxy.Enhancer;
public class CglibTest {
public static void main(String[] args) {
DaoProxy daoProxy = new DaoProxy();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Dao.class); // 設置超類,cglib是經過繼承來實現的
enhancer.setCallback(daoProxy);
Dao dao = (Dao)enhancer.create(); // 建立代理類
dao.update();
dao.select();
}
}
複製代碼
運行結果
log start time [Fri Dec 21 00:06:40 CST 2018]
UserDao 查詢 selectById
log end time [Fri Dec 21 00:06:40 CST 2018]
log start time [Fri Dec 21 00:06:40 CST 2018]
UserDao 更新 update
log end time [Fri Dec 21 00:06:40 CST 2018]
複製代碼
還能夠進一步多個 MethodInterceptor 進行過濾篩選
public class LogInterceptor2 implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(object, objects);
after();
return result;
}
private void before() {
System.out.println(String.format("log2 start time [%s] ", new Date()));
}
private void after() {
System.out.println(String.format("log2 end time [%s] ", new Date()));
}
}
// 回調過濾器: 在CGLib回調時能夠設置對不一樣方法執行不一樣的回調邏輯,或者根本不執行回調。
public class DaoFilter implements CallbackFilter {
@Override
public int accept(Method method) {
if ("select".equals(method.getName())) {
return 0; // Callback 列表第1個攔截器
}
return 1; // Callback 列表第2個攔截器,return 2 則爲第3個,以此類推
}
}
複製代碼
再次測試
public class CglibTest2 {
public static void main(String[] args) {
LogInterceptor logInterceptor = new LogInterceptor();
LogInterceptor2 logInterceptor2 = new LogInterceptor2();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserDao.class); // 設置超類,cglib是經過繼承來實現的
enhancer.setCallbacks(new Callback[]{logInterceptor, logInterceptor2, NoOp.INSTANCE}); // 設置多個攔截器,NoOp.INSTANCE是一個空攔截器,不作任何處理
enhancer.setCallbackFilter(new DaoFilter());
UserDao proxy = (UserDao) enhancer.create(); // 建立代理類
proxy.select();
proxy.update();
}
}
複製代碼
運行結果
log start time [Fri Dec 21 00:22:39 CST 2018]
UserDao 查詢 selectById
log end time [Fri Dec 21 00:22:39 CST 2018]
log2 start time [Fri Dec 21 00:22:39 CST 2018]
UserDao 更新 update
log2 end time [Fri Dec 21 00:22:39 CST 2018]
複製代碼
CGLIB 建立動態代理類的模式是:
JDK動態代理:基於Java反射機制實現,必需要實現了接口的業務類才能用這種辦法生成代理對象。
cglib動態代理:基於ASM機制實現,經過生成業務類的子類做爲代理類。
JDK Proxy 的優點:
基於相似 cglib 框架的優點:
來源於網上,用於幫助理解和掌握,歡迎補充
代理能夠分爲 "靜態代理" 和 "動態代理",動態代理又分爲 "JDK動態代理" 和 "CGLIB動態代理" 實現。
靜態代理:代理對象和實際對象都繼承了同一個接口,在代理對象中指向的是實際對象的實例,這樣對外暴露的是代理對象而真正調用的是 Real Object
JDK 動態代理:
爲了解決靜態代理中,生成大量的代理類形成的冗餘;
JDK 動態代理只須要實現 InvocationHandler 接口,重寫 invoke 方法即可以完成代理的實現,
jdk的代理是利用反射生成代理類 Proxyxx.class 代理類字節碼,並生成對象
jdk動態代理之因此只能代理接口是由於代理類自己已經extends了Proxy,而java是不容許多重繼承的,可是容許實現多個接口
優勢:解決了靜態代理中冗餘的代理實現類問題。
缺點:JDK 動態代理是基於接口設計實現的,若是沒有接口,會拋異常。
CGLIB 代理:
因爲 JDK 動態代理限制了只能基於接口設計,而對於沒有接口的狀況,JDK方式解決不了;
CGLib 採用了很是底層的字節碼技術,其原理是經過字節碼技術爲一個類建立子類,並在子類中採用方法攔截的技術攔截全部父類方法的調用,順勢織入橫切邏輯,來完成動態代理的實現。
實現方式實現 MethodInterceptor 接口,重寫 intercept 方法,經過 Enhancer 類的回調方法來實現。
可是CGLib在建立代理對象時所花費的時間卻比JDK多得多,因此對於單例的對象,由於無需頻繁建立對象,用CGLib合適,反之,使用JDK方式要更爲合適一些。
同時,因爲CGLib因爲是採用動態建立子類的方法,對於final方法,沒法進行代理。
優勢:沒有接口也能實現動態代理,並且採用字節碼加強技術,性能也不錯。
缺點:技術實現相對難理解些。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import proxy.UserService;
import java.lang.reflect.Method;
/** * 建立代理類的工廠 該類要實現 MethodInterceptor 接口。 * 該類中完成三樣工做: * (1)聲明目標類的成員變量,並建立以目標類對象爲參數的構造器。用於接收目標對象 * (2)定義代理的生成方法,用於建立代理對象。方法名是任意的。代理對象即目標類的子類 * (3)定義回調接口方法。對目標類的加強這在這裏完成 */
public class CGLibFactory implements MethodInterceptor {
// 聲明目標類的成員變量
private UserService target;
public CGLibFactory(UserService target) {
this.target = target;
}
// 定義代理的生成方法,用於建立代理對象
public UserService myCGLibCreator() {
Enhancer enhancer = new Enhancer();
// 爲代理對象設置父類,即指定目標類
enhancer.setSuperclass(UserService.class);
/** * 設置回調接口對象 注意,只因此在setCallback()方法中能夠寫上this, * 是由於MethodIntecepter接口繼承自Callback,是其子接口 */
enhancer.setCallback(this);
return (UserService) enhancer.create();// create用以生成CGLib代理對象
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("start invoke " + method.getName());
Object result = method.invoke(target, args);
System.out.println("end invoke " + method.getName());
return result;
}
}
複製代碼
參考:
《Java核心技術》卷1
《深刻理解Java虛擬機》7.3
java docs: docs.oracle.com/javase/8/do…
Java三種代理模式:靜態代理、動態代理和cglib代理
描述動態代理的幾種實現方式 分別說出相應的優缺點
JDK動態代理詳解
Java動態代理機制詳解(JDK 和CGLIB,Javassist,ASM)
靜態代理和動態代理的理解
歡迎評論、轉發、分享,您的支持是我最大的動力
更多內容可訪問個人我的博客:laijianfeng.org
關注【小旋鋒】微信公衆號,及時接收博文推送