JDK动态代理牛在哪里

前言

前几篇我们讲了什么是静态代理,因为它会导致类爆炸。所以我们引入了动态代理,并且模拟实现了一个动态代理的例子,作为辅助讲解。其实上一篇博客留了一个小点没有做完,那就是实现内容的动态。本篇博客将会接着上一篇的例子,来构建一个动态内容的代理过程,并且通过自己构建的动态代理的例子来对比JDK源码来分析,大神们写的代码道理厉害在哪里。建议阅读本篇博客前先阅读【什么是代理(Proxy)】【什么是动态代理?】 这两篇博客,并且跟着博主一起实际敲一敲代码。更多Spring内容进入【Spring解读系列目录】

改造自创的动态代理例子

首先我们新创建一个接口MyDao,用MyDaoImpl去实现它。为了简化代码全部抛出异常,这样在构建新的代理类的时候就不用考虑try-catch了。

public interface MyDao {
    
    
    String proxyPrint()throws Exception;
}
public class MyDaoImpl implements MyDao{
    
    
    @Override
    public String proxyPrint() throws Exception {
    
    
        return "MyDao proxyPrint()";
    }
}

然后我们改造一下动态代理生成类ProxyUtil中的methodDefine,让其能够适配具有返回值的方法。

改造
methodDefine += tab + "public " + returnType + " " + methodName + "(" + param + ") {" + line
        + tab + tab + "System.out.println(\"---interface self proxy log---\");" + line;
if (returnType.equals("void")) {
    
    //加一个判断,如果是void就不构造return
    methodDefine += tab + tab + "target." + methodName + "(" + paramsLine + ");" + line
            + tab + "}";//拼成方法行,对应:public void query() {...}
} else {
    
    
    methodDefine += tab + tab + "return target." + methodName + "(" + paramsLine + ");" + line
            + tab + "}"; //拼成方法行,对应:public Type query() {...}
}

Main方法调用运行就生成了如下的代理类,但是我们看到System.out.println("---interface self proxy log---");这一行是写死的,就意味着我们的代理类增强的这部分逻辑通用性并不好。最终我们生了这样的一个代理类。

package com.demo.proxyDyn;
import com.demo.dao.MyDao;
public class $Proxy implements MyDao {
    
    
    private MyDao target;
    public $Proxy (MyDao target){
    
    
        this.target = target;
    }
    public String proxyPrint() {
    
    
        System.out.println("---interface self proxy log---");
        return target.proxyPrint();
    }
}

JDK的动态代理

在做进一步改造前,我们先看下JDK的动态代理是怎么做的。JDK通过下面这个方法构造一个动态代理对象出来Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h),我们看下这三个参数:

第一个要传入ClassLoader。这是因为项目启动的流程,当JVM的启动的时候就会把项目里面所有的类给load到JVM中,但是这些类有个前提条件,就是已经编译好的也就是生成了class文件的那些。但是我们项目运行过程中动态产生了一些类,但是这些类并不会在一开始就被加载到JVM中。因此在生成动态代理的时候需要再次使用类加载器把生成的类和class文件重新加载到JVM中。因此我们调用JDK的动态代理的时候要使用一个类加载器把这个类再次load一遍,因此我们要传递当前类的ClassLoader进去。
在这里插入图片描述
第二个interfaces。这个是一个类数组用来接收哪些接口需要代理。

最后一个InvocationHandler。点进去发现是一个接口,里面只有一个invoke方法,这个方法就是反射的目标对象里的方法,那么我们实现这个接口。

public class MyInvocationHandler implements InvocationHandler {
    
    
    Object target;//目标对象
    public MyInvocationHandler(Object target) {
    
    
	//目标对象通过构造参数传递进来,多提一下,这种方法就是所谓的装配者模式
        this.target=target;
    }
	//参数:proxy代理对象;method目标对象里面的方法;args目标对象里面的方法的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    

        System.out.println("MyInvocationHandler self invoke");       //执行逻辑
        //目标对象要执行,至少要传递一个目标对象进去,但是看这里的参数,只有代理对象。
        //所以我们怎么传进来呢?可以通过构造方法传入。
        return  method.invoke(target,args);
    }
}

构造完InvocationHandler以后我们就可以使用了。

public class MainTest {
    
    
    public static void main(String[] args) {
    
    
        //JDK
        MyDao jdkDao = (MyDao) Proxy.newProxyInstance(MainTest.class.getClassLoader(),
                new Class[]{
    
    MyDao.class},
                new MyInvocationHandler(new MyDaoImpl()));
        System.out.println(jdkDao.proxyPrint());
    }
}
运行结果,完成代理
MyInvocationHandler self invoke  	//增强的功能
MyDao proxyPrint()				//主逻辑

那么先总结JDK是怎么做到动态代理的呢?首先加载动态代理类getClassLoader(),然后把要代理的所有接口传进去new Class[]{MyDao.class},并且告知这些接口的代理逻辑MyInvocationHandler(new MyDaoImpl())是哪些实现类。然后JDK就能帮助我们完成代理。

JDK整个逻辑就是:首先要告诉JDK需要代理的接口,然后JDK会把这些接口下面所有的方法都进行代理。代理增强的逻辑就是invoke方法里面的逻辑,这就完成了动态的逻辑。这个和我们自己写的有什么区别呢?因为我们的方法是写死的字符串,所以我们就要想办法拿到这个method进行反射执行。所以JDK就给我们提供了一个很好的解决思路,按照这个思路,我们自己的代理对象里面应该有一个InvocationHandler h,然后由这个h把方法调起来。

实现自己的InvocationHandler

既然我们有了思路,重新构建一个CustomerInvocationHandler接口,然后声明一个实现类TestInvocationHandler用于实现我们的增强逻辑。

public interface CustomerInvocationHandler {
    
    
    public Object invoke(Method method);
}
public class TestInvocationHandler implements CustomerInvocationHandler {
    
    
    Object target; 
    //把目标对象传进来,我们就是用这个来命中目标对象的方法的
    public TestInvocationHandler(Object target){
    
    
        this.target=target;
    }
    @Override
    public Object invoke(Method method) {
    
    
        try {
    
    
            System.out.println("this is my TestInvocationHandler");
            return method.invoke(target);
        } catch (IllegalAccessException e) {
    
    
            e.printStackTrace();
        } catch (InvocationTargetException e) {
    
    
            e.printStackTrace();
        }
        return null;
    }
}

既然我们自己完成了InvocationHandler这个功能,那么我们要对上一篇博客做的ProxyUtil类进行改造,生成一个满足我们需求的代理类$Proxy。因为在现有的模型下,我们需要传入到代理里面的是InvocationHandler,因此代理类构造方法要装配的是Handler,然后将使用这个Handler调起我们的目标对象里面的方法。所以关键就在于如何获取目标对象中的Method对象。所以我们只要让$Proxy这个代理类实现了MyDao,就可以通过Class.forName拿到这个类对象,进而获取方法对象,于是我们的新的$Proxy,就应该是下面的样子。

package com.demo.proxyDyn; 
import com.demo.dao.MyDao;
import com.demo.dao.CustomerInvocationHandler;
import java.lang.Exception;
import java.lang.reflect.Method;
public class $Proxy implements MyDao {
    
    
    private CustomerInvocationHandler h; //传入handler而不是代理对象
    public $Proxy (CustomerInvocationHandler h){
    
    
        this.h = h;
    }
    public String proxyPrint() throws Exception{
    
    
        Method method = Class.forName("com.demo.dao.MyDao").getDeclaredMethod("proxyPrint");
        return (String)h.invoke(method);
    }
}

改造ProxyUtil完了以后,修改Main方法运行,生成上述代理类和class文件并打印。那么我们就完成了一个对JDK的动态代理的过程。改造ProxyUtil后的代码太长,会在最后贴出来。

public class MainTest {
    
    
    public static void main(String[] args) {
    
    
         MyDao dao= (MyDao) ProxyUtil.newInstance(MyDao.class,new TestInvocationHandler(new MyDaoImpl()));
        try {
    
    
            System.out.println(dao.proxyPrint());
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}
打印代理
this is my TestInvocationHandler
MyDao proxyPrint()

从这个例子可以看出,我们传递了接口,使用InvocationHandler对目标对象MyDaoImpl其中的方法进行反射调用,然后通过代理对象实现了目标功能。

对比

既然我们能够手写一个模拟的JDK动态代理,那就要看下JDK的原生代理比我们高明到哪里。首先我们用点从表面上看,基本上在运行时是一模一样的,都是dao->handler->target(MyDaoImpl)
我们自己的:
在这里插入图片描述
JDK的
在这里插入图片描述
然后我们看代码层面。JDK实现的是自己的InvocationHandler,我们自己实现了CustomerInvocationHandler。两者里面都是只有一个invoke方法,只不过我们的更加简单。

纠错

那么我们现在就开始进入正式找茬模式,看看我们自己写的有什么缺点:

  1. 首先要生成文件
  2. 动态编译文件
  3. 需要外部类加载器,没有办法很好的是适配当前项目。

我们自己写的代码尽管很像JDK提供的功能,但是不可避免的进行IO磁盘操作,频繁的IO操作会导致我们的动态代理产生的非常的慢,最终影响到整个系统的效率。

JDK是怎么做的

点进JDK的原生方法ProxyUtil.newInstance()看看JDK到底怎么玩的,删掉不相关的部分代码。

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
        throws IllegalArgumentException
{
    
    
	//这里产生了$Proxy对象cl    
	Class<?> cl = getProxyClass0(loader, intfs);
    try {
    
    
        if (sm != null) {
    
    
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }
        //通过cl创建了一个构造方法
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        //通过cons对象new一个目标对象的代理对象,在例子里new的就是MyInvocationHandler
        return cons.newInstance(new Object[]{
    
    h}); //h就是InvocationHandler
    }
}

进入以后看到Objectreturn出去的,那么往上找cons怎么来的,发现cons通过cl创建了一个构造方法来的,再接着往上Class<?> cl = getProxyClass0(loader, intfs);,也就是说getProxyClass0这个方法就是产生代理对象的地方。那么继续进入getProxyClass0(loader, intfs);

private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
    
    
    if (interfaces.length > 65535) {
    
    
        throw new IllegalArgumentException("interface limit exceeded");
    }
    //缓存拿到数据
    return proxyClassCache.get(loader, interfaces);
}

这里通过一个cache缓存拿到了ClassLoader和要代理的各个接口,因此我们接着进入get方法:

public synchronized V get() {
    
    
	V value = null;
	try {
    
    
		//给value赋值
        value = Objects.requireNonNull(valueFactory.apply(key, parameter));
        } finally {
    
    
            if (value == null) {
    
    
                valuesMap.remove(subKey, this);
            }
        }
        return value;
    }
}

进入以后发现最终return value;,所以把所有和value不相关的代码全部清掉,就发现了value唯一赋值的地方,进入这个valueFactory.apply(key, parameter)接着进入apply()方法。这是Proxy下的一个内部类中的方法,代码很长我们只写关键的。

ProxyClassFactory.apply(ClassLoader loader, Class<?>[] interfaces):
找到最关键的一句代码:
生成指定的代理类,而且这个所谓的代理类是一个byte数组,我们看传入的是代理类名proxyName和接口interfaces
byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags);
然后直接返回一个class出去了。
return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);

JDK这里直接就 用byte[]-直接->object(class的对象),点进去发现defineClass0方法是一个native方法,下面就是c语言搞得事情了。对比一下我们自己的代码步骤,首先生成java file-->class file-->classLoader-->JVM:byte[]-->object(class的对象)。所以JDK比我们强悍在哪里呢?就是这里,JDK通过接口反射得到字节码,然后把字节码转成class,再用native方法直接拿到对象返回出去,全部过程在内存里完成。看了源码感叹一句,能够熟练的调度使用各种系统资源,无缝的使底层与上层的代码连接,也许就是大神和普通人的差距吧。

附:改造后的ProxyUtil

public class ProxyUtil {
    
    
    public static Object newInstance(Class target, CustomerInvocationHandler h) {
    
    
        Object proxy=null;

        String line = "\n"; //换行
        String tab = "\t"; //空格
        String targetName = target.getSimpleName();  //构造类名行
        //构造包行,对应package com.demo.proxyImpl;
        String packagePath = "package com.demo.proxyDyn;" + line;
        //构造导入行,对应 import 导入各种包
        String importPath = "import " + target.getName() + ";" + line
                +"import com.demo.dao.CustomerInvocationHandler;"+line
                +"import java.lang.Exception;"
                +"import java.lang.reflect.Method;"+line;;
        //构造类定义行,对应public class $Proxy implements QueryDao
        String classDefine = "public class $Proxy implements " + targetName + " {" + line;
        //构造字段行,对应private CustomerInvocationHandler h;
        String fieldDefine = tab + "private CustomerInvocationHandler h;" + line;
        //构造构造方法行,对应public QueryDaoLog(QueryDao target) { ... }
        String constructorDefine = tab + "public $Proxy (CustomerInvocationHandler h){ " + line
                + tab + tab + "this.h = h; " + line
                + tab + "}" + line;
        //得到接口的所有方法,用数组存起来
        Method[] methods = target.getDeclaredMethods();
        String methodDefine = ""; //构造方法定义行
        for (Method method : methods) {
    
     //循环遍历拿到的方法
            String returnType = method.getReturnType().getSimpleName(); //拿到返回值
            String methodName = method.getName(); //拿到方法名字
            //得到方法的所有参数,用数组存起来
            Class[] params = method.getParameterTypes();
            String param = "";
            String paramsLine = ""; //构造参数行
            int count = 0;
            for (Class obj : params) {
    
     //循环遍历参数类型
                String temp = obj.getSimpleName(); //拿到参数名字
                param += temp + " a" + count + ","; //构建一个循环的参数,这里就是a0
                paramsLine += "a" + count + ", "; //拼成一个参数行
                count++; //参数名+1
            }
            if (param.length() > 0) {
    
     //如果拿出来有参数
                param = param.substring(0, param.lastIndexOf(",") - 1); //去掉最后的","
                paramsLine = paramsLine.substring(0, paramsLine.lastIndexOf(",") - 1); //参数行去掉最后的","
            }
            //构造方法定义的行
            methodDefine += tab + "public " + returnType + " " + methodName + "(" + param + ") throws Exception{" + line
                    +tab+tab+"Method method = Class.forName(\""+target.getName()+"\").getDeclaredMethod(\""+methodName+"\");"+line
                    +tab+tab+"return ("+returnType+")h.invoke(method);"+line;
            methodDefine+=tab+"}"+line;
        }

        //最后把所有的字段组装在一起
        String content = packagePath + importPath + classDefine + fieldDefine + constructorDefine + methodDefine + line + "}";

        //写出去。
        File file = new File("d:\\com\\demo\\proxyDyn");
        try {
    
    
            if (!file.exists()) {
    
    
                file.mkdirs();
                file = new File("d:\\com\\demo\\proxyDyn\\$Proxy.java");
                file.createNewFile();
            } else {
    
    
                file = new File("d:\\com\\demo\\proxyDyn\\$Proxy.java");
                file.createNewFile();
            }
            FileOutputStream fw = new FileOutputStream(file);
            fw.write(content.getBytes());
            fw.flush();
            fw.close();

            //动态编译Java文件
            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();

            //外部获取java类
            URL[] urls = new URL[]{
    
    new URL("file:D:\\\\")};
            URLClassLoader urlClassLoader = new URLClassLoader(urls);
            Class clazz = urlClassLoader.loadClass("com.demo.proxyDyn.$Proxy");
            //这里构造方法构造的是Handler不再是目标方法了
            Constructor constructor = clazz.getConstructor(CustomerInvocationHandler.class);
            proxy = constructor.newInstance(h); //通过构造方法拿到代理类

        } catch (Exception e) {
    
    
            e.printStackTrace();
        }

        return proxy;

    }
}

猜你喜欢

转载自blog.csdn.net/Smallc0de/article/details/108226988