java基础学习总结(二十一):自己写一个java.lang.reflect.Proxy代理的实现

     动态代理里面用到了一个类就是java.lang.reflect.Proxy,这个类是根据代理内容为传入的接口生成代理用的。本文就自己写一个Proxy类出来,功能和java.lang.reflect.Proxy一样,传入接口、代理内容,生成代理。

 动态代理的实现应用到的技术

      1、动态编译技术,可以使用Java自带的JavaCompiler类,也可以使用CGLIB、ASM等字节码增强技术,Java的动态代理包括Spring的内部实现貌似用的都是这个

      2、反射,包括对于类.class和getClass()方法的理解,Method类、Constructor类的理解

     3、IO流,主要就是字符输出流FileWriter

     4、对于ClassLoader的理解

基础类

先把基础类定义在这儿,首先是一个HelloWorld接口:

public interface HelloWorld
{
    void print();
}

HelloWorld接口的实现类:

public class HelloWorldImpl implements HelloWorld
{
    public void print()
    {
        System.out.println("Hello World");
    }
}

为这个接口写一个简单的静态代理类:

public class StaticProxy implements HelloWorld
{
    private HelloWorld helloWorld;
    
    public StaticProxy(HelloWorld helloWorld)
    {
        this.helloWorld = helloWorld;
    }
    
    public void print()
    {
        System.out.println("Before Hello World!");
        helloWorld.print();
        System.out.println("After Hello World!");
    }
}

版本1:为一个静态代理动态生成一个代理类

      我们知道如果用静态代理的话,那么每个接口都要为之写一个.java的代理类,这样就可能造成代理类无限膨胀,如果可以让Java帮我们自动生成一个就好了,不过还真的可以,看下第一个版本的代码:

package proxy;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.Serializable;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * @author of  laomumu
 * @create 2018-10-22 14:59
 **/
public class ProxyVersion_0 implements Serializable {

    private static final  long serialVersionUID = 1L;

    public static  Object newProxyInstance() throws Exception{

        String src = "package proxy;\n\n" +
                "public class StaticProxy implements HelloWorld\n" +
                "{\n" +
                "\tHelloWorld helloWorld;\n\n" +
                "\tpublic StaticProxy(HelloWorld helloWorld)\n" +
                "\t{\n" +
                "\t\tthis.helloWorld = helloWorld;\n" +
                "\t}\n\n" +
                "\tpublic void print()\n" +
                "\t{\n" +
                "\t\tSystem.out.println(\"Before Hello World!\");\n" +
                "\t\thelloWorld.print();\n" +
                "\t\tSystem.out.println(\"After Hello World!\");\n" +
                "\t}\n" +
                "}";
        /** 生成一段Java代码 */
        String fileDir = System.getProperty("user.dir");
        String fileName = fileDir + "\\src\\main\\java\\proxy\\StaticProxy.java";
        File javaFile = new File(fileName);
        Writer writer = new FileWriter(javaFile);
        writer.write(src);
        writer.close();
        /** 动态编译这段Java代码,生成.class文件 */
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);
        JavaCompiler.CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);
        ct.call();
        sjfm.close();
        /** 将生成的.class文件载入内存,默认的ClassLoader只能载入CLASSPATH下的.class文件 */
        URL[] urls = new URL[] {(new URL("file:\\" + System.getProperty("user.dir") + "\\src"))};
        URLClassLoader ul = new URLClassLoader(urls);
        Class<?> c = ul.loadClass("proxy.StaticProxy");
        /** 利用反射将c实例化出来 */
        Constructor<?> constructor = c.getConstructor(HelloWorld.class);
        HelloWorld helloWorldImpl = new HelloWorldImpl();
        HelloWorld helloWorld = (HelloWorld)constructor.newInstance(helloWorldImpl);
        /** 使用完毕删除生成的代理.java文件和.class文件,这样就看不到动态生成的内容了 */
        File classFile = new File(fileDir + "\\src\\main\\java\\proxy\\StaticProxy.class");
        javaFile.delete();
        classFile.delete();
        return helloWorld;
    }

}

每一步的注释都在上面了,解释一下大致思路:

1、我们在另外一个类里面自己拼一段静态代理的代码的字符串

2、为这个字符串生成一个.java文件,并放在我们工程的某个目录下面,因为是.java文件,所以在src下

3、利用JavaCompiler类动态编译这段.java代码使之被编译成一个.class文件,JavaCompiler不熟悉没关系,知道就好了

4、因为在src下生成编译之后的.java文件,而默认的ClassLoader只能加载CLASSPATH下的.class文件,所以用URLClassLoader

5、由于代理类只有一个带参数的构造方法,所以要用java.lang.reflect.Constructor

6、最后把生成的StaticProxy.class文件删除(最好生成的StaticProxy.java也删除,这里没删除,是因为StaticProxy是生成的一个重要的中间类,功能都在它这儿,所以不删,出了错都要靠看这个类来定位问题的),这样代理的中间内容都没了,把反射newInstance()出来的内容返回出去就大功告成了

可以自己看一下生成的StaticProxy.java对不对,写一段代码测试一下:

public static void main(String[] args) throws Exception
{    
    long start = System.currentTimeMillis();
    HelloWorld helloWorld = (HelloWorld)ProxyVersion_0.newProxyInstance();
    System.out.println("动态生成代理耗时:" + (System.currentTimeMillis() - start) + "ms");
    helloWorld.print();
    System.out.println();        
}

结果为:

动态代理耗时:827ms
Before Hello World!
Hello World
After Hello World!

   没有问题。可能有些人运行会报错"Exception in thread "main" java.lang.ClassNotFoundException: proxy.StaticProxy",没关系,那是因为虽然你的src目录下生成了StaticProxy.class,但没有出来,点击src文件夹,再按F5(或者右键,点击Refresh也行)刷新一下就可以了。或者将StaticProxy.class文件删除重新执行。

版本二:为指定接口生成代理类

    版本一已经实现了动态生成一个代理的.class文件了,算是成功的第一步,接下来要做进一步的改进。版本一只可以为固定的一个接口生成代理,现在改进成,传入某个接口的java.lang.Class对象,可以为这个接口及里面的方法都生成代理内容,代码这么写:

package proxy;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.Serializable;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * 
 * @create 2018-10-22 15:41
 **/
public class ProxyVersion_1 implements Serializable {

    private static final long serialVersionUID = 1L;

    public static Object newProxyInstance(Class<?> interfaces) throws Exception {
        Method[] methods = interfaces.getMethods();

        StringBuilder sb = new StringBuilder(700);

        sb.append("package proxy;\n\n");
        sb.append("public class StaticProxy implements " + interfaces.getSimpleName() + "\n");
        sb.append("{\n");
        sb.append("\t" + interfaces.getSimpleName() + " interfaces;\n\n");
        sb.append("\tpublic StaticProxy(" + interfaces.getSimpleName() + " interfaces)\n");
        sb.append("\t{\n");
        sb.append("\t\tthis.interfaces = interfaces;\n");
        sb.append("\t}\n\n");
        for (Method m : methods) {
            sb.append("\tpublic " + m.getReturnType() + " " + m.getName() + "()\n");
            sb.append("\t{\n");
            sb.append("\t\tSystem.out.println(\"Before Hello World!\");\n");
            sb.append("\t\tinterfaces." + m.getName() + "();\n");
            sb.append("\t\tSystem.out.println(\"After Hello World!\");\n");
            sb.append("\t}\n");
        }
        sb.append("}");

        /** 生成一段Java代码 */
        String fileDir = System.getProperty("user.dir");
        String fileName = fileDir + "\\src\\main\\java\\proxy\\StaticProxy.java";
        File javaFile = new File(fileName);
        Writer writer = new FileWriter(javaFile);
        writer.write(sb.toString());
        writer.close();
        /** 动态编译这段Java代码,生成.class文件 */
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);
        JavaCompiler.CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);
        ct.call();
        sjfm.close();
        /** 将生成的.class文件载入内存,默认的ClassLoader只能载入CLASSPATH下的.class文件 */
        URL[] urls = new URL[]{(new URL("file:\\" + System.getProperty("user.dir") + "\\src"))};
        URLClassLoader ul = new URLClassLoader(urls);
        Class<?> c = ul.loadClass("proxy.StaticProxy");
        /** 利用反射将c实例化出来 */
        Constructor<?> constructor = c.getConstructor(HelloWorld.class);
        HelloWorld helloWorldImpl = new HelloWorldImpl();
        Object obj = constructor.newInstance(helloWorldImpl);
        /** 使用完毕删除生成的代理.java文件和.class文件,这样就看不到动态生成的内容了 */
         /*File classFile = new File(fileDir + "\\src\\com\\xrq\\proxy\\StaticProxy.class");
        javaFile.delete();
        classFile.delete();*/
        return obj;
    }

}

      看到下面都没有变化,变化的地方就是在生成StaticProxy.java的地方,通过反射获取接口及方法的信息,这个版本的改进应该很好理解,写一段代码测试一下:

public static void main(String[] args) throws Exception
{    
    long start = System.currentTimeMillis();
    HelloWorld helloWorld = (HelloWorld)ProxyVersion_1.newProxyInstance(HelloWorld.class);
    System.out.println("动态生成代理耗时:" + (System.currentTimeMillis() - start) + "ms");
    helloWorld.print();
    System.out.println();
}

运行结果为:

动态生成代理耗时:883ms
Before Hello World!
Hello World
After Hello World!

版本三:让代理内容可复用

       接下来要到最后一个版本了,版本二解决的问题是可以为任何接口生成代理,那最后一个版本要解决的问题自然是可以为任何接口生成任何代理的问题了,首先定义一个接口InvocationHandler,这么起名字是因为JDK提供的代理实例处理程序的接口也是InvocationHandler:

public interface InvocationHandler
{
    void invoke(Object proxy, Method method) throws Exception;
}

所以我们的Proxy类也要修改了,改为:

public class ProxyVersion_2 implements Serializable
{
    private static final long serialVersionUID = 1L;
    
    public static Object newProxyInstance(Class<?> interfaces, InvocationHandler h) throws Exception
    {
        Method[] methods = interfaces.getMethods();        
        StringBuilder sb = new StringBuilder(1024);
        
        sb.append("package proxy;\n\n");
        sb.append("import java.lang.reflect.Method;\n\n");
        sb.append("public class $Proxy1 implements " +  interfaces.getSimpleName() + "\n");
        sb.append("{\n");
        sb.append("\tInvocationHandler h;\n\n");
        sb.append("\tpublic $Proxy1(InvocationHandler h)\n");
        sb.append("\t{\n");
        sb.append("\t\tthis.h = h;\n");
        sb.append("\t}\n\n");
        for (Method m : methods)
        {
            sb.append("\tpublic " + m.getReturnType() + " " + m.getName() + "()\n");
            sb.append("\t{\n");
            sb.append("\t\ttry\n");
            sb.append("\t\t{\n");
            sb.append("\t\t\tMethod md = " + interfaces.getName() + ".class.getMethod(\"" + m.getName() + "\");\n");
            sb.append("\t\t\th.invoke(this, md);\n");
            sb.append("\t\t}\n");
            sb.append("\t\tcatch (Exception e)\n");
            sb.append("\t\t{\n");
            sb.append("\t\t\te.printStackTrace();\n");
            sb.append("\t\t}\n");
            sb.append("\t}\n");
        }
        sb.append("}");
        
        /** 生成一段Java代码 */
        String fileDir = System.getProperty("user.dir");
        String fileName = fileDir + "\\src\\main\\java\\proxy\\$Proxy1.java";
        File javaFile = new File(fileName);
        Writer writer = new FileWriter(javaFile);
        writer.write(sb.toString());
        writer.close();
        
        /** 动态编译这段Java代码,生成.class文件 */
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);
        CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);
        ct.call();
        sjfm.close();
        
        /** 将生成的.class文件载入内存,默认的ClassLoader只能载入CLASSPATH下的.class文件 */
        URL[] urls = new URL[] {(new URL("file:\\" + System.getProperty("user.dir") + "\\src"))};
        URLClassLoader ul = new URLClassLoader(urls);
        Class<?> c = Class.forName("proxy.$Proxy1", false, ul);
        
        /** 利用反射将c实例化出来 */
        Constructor<?> constructor = c.getConstructor(InvocationHandler.class);
        Object obj = constructor.newInstance(h);
        
        /** 使用完毕删除生成的代理.java文件和.class文件,这样就看不到动态生成的内容了 */
        File classFile = new File(fileDir + "\\src\\main\\java\\proxy\\$Proxy1.class");
        javaFile.delete();               
        classFile.delete();
        
        return obj;
    }
}

 

最明显的变化,代理的名字变了,从StaticProxy变成了 Proxy1,因为JDK也是这么命名的,用过代理的应该有印象。这个改进中拼接Proxy1,因为JDK也是这么命名的,用过代理的应该有印象。这个改进中拼接Proxy1的.java文件是一个难点,不过我觉得可以不用纠结在这里,关注重点,看一下生成的$Proxy1.java的内容是什么:

public class $Proxy1 implements HelloWorld
{
    InvocationHandler h;

    public $Proxy1(InvocationHandler h)
    {
        this.h = h;
    }

    public void print()
    {
        try
        {
            Method md = proxy.HelloWorld.class.getMethod("print");
            h.invoke(this, md);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

       看到,我们把对于待生成代理的接口方法的调用,变成了对于InvocationHandler接口实现类的invoke方法的调用(这就是动态代理最关键的一点),并传入了待调用的接口方法,这样不就实现了我们的要求了吗?我们InvocationHandler接口的实现类写invoke方法的具体实现,传入的第二个参数md.invoke就是调用被代理对象的方法,在这个方法前后都是代理内容,想加什么加什么,不就实现了动态代理了?所以,我们看一个InvocationHandler实现类的写法:

public class HelloInvocationHandler implements InvocationHandler
{
    private Object obj;
    
    public HelloInvocationHandler(Object obj)
    {
        this.obj = obj;
    }
    
    public void invoke(Object proxy, Method method)
    {
        System.out.println("Before Hello World!");
        try
        {
            method.invoke(obj, new Object[]{});
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }
        System.out.println("After Hello World!");
    }
}

写个main函数测试一下:

public static void main(String[] args) throws Exception
{    
    long start = System.currentTimeMillis();
    HelloWorld helloWorldImpl = new HelloWorldImpl();
    InvocationHandler ih = new HelloInvocationHandler(helloWorldImpl);
    HelloWorld helloWorld = (HelloWorld)ProxyVersion_2.newProxyInstance(HelloWorld.class, ih);
    System.out.println("动态生成代理耗时:" + (System.currentTimeMillis() - start) + "ms");
    helloWorld.print();
    System.out.println();
}

运行结果为:

动态生成代理耗时:909ms
Before Hello World!
Hello World
After Hello World!

没有问题。

后记

虽然我们自己写了Proxy,但是JDK绝对不会用这种方式实现,原因无他,就是太慢。看到三个版本的代码,运行时间都在300ms以上,效率如此低的实现,如何能给开发者使用?我拿JDK提供的Proxy和InvocationHandler自己写了一个简单的动态代理,耗时基本只在5ms左右。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值