1. 程式人生 > >手動模擬JDK動態代理

手動模擬JDK動態代理

為哪些方法代理?

實現自己動態代理,首先需要關注的點就是,代理物件需要為哪些方法代理? 原生JDK的動態代理的實現是往上抽象出一層介面,讓目標物件和代理物件都實現這個介面,怎麼把介面的資訊告訴jdk原生的動態代理呢? 如下程式碼所示,Proxy.newProxyInstance()方法的第二個引數將介面的資訊傳遞了進去第一個引數的傳遞進去一個類載入器,在jdk的底層用它對比物件是否是同一個,標準就是相同物件的類載入器是同一個

ServiceInterface) Proxy.newProxyInstance(service.getClass().getClassLoader()
                , new Class[]{ServiceInterface.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("前置通知");
                method.invoke(finalService,args);
                System.out.println("後置通知");
                return proxy;
            }
        });

我們也效仿它的做法. 程式碼如下:

public class Test {
    public static void main(String[] args) {
        IndexDao indexDao = new IndexDao();
        Dao  dao =(Dao) ProxyUtil.newInstance(Dao.class,new MyInvocationHandlerImpl(indexDao));
        assert dao != null;
        System.out.println(dao.say("changwu"));
    }
}

拿到了介面的Class物件後,通過反射就得知了介面中有哪些方法描述物件Method,獲取到的所有的方法,這些方法就是我們需要增強的方法

如何將增強的邏輯動態的傳遞進來呢?

JDK的做法是通過InvocationHandler的第三個引數完成,他是個介面,裡面只有一個抽象方法如下: 可以看到它裡面有三個入參,分別是 代理物件,被代理物件的方法,被代理物件的方法的引數

    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

當我們使用jdk的動態代理時,就是通過這個重寫這個鉤子函式,將邏輯動態的傳遞進去,並且可以選擇在適當的地方讓目標方法執行

InvocationHandler介面必須存在必要性1:

為什麼不傳遞進去Method,而是傳遞進去InvocationHandler物件呢? 很顯然,我們的初衷是藉助ProxyUtil工具類完成對代理物件的拼串封裝,然後讓這個代理物件去執行method.invoke(), 然而事與願違,傳遞進來的Method物件的確可以被ProxyUtil使用,呼叫method.invoke(), 但是我們的代理物件不能使用它,因為代理物件在這個ProxyUtil還以一堆等待拼接字串, ProxyUtil的作用只能是往代理物件上疊加字串,卻不能直接傳遞給它一個物件,所以只能傳遞一個物件進來,然後通過反射獲取到這個物件的例項,繼而有可能實現method.invoke()

InvocationHandler介面必須存在必要性2:

通過這個介面的規範,我們可以直接得知回撥方法的名字就是invoke()所以說,在拼接字串完成對代理物件的拼接時,可以直接寫死它


思路

我們需要通過上面的ProxyUtil.newInstance(Dao.class,new MyInvocationHandlerImpl(indexDao))方法完成如下幾件事

  • 根據入參位置的資訊,提取我們需要的資訊,如包名,方法名,等等
  • 根據我們提取的資訊通過字串的拼接完成一個全新的java的拼接
    • 這個java類就是我們的代理物件
  • 拼接好的java類是一個String字串,我們將它寫入磁碟取名XXX.java
  • 通過ProxyUtil使用類載入器,將XXX.java讀取JVM中,形成Class物件
  • 通過Class物件反射出我們需要的代理物件

ProxyUtil的實現如下:

public static Object newInstance(Class targetInf, MyInvocationHandler invocationHandler) {

    Method methods[] = targetInf.getDeclaredMethods();
    String line = "\n";
    String tab = "\t";
    String infName = targetInf.getSimpleName();
    String content = "";
    String packageContent = "package com.myproxy;" + line;
    //   導包,全部匯入介面層面,換成具體的實現類就會報錯
    //   
    String importContent = "import " + targetInf.getName() + ";" + line
                           + "import com.changwu.代理技術.模擬jdk實現動態代理.MyInvocationHandler;" + line
                           + "import java.lang.reflect.Method;" + line
                           + "import java.lang.Exception;" + line;

    String clazzFirstLineContent = "public class $Proxy implements " + infName +"{"+ line;
    String filedContent = tab + "private MyInvocationHandler handler;"+ line;
    String constructorContent = tab + "public $Proxy (MyInvocationHandler  handler){" + line
            + tab + tab + "this.handler =handler;"
            + line + tab + "}" + line;
    String methodContent = "";
    // 遍歷它的全部方法,接口出現的全部方法進行增強
    for (Method method : methods) {
        String returnTypeName = method.getReturnType().getSimpleName();         method.getReturnType().getSimpleName());

        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();

        // 引數的.class
        String paramsClass = "";
        for (Class<?> parameterType : parameterTypes) {
            paramsClass+= parameterType.getName()+",";
        }

        String[] split = paramsClass.split(",");

        //方法引數的型別陣列 Sting.class String.class
        String argsContent = "";
        String paramsContent = "";
        int flag = 0;
        for (Class arg : parameterTypes) {
            // 獲取方法名
            String temp = arg.getSimpleName();
            argsContent += temp + " p" + flag + ",";
            paramsContent += "p" + flag + ",";
            flag++;
        }
        // 去掉方法引數中最後面多出來的,
        if (argsContent.length() > 0) {
            argsContent = argsContent.substring(0, argsContent.lastIndexOf(",") - 1);
            paramsContent = paramsContent.substring(0, paramsContent.lastIndexOf(",") - 1);
        }
        methodContent += tab + "public " + returnTypeName + " " + methodName + "(" + argsContent + ") {" + line
                + tab + tab+"Method method = null;"+line
                + tab + tab+"String [] args0 = null;"+line
                + tab + tab+"Class<?> [] args1= null;"+line

                // invoke入參是Method物件,而不是上面的字串,所以的得通過反射創建出Method物件
                + tab + tab+"try{"+line
                // 反射得到引數的型別陣列
                 + tab + tab + tab + "args0 = \""+paramsClass+"\".split(\",\");"+line
                 + tab + tab + tab + "args1 = new Class[args0.length];"+line
                 + tab + tab + tab + "for (int i=0;i<args0.length;i++) {"+line
                 + tab + tab + tab + "   args1[i]=Class.forName(args0[i]);"+line
                 + tab + tab + tab + "}"+line
                // 反射目標方法
                + tab + tab + tab + "method = Class.forName(\""+targetInf.getName()+"\").getDeclaredMethod(\""+methodName+"\",args1);"+line
                + tab + tab+"}catch (Exception e){"+line
                + tab + tab+ tab+"e.printStackTrace();"+line
                + tab + tab+"}"+line
                + tab + tab + "return ("+returnTypeName+") this.handler.invoke(method,\"暫時不知道的方法\");" + line; //
                 methodContent+= tab + "}"+line;
    }

    content = packageContent + importContent + clazzFirstLineContent + filedContent + constructorContent + methodContent + "}";

    File file = new File("d:\\com\\myproxy\\$Proxy.java");
    try {
        if (!file.exists()) {
            file.createNewFile();
        }

        FileWriter fw = new FileWriter(file);
        fw.write(content);
        fw.flush();
        fw.close();

        // 將生成的.java的檔案編譯成 .class檔案
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        Iterable units = fileMgr.getJavaFileObjects(file);
        JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
        t.call();
        fileMgr.close();

        // 使用類載入器將.class檔案載入進jvm
        // 因為產生的.class不在我們的工程當中
        URL[] urls = new URL[]{new URL("file:D:\\\\")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class clazz = urlClassLoader.loadClass("com.myproxy.$Proxy");
        return clazz.getConstructor(MyInvocationHandler.class).newInstance(invocationHandler);
    } catch (Exception e) {
        e.printStackTrace();
    }
       return null;
}
}

執行的效果:

package com.myproxy;
import com.changwu.myproxy.pro.Dao;
import com.changwu.myproxy.pro.MyInvocationHandler;
import java.lang.reflect.Method;
import java.lang.Exception;
public class $Proxy implements Dao{
    private MyInvocationHandler handler;
    public $Proxy (MyInvocationHandler  handler){
        this.handler =handler;
    }
    public String say(String p) {
        Method method = null;
        String [] args0 = null;
        Class<?> [] args1= null;
        try{
            args0 = "java.lang.String,".split(",");
            args1 = new Class[args0.length];
            for (int i=0;i<args0.length;i++) {
               args1[i]=Class.forName(args0[i]);
            }
            method = Class.forName("com.changwu.myproxy.pro.Dao").getDeclaredMethod("say",args1);
        }catch (Exception e){
            e.printStackTrace();
        }
        return (String) this.handler.invoke(method,"暫時不知道的方法");
    }
}

解讀

通過newInstance()使用者獲取到的代理物件就像上面的代理一樣,這個過程是在java程式碼執行時生成的,但是直接看他的結果和靜態代理差不錯,這時使用者再去呼叫代理物件的say(), 實際上就是在執行使用者傳遞進去的InvocationHandeler裡面的invoke方法, 但是亮點是我們把目標方法的描述物件Method同時給他傳遞進去了,讓使用者可以執行目標方法+增強的邏輯

當通過反射區執行Method物件的invoke()方法時,指定的哪個物件的當前方法呢? 這個引數其實是我們手動傳遞進去的代理物件程式碼如下

public class MyInvocationHandlerImpl implements MyInvocationHandler {
    private Object obj;
    public MyInvocationHandlerImpl(Object obj) {
        this.obj = obj;
    }
    @Override
    public Object invoke(Method method, Object[] args) {
        System.out.println("前置通知");
        try {
            method.invoke(obj,args);
        } catch (Exception e) {
            e.printStackTrace();
        }  
        System.out.println("後置通知");
        return null;
    }
}