前言

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很多知识点的理解,绝对是一个非常大的提高。

自己写一个java.lang.reflect.Proxy代理的实现的更多相关文章

  1. java 编程基础 Class对象 反射:动态代理 和AOP:java.lang.reflect.Proxy:(Proxy.newProxyInstance(newProxyInstance​(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h))

    为什么我们使用动态代理 静态代理会让类变多了,多了代理类,工作量变大了,且不易扩展.比如我们上节课的例子,要实现不同的扩展方法就要编写不同的代理类,非常麻烦.   Proxy类的使用规则 Proxy提 ...

  2. JAVA中反射机制五(java.lang.reflect包)

    一.简介 java.lang.reflect包提供了用于获取类和对象的反射信息的类和接口.反射API允许对程序访问有关加载类的字段,方法和构造函数的信息进行编程访问.它允许在安全限制内使用反射的字段, ...

  3. Java反射API研究(2)——java.lang.reflect详细内容与关系

    对于最新的java1.8而言,reflect中接口的结构是这样的: java.lang.reflect.AnnotatedElement java.lang.reflect.AnnotatedType ...

  4. JAVA中反射机制六(java.lang.reflect包)

    一.简介 java.lang.reflect包提供了用于获取类和对象的反射信息的类和接口.反射API允许对程序访问有关加载类的字段,方法和构造函数的信息进行编程访问.它允许在安全限制内使用反射的字段, ...

  5. 设计模式 - 动态代理原理及模仿JDK Proxy 写一个属于自己的动态代理

    本篇文章代码内容较多,讲的可能会有些粗糙,大家可以选择性阅读. 本篇文章的目的是简单的分析动态代理的原理及模仿JDK Proxy手写一个动态代理以及对几种代理做一个总结. 对于代理模式的介绍和讲解,网 ...

  6. java设计模式之Proxy(代理模式)

    java设计模式之Proxy(代理模式) 2008-03-25 20:30 227人阅读 评论(0) 收藏 举报 设计模式javaauthorizationpermissionsstringclass ...

  7. session.createQuery()不执行和java.lang.reflect.InvocationTargetException

    今天写SSH的工程的时候,执行到一个DAO中的Query query = session.createQuery(hql)的时候,没有成功执行,直接跳到了finally,然后前台报了500和java. ...

  8. (转)Java.lang.reflect.Method invoke方法 实例

    背景:今天在项目中用到Method 的invoke方法,但是并不理解,查完才知道,原来如此! import java.lang.reflect.Method; /** * Java.lang.refl ...

  9. 自己写一个java的mvc框架吧(四)

    自己写一个mvc框架吧(四) 写一个请求的入口,以及初始化框架 上一章写了获取方法的入参,并根据入参的参数类型进行数据转换.这时候,我们已经具备了通过反射调用方法的一切必要条件.现在我们缺少一个htt ...

随机推荐

  1. 2-4. Using auto with Functions

    在C++14中允许使用type deduction用于函数参数和函数返回值 Return Type Deduction in C++11 #include <iostream> using ...

  2. Winform打砖块游戏制作step by step第6节---双缓冲应用

    一 引子 为了让更多的编程初学者,轻松愉快地掌握面向对象的思考方法,对象继承和多态的妙用,故推出此系列随笔,还望大家多多支持. 二 本节内容---双缓冲应用 1.  主界面截图如下: 2.  什么是双 ...

  3. linux启动执行某个脚本

    如果是开机马上执行的脚本,可以将脚本写到rc.local中: 如果是用户登录后自动执行脚本,可以将脚本写到相应的用户目录下“-/.bash_profile”,若脚本“-/.bash_profile”不 ...

  4. MVC 之 WebAPI 系列二

    今天,我想在此记录下 WebApi 跨域调用 1. 什么叫跨域: 跨域问题简单理解就是JavaScript同源策略的限制,其根本原因是因为浏览器对于这种请求,所给予的权限是较低的,通常只允许调用本域中 ...

  5. iOS的后台任务

    翻译自:http://www.raywenderlich.com/29948/backgrounding-for-ios (代码部分若乱码,请移步原链接拷贝) 自ios4开始,用户点击home按钮时, ...

  6. javascript面向对象(2)

    主要内容: 作用域 在了解作用域之前,请先看一段代码: 通过运行示例可知,变量d和c报错.在预处理阶段,预处理会将全局中的判断语句忽略,直接加var声明的变量和function声明的函数. 作用域的分 ...

  7. arch 安装图形界面

    图形界面的安装.--- 引导: .显卡驱动,这里我们是集成显卡,根据wiki提示安装xf86-video-intel .安装xorg-server,xorg-server-utils,xorg-xin ...

  8. Java 内部类摘抄

    关于Java的内部类,要说的东西实在太多,这篇博文中也无法一一具体说到,所以就挑些重点的讲.关于内部类的使用,你可能会疑问,为什么我们要使用内部类?为了回答这个问题,你需要知道一些关于内部类的重点.所 ...

  9. Maemo平台上如何使用Openvpn

    Maemo是一个开源的智能手机软件平台社区,它是基于Debia的LInux发行版本,Maemo的大多是开源的,并已经制定了Maemo和诺基亚内部的设备与许多开源项目,例如,Debian的Linux内核 ...

  10. 解决EasyUI动态添加标签渲染问题

    以下代码用于Js脚本中: var Work_Content_Back = "<table width='99%' class='table' style='margin-bottom: ...