JDK动态代理底层实现案例

1,882 阅读3分钟

说在前面的话:本人在学习JDK动态代理时,以案例的方式学习了JDK动态代理底层实现,总结了个人的实现步骤,由于本人水平有限,如有不当之处,望指出。

JDK动态代理底层的底层类似于我们手写HelloWorld的过程,不过JDK是在运行期生成Java文件并编译加载
动态生成代理类的.java    ->    动态编译为.class文件 ->     将.class文件加载到JVM中
->   使用该类创建对象

一、案例需求

首先描述案例需求,现在有Flyable接口,抽象方法为fly(time)方法,Bird类实现Flyable接口。 现需要生成代理对象,代理该Bird对象中Flyable接口的方法,记录鸟儿每次飞行的日期时分秒。

二、准备工作

1.写一个Flyable接口

public interface Flyable {

    void fly(long time);

}

没什么 具体的东西,就时一个飞的方法,参数为飞多久

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) {
        }
    }
}

鸟实现会飞的接口

三、开始实现代理

1.写一个执行接口

//执行方法
public interface InvocationHandler {
	//第一个参数,代理对象,第二个参数,所执行的方法反射对象,第三个参数及以后,传入的参数
    Object invoke(Object proxy, Method method, Object... args);

}

此接口作用:我们可以定制代理对象的方法功能

动态代理执行的所有方法其实都是在里面执行了这一个方法,所以此接口尤为重要

2.写获取代理对象的类

==重头戏==

用来获取代理对象的类

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 {
            //一、 生成java文件
            generateProxyJavaFile(clazz, proxyClassName);

            //二、 编译
            compileJavaFile();

            //三、 添加class文件所在目录添加到类路径
            ClassUtil.addClassPath(new File(CLASS_BASE_PATH));

            //四、 加载类并创建对象
            Class proxyClass = Class.forName(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 {

    //添加一个文件夹到类路径
   public static <T> void addClassPath(File classFolder) throws Exception {
        //获取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);
        }
    }
}

三.测试代理对象

经过一系列努力,终于完成了动态代理的编写,进入到测试阶段

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毫秒