JDK動態代理底層實現案例

說在前面的話:本人在學習JDK動態代理時,以案例的方式學習了JDK動態代理底層實現,總結了我的的實現步驟,因爲本人水平有限,若有不當之處,望指出。java

JDK動態代理底層的底層相似於咱們手寫HelloWorld的過程,不過JDK是在運行期生成Java文件並編譯加載
動態生成代理類的.java -> 動態編譯爲.class文件 -> 將.class文件加載到JVM中 -> 使用該類建立對象編程

(想自學習編程的小夥伴請搜索圈T社區,更多行業相關資訊更有行業相關免費視頻教程。徹底免費哦!)app

1、案例需求

首先描述案例需求,如今有Flyable接口,抽象方法爲fly(time)方法,Bird類實現Flyable接口。
現須要生成代理對象,代理該Bird對象中Flyable接口的方法,記錄鳥兒每次飛行的日期時分秒。jvm

2、準備工做

#### 1.寫一個Flyable接口
public interface Flyable {

    void fly(long time);

}

沒什麼 具體的東西,就時一個飛的方法,參數爲飛多久maven

2.寫一個Bird實現接口

public class Bird implements Flyable {
    
    @Override
    public void fly(long time) {
        try {
            System.out.println("我是小鳥,我開始飛了");
            Thread.sleep(time);
            System.out.println("我是小鳥,我飛了" + time + "毫秒");
        } catch (InterruptedException e) {
        }
    }
}

鳥實現會飛的接口ide

3、開始實現代理

1.寫一個執行接口

//執行方法
public interface InvocationHandler {
    //第一個參數,代理對象,第二個參數,所執行的方法反射對象,第三個參數及之後,傳入的參數
    Object invoke(Object proxy, Method method, Object... args);

}

此接口做用:咱們能夠定製代理對象的方法功能工具

動態代理執行的全部方法其實都是在裏面執行了這一個方法,因此此接口尤其重要學習

2.寫獲取代理對象的類

==重頭戲==測試

用來獲取代理對象的類ui

public class MyProxy {

    private static final String CLASS_BASE_PATH = "C:\\Users\\bai\\Desktop\\Java生成代碼";
    private static final String PACKAGE_NAME = "myproxy";

    //獲取代理對象
    public static <T> T newProxyInstance(Class<T> clazz, InvocationHandler invocationHandler) {
        String proxyClassName = clazz.getSimpleName() + "$MyProxy";
        
        try {
            //1、 生成java文件
            generateProxyJavaFile(clazz, proxyClassName);

            //2、 編譯
            compileJavaFile();

            //3、 加載class文件到jvm中
            ClassUtil.loadClass(new File(CLASS_BASE_PATH));

            //4、 建立對象並返回
            Class proxyClass = MyProxy.class.getClassLoader().loadClass(PACKAGE_NAME + "." + proxyClassName);
            return (T) proxyClass.getConstructor(InvocationHandler.class).newInstance(invocationHandler);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //生成代理類的java文件
    private static void generateProxyJavaFile(Class clazz, String proxyClassName) throws IOException {
        //...下面講
    }
    
    //把java文件編譯成.class文件
    private static void compileJavaFile() throws FileNotFoundException{
        //...下面講
    }

}

分四步走,和寫一個java的helloWord相似

1)建立Java文件

這裏是使用Java代碼生成Java文件,要藉助一個開源工具包,Javapoet,maven倉庫中有。

和手寫java文件相似,類頭,屬性,構造器,方法等。

按Javapoet規則寫,並不難,平時能夠手寫的,用它均可以實現

構造方法的地方有些麻煩,能夠參照生成後的java文件看代碼

//生成代理類的java文件
    private static void generateProxyJavaFile(Class clazz, String proxyClassName) throws IOException {

        //構造一個類,實現傳入接口,addSuperinterface功能是添加一個實現接口
        TypeSpec.Builder classBuilder = TypeSpec.classBuilder(proxyClassName).addSuperinterface(clazz);

        //構建一個屬性,用於保存執行對象
        FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class, "handler", Modifier.PRIVATE).build();

        //添加到類中
        classBuilder.addField(fieldSpec);

        //構建一個構造器,初始化執行對象
        MethodSpec constructor = MethodSpec.constructorBuilder()
                //添加權限修飾符
                .addModifiers(Modifier.PUBLIC)
                //添加參數
                .addParameter(InvocationHandler.class, "handler")
                //方法體內容
                .addStatement("this.handler = handler")
                .build();

        //把構造器添加到類中
        classBuilder.addMethod(constructor);
        
        //獲取傳入接口的全部公有方法(本身的,外部能夠訪問的方法,不包括繼承的方法)
        Method[] methods = clazz.getDeclaredMethods();

        for (Method method : methods) {
            MethodSpec.Builder methodSpec = MethodSpec.methodBuilder(method.getName())
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .returns(method.getReturnType());
            //生成handler.invoke()執行語句(實際執行方法),添加到方法體
            StringBuilder invokeString = new StringBuilder("\tthis.handler.invoke(this, " + clazz.getName() + ".class.getMethod(\"" + method.getName() + "\",");
            //存儲執行方法參數列表
            StringBuilder paramNames = new StringBuilder();
            //這部分若是看不太懂,能夠對照生成後的java文件
            for (Parameter parameter : method.getParameters()) {
                //添加外部方法參數列表
                methodSpec.addParameter(parameter.getType(), parameter.getName());
                //添加實際執行方法中
                invokeString.append(parameter.getType() + ".class, ");
                //存到執行方法參數列表中
                paramNames.append(parameter.getName() + ",");
            }
            //把最後一個逗號替換爲)
            int lastCommaIndex = invokeString.lastIndexOf(",");
            invokeString.replace(lastCommaIndex, invokeString.length(), "), ");
            lastCommaIndex = paramNames.lastIndexOf(",");
            paramNames.replace(lastCommaIndex, lastCommaIndex + 1, ")");
            //把屬性名追加到最後一個參數列表
            invokeString.append(paramNames);
            //添加方法體,執行InvocationHandler的invoke方法,並抓取異常
            methodSpec.addCode("try{\n");
            methodSpec.addStatement(invokeString.toString());
            methodSpec.addCode("} catch (NoSuchMethodException e) {\n");
            methodSpec.addCode("\te.printStackTrace();\n");
            methodSpec.addCode("}\n");

            //添加到類中
            classBuilder.addMethod(methodSpec.build());
        }

        //生成java文件,第一個參數是包名
//        String path = MyProxy.class.getResource("/").toString();
        JavaFile javaFile = JavaFile.builder(PACKAGE_NAME, classBuilder.build()).build();

        //把java文件寫到執行路徑下(默認會把包生成文件夾)
        javaFile.writeTo(new File(CLASS_BASE_PATH));
    }

生成後的Java文件

package myproxy;

import java.lang.Override;

class Flyable$MyProxy implements Flyable {
  private InvocationHandler handler;

  public Flyable$MyProxy(InvocationHandler handler) {
    this.handler = handler;
  }

  @Override
  public void fly(long arg0) {
    try{
        this.handler.invoke(this, myproxy.Flyable.class.getMethod("fly",long.class), arg0);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }
  }
}
2)編譯爲.class文件

Java提供的有相似Javac編譯功能的工具,過程並不複雜,能夠靈活設置編譯期jvm的參數,若是爲null則用當前環境的參數

//把java文件編譯成.class文件
private static void compileJavaFile() throws FileNotFoundException {
    //1.獲取javac編譯器
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    //2.經過javac編譯器獲取一個編譯java文件管理器
    StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);

    //3.獲取java文件對象   
    //-調用一個工具類,從指定路徑下,遞歸獲取全部指定後綴的文件
    Set<File> javaFiles = FileUtil.getFilesForSuffix(new File(CLASS_BASE_PATH), ".java");
    //-這裏就一個java文件,就直接使用了
    Iterator<File> iterator = javaFiles.iterator();
    Iterable<? extends JavaFileObject> it = manager.getJavaFileObjects(iterator.next().getAbsoluteFile());

    //4.編譯
    JavaCompiler.CompilationTask task = compiler.getTask(null, manager,
            null, null, null, it);
    task.call();
}
3)加載.class文件

加載class文件到jvm中

public class ClassUtil {

    //加載class文件
    public static <T> void loadClass(File classFolder) throws Exception {
        //使用url類加載器,把文件夾路徑添加進去,就能夠直接加載文件夾下的全部class文件
        //至關於把這個路徑設置爲源碼路徑之一
        //獲取URLClassLoader的addURL方法
        Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        boolean accessible = method.isAccessible();
        try {
            //若是方法沒有權限訪問,則設置可訪問權限
            if (accessible == false) {
                method.setAccessible(true);
            }
            // 設置類加載器
            URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
            // 將當前類路徑加入到類加載器中
            method.invoke(classLoader, classFolder.toURI().toURL());
        } finally {
            //把方法的權限設置回去
            method.setAccessible(accessible);
        }
        //獲取文件夾中全部的.class文件
        Set<File> files = FileUtil.getFilesForSuffix(classFolder, ".class");
        for (File file : files) {
            // 把文件名稱轉化爲,和java.lang.String相似的全類名
            String className = file.getAbsolutePath();
            className = className.substring(classFolder.getAbsolutePath().length() + 1, className.length() - 6);
            className = className.replace(File.separatorChar, '.');
            // 加載Class類
            Class.forName(className);
        }

    }
}

三.測試代理對象

通過一系列努力,終於完成了動態代理的編寫,進入到測試階段

1.編寫Invoke方法體

public class MyInvocationHandler implements InvocationHandler {

    private Bird bird;

    public MyInvocationHandler(Bird bird) {
        this.bird = bird;
    }

    @Override   //第一個參數爲代理對象,第二個參數爲方法對象,後面的參數爲方法參數
    public Object invoke(Object proxy, Method method, Object... args) {
        String dateString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis());
        System.out.println(dateString + "小鳥起飛");
        try {
            Object invoke = method.invoke(bird, args);
            return invoke;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

2.測試代理對象

public class MyProxyTest {

    @Test
    public void testProxy() {
        Flyable flyable = MyProxy.newProxyInstance(Flyable.class, new MyInvocationHandler(new Bird()));
        flyable.fly(1000);
    }

}

控制檯打印

2019-07-25 20:44:59
我是小鳥,我開始飛了
我是小鳥,我飛了1000毫秒
相關文章
相關標籤/搜索