动态代理是java语言中常用的设计模式,java在1.3版本以后也提供了动态代理技术,允许开发者在运行期间创建接口的代理对象。 很多框架底层都使用了java的动态代理技术来实现的,比如大名鼎鼎的springAOP;这篇文章将带你一步一步揭开JDK动态代理技术的神秘面纱。
我们先来定义一个接口:
package com.yanghui.study.proxy;
public interface IFlyable {
int fly(int x,int y);
}
再来一个实现类:
package com.yanghui.study.proxy;
public class Plane implements IFlyable{
@Override
public int fly(int x, int y) {
int result = x * x + y * y;
try {
Thread.sleep(new Random().nextInt(700));
} catch (InterruptedException e) {
e.printStackTrace();
}
return result;
}
}
如果我们要统计一下这个fly方法的运行时间,该怎么做呢?很简单,可以修改源码在方法fly方法里面加上两句代码①、②,这样就打印出方法的运行时间了,如下:
//省略不必要代码......
public int fly(int x, int y) {
long start = System.currentTimeMillis();//①记录开始时间
int result = x * x + y * y;
try {
Thread.sleep(new Random().nextInt(700));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②结束时间减去开始时间
return result;
}
但是如果我们没有这个方法的源码,这个类是别人写好打好jar包提供给我们用的,这时如果你还想统计下这个方法运行时间,又该怎么办呢?至少有两种方式可以来实现:
1、使用继承,写一个类继承Plane,重写fly方法,在调用父类的fly方法前后加上①②处的代码,这样就可以统计fly方法的执行时间了。
package com.yanghui.study.proxy;
public class PlaneTimerProxy1 extends Plane{
@Override
public int fly(int x, int y) {
long start = System.currentTimeMillis();//①
int result = super.fly(x, y);
System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②
return result;
}
}
2、使用聚合的方式,写一个类PlaneTimerProxy2
实现跟Plane一样的接口,并且持有IFlyable的引用,当调用fly方法时,实际调用的是IFlyable的fly方法,这样就可以在方法调用前后加上①②处的代码统计fly方法的执行的时间。
public class PlaneTimerProxy2 implements IFlyable{
private IFlyable flyable;
public PlaneTimerProxy2(IFlyable flyable) {
this.flyable = flyable;
}
@Override
public int fly(int x, int y) {
long start = System.currentTimeMillis();//①
int result = this.flyable.fly(x, y);
System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②
return result;
}
}
这两种方式都可以实现,那么哪种方式更好呢?答案是聚合的方式更好,为什么呢?想象一下,如果我还想实现更多的功能,比如给fly方法执行前后加上日志,事务控制,权限控制,这时用继承的方式你会需要新建更多的类来实现,可能你会想,聚合的实现方式不也是要新建更多的类来实现吗?是的,但是如果我要你先记录日志再记录时间,有如果我要你先记录时间再记录日志,需要实现这样随意的组合的功能,继承就显得很麻烦了,而聚合的方式就会很灵活了。在思考下,如果想给不同类的100个方法记录下时间和日志,那么你想想看是不是要产生100个代理类呢?类的数量又在不停的膨胀了。如果我们能够为实现了某个接口的类动态生成代理类就好了?想法很好,先来新建一个类Proxy
,提供一个方法newProxyInstance
,这个方法可以为一个实现了IFlyable
接口的类产生代理类,那么客户端调用就可以这样做:
package com.yanghui.study.proxy.custom;
public class Client {
public static void main(String[] args) {
IFlyable flyable = (IFlyable)Proxy.newProxyInstance();
flyable.fly(1, 2);
}
}
那么我们如何在newProxyInstance
方法里面动态的生成一个代理类呢?为了模拟JDK的实现,先定义一个接口InvocationHandler
:
package com.yanghui.study.proxy.custom;
import java.lang.reflect.Method;
public interface InvocationHandler {
Object invoke(Object proxy,Method method,Object[] args)throws Throwable;
}
下面来个完整代码:
public class Proxy {
private static final Map bytesMap = new HashMap<>();
private static final AtomicInteger count = new AtomicInteger();
public static Object newProxyInstance(Class> intaface,InvocationHandler handler) {
//代码①处
String rn = "\r\n";
String className = "Proxy" + count.getAndIncrement();
String str = "package com.yanghui.study.proxy.custom;" + rn +
"public class " + className + " implements " + intaface.getName() + "{" + rn +
" private InvocationHandler handler;" + rn +
" public " + className + "(InvocationHandler handler){" + rn +
" this.handler=handler;" + rn +
" }" + rn;
String methodStr = "";
for(Method m : intaface.getMethods()) {
methodStr = methodStr + " @Override" + rn +
" public " + m.getReturnType().getName() + " " + m.getName() + "(";
String parameterStr = "";
String psType = "";
String pname = "";
for(Parameter p : m.getParameters()) {
parameterStr = parameterStr + p + ",";
psType = psType + p.getType().getName() + ".class,";
pname = pname + p.getName() + ",";
}
if(!parameterStr.equals("")) {
parameterStr = parameterStr.substring(0, parameterStr.length() - 1);
}
parameterStr = parameterStr + "){" + rn +
" try{" + rn +
" " + Method.class.getName() + " method = " + intaface.getName() + ".class.getDeclaredMethod(\"" + m.getName() + "\"";
if(!psType.equals("")) {
psType = psType.substring(0, psType.length() - 1);
parameterStr = parameterStr + "," + psType + ");" + rn;
}else {
parameterStr = parameterStr + ");" + rn;
}
if(pname.length() > 0) {
pname = pname.substring(0, pname.length() - 1);
}
String returnStr = "";
if(!"void".equals(m.getReturnType().getName())) {
returnStr = returnStr + " return (" + m.getReturnType().getName() + ")";
}
parameterStr = parameterStr +
returnStr + "this.handler.invoke(this,method," + (pname.length() == 0 ? "null" : "new Object[]{" + pname + "}") + ");" + rn +
" } catch (Throwable e) {" + rn +
" throw new RuntimeException(e);" + rn +
" }" + rn +
" }" + rn;
methodStr = methodStr + parameterStr;
}
String endStr = "}";
str = str + methodStr + endStr;
String path = Thread.currentThread().getContextClassLoader().getResource("").getPath() + "com/yanghui/study/proxy/custom/";
String fileStr = path + className + ".java";
//代码②处
//写入文件
writeToFile(fileStr, str);
//代码③处
//动态编译
String className1 = "com.yanghui.study.proxy.custom." + className;
return compileToFileAndLoadclass(className1, fileStr, handler);
}
/**
* 从源文件到字节码文件的编译方式
* @param className
* @param fileStr
* @param handler
* @return
*/
private static Object compileToFileAndLoadclass(String className,String fileStr,InvocationHandler handler) {
//获取系统Java编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//获取Java文件管理器
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
//定义要编译的源文件
File file = new File(fileStr);
//通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个 JavaFileObject,也被称为一个汇编单元
Iterable extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);
//生成编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
//执行编译任务
task.call();
try {
fileManager.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
Class> c = Thread.currentThread().getContextClassLoader().loadClass(className);
Constructor> ct = c.getConstructor(InvocationHandler.class);
Object object = ct.newInstance(handler);
return object;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void writeToFile(String file,String context) {
FileWriter fw = null;
try {
fw = new FileWriter(new File(file));
fw.write(context);
} catch (IOException e) {
e.printStackTrace();
}finally {
if(fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
我来解释下上面代码的意思:
1、代码①处,根据传入的接口动态生成java代码的字符串,类名取名为Proxy+序号,该类实现了传入的接口,真正的方法调用将委托传入InvocationHandler
的实现类来实现。
2、代码②处,将生成的java代码的字符串写入文件
3、代码③处,真正的核心,动态编译2步生成的java文件,再通过classLoader把编译生成的class文件加载进内存,然后反射创建实例。
接下来客户端就可以这样使用了:
public class Client {
public static void main(String[] args) {
Plane plane = new Plane();
IFlyable flyable = (IFlyable)Proxy.newProxyInstance(IFlyable.class,new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object result = method.invoke(plane, args);
System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");
return result;
}
});
System.out.println(flyable.fly(1, 2));
}
}
到目前为止,我们实现的Proxy
类可以为任何接口生成代理类了,是不是很神奇。当然我们这里只是模拟实现了JDk的动态代理,还有很多细节是没有考虑的,有兴趣的同学可以自己阅读JDK源码,相信您理解了其背后的原理后,看起来也不会太费力了。
扩展
在上面我们实现了动态生成java文件,动态编译java文件,需要把文件写入磁盘,也会在java源文件的目录生成编译后的.class文件,那么可以不可以只在内存中编译加载呢?答案是可以的,代码如下(方法是Proxy
类下的方法):
/**
* 从内存到内存的编译方式
* @param className
* @param code
* @param handler
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Object compileMemoryToMemoryAndLoadClass(String className,String code,InvocationHandler handler) {
if(bytesMap.get(className) != null) {
return loadClass(className, bytesMap.get(className), handler);
}
//获取系统Java编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//获取Java文件管理器
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
ForwardingJavaFileManager fjf = new ForwardingJavaFileManager(fileManager) {
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind,
FileObject sibling) throws IOException {
if(kind == JavaFileObject.Kind.CLASS) {
return new SimpleJavaFileObject(URI.create(""), JavaFileObject.Kind.CLASS) {
public OutputStream openOutputStream() {
return new FilterOutputStream(new ByteArrayOutputStream()) {
public void close() throws IOException{
out.close();
ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
bytesMap.put(className, bos.toByteArray());
}
};
}
};
}else{
return super.getJavaFileForOutput(location, className, kind, sibling);
}
}
};
SimpleJavaFileObject sourceJavaFileObject = new SimpleJavaFileObject(URI.create(className.replace('.', '/') + Kind.SOURCE.extension),JavaFileObject.Kind.SOURCE){
@Override
public CharBuffer getCharContent(boolean b) {
return CharBuffer.wrap(code);
}
};
//生成编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, fjf, null, null, null, Arrays.asList(new JavaFileObject[] {sourceJavaFileObject}));
//执行编译任务
task.call();
try {
fileManager.close();
fjf.close();
} catch (IOException e) {
e.printStackTrace();
}
return loadClass(className, bytesMap.get(className), handler);
}
private static Object loadClass(String className,byte[] bytes,InvocationHandler handler) {
try {
Class> c = new MyClassLoader(bytes).loadClass(className);
Constructor> ct = c.getConstructor(InvocationHandler.class);
Object object = ct.newInstance(handler);
return object;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
首先通过自己定义sourceJavaFileObject
类来加载java格式的字符串,通过ForwardingJavaFileManager
类来重新定义编译文件的输出行为,这里我直接写入内存,用一个map(bytesMap)来保存,key就是类名,value就是编译好的.class的二进制文件。