深刻理解Java動態代理及手動實現

前言

文章目錄以下,便於快速索引java

1、什麼叫代理?git

2、什麼叫動態代理?github

3、動態代理有什麼優點?數組

4、動態代理的JDK實現原理緩存

    4.1核心類/接口ide

    4.2 代理類$Proxy0解析函數

    4.3 動態代理的經典使用工具

5、手寫代碼模擬JDK動態代理學習

6、參考資料測試

先將本身總結的Java動態代理UML圖放在前面,用相同的顏色表明同一個或本質上相同的類與方法,便於你們理解。接口UserService是咱們本身定義的接口——接口中有方法execute();被代理類是實現了該接口的具體類;代理類則是存在於內存中,也實現了UserService接口的類,在內存中,該類名爲$Proxy0。

    下面進入正文,今天想跟你們分享一下我本身在學習Java動態代理過程當中的理解與收穫,最後用本身的代碼模擬實現JDK的動態代理。

1、什麼叫代理?

    這個概念不是我想表達的重點,因此這裏我引用別人一篇博文的例子

    「動態代理技術就是用來產生一個對象的代理對象的。在開發中爲何須要爲一個對象產生代理對象呢?
    舉一個現實生活中的例子:歌星或者明星都有一個本身的經紀人,這個經紀人就是他們的代理人,當咱們須要找明星表演時,不能直接找到該明星,只能是找明星的代理人。好比劉德華在現實生活中很是有名,會唱歌,會跳舞,會拍戲,劉德華在沒有出名以前,咱們能夠直接找他唱歌,跳舞,拍戲,劉德華出名以後,他乾的第一件事就是找一個經紀人,這個經紀人就是劉德華的代理人(代理),當咱們須要找劉德華表演時,不能直接找到劉德華了(劉德華說,你找我代理人商談具體事宜吧!),只能是找劉德華的代理人,所以劉德華這個代理人存在的價值就是攔截咱們對劉德華的直接訪問!
    這個現實中的例子和咱們在開發中是同樣的,咱們在開發中之因此要產生一個對象的代理對象,主要用於攔截對真實業務對象的訪問。那麼代理對象應該具備什麼方法呢?代理對象應該具備和目標對象相同的方法

    因此在這裏明確代理對象的兩個概念:

    一、代理對象存在的價值主要用於攔截對真實業務對象的訪問。(事務的開啓與關閉)

    二、代理對象應該具備和目標對象(真實業務對象)相同的方法。(要求實現同一接口)

    劉德華(真實業務對象)會唱歌,會跳舞,會拍戲,咱們如今不能直接找他唱歌,跳舞,拍戲了,只能找他的代理人(代理對象)唱歌,跳舞,拍戲,一我的要想成爲劉德華的代理人,那麼他必須具備和劉德華同樣的行爲(會唱歌,會跳舞,會拍戲),劉德華有什麼方法,他(代理人)就要有什麼方法,咱們找劉德華的代理人唱歌,跳舞,拍戲,可是代理人不是真的懂得唱歌,跳舞,拍戲的,真正懂得唱歌,跳舞,拍戲的是劉德華,在現實中的例子就是咱們要找劉德華唱歌,跳舞,拍戲,那麼只能先找他的經紀人,交錢給他的經紀人,而後經紀人再讓劉德華去唱歌,跳舞,拍戲。」

2、什麼叫動態代理?

    代理類在程序運行時建立的代理方式被成爲動態代理。 也就是說,這種狀況下,代理類並非在Java代碼中定義的,而是在運行時根據咱們在Java代碼中的「指示」動態生成的。

3、動態代理有什麼優點?

    經過使用代理,一般有兩個優勢:

    優勢一:能夠隱藏被代理類的實現;

    優勢二:能夠實現客戶與被代理類間的解耦,在不修改被代理類代碼的狀況下可以作一些額外的處理。

4、動態代理的JDK實現原理

    在java的動態代理機制中,有兩個重要的類和接口,一個是 InvocationHandler(Interface)、另外一個則是 Proxy(Class),這一個類和接口是實現咱們動態代理所必須用到的。固然,我還想帶各位深刻了解一下存在於JVM中神祕的動態代理類——$Proxy0。最後再給出java動態代理的經典使用流程。

4.1 核心類/接口

4.1.1 java.lang.reflect.Proxy類

    Proxy類提供了用於建立動態代理類和實例的靜態方法,它也是由這些方法建立的全部動態代理類的超類。

public class Proxy extends Object implements Serializable

    (1)Proxy的主要靜態變量

// 映射表:用於維護類裝載器對象到其對應的代理類緩存
private static Map loaderToCache = new WeakHashMap(); 
 
// 標記:用於標記一個動態代理類正在被建立中
private static Object pendingGenerationMarker = new Object(); 
 
// 同步表:記錄已經被建立的動態代理類類型,主要被方法 isProxyClass 進行相關的判斷
private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap()); 
 
// 關聯的調用處理器引用
protected InvocationHandler h;

    (2)Proxy的構造方法

// 因爲 Proxy 內部從不直接調用構造函數,因此 private 類型意味着禁止任何調用
private Proxy() {} 
 
// 因爲 Proxy 內部從不直接調用構造函數,因此 protected 意味着只有子類能夠調用
protected Proxy(InvocationHandler h) {this.h = h;}

    (3)Proxy靜態方法newProxyInstance

public static Object newProxyInstance(ClassLoader loader, 
            Class<?>[] interfaces, 
            InvocationHandler h) 
            throws IllegalArgumentException { 
 
    // 檢查 h 不爲空,不然拋異常
    if (h == null) { 
        throw new NullPointerException(); 
    } 
 
    // 得到與制定類裝載器和一組接口相關的代理類類型對象
    /*
     * Look up or generate the designated proxy class.
     */
        Class<?> cl = getProxyClass0(loader, interfaces); 
 
    // 經過反射獲取構造函數對象並生成代理類實例
    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            SecurityManager sm = System.getSecurityManager();
            if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {
                // create proxy instance with doPrivilege as the proxy class may
                // implement non-public interfaces that requires a special permission
                return AccessController.doPrivileged(new PrivilegedAction<Object>() {
                    public Object run() {
                        return newInstance(cons, ih);
                    }
                });
            } else {
                return newInstance(cons, ih);
            }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString());
    } 
    }
 
private static Object newInstance(Constructor<?> cons, InvocationHandler h) {
        try {
            return cons.newInstance(new Object[] {h} );
        } catch (IllegalAccessException e) {
            throw new InternalError(e.toString());
        } catch (InstantiationException e) {
            throw new InternalError(e.toString());
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString());
            }
        }
    }

    這個方法的做用就是獲得一個動態的代理對象,其接收三個參數,咱們來看看這三個參數所表明的含義。

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
 
//loader:  一個ClassLoader對象,定義了由哪一個ClassLoader對象來對生成的代理對象進行加載
//interfaces:  一個Interface對象的數組,表示的是我將要給我須要代理的對象提供一組什麼接口,若是我提供了一組接口給它,那麼這個代理對象就宣稱實現了該接口(多態),這樣我就能調用這組接口中的方法了
//h:  一個InvocationHandler對象,表示的是當我這個動態代理對象在調用方法的時候,會關聯到哪個InvocationHandler對象上

    官方JDK文檔給出了使用該方法建立一個動態代理類的模板:

Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
                                          new Class[] { Foo.class },
                                          handler);

    動態代理真正的關鍵是在 getProxyClass0 方法。這個咱們在後面手動實現模擬JDK動態代碼的時候能夠看到。

4.1.2  java.lang.reflect.InvocationHandler接口

    每個動態代理類中都有一個實現了InvocationHandler這個接口(代碼中的中介)的實例handler類(即前言中的MyInvocationHandler類),當咱們經過代理對象調用一個方法的時候,這個方法的調用就會被轉發爲由InvocationHandler這個接口的 invoke(對方法的加強就寫在這裏面) 方法來進行調用。

import java.lang.reflect.Method;
 
public interface MyInvocationHandler {
 
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
 
}

    咱們看到這個方法一共接受三個參數,那麼這三個參數分別表明什麼呢?

Object invoke(Object proxy, Method method, Object[] args) throws Throwable
//proxy:  內存中的代理實例 $proxy0
//method:  內存代理實例中class.forName("被代理類").getMethod("目標方法") 即被代理的類的方法對象
//args:  指代的是調用真實對象某個方法時接受的參數

    咱們怎麼確認上面幾個參數就是這個意思呢?那就看看下面這節吧~

4.2 代理類$Proxy0解析

(1)爲何內存中的動態代理類叫作$Proxy0?

    這個能夠經過斷點查看到~

(2)怎麼拿到動態代理類的字節碼文件?

public static void createProxyFile() throws IOException {
        byte[] generateProxyClass = ProxyGenerator.generateProxyClass("$Proxy0", new Class<?>[]{UserService.class});
 
        FileOutputStream outputStream = new FileOutputStream("$Proxy0.class");
        outputStream.write(generateProxyClass);
        outputStream.close();
    }

    在4.3 (5)中最終輸出動態代理類執行結果後能夠調用上面的方法,便可獲得字節碼文件

(3)動態代理類$Proxy0字節碼文件解析

    很清楚,動態代理類實現了UserService接口,繼承了Proxy類

    首先咱們看左邊爲動態代理類的代碼結構。

 

  • 構造方法
//$Proxy類
public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
}
//父類Proxy
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
}

    能夠看到動態代理類包含一個有參構造方法,內部調用了父類方法,其實也就是完成了調用處理器 InvocationHandler的實例化。該構造方法你們可得注意,在4.3(6)中提到的動態代理建立流程第3步,使用JAVA反射機制獲取動態代理對象時:

Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});

    就是爲了調用本構造方法。

 

  • Object方法

    你們能夠看到動態代理類有m0~m3四個方法,這四個方法分別是什麼呢?

static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("proxy.test.UserService").getMethod("execute");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }

    能夠看到,經過java反射機制,除了m3是UserService接口方法的實現之外,其餘方法都是Object的方法。那這些方法有什麼特色呢?

public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    其實核心就一句話 h.invoke() 也就是說動態代理類中的方法都使用了java反射機制去調用調用處理器 InvocationHandler中的方法。

 

  • 接口方法

    接口方法與Object方法同樣,內部就一句話。咱們注意到,invoke方法傳入3個參數,這個invoke方法也就是4.1.2中咱們提到的InvocationHandler接口的 invoke方法,那理解3個參數的意義也就很簡單了。

    參數1傳入的爲this——即$Proxy0自己,因此是內存中的動態代理對象

   參數2傳入的爲m3——也就是proxy.test.UserService中名爲execute的方法,即接口中的方法。而這也徹底證明了以前在「1、什麼是代理?」部分提到的第二個特色——代理對象應該具備和目標對象(真實業務對象)相同的方法。(要求實現同一接口)

    參數3傳入的爲null——由於execute方法沒有參數,因此爲空。

public final String execute() throws  {
        try {
            return (String)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

4.3 動態代理的經典使用

(1)理解UML圖

    再把本圖放上來暖場~JAVA中動態代理的使用基本就是如上圖所示。下圖是咱們程序的目錄結構,下面咱們用代碼來實現。

(2)定義對象的行爲接口UserService

package proxy.test;
 
public interface UserService {
 
    public String execute() throws Throwable ;
}

(3)定義目標業務對象類UserServiceImpl

package proxy.test;
 
public class UserServiceImpl implements UserService{
    @Override
    public String execute() throws Throwable {
        System.out.println("step 2 執行方法啦!!");
        return "step 2 執行方法啦!!";
    }
}

(4)自定義「調用處理程序」——MyInvocationHandler

package proxy.test;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
 
public class MyInvocationHandler implements InvocationHandler {
 
    private UserService userService;
 
    public MyInvocationHandler(UserService userService) {
        this.userService = userService;
    }
    
    /*
    * @proxy 內存中的代理實例 $proxy0
    * @method 內存代理實例中class.forName("被代理類").getMethod("目標方法") 即被代理的類的方法對象
    * @args 方法參數
    * */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();   //事務開啓
 
        method.invoke(userService, args);
 
        after();    //事務關閉
 
        return "成功了";
    }
 
    private void before() {
        System.out.println("事務開啓!");
    }
 
    private void after() {
        System.out.println("事務關閉");
    }
}

    動態代理最具魅力所在——也就是上面代碼中事務開啓和關閉部分,總結起來就是:實現了方法的加強,讓你能夠在不修改源碼的狀況下,加強一些方法,在方法執行先後作任何你想作的事情(甚至根本不去執行這個方法),由於在InvocationHandler的invoke方法中,你能夠直接獲取正在調用方法對應的Method對象,具體應用的話,好比能夠添加調用日誌,作事務控制等。

(5)代理類生成並測試代碼

package proxy.test;
import java.lang.reflect.Proxy;
 
public class MyTest {
 
    public static void main(String[] args) throws Throwable {
        System.out.println("---------------JDK動態代理----------------");
        UserService userService = (UserService) Proxy.newProxyInstance(MyTest.class.getClassLoader(),
                new Class<?>[]{UserService.class},
                new MyInvocationHandler(new UserServiceImpl()));
 
        userService.execute();
    }
}

    上面的代碼也就是4.1.1(3)中提到的JDK文檔給出的建立代理類方式。經過強制類型轉換並執行相應方法,獲得輸出以下:

(6)動態代理建立流程總結

 

    動態代理的建立是基於java反射的,一個典型的動態代理建立對象過程可分爲如下四個步驟:

  • 1.經過實現InvocationHandler接口建立本身的調用處理器 

        InvocationHandler handler = new InvocationHandlerImpl(...);

  • 2.經過爲Proxy類指定ClassLoader對象和一組interface建立動態代理類

        Class clazz = Proxy.getProxyClass(classLoader,new Class[]{...});

  • 3.經過反射機制獲取動態代理類$Proxy0的構造函數,其參數類型是調用處理器接口類型

        Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});

  • 4.經過構造函數建立代理類實例,此時需將調用處理器對象做爲參數被傳入

        Interface Proxy = (Interface)constructor.newInstance(new Object[] (handler));

    爲了簡化對象建立過程,Proxy類中的newInstance方法封裝了2~4,只需兩步便可完成代理對象的建立。生成的動態代理類$Proxy0繼承Proxy類實現UserService接口,實現的UserService的方法實際調用調用處理器的invoke方法,而invoke方法利用反射調用的是被代理對象的的方法(Object result=method.invoke(proxied,args))。

(7)JDK動態代理的不足

    誠然,Proxy已經設計得很是優美,可是仍是有一點點小小的遺憾之處,那就是它始終沒法擺脫僅支持interface代理的桎梏,由於它的設計註定了這個遺憾。擺脫這個遺憾就得依靠CGLIB(此處等待下文)。

5、手寫代碼模擬JDK動態代理

5.1 原理解析

    手擼代碼模擬JDK動態代理,其實也就是對java.lang.reflect.Proxy類的功能進行模擬。其步驟主要有如下四步:

(1)建立代理類的源碼; 

    拿到被代理類(如UserServiceImpl)實現的接口類對象(如UserService.class),遍歷裏面的方法(如execute()方法),以字符串的形式拼湊出代理類源碼(動態代理類與被代理類實現同一接口在此體現),將代理類的源碼寫到本地java文件

(2)將源碼進行編譯成字節碼; 

    讀取源碼,編譯java文件,獲得.class字節碼文件(的路徑)

(3)將字節碼加載到內存; 

(4)實例化代理類對象並返回給調用者。

    其中步驟(1)、(2)、(3)就是咱們自定義的Proxy類所要完成的功能,類的結構以下圖;步驟(4)是咱們功能代碼/測試代碼要實現的。下面咱們對每一步進行解析。

5.2 建立代理類的源碼

項目實現源碼已經上傳,歡迎點擊下載~

(1)使用字符串拼湊動態代理對象的java源碼

//用字符串的形式拼湊出內存裏的代理類
    static String rt = "\r\n";
    private static String get$Proxy0(Class<?> interfaces) {
 
        Method[] methods = interfaces.getMethods();
 
        String proxyClass = "package proxy;" + rt
                + "import java.lang.reflect.Method;" + rt
                + "public class $Proxy0 implements " + interfaces.getName() + "{"
                + rt + "MyInvocationHandler h;" + rt
                + "public $Proxy0(MyInvocationHandler h) {" + rt
                + "this.h = h;" + rt + "}" + getMethodString(methods, interfaces)
                + rt + "}";
        return proxyClass;
    }
 
    private static String getMethodString(Method[] methods, Class<?> interfaces) {
        String proxyMethod = "";
 
        for (Method method : methods) {
            proxyMethod += "public String " + method.getName()
                    + "() throws Throwable {" + rt + "Method md = "
                    + interfaces.getName() + ".class.getMethod(\"" + method.getName()
                    + "\",new Class[]{});" + rt
                    + "return (String)this.h.invoke(this, md, null);" + rt + "}" + rt;
        }
 
        return proxyMethod;
    }

    上面這段代碼所模擬的具體代碼實如今JDK中是在jar包中的,其獲得的結果就是生成了$Proxy0.java

package proxy;
import java.lang.reflect.Method;
 
public class $Proxy0 implements proxy.test.UserService{
    MyInvocationHandler h;
    public $Proxy0(MyInvocationHandler h) {
        this.h = h;
    }
    
    public String execute() throws Throwable {
        Method md = proxy.test.UserService.class.getMethod("execute",new Class[]{});
        return (String)this.h.invoke(this, md, null);
    }
}

(2)將源碼寫入本地文件

private static void outputFile(String proxyClass, String path) {
        File f = new File(path);
        try {
            FileWriter fw = new FileWriter(f);
            fw.write(proxyClass);
            fw.flush();
            fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

5.3 將源碼進行編譯成字節碼

private static void compileJavaFile(String fileName) {
        try {
            //得到當前系統中的編譯器
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            //得到文件管理者
            StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
            Iterable<? extends JavaFileObject> fileObjects = manager.getJavaFileObjects(fileName);
            //編譯任務
            JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, fileObjects);
            //開始編譯,執行完可在當前目錄下看到.class文件
            task.call();
            //關閉文件管理者
            manager.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    這段代碼用的都是JAVA自帶的編譯工具,不作過多解釋。

5.4 將字節碼加載到內存

private static Object loadClassToJvm(MyInvocationHandler h) {
        try {
            //使用自定義類加載器
            MyClassLoader loader = new MyClassLoader("C:\\HailinLuo\\CODING\\JAVA\\JavaProgramming\\src\\proxy");
            //獲得動態代理類的反射對象
            Class<?> $Proxy0 = loader.findClass("$Proxy0");
            //經過反射機制獲取動態代理類$Proxy0的構造函數,其參數類型是調用處理器接口類型
            Constructor<?> constructors = $Proxy0.getConstructor(MyInvocationHandler.class);
            //經過構造函數建立動態代理類實例
            return constructors.newInstance(h);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }

    這裏使用了自定義的類加載器MyClassLoader 

package proxy;
 
import java.io.*;
 
public class MyClassLoader extends ClassLoader {
    File dir;
    //把文件路徑用構造函數傳進來
    public MyClassLoader(String path) {
        dir = new File(path);
    }
    /*
     * 本方法就是去加載對應的字節碼文件
     * */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //若是文件路徑可用
        if (dir != null) {
            File clazzFile = new File(dir, name + ".class");
            //若是字節碼文件存在
            if (clazzFile.exists()) {
                //把字節碼文件加載到VM
                try {
                    //文件流對接class文件
                    FileInputStream inputStream = new FileInputStream(clazzFile);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    byte[] buffer = new byte[1024];
                    int len;
                    //將class文件讀取到buffer中
                    while ((len = inputStream.read(buffer)) != -1) {
                        //將buffer中的內容讀取到baos中的buffer
                        baos.write(buffer, 0, len);
                    }
                    //將buffer中的字節讀到內存加載爲class
                    return defineClass("proxy." + name, baos.toByteArray(), 0, baos.size());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return super.findClass(name);
    }
}

    也可使用URLClassLoader 加載,以下所示

//load到內存
URL[] urls = new URL[]{new URL("C:\\HailinLuo\\CODING\\JAVA\\JavaProgramming\\src\\proxy")};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class cls = urlClassLoader.loadClass("proxy.$Proxy0");

5.5 實例化代理類對象並返回給調用者

UserService service = (UserService) MyProxy.newProxyInstance(MyTest.class.getClassLoader(),
                UserService.class,
                new MyInvocationHandlerImpl(new UserServiceImpl()));
 
        service.execute();

    沒什麼好說的。。。

5.6 輸出結果

    如上圖所示,下方紅框所示輸出結果與JDK動態代理效果一致。說明咱們的模擬是成功的!而上面那個紅框中的$Proxy0.java和$Proxy0.class則是5.2(生成源碼)與5.3(編譯爲字節碼)執行的結果。

    OK,分享就到這裏~

6、參考資料

項目源碼已經上傳,歡迎點擊下載~

http://www.jb51.net/article/86531.htm

https://blog.csdn.net/pangqiandou/article/details/52964066

https://blog.csdn.net/scplove/article/details/52451899

https://www.jianshu.com/p/dbce090d5c3e?1487292535486

https://blog.csdn.net/ljt2724960661/article/details/52507314

相關文章
相關標籤/搜索