大数据培训新三板挂牌机构 股票代码:837906 | EN CN
Java是什么?
Java历史
Java语言特点
C++ VS Java比较
Java工厂设计模式
Java抽象工厂模式
Java单例模式
Java建造者(Builder)模式
Java原型模式
Java适配器模式
Java桥接模式
Java获取网络文件大小
Java套接字到单一的客户端
Java连接套接字
Java URL部分
Java URL连接日期
Java下载网页
Java主机指定IP地址
Java确定本地IP地址
Java检查端口占用
Java查找代理服务器设置
Java创建Socket
Java线程实例
Java检查线程活着
Java如何检查一个线程停止或没有?
Java解决死锁实例
Java如何获取正在运行的线程的优先级?
Java如何监视线程的状态?
Java获取线程名称
Java线程生产者消费者问题
Java如何设置线程的优先级?
Java如何停止线程一会儿?
Java如何暂停线程?
Java获取线程ID
Java如何检查线程的优先级?
Java显示所有正在运行的线程?
Java显示线程状态
Java中断一个线程
Java Applet实例
Java创建Applet
Java使用Applet创建横幅
Java使用Applet显示时钟?
Java在一个Applet创建不同形状
Java如何使用Applet填充形状的颜色?
Java使用Applet跳转到一个链接
Java在Applet创建事件监听器
Java使用Applet显示图像
Java使用Applet在新窗口中打开链接
Java使用Applet播放声音?
Java使用Applet读取文件
Java使用Applet写入文件
Java中Swing应用程序applet
Java简单的图形用户界面-GUI
Java以不同的字体显示文本
Java使用GUI画一条线
Java创建框架-frame
Java使用GUI显示多边形
Java在矩形中显示文本
Java GUI显示不同形状
Java如何绘制GUI实心矩形?
Java创建GUI透明光标
Java检查GUI平滑处理状态
Java在框架中显示颜色
Java GUI显示饼图
Java使用图形用户界面绘制文本
Java编辑表-table
Java 使用prepared语句
Java使用保存点和回滚
Java同时执行数据库多个SQL命令
Java使用行方法
Java使用列方法
Java正则表达式实例
Java将字符串分割
Java搜索重复单词
Java查找出现的单词
Java最后一个词的索引
Java模式匹配
Java删除空格
Java匹配电话号码
Java计数组词
Java搜索词组
Java拆分正则表达式
Java替换第一个出现字符串
Java检查日期格式
Java验证电子邮件地址格式
Java替换所有匹配字符串
Java使每个单词的第一个字母大写
从XML创建SqlSessionFactory实例
不使用XML来创建SqlSessionFactory
从SqlSessionFactory获取SqlSession
映射SQL语句
作用域和生命周期
Mapper XML配置
properties元素
Settings元素
typeAliases 元素
typeHandlers元素
理解CacheLine与写出更好的JAVA
Java核心技术点之动态代理
更好的使用JAVA线程池
理解Java中字符流与字节流的区别
深入分析Java方法反射的实现原理
关于Java面试,你应该准备这些知识点
Java内存模型
2017年你不能错过的Java类库
Leakcanary Square的一款Android/Java内存泄漏检测工具
Java Synchronised机制
Java核心技术点之注解
JVM(8):JVM知识点总览-高级Java工程师面试必备
JVM(3):Java GC算法 垃圾收集器
JVM(1):Java 类的加载机制
解决ActiveMQ中,Java与C++交互中文乱码问题
关于Java Collections的几个常见问题
Java I/O 总结
JVM源码分析之Java对象的创建过程
JVM源码分析之Java类的加载过程
Java GC的那些事(下)
Java GC的那些事(上)
java对象头的HotSpot实现分析
面试的角度诠释Java工程师(一)
面试的角度诠释Java工程师(二)
框架开发之Java注解的妙用
谈谈Java反射机制
Java并发:volatile内存可见性和指令重排
死磕Java并发:Java内存模型之happens-before
死磕Java并发:深入分析volatile的实现原理
死磕Java并发:深入分析synchronized的实现原理
Java 10 可能对 Lambda 表达式进行升级
G1垃圾回收器中的字符串去重(Java 8 Update 20)
Java RESTful框架的性能比较
理解RxJava的线程模型
继续了解Java的纤程库 – Quasar
Java中的纤程库 – Quasar
Java豆瓣电影爬虫——抓取电影详情和电影短评数据
Java集合框架源码剖析:LinkedHashSet 和 LinkedHashMap
Java Lambda表达式初探
Java中的陷阱题
Java 9的这一基本功能,你可能从未听过
关于Java并发编程的总结和思考
几种简单的负载均衡算法及其Java代码实现
JAVA虚拟机关闭钩子(Shutdown Hook)
Java 脚本化编程指南
Java Scripting API 使用示例
Java 8 的 Nashorn 脚本引擎教程
如何开始使用 Java 机器学习
CognitiveJ —— Java 的图像分析库
Java 性能优化的五大技巧
Java 解惑:Comparable 和 Comparator 的区别
Google Java编程风格指南
java NIO详解
Java 异常处理的误区和经验总结
Java语法糖(4):内部类
Java语法糖(3):泛型
Java语法糖(2):自动装箱和自动拆箱
Java消息队列任务的平滑关闭
Java语法糖(1):可变长度参数以及foreach循环原理
2016最流行的Java EE服务器
自己写一个java.lang.reflect.Proxy代理的实现
java 如何在pdf中生成表格
如何防止单例模式被JAVA反射攻击
java虚拟机 jvm 局部变量表实战
聊聊并发-Java中的Copy-On-Write容器
java.lang.Instrument 代理Agent使用
Java开发者需要了解的移动开发编程语言
13个不容错过的Java项目
2016年7款最佳 Java 框架推荐
Java 开发者值得关注的 11 个技术博客
Redmonk发布Java框架流行度调研结果
Java 8开发的4大顶级技巧
GitHub漫游指南:10个值得你关注的Java项目
除了Guava,Java开发者还值得了解的5个谷歌类库
Java中创建对象的5种不同方法
Java性能优化全攻略
奇怪的Java题:为什么1000 == 1000返回为False,而100 == 100会返回为True?
11个最值得Java开发者收藏的网站
Java的常见误区与细节
对Java意义重大的7个性能指标
Java调优经验谈
关于Java并发编程的总结和思考
HDFS Federation设计动机与基本原理
《Effective STL》学习笔记(第三部分)
《Effective STL》学习笔记(第二部分)
《Effective STL》学习笔记(第一部分)
数据结构之位图
Thrift使用指南
Cassandra概要介绍
Cassandra部署与安装
Cassandra客户端
Cassandra数据模型
Cassandra中的各种策略
数据结构之树状数组
数据结构之伸展树
数据结构之后缀数组
数据结构之堆
浅析MRv1与MRv2的API兼容性
Apache Tez最新进展
运行在YARN上的计算框架
从传统操作系统角度理解Hadoop YARN

自己写一个java.lang.reflect.Proxy代理的实现

于2017-05-10由小牛君创建

分享到:


前言

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

抛砖引玉吧,个人觉得自己写一些JDK里面的那些类挺好的,写一遍和看一遍真的是两个不同的概念,写一遍既加深了对于这些类的理解、提升了自己的写代码水平,也可以在写完之后对比一下自己的实现有哪些写得不好、又有哪些没考虑到的地方,这样可以显著地提高自己,像我就自己写过JDK里面主要的集合类、工具类、String里面常用方法等。

本文的代码基础来源于马士兵Proxy的视频(顺便说一句,个人觉得马士兵的视频讲得比较拖拉,但是关于一些原理性、偏底层的东西讲得还蛮好的),一共分三个版本。可能有人觉得,人家视频上的内容拿过来写个文章,有意思吗?真不是,我是这么认为的:

1、把别人的东西变成自己的东西是一个过程,尽管代码是基于马士兵Proxy的视频的,但是所有的代码都是在自己这里手打、运行通过并自己充分理解了的,把别人的东西不加思考地复制黏贴没有意义,但是把别人的知识变成自己的理解并分享我觉得是一件好事

2、代码尽管基于马士兵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帮我们自动生成一个就好了,不过还真的可以,看下第一个版本的代码:

public class ProxyVersion_0 implements Serializable
{
    private static final long serialVersionUID = 1L;

    public static Object newProxyInstance() throws Exception
    {
        String src = "package com.xrq.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\\com\\xrq\\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);
        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("com.xrq.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\\com\\xrq\\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();        
}

结果为:

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

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

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

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

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 com.xrq.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\\com\\xrq\\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);
        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("com.xrq.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();
}

运行结果为:

动态生成代理耗时:389ms
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 com.xrq.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\\com\\xrq\\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("com.xrq.proxy.$Proxy1", false, ul);

        /** 利用反射将c实例化出来 */
        Constructor<?> constructor = c.getConstructor(InvocationHandler.class);
        Object obj = constructor.newInstance(h);

        /** 使用完毕删除生成的代理.java文件和.class文件,这样就看不到动态生成的内容了 */
        File classFile = new File(fileDir + "\\src\\com\\xrq\\proxy\\$Proxy1.class");
        javaFile.delete();
        classFile.delete();

        return obj;
    }
}

最明显的变化,代理的名字变了,从StaticProxy变成了$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 = com.xrq.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();
}

运行结果为:

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

没有问题

后记

虽然我们自己写了Proxy,但是JDK绝对不会用这种方式实现,原因无他,就是太慢。看到三个版本的代码,运行时间都在300ms以上,效率如此低的实现,如何能给开发者使用?我拿JDK提供的Proxy和InvocationHandler自己写了一个简单的动态代理,耗时基本只在5ms左右。所以,文章的内容仅供学习、研究,知识点很多,如果能把这篇文章里面的东西都弄懂,对于个人水平、对于Java很多知识点的理解,绝对是一个非常大的提高。