前言
前几篇我们讲了什么是静态代理,因为它会导致类爆炸。所以我们引入了动态代理,并且模拟实现了一个动态代理的例子,作为辅助讲解。其实上一篇博客留了一个小点没有做完,那就是实现内容的动态。本篇博客将会接着上一篇的例子,来构建一个动态内容的代理过程,并且通过自己构建的动态代理的例子来对比JDK源码来分析,大神们写的代码道理厉害在哪里。建议阅读本篇博客前先阅读【什么是代理(Proxy)】和【什么是动态代理?】 这两篇博客,并且跟着博主一起实际敲一敲代码。更多Spring内容进入【Spring解读系列目录】。
改造自创的动态代理例子
首先我们新创建一个接口MyDao
,用MyDaoImpl
去实现它。为了简化代码全部抛出异常,这样在构建新的代理类的时候就不用考虑try-catch
了。
public interface MyDao {
String proxyPrint()throws Exception;
}
public class MyDaoImpl implements MyDao{
@Override
public String proxyPrint() throws Exception {
return "MyDao proxyPrint()";
}
}
然后我们改造一下动态代理生成类ProxyUtil
中的methodDefine
,让其能够适配具有返回值的方法。
改造
methodDefine += tab + "public " + returnType + " " + methodName + "(" + param + ") {" + line
+ tab + tab + "System.out.println(\"---interface self proxy log---\");" + line;
if (returnType.equals("void")) {
//加一个判断,如果是void就不构造return
methodDefine += tab + tab + "target." + methodName + "(" + paramsLine + ");" + line
+ tab + "}";//拼成方法行,对应:public void query() {...}
} else {
methodDefine += tab + tab + "return target." + methodName + "(" + paramsLine + ");" + line
+ tab + "}"; //拼成方法行,对应:public Type query() {...}
}
Main方法调用运行就生成了如下的代理类,但是我们看到System.out.println("---interface self proxy log---");
这一行是写死的,就意味着我们的代理类增强的这部分逻辑通用性并不好。最终我们生了这样的一个代理类。
package com.demo.proxyDyn;
import com.demo.dao.MyDao;
public class $Proxy implements MyDao {
private MyDao target;
public $Proxy (MyDao target){
this.target = target;
}
public String proxyPrint() {
System.out.println("---interface self proxy log---");
return target.proxyPrint();
}
}
JDK的动态代理
在做进一步改造前,我们先看下JDK的动态代理是怎么做的。JDK通过下面这个方法构造一个动态代理对象出来Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
,我们看下这三个参数:
第一个要传入ClassLoader
。这是因为项目启动的流程,当JVM的启动的时候就会把项目里面所有的类给load到JVM中,但是这些类有个前提条件,就是已经编译好的也就是生成了class文件的那些。但是我们项目运行过程中动态产生了一些类,但是这些类并不会在一开始就被加载到JVM中。因此在生成动态代理的时候需要再次使用类加载器把生成的类和class文件重新加载到JVM中。因此我们调用JDK的动态代理的时候要使用一个类加载器把这个类再次load一遍,因此我们要传递当前类的ClassLoader进去。
第二个interfaces
。这个是一个类数组用来接收哪些接口需要代理。
最后一个InvocationHandler
。点进去发现是一个接口,里面只有一个invoke
方法,这个方法就是反射的目标对象里的方法,那么我们实现这个接口。
public class MyInvocationHandler implements InvocationHandler {
Object target;//目标对象
public MyInvocationHandler(Object target) {
//目标对象通过构造参数传递进来,多提一下,这种方法就是所谓的装配者模式
this.target=target;
}
//参数:proxy代理对象;method目标对象里面的方法;args目标对象里面的方法的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("MyInvocationHandler self invoke"); //执行逻辑
//目标对象要执行,至少要传递一个目标对象进去,但是看这里的参数,只有代理对象。
//所以我们怎么传进来呢?可以通过构造方法传入。
return method.invoke(target,args);
}
}
构造完InvocationHandler
以后我们就可以使用了。
public class MainTest {
public static void main(String[] args) {
//JDK
MyDao jdkDao = (MyDao) Proxy.newProxyInstance(MainTest.class.getClassLoader(),
new Class[]{
MyDao.class},
new MyInvocationHandler(new MyDaoImpl()));
System.out.println(jdkDao.proxyPrint());
}
}
运行结果,完成代理
MyInvocationHandler self invoke //增强的功能
MyDao proxyPrint() //主逻辑
那么先总结JDK是怎么做到动态代理的呢?首先加载动态代理类getClassLoader()
,然后把要代理的所有接口传进去new Class[]{MyDao.class}
,并且告知这些接口的代理逻辑MyInvocationHandler(new MyDaoImpl())
是哪些实现类。然后JDK就能帮助我们完成代理。
JDK整个逻辑就是:首先要告诉JDK需要代理的接口,然后JDK会把这些接口下面所有的方法都进行代理。代理增强的逻辑就是invoke
方法里面的逻辑,这就完成了动态的逻辑。这个和我们自己写的有什么区别呢?因为我们的方法是写死的字符串,所以我们就要想办法拿到这个method
进行反射执行。所以JDK就给我们提供了一个很好的解决思路,按照这个思路,我们自己的代理对象里面应该有一个InvocationHandler h
,然后由这个h
把方法调起来。
实现自己的InvocationHandler
既然我们有了思路,重新构建一个CustomerInvocationHandler
接口,然后声明一个实现类TestInvocationHandler
用于实现我们的增强逻辑。
public interface CustomerInvocationHandler {
public Object invoke(Method method);
}
public class TestInvocationHandler implements CustomerInvocationHandler {
Object target;
//把目标对象传进来,我们就是用这个来命中目标对象的方法的
public TestInvocationHandler(Object target){
this.target=target;
}
@Override
public Object invoke(Method method) {
try {
System.out.println("this is my TestInvocationHandler");
return method.invoke(target);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
既然我们自己完成了InvocationHandler
这个功能,那么我们要对上一篇博客做的ProxyUtil
类进行改造,生成一个满足我们需求的代理类$Proxy
。因为在现有的模型下,我们需要传入到代理里面的是InvocationHandler
,因此代理类构造方法要装配的是Handler
,然后将使用这个Handler
调起我们的目标对象里面的方法。所以关键就在于如何获取目标对象中的Method
对象。所以我们只要让$Proxy
这个代理类实现了MyDao
,就可以通过Class.forName
拿到这个类对象,进而获取方法对象,于是我们的新的$Proxy
,就应该是下面的样子。
package com.demo.proxyDyn;
import com.demo.dao.MyDao;
import com.demo.dao.CustomerInvocationHandler;
import java.lang.Exception;
import java.lang.reflect.Method;
public class $Proxy implements MyDao {
private CustomerInvocationHandler h; //传入handler而不是代理对象
public $Proxy (CustomerInvocationHandler h){
this.h = h;
}
public String proxyPrint() throws Exception{
Method method = Class.forName("com.demo.dao.MyDao").getDeclaredMethod("proxyPrint");
return (String)h.invoke(method);
}
}
改造ProxyUtil
完了以后,修改Main
方法运行,生成上述代理类和class
文件并打印。那么我们就完成了一个对JDK的动态代理的过程。改造ProxyUtil
后的代码太长,会在最后贴出来。
public class MainTest {
public static void main(String[] args) {
MyDao dao= (MyDao) ProxyUtil.newInstance(MyDao.class,new TestInvocationHandler(new MyDaoImpl()));
try {
System.out.println(dao.proxyPrint());
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印代理
this is my TestInvocationHandler
MyDao proxyPrint()
从这个例子可以看出,我们传递了接口,使用InvocationHandler
对目标对象MyDaoImpl
其中的方法进行反射调用,然后通过代理对象实现了目标功能。
对比
既然我们能够手写一个模拟的JDK动态代理,那就要看下JDK的原生代理比我们高明到哪里。首先我们用点从表面上看,基本上在运行时是一模一样的,都是dao->handler->target(MyDaoImpl)
。
我们自己的:
JDK的
然后我们看代码层面。JDK实现的是自己的InvocationHandler
,我们自己实现了CustomerInvocationHandler
。两者里面都是只有一个invoke
方法,只不过我们的更加简单。
纠错
那么我们现在就开始进入正式找茬模式,看看我们自己写的有什么缺点:
- 首先要生成文件
- 动态编译文件
- 需要外部类加载器,没有办法很好的是适配当前项目。
我们自己写的代码尽管很像JDK提供的功能,但是不可避免的进行IO磁盘操作,频繁的IO操作会导致我们的动态代理产生的非常的慢,最终影响到整个系统的效率。
JDK是怎么做的
点进JDK的原生方法ProxyUtil.newInstance()
看看JDK到底怎么玩的,删掉不相关的部分代码。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
throws IllegalArgumentException
{
//这里产生了$Proxy对象cl
Class<?> cl = getProxyClass0(loader, intfs);
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
//通过cl创建了一个构造方法
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
//通过cons对象new一个目标对象的代理对象,在例子里new的就是MyInvocationHandler
return cons.newInstance(new Object[]{
h}); //h就是InvocationHandler
}
}
进入以后看到Object
是return
出去的,那么往上找cons
怎么来的,发现cons
通过cl
创建了一个构造方法来的,再接着往上Class<?> cl = getProxyClass0(loader, intfs);
,也就是说getProxyClass0
这个方法就是产生代理对象的地方。那么继续进入getProxyClass0(loader, intfs);
。
private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
//缓存拿到数据
return proxyClassCache.get(loader, interfaces);
}
这里通过一个cache
缓存拿到了ClassLoader
和要代理的各个接口,因此我们接着进入get
方法:
public synchronized V get() {
V value = null;
try {
//给value赋值
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
} finally {
if (value == null) {
valuesMap.remove(subKey, this);
}
}
return value;
}
}
进入以后发现最终return value;
,所以把所有和value
不相关的代码全部清掉,就发现了value
唯一赋值的地方,进入这个valueFactory.apply(key, parameter)
接着进入apply()
方法。这是Proxy下的一个内部类中的方法,代码很长我们只写关键的。
ProxyClassFactory.apply(ClassLoader loader, Class<?>[] interfaces):
找到最关键的一句代码:
生成指定的代理类,而且这个所谓的代理类是一个byte数组,我们看传入的是代理类名proxyName和接口interfaces
byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags);
然后直接返回一个class出去了。
return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
JDK这里直接就 用byte[]-直接->object(class的对象)
,点进去发现defineClass0
方法是一个native
方法,下面就是c语言搞得事情了。对比一下我们自己的代码步骤,首先生成java file-->class file-->classLoader-->JVM:byte[]-->object(class的对象)
。所以JDK比我们强悍在哪里呢?就是这里,JDK通过接口反射得到字节码,然后把字节码转成class,再用native方法直接拿到对象返回出去,全部过程在内存里完成。看了源码感叹一句,能够熟练的调度使用各种系统资源,无缝的使底层与上层的代码连接,也许就是大神和普通人的差距吧。
附:改造后的ProxyUtil
public class ProxyUtil {
public static Object newInstance(Class target, CustomerInvocationHandler h) {
Object proxy=null;
String line = "\n"; //换行
String tab = "\t"; //空格
String targetName = target.getSimpleName(); //构造类名行
//构造包行,对应package com.demo.proxyImpl;
String packagePath = "package com.demo.proxyDyn;" + line;
//构造导入行,对应 import 导入各种包
String importPath = "import " + target.getName() + ";" + line
+"import com.demo.dao.CustomerInvocationHandler;"+line
+"import java.lang.Exception;"
+"import java.lang.reflect.Method;"+line;;
//构造类定义行,对应public class $Proxy implements QueryDao
String classDefine = "public class $Proxy implements " + targetName + " {" + line;
//构造字段行,对应private CustomerInvocationHandler h;
String fieldDefine = tab + "private CustomerInvocationHandler h;" + line;
//构造构造方法行,对应public QueryDaoLog(QueryDao target) { ... }
String constructorDefine = tab + "public $Proxy (CustomerInvocationHandler h){ " + line
+ tab + tab + "this.h = h; " + line
+ tab + "}" + line;
//得到接口的所有方法,用数组存起来
Method[] methods = target.getDeclaredMethods();
String methodDefine = ""; //构造方法定义行
for (Method method : methods) {
//循环遍历拿到的方法
String returnType = method.getReturnType().getSimpleName(); //拿到返回值
String methodName = method.getName(); //拿到方法名字
//得到方法的所有参数,用数组存起来
Class[] params = method.getParameterTypes();
String param = "";
String paramsLine = ""; //构造参数行
int count = 0;
for (Class obj : params) {
//循环遍历参数类型
String temp = obj.getSimpleName(); //拿到参数名字
param += temp + " a" + count + ","; //构建一个循环的参数,这里就是a0
paramsLine += "a" + count + ", "; //拼成一个参数行
count++; //参数名+1
}
if (param.length() > 0) {
//如果拿出来有参数
param = param.substring(0, param.lastIndexOf(",") - 1); //去掉最后的","
paramsLine = paramsLine.substring(0, paramsLine.lastIndexOf(",") - 1); //参数行去掉最后的","
}
//构造方法定义的行
methodDefine += tab + "public " + returnType + " " + methodName + "(" + param + ") throws Exception{" + line
+tab+tab+"Method method = Class.forName(\""+target.getName()+"\").getDeclaredMethod(\""+methodName+"\");"+line
+tab+tab+"return ("+returnType+")h.invoke(method);"+line;
methodDefine+=tab+"}"+line;
}
//最后把所有的字段组装在一起
String content = packagePath + importPath + classDefine + fieldDefine + constructorDefine + methodDefine + line + "}";
//写出去。
File file = new File("d:\\com\\demo\\proxyDyn");
try {
if (!file.exists()) {
file.mkdirs();
file = new File("d:\\com\\demo\\proxyDyn\\$Proxy.java");
file.createNewFile();
} else {
file = new File("d:\\com\\demo\\proxyDyn\\$Proxy.java");
file.createNewFile();
}
FileOutputStream fw = new FileOutputStream(file);
fw.write(content.getBytes());
fw.flush();
fw.close();
//动态编译Java文件
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
Iterable units = fileMgr.getJavaFileObjects(file);
JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
t.call();
fileMgr.close();
//外部获取java类
URL[] urls = new URL[]{
new URL("file:D:\\\\")};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class clazz = urlClassLoader.loadClass("com.demo.proxyDyn.$Proxy");
//这里构造方法构造的是Handler不再是目标方法了
Constructor constructor = clazz.getConstructor(CustomerInvocationHandler.class);
proxy = constructor.newInstance(h); //通过构造方法拿到代理类
} catch (Exception e) {
e.printStackTrace();
}
return proxy;
}
}