一步一步手撕动态代理! → 要是还看不懂,那就当我没撕

开心一刻

周末,带着老婆儿子一起逛公园。

儿子一个人跑在前面,吧唧一下不小心摔了一跤,脑袋瓜子摔了个包。

稀里哗啦的哭道:爸爸,我会不会摔成傻子!

我指了指我头上的伤痕安慰道:不会的,你看,这是爸爸小时候摔的。

话还没有说完,小家伙哭的更厉害了:那就是说我长大后就会和你一样傻了,我不要,我不要!

老婆忍不住发飙:别哭了,你怎么会变傻呢?你看你爸,你爸傻吗?

我赶紧回应道:是啊,你看我多聪明!

儿子:真的,不骗我?

老婆:当然!

儿子:可是如果老爸不是傻子,当年怎么会娶你这个母老虎呢?

我、老婆:......

静态代理

静态代理需要代理对象和被代理对象实现一样的接口,我们来看个例子就清楚了

示例代码:static-proxy

代理类:UserDaoProxy.java

java 复制代码
/**
 * 代理逻辑在代理类中,而不是由用户自定义
 */
public class UserDaoProxy implements IUserDao {

    private IUserDao target;            // 被代理对象

    public UserDaoProxy(IUserDao target) {
        this.target = target;
    }

    /**
     *  前置/后置 处理一旦写完,就固定死了,后续想修改的话需要改此代理类
     * @param id
     * @return
     */
    public int delete(int id) {
        // 前置处理,例如开启事务
        System.out.println("前置处理...");

        // 调用目标对象方法
        int count = target.delete(id);

        // 后置处理,例如提交事务或事务回滚
        System.out.println("前置处理...");
        return count;
    }
}

UserDaoProxy 代理 IUserDao 类型,此时也只能代理 IUserDao 类型的被代理对象

测试结果就不展示了,相信大家看了代码也知道了

优点:可以在不修改目标对象的前提下扩展目标对象的功能

缺点:1、如果需要代理多个类,每个类都会有一个代理类,会导致代理类无限制扩展;2、如果类中有多个方法,同样的代理逻辑需要反复实现、应用到每个方法上,一旦接口增加方法,目标对象与代理对象都要进行修改

一个静态代理只能代理一个类,那么有没有什么方式可以实现同一个代理类来代理任意对象呢?

肯定有的,也就是下面讲到的:动态代理

动态代理

代理类在程序运行时创建的代理方式被称为 动态代理

也就是说,这种情况下,代理类并不是在 Java 代码中定义的, 而是在运行时根据我们在 Java 代码中的 指示 动态生成的

下面我们一步一步手动来实现动态代理,后续的示例都是直接针对接口的,就不是针对接口的具体实现类了

静态代理示例中,UserDaoProxy 代理的是 IUserDao 的实现类:UserDaoImpl,那么动态代理示例就直接针对接口了,下面示例针对的都是 UserMapper 接口,模拟的 Mybatis,但不局限于 UserMapper 接口

代理类源代码持久化

1、先利用反射动态生成代理类,并持久化代理类到磁盘(也就是生成代理类的java源文件)
generateJavaFile 方法如下

java 复制代码
/**
 * 生成接口实现类的源代码, 并持久化到java文件
 * @param interface_
 * @param proxyJavaFileDir
 * @throws Exception
 */
private static void generateJavaFile(Class<?> interface_, String proxyJavaFileDir) throws Exception {
	StringBuilder proxyJava = new StringBuilder();
	proxyJava.append("package ").append(interface_.getPackage().getName()).append(";").append(ENTER).append(ENTER)
			.append("public class ").append(PROXY_CLASS_NAME).append(" implements ").append(interface_.getName()).append(" {");
	Method[] methods = interface_.getMethods();
	for(Method method : methods) {
		Type returnType = method.getGenericReturnType();
		Type[] paramTypes = method.getGenericParameterTypes();
		proxyJava.append(ENTER).append(ENTER).append(TAB_STR).append("@Override").append(ENTER)
				.append(TAB_STR).append("public ").append(returnType.getTypeName()).append(" ").append(method.getName()).append("(");
		for(int i=0; i<paramTypes.length; i++) {
			if (i != 0) {
				proxyJava.append(", ");
			}
			proxyJava.append(paramTypes[i].getTypeName()).append(" param").append(i);
		}
		proxyJava.append(") {").append(ENTER)
				.append(TAB_STR).append(TAB_STR)
				.append("System.out.println(\"数据库操作, 并获取执行结果...\");").append(ENTER); // 真正数据库操作,会有返回值,下面的return返回应该是此返回值
		if (!"void".equals(returnType.getTypeName())) {
			proxyJava.append(TAB_STR).append(TAB_STR).append("return null;").append(ENTER);      // 这里的"null"应该是上述中操作数据库后的返回值,为了演示写成了null
		}
		proxyJava.append(TAB_STR).append("}").append(ENTER);
	}
	proxyJava .append("}");

	// 写入文件
	File f = new File(proxyJavaFileDir + PROXY_CLASS_NAME + ".java");
	FileWriter fw = new FileWriter(f);
	fw.write(proxyJava.toString());
	fw.flush();
	fw.close();
}

生成的代理类:$Proxy0.java 如下

java 复制代码
package com.lee.mapper;

public class $Proxy0 implements com.lee.mapper.UserMapper {

    @Override
    public java.lang.Integer save(com.lee.model.User param0) {
        System.out.println("数据库操作, 并获取执行结果...");
        return null;
    }


    @Override
    public com.lee.model.User getUserById(java.lang.Integer param0) {
        System.out.println("数据库操作, 并获取执行结果...");
        return null;
    }
}

这个代理类的生成过程是我们自己实现的,实现不难,但排版太繁琐,我们可以用 javapoet 来生成代理类源代码
generateJavaFileByJavaPoet 方法如下

java 复制代码
/**
 * 用JavaPoet生成接口实现类的源代码,并持久化到java文件
 * @param interface_ 目标接口类
 * @return
 */
public static void generateJavaFileByJavaPoet(Class<?> interface_) throws Exception {

    // 类名、实现的接口,以及类访问限定符
    TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("JavaPoet$Proxy0")
            .addSuperinterface(interface_)
            .addModifiers(Modifier.PUBLIC);

    Method[] methods = interface_.getDeclaredMethods();
    for (Method method : methods) {

        // 方法参数列表
        List<ParameterSpec> paramList = new ArrayList<>();
        Type[] paramTypes = method.getGenericParameterTypes();
        int count = 1 ;
        for (Type param : paramTypes) {
            ParameterSpec paramSpec = ParameterSpec.builder(Class.forName(param.getTypeName()), "param" + count)
                    .build();
            count ++;
            paramList.add(paramSpec);
        }

        // 方法名、方法访问限定符、参数列表、返回值、方法体等
        Class<?> returnType = method.getReturnType();
        MethodSpec.Builder builder = MethodSpec.methodBuilder(method.getName())
                .addModifiers(Modifier.PUBLIC)
                .addParameters(paramList)
                .addAnnotation(Override.class)
                .returns(returnType)
                .addCode("\n")
                .addStatement("$T.out.println(\"数据库操作, 并获取执行结果...\")", System.class)    // 真正数据库操作,会有返回值,下面的return返回应该是此返回值
                .addCode("\n");
        if (!"void".equals(returnType.getName())) {
            builder.addStatement("return null");       // 这里的"null"应该是上述中操作数据库后的返回值,为了演示写成了null
        }

        MethodSpec methodSpec = builder.build();
        typeSpecBuilder.addMethod(methodSpec);

    }

    JavaFile javaFile = JavaFile.builder(interface_.getPackage().getName(), typeSpecBuilder.build()).build();
    javaFile.writeTo(new File(SRC_JAVA_PATH));
}

生成的代理类:JavaPoet$Proxy0.java 如下

java 复制代码
package com.lee.mapper;

import com.lee.model.User;
import java.lang.Integer;
import java.lang.Override;
import java.lang.System;

public class JavaPoet$Proxy0 implements UserMapper {
  @Override
  public Integer save(User param1) {

    System.out.println("数据库操作, 并获取执行结果...");

    return null;
  }

  @Override
  public User getUserById(Integer param1) {

    System.out.println("数据库操作, 并获取执行结果...");

    return null;
  }
}

利用 javapoet 生成的代理类更接近我们平时手动实现的类,排版更符合我们的编码习惯,看上去更自然一些

两者的实现过程是一样的,只是 javapoet 排版更好

2、既然代理类的源代码已经有了,那么需要对其编译了,compileJavaFile 方法如下

java 复制代码
/**
 * 编译代理类源代码生成代理类class
 * @param proxyJavaFileDir
 */
private static void compileJavaFile(String proxyJavaFileDir) throws Exception {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    //获得文件管理者
    StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
    Iterable<? extends JavaFileObject> fileObjects = manager.getJavaFileObjects(proxyJavaFileDir + PROXY_CLASS_NAME + ".java");
    //编译任务
    JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, fileObjects);
    //开始编译,执行完可在指定目录下看到.class文件
    task.call();
    //关闭文件管理者
    manager.close();
}

会在指定目录下看到:$Proxy0.class

3、加载 $Proxy0.class,并创建其实例对象(代理实例对象)

java 复制代码
public static <T> T newInstance(Class<T> interface_) throws Exception{
    String proxyJavaFileDir = SRC_JAVA_PATH + interface_.getPackage().getName().replace(".", File.separator) + File.separator;

    // 1、生成interface_接口的实现类,并持久化到磁盘:$Proxy0.java
    generateJavaFile(interface_, proxyJavaFileDir);

    // 2、编译$Proxy0.java,生成$Proxy0.class到磁盘
    compileJavaFile(proxyJavaFileDir);

    // 3、加载$Proxy0.class,并创建其实例对象(代理实例对象)
    MyClassLoader loader = new MyClassLoader(proxyJavaFileDir, interface_);
    Class<?> $Proxy0 = loader.findClass(PROXY_CLASS_NAME);
    return (T)$Proxy0.newInstance();
}

private static class MyClassLoader<T> extends ClassLoader {

    private String proxyJavaFileDir;
    private Class<T> interface_;

    public MyClassLoader(String proxyJavaFileDir, Class<T> interface_) {
        this.proxyJavaFileDir = proxyJavaFileDir;
        this.interface_ = interface_;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        File clazzFile = new File(proxyJavaFileDir, name + ".class");
        //如果字节码文件存在
        if (clazzFile.exists()) {
            //把字节码文件加载到VM
            try {
                //文件流对接class文件
                FileInputStream inputStream = new FileInputStream(clazzFile);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int len;
                while ((len = inputStream.read(buffer)) != -1) {
                    baos.write(buffer, 0, len);                     // 将buffer中的内容读取到baos中的buffer
                }
                //将buffer中的字节读到内存加载为class
                return defineClass(interface_.getPackage().getName() + "." + name, baos.toByteArray(), 0, baos.size());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return super.findClass(name);
    }
}

有了代理实例对象,我们就可以利用它进行操作了,演示结果如下

完整工程地址:proxy-java-file,完整流程图如下

此时的 Proxy 类能创建任何接口的实例,解决了静态代理存在的代理类泛滥、多个方法中代理逻辑反复实现的问题

但有个问题不知道大家注意到:$Proxy0.java 有必要持久化到磁盘吗,我们能不能直接编译内存中的代理类的字符串源代码,得到 $Proxy0.class 呢?

代理类源代码不持久化

$Proxy0.java$Proxy0.class 是没必要生成到磁盘的,我们直接编译内存中的代理类的字符串源代码,同时直接在内存中加载 $Proxy0.class,不用写、读磁盘,可以提升不少性能

完整工程地址:proxy-none-java-file,此时的流程图如下

Proxy.java 源代码如下

java 复制代码
package com.lee.proxy;

import com.itranswarp.compiler.JavaStringCompiler;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Map;

public class Proxy {

    private static final String ENTER = "\r\n";
    private static final String TAB_STR = "    ";
    private static final String PROXY_FILE_NAME = "$Proxy0";

    /**
     * 生成接口实现类的源代码, 并持久化到java文件
     * @param interface_
     * @throws Exception
     */
    private static String generateJavaFile(Class<?> interface_) throws Exception {
        StringBuilder proxyJava = new StringBuilder();
        proxyJava.append("package ").append(interface_.getPackage().getName()).append(";").append(ENTER).append(ENTER)
                .append("public class ").append(PROXY_FILE_NAME).append(" implements ").append(interface_.getName()).append(" {");
        Method[] methods = interface_.getMethods();
        for(Method method : methods) {
            Type returnType = method.getGenericReturnType();
            Type[] paramTypes = method.getGenericParameterTypes();
            proxyJava.append(ENTER).append(ENTER).append(TAB_STR).append("@Override").append(ENTER)
                    .append(TAB_STR).append("public ").append(returnType.getTypeName()).append(" ").append(method.getName()).append("(");
            for(int i=0; i<paramTypes.length; i++) {
                if (i != 0) {
                    proxyJava.append(", ");
                }
                proxyJava.append(paramTypes[i].getTypeName()).append(" param").append(i);
            }
            proxyJava.append(") {").append(ENTER)
                    .append(TAB_STR).append(TAB_STR)
                    .append("System.out.println(\"数据库操作, 并获取执行结果...\");").append(ENTER); // 真正数据库操作,会有返回值,下面的return返回应该是此返回值
            if (!"void".equals(returnType.getTypeName())) {
                proxyJava.append(TAB_STR).append(TAB_STR).append("return null;").append(ENTER);      // 这里的"null"应该是上述中操作数据库后的返回值,为了演示写成了null
            }
            proxyJava.append(TAB_STR).append("}").append(ENTER);
        }
        proxyJava .append("}");

        return proxyJava.toString();
    }

    private final static Class<?> compile(String className, String content) throws Exception {
        JavaStringCompiler compiler = new JavaStringCompiler();
        Map<String, byte[]> byteMap = compiler.compile(PROXY_FILE_NAME + ".java", content);
        Class<?> clazz = compiler.loadClass(className, byteMap);
        return clazz;
    }

    public static <T> T newInstance(Class<T> interface_) throws Exception{

        // 1、生成源代码字符串
        String proxyCodeStr = generateJavaFile(interface_);

        // 2、字符串编译成Class对象
        Class<?> clz = compile(interface_.getPackage().getName() + "." + PROXY_FILE_NAME, proxyCodeStr);
        return (T)clz.newInstance();
    }
}

相比 有代理类源代码持久化,核心的动态代理生成过程不变,只是减少了.java.class 文件的持久化

其中用到了第三方工具:com.itranswarp.compile(我们也可以拓展 jdk,实现内存中操作),完成了字符串在内存中的编译、class 在内存中的加载

如果直接用 jdk 的编译工具,会在磁盘生成 $Proxy0.class

测试结果如下

可以看到,没有 .java.class 的持久化 此时就完美了吗?

如果现在有另外一个接口 ISendMessage,代理逻辑不是

java 复制代码
System.out.println("数据库操作, 并获取执行结果...")

试问阁下,该如何应对?

难道针对 ISendMessage 又重新写一个 Proxy

显然还不够灵活,说的简单点:此种代理可以代理任何接口,但是代理逻辑确是固定死的,不能自定义,这样会造成一种代理逻辑会有一个代理工厂(Proxy),会造成代理工厂的泛滥

代理逻辑接口化,供用户自定义

既然 无代理类源代码持久化 中的代理逻辑不能自定义,那么我们就将它抽出来,提供代理逻辑接口

完整工程地址:proxy-none-java-file-plus,流程图与 无代理类源代码持久化 中一样

此时代理类的生成过程复杂了不少,涉及到代理逻辑接口:InvacationHandler 的处理
generateJavaFile(...) 方法

java 复制代码
/**
 * 生成接口实现类的源代码
 * @param interface_
 * @throws Exception
 */
private static String generateJavaFile(Class<?> interface_, InvocationHandler handler) throws Exception {
    StringBuilder proxyJava = new StringBuilder();
    proxyJava.append("package ").append(PROXY_PACKAGE_NAME).append(";").append(ENTER).append(ENTER)
            .append("import java.lang.reflect.Method;").append(ENTER).append(ENTER)
            .append("public class ").append(PROXY_FILE_NAME).append(" implements ").append(interface_.getName()).append(" {").append(ENTER)
            .append(ENTER).append(TAB_STR).append("private InvocationHandler  handler;").append(ENTER).append(ENTER);

    // 代理对象构造方法
    proxyJava.append(TAB_STR).append("public ").append(PROXY_FILE_NAME).append("(InvocationHandler handler) {").append(ENTER)
            .append(TAB_STR).append(TAB_STR).append("this.handler = handler;").append(ENTER)
            .append(TAB_STR).append("}").append(ENTER);

    // 接口方法
    Method[] methods = interface_.getMethods();
    for(Method method : methods) {
        String returnTypeName = method.getGenericReturnType().getTypeName();
        Type[] paramTypes = method.getGenericParameterTypes();
        proxyJava.append(ENTER).append(TAB_STR).append("@Override").append(ENTER)
                .append(TAB_STR).append("public ").append(returnTypeName).append(" ").append(method.getName()).append("(");

        List<String> paramList = new ArrayList<>();     // 方法参数值
        List<String> paramTypeList = new ArrayList<>(); // 方法参数类型
        for(int i=0; i<paramTypes.length; i++) {
            if (i != 0) {
                proxyJava.append(", ");
            }
            String typeName = paramTypes[i].getTypeName();
            proxyJava.append(typeName).append(" param").append(i);
            paramList.add("param" + i);
            paramTypeList.add(typeName+".class");
        }
        proxyJava.append(") {").append(ENTER)
                .append(TAB_STR).append(TAB_STR).append("try {").append(ENTER)
                .append(TAB_STR).append(TAB_STR).append(TAB_STR)
                .append("Method method = ").append(interface_.getName()).append(".class.getDeclaredMethod(\"")
                .append(method.getName()).append("\",").append(String.join(",", paramTypeList)).append(");")
                .append(ENTER).append(TAB_STR).append(TAB_STR).append(TAB_STR);
        if (!"void".equals(returnTypeName)) {
            proxyJava.append("return (").append(returnTypeName).append(")");
        }
        proxyJava.append("handler.invoke(this, method, new Object[]{")
                .append(String.join(",", paramList)).append("});").append(ENTER)
                .append(TAB_STR).append(TAB_STR).append("} catch(Exception e) {").append(ENTER)
                .append(TAB_STR).append(TAB_STR).append(TAB_STR).append("e.printStackTrace();").append(ENTER)
                .append(TAB_STR).append(TAB_STR).append("}").append(ENTER);
        if (!"void".equals(returnTypeName)) {
            proxyJava.append(TAB_STR).append(TAB_STR).append("return null;").append(ENTER);
        }
        proxyJava.append(TAB_STR).append("}").append(ENTER);
    }
    proxyJava .append("}");

    // 这里可以将字符串生成java文件,看看源代码对不对
    /*String proxyJavaFileDir = System.getProperty("user.dir") + File.separator + "proxy-none-java-file-plus"
            + String.join(File.separator, new String[]{"","src","main","java",""})
            + PROXY_PACKAGE_NAME.replace(".", File.separator) + File.separator;
    File f = new File(proxyJavaFileDir + PROXY_FILE_NAME + ".java");
    FileWriter fw = new FileWriter(f);
    fw.write(proxyJava.toString());
    fw.flush();
    fw.close();*/

    return proxyJava.toString();
}

测试结果如下

此时各组件之间关系、调用情况如下

此时 Proxy 就可以完全通用了,可以生成任何接口的代理对象了,也可以实现任意的代理逻辑

至此,我们完成了一个简易的仿 JDK 实现的动态代理

有没有觉得很厉害?

JDK的动态代理

我们来看看 JDK 下动态代理的实现,示例工程:proxy-jdk,测试结果就不展示了

我们来看看 JDKProxy.newInstance 方法,有三个参数

1、Classloader:类加载器,我们可以使用自定义的类加载器;上述手动实现示例中,直接在 Proxy 写死了

2、Class<?>[]:接口类数组,这个其实很容易理解,我们应该允许我们自己实现的代理类同时实现多个接口;我们上述手动实现中只传入一个接口,是为了简化实现

3、InvocationHandler:这个没什么好说的,与我们的实现一致,用于自定义代理逻辑

我们来追下源码,看看 JDK 的动态代理是否与我们的手动实现是否一致

与我们的自定义实现差不多,利用反射,逐个接口、逐个方法进行处理
ProxyClassFactory 负责生成代理类的 Class 对象,主要由 apply 方法负责,调用了

java 复制代码
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);

来生成代理类的 Class
ProxyGenerator 中有个是有静态常量:saveGeneratedFiles,标识是否持久化代理类的 class 文件,默认值是 false,也就是不持久化,我们可以通过设置 JDK 系统参数,实现 JDK 的动态代理持久化代理类的 class 文件

CGLIB代理

代码示例:proxy-cglib

使用方式与 JDK 的动态代理类似,实现的效果也基本一致,但是实现原理和性能上还是有区别的

具体差别,你们自己去查

应用场景

长篇大论讲了那么多,却一直没有讲动态代理的作用,不要觉得我很唠叨,我就是唠叨!

使用动态代理我们可以在不改变源码的情况下,对目标对象的目标方法进行前置或后置增强处理

这有点不太符合我们的一条线走到底的编程逻辑,但确实很有用

这种编程模型有一个专业名称叫 AOP面向切面编程,具体案例有如下:

1、Spring 的事务,事务的开启可以作为前置增强,事务的提交或回滚作为后置增强,业务操作处在两者之间

2、日志记录,我们可以在不改变原有实现的基础上,对目标对象进行日志的输出,可以前置处理,记录参数情况,也可以后置处理,记录返回的结果

3、参数校验

4、权限控制

等等,只要明白了 AOP,那么哪些场景能使用动态代理也就比较明了了

说到 AOP,我再补充一个问题:Spring AOPAspectJ AOP 有什么区别

总结

1、示例代码中的 Proxy 是代理工厂,负责生产代理对象的,不是代理对象类

2、手动实现动态代理,我们分了三版

第一版:代理类源代码持久化,为了便于理解,我们将代理类的 java 文件和 class 文件持久化到了磁盘,此时解决了静态代理中代理类泛滥的问题,我们的代理类工厂(Proxy)能代理任何接口

第二版:代理类源代码不持久化,代理类的 java 文件和和 class 文件本来就只是临时文件,将其去掉,不用读写磁盘,可以提高效率;但此时有个问题,我们的代理逻辑却写死了,也就说一个代理类工厂只能生产一种代理逻辑的代理类对象,如果我们有多种代理逻辑,那么就需要有多个代理类工厂,显然灵活性不够高,还有优化空间

第三版:代理逻辑接口化,供用户自定义,此时代理类工厂就可以代理任何接口、任何代理逻辑了,反正代理逻辑是用户自定义传入,用户想怎么定义就怎么定义

3、示例参考的是 MybatisMapper 的生成过程,虽然只是简单的模拟,但流程却是一致的

相关推荐
WaaTong几秒前
《重学Java设计模式》之 单例模式
java·单例模式·设计模式
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠2 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
WaaTong2 小时前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
AskHarries2 小时前
Java字节码增强库ByteBuddy
java·后端
霁月风2 小时前
设计模式——观察者模式
c++·观察者模式·设计模式
佳佳_2 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平4 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono