Java動態代理實現原理(模擬實現)

​ 動態代理是java語言中經常使用的設計模式,java在1.3版本之後也提供了動態代理技術,容許開發者在運行期間建立接口的代理對象。 不少框架底層都使用了java的動態代理技術來實現的,好比大名鼎鼎的springAOP;這篇文章將帶你一步一步揭開JDK動態代理技術的神祕面紗。java

​ 咱們先來定義一個接口:spring

package com.yanghui.study.proxy;
public interface IFlyable {
    int fly(int x,int y);
}

再來一個實現類:設計模式

package com.yanghui.study.proxy;
public class Plane implements IFlyable{
    @Override
    public int fly(int x, int y) {
        int result = x * x + y * y;
        try {
            Thread.sleep(new Random().nextInt(700));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return result;
    }
}

若是咱們要統計一下這個fly方法的運行時間,該怎麼作呢?很簡單,能夠修改源碼在方法fly方法裏面加上兩句代碼①、②,這樣就打印出方法的運行時間了,以下:框架

//省略沒必要要代碼......
public int fly(int x, int y) {
    long start = System.currentTimeMillis();//①記錄開始時間
    int result = x * x + y * y;
    try {
        Thread.sleep(new Random().nextInt(700));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②結束時間減去開始時間
    return result;
}

可是若是咱們沒有這個方法的源碼,這個類是別人寫好打好jar包提供給咱們用的,這時若是你還想統計下這個方法運行時間,又該怎麼辦呢?至少有兩種方式能夠來實現:dom

一、使用繼承,寫一個類繼承Plane,重寫fly方法,在調用父類的fly方法先後加上①②處的代碼,這樣就能夠統計fly方法的執行時間了。ide

package com.yanghui.study.proxy;
public class PlaneTimerProxy1 extends Plane{
    @Override
    public int fly(int x, int y) {
        long start = System.currentTimeMillis();//①
        int result = super.fly(x, y);
        System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②
        return result;
    }
}

二、使用聚合的方式,寫一個類PlaneTimerProxy2實現跟Plane同樣的接口,而且持有IFlyable的引用,當調用fly方法時,實際調用的是IFlyable的fly方法,這樣就能夠在方法調用先後加上①②處的代碼統計fly方法的執行的時間。ui

public class PlaneTimerProxy2 implements IFlyable{
    private IFlyable flyable;
    public PlaneTimerProxy2(IFlyable flyable) {
        this.flyable = flyable;
    }
    @Override
    public int fly(int x, int y) {
        long start = System.currentTimeMillis();//①
        int result = this.flyable.fly(x, y);
        System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②
        return result;
    }
}

這兩種方式均可以實現,那麼哪一種方式更好呢?答案是聚合的方式更好,爲何呢?想象一下,若是我還想實現更多的功能,好比給fly方法執行先後加上日誌,事務控制,權限控制,這時用繼承的方式你會須要新建更多的類來實現,可能你會想,聚合的實現方式不也是要新建更多的類來實現嗎?是的,可是若是我要你先記錄日誌再記錄時間,有若是我要你先記錄時間再記錄日誌,須要實現這樣隨意的組合的功能,繼承就顯得很麻煩了,而聚合的方式就會很靈活了。在思考下,若是想給不一樣類的100個方法記錄下時間和日誌,那麼你想一想看是否是要產生100個代理類呢?類的數量又在不停的膨脹了。若是咱們可以爲實現了某個接口的類動態生成代理類就行了?想法很好,先來新建一個類Proxy,提供一個方法newProxyInstance,這個方法能夠爲一個實現了IFlyable接口的類產生代理類,那麼客戶端調用就能夠這樣作:this

package com.yanghui.study.proxy.custom;
public class Client {
    public static void main(String[] args) {
        IFlyable flyable = (IFlyable)Proxy.newProxyInstance();
        flyable.fly(1, 2);
    }
}

那麼咱們如何在newProxyInstance方法裏面動態的生成一個代理類呢?爲了模擬JDK的實現,先定義一個接口InvocationHandler:設計

package com.yanghui.study.proxy.custom;
import java.lang.reflect.Method;
public interface InvocationHandler {
    Object invoke(Object proxy,Method method,Object[] args)throws Throwable;
}

下面來個完整代碼:代理

public class Proxy {
    private static final Map<String,byte[]> bytesMap = new HashMap<>();
    private static final AtomicInteger count = new AtomicInteger();
    public static Object newProxyInstance(Class<?> intaface,InvocationHandler handler) {
        //代碼①處
        String rn = "\r\n";
        String className = "Proxy" + count.getAndIncrement();
        String str = "package com.yanghui.study.proxy.custom;" + rn +
                     "public class " + className + " implements " + intaface.getName() + "{" + rn +
                     "    private InvocationHandler handler;" + rn +
                     "    public " + className + "(InvocationHandler handler){" + rn + 
                     "        this.handler=handler;" + rn + 
                     "    }" + rn;
        
        String methodStr = "";
        for(Method m : intaface.getMethods()) {
            methodStr = methodStr + "    @Override" + rn +
             "    public " + m.getReturnType().getName() + " " + m.getName() + "(";
            String parameterStr = "";
            String psType = "";
            String pname = "";
            for(Parameter p : m.getParameters()) {
                parameterStr = parameterStr + p + ",";
                psType = psType + p.getType().getName() + ".class,";
                pname = pname + p.getName() + ",";
            }
            if(!parameterStr.equals("")) {
                parameterStr = parameterStr.substring(0, parameterStr.length() - 1);
            }
            parameterStr = parameterStr + "){" + rn + 
                     "        try{" + rn +
                     "            " + Method.class.getName() + " method = " + intaface.getName() + ".class.getDeclaredMethod(\"" + m.getName() + "\"";
            if(!psType.equals("")) {
                psType = psType.substring(0, psType.length() - 1);
                parameterStr = parameterStr + "," + psType + ");" + rn;
            }else {
                parameterStr = parameterStr + ");" + rn;
            }
            if(pname.length() > 0) {
                pname = pname.substring(0, pname.length() - 1);
            }
            String returnStr = "";
            if(!"void".equals(m.getReturnType().getName())) {
                returnStr = returnStr + "            return (" + m.getReturnType().getName() + ")";
            }
            parameterStr = parameterStr + 
                    returnStr + "this.handler.invoke(this,method," + (pname.length() == 0 ? "null" : "new Object[]{" + pname + "}") + ");" + rn +
             "        } catch (Throwable e) {" + rn +
             "        throw new RuntimeException(e);" + rn +
             "    }" + rn +
             "    }" + rn;
            methodStr = methodStr + parameterStr;
        }
        String endStr = "}";
        str = str + methodStr + endStr;
        String path = Thread.currentThread().getContextClassLoader().getResource("").getPath() + "com/yanghui/study/proxy/custom/";
        String fileStr = path + className + ".java";
        //代碼②處
        //寫入文件
        writeToFile(fileStr, str);
        //代碼③處
        //動態編譯
        String className1 = "com.yanghui.study.proxy.custom." + className;
        return compileToFileAndLoadclass(className1, fileStr, handler);
    }
    
    /**
     * 從源文件到字節碼文件的編譯方式
     * @param className
     * @param fileStr
     * @param handler
     * @return
     */
    private static Object compileToFileAndLoadclass(String className,String fileStr,InvocationHandler handler) {
        //獲取系統Java編譯器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //獲取Java文件管理器
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        //定義要編譯的源文件
        File file = new File(fileStr);
        //經過源文件獲取到要編譯的Java類源碼迭代器,包括全部內部類,其中每一個類都是一個 JavaFileObject,也被稱爲一個彙編單元
        Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);
        //生成編譯任務
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
        //執行編譯任務
        task.call();
        try {
            fileManager.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            Class<?> c = Thread.currentThread().getContextClassLoader().loadClass(className);
            Constructor<?> ct = c.getConstructor(InvocationHandler.class);
            Object object = ct.newInstance(handler);
            return object;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    private static void writeToFile(String file,String context) {
        FileWriter fw = null;
        try {
            fw = new FileWriter(new File(file));
            fw.write(context);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(fw != null) {
                try {
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

我來解釋下上面代碼的意思:

一、代碼①處,根據傳入的接口動態生成java代碼的字符串,類名取名爲Proxy+序號,該類實現了傳入的接口,真正的方法調用將委託傳入InvocationHandler的實現類來實現。

二、代碼②處,將生成的java代碼的字符串寫入文件

三、代碼③處,真正的核心,動態編譯2步生成的java文件,再經過classLoader把編譯生成的class文件加載進內存,而後反射建立實例。

接下來客戶端就能夠這樣使用了:

public class Client {
    public static void main(String[] args) {
        Plane plane = new Plane();
        IFlyable flyable = (IFlyable)Proxy.newProxyInstance(IFlyable.class,new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                long start = System.currentTimeMillis();
                Object result = method.invoke(plane, args);
                System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");
                return result;
            }
        });
        System.out.println(flyable.fly(1, 2));
    }
}

到目前爲止,咱們實現的Proxy類能夠爲任何接口生成代理類了,是否是很神奇。固然咱們這裏只是模擬實現了JDk的動態代理,還有不少細節是沒有考慮的,有興趣的同窗能夠本身閱讀JDK源碼,相信您理解了其背後的原理後,看起來也不會太費力了。

擴展

在上面咱們實現了動態生成java文件,動態編譯java文件,須要把文件寫入磁盤,也會在java源文件的目錄生成編譯後的.class文件,那麼能夠不能夠只在內存中編譯加載呢?答案是能夠的,代碼以下(方法是Proxy類下的方法):

/**
     * 從內存到內存的編譯方式
     * @param className
     * @param code
     * @param handler
     * @return
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private static Object compileMemoryToMemoryAndLoadClass(String className,String code,InvocationHandler handler) {
        if(bytesMap.get(className) != null) {
            return loadClass(className, bytesMap.get(className), handler);
        }
        //獲取系統Java編譯器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //獲取Java文件管理器
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        ForwardingJavaFileManager fjf = new ForwardingJavaFileManager(fileManager) {
            @Override
            public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind,
                    FileObject sibling) throws IOException {
                if(kind == JavaFileObject.Kind.CLASS) {
                    return new SimpleJavaFileObject(URI.create(""), JavaFileObject.Kind.CLASS) {
                        public OutputStream openOutputStream() {
                            return new FilterOutputStream(new ByteArrayOutputStream()) {
                                public void close() throws IOException{
                                    out.close();
                                    ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
                                    bytesMap.put(className, bos.toByteArray());
                                }
                            };
                        }
                    };
                }else{
                    return super.getJavaFileForOutput(location, className, kind, sibling);
                }
            }
        };
        
        SimpleJavaFileObject sourceJavaFileObject = new SimpleJavaFileObject(URI.create(className.replace('.', '/') + Kind.SOURCE.extension),JavaFileObject.Kind.SOURCE){
            @Override
            public CharBuffer getCharContent(boolean b) {
                return CharBuffer.wrap(code);
            }
        };
        //生成編譯任務
        JavaCompiler.CompilationTask task = compiler.getTask(null, fjf, null, null, null, Arrays.asList(new JavaFileObject[] {sourceJavaFileObject}));
        //執行編譯任務
        task.call();
        try {
            fileManager.close();
            fjf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return loadClass(className, bytesMap.get(className), handler);
    }
    
    private static Object loadClass(String className,byte[] bytes,InvocationHandler handler) {
        try {
            Class<?> c = new MyClassLoader(bytes).loadClass(className);
            Constructor<?> ct = c.getConstructor(InvocationHandler.class);
            Object object = ct.newInstance(handler);
            return object;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

首先經過本身定義sourceJavaFileObject類來加載java格式的字符串,經過ForwardingJavaFileManager類來從新定義編譯文件的輸出行爲,這裏我直接寫入內存,用一個map(bytesMap)來保存,key就是類名,value就是編譯好的.class的二進制文件。

相關文章
相關標籤/搜索