所谓代理,是指具有与被代理对象相同的接口的类,客户端必须通过代理与被代理的目标类进行交互,而代理一般在交互的过程中(交互前后),进行某些特定的处理。
动态代理的实现应用到的技术
- 动态编译技术,可以使用Java自带的JavaCompiler类,也可以使用CGLIB、ASM等字节码增强技术
- 反射,包括对于类.class和getClass()方法的理解,Method类、Constructor类的理解
- IO流,主要就是字符输出流FileWriter
- 对于ClassLoader的理解
基础类
先把基础类定义在这儿,首先是一个HelloWorld接口:
package com.system.net.proxy;
public interface HelloWorld {
void print();
}
HelloWorld接口的实现类:
package com.system.net.proxy;
public class HelloWorldImpl implements HelloWorld {
public void print() {
System.out.println("Hello World");
}
}
为这个接口写一个简单的静态代理类:
package com.system.net.proxy;
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 com.system.net.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;
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\\system\\net\\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("com.system.net.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;
}
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();
}
}
每一步的注释都在上面了,解释一下大致思路:
- 我们在另外一个类里面自己拼一段静态代理的代码的字符串
- 为这个字符串生成一个.java文件,并放在我们工程的某个目录下面,因为是.java文件,所以在src下
- 利用JavaCompiler类动态编译这段.java代码使之被编译成一个.class文件,JavaCompiler不熟悉没关系,知道就好了
- 因为在src下生成编译之后的.java文件,而默认的ClassLoader只能加载CLASSPATH下的.class文件,所以用URLClassLoader
- 由于代理类只有一个带参数的构造方法,所以要用java.lang.reflect.Constructor
- 最后把生成的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对象,可以为这个接口及里面的方法都生成代理内容,代码这么写:
package com.system.net.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;
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\\system\\net\\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("com.system.net.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;
}
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();
}
}
看到下面都没有变化,变化的地方就是在生成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:
package com.system.net.proxy;
import java.lang.reflect.Method;
public interface InvocationHandler {
void invoke(Object proxy, Method method) throws Exception;
}
所以我们的Proxy类也要修改了,改为:
package com.system.net.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;
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);
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 = Class.forName("com.system.net.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
的内容是什么:
package com.system.net.proxy;
import java.lang.reflect.Method;
public class $Proxy1 implements HelloWorld {
InvocationHandler h;
public $Proxy1(InvocationHandler h) {
this.h = h;
}
public void print() {
try {
Method md = HelloWorld.class.getMethod("print");
h.invoke(this, md);
} catch (Exception e) {
e.printStackTrace();
}
}
}
看到,我们把对于待生成代理的接口方法的调用,变成了对于InvocationHandler接口实现类的invoke方法的调用(这就是动态代理最关键的一点),并传入了待调用的接口方法,这样不就实现了我们的要求了吗?我们InvocationHandler接口的实现类写invoke方法的具体实现,传入的第二个参数md.invoke就是调用被代理对象的方法,在这个方法前后都是代理内容,想加什么加什么,不就实现了动态代理了?所以,我们看一个InvocationHandler实现类的写法:
package com.system.net.proxy;
import java.lang.reflect.Method;
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!
没有问题