**
Java动态代理的理解
动态代理可以拆开来看,动态表示在运行时进行的,既然有动态,肯定有一个相对应的静态,确实,静态代理是指在代码编译前就已经确定的。至于代理,从字面上理解就是让别人帮忙做你想做的事情。
java中代理主要分为动态代理和静态代理,由于静态代理比较简单,本文主要着重于动态代理,动态代理用书面话来讲就是指在运行期间针对需要进行动态扩展的方法,可以不用修改原来的代码块,也能达到对方法扩展的目的,Spring AOP就是应用动态代理的一个典型。
本文先从动态代理的基本使用说起,然后到原理,再手动实现一个动态代理模型,最后聊一些动态代理性能相关的事
先简单的回忆一下JDK动态代理(熟悉的同学可以直接跳过)
package com.proxy.jdk;
public interface Employee {
public void work();
}
package com.proxy.jdk;
public class BankEmployee implements Employee{
@Override
public void work() {
System.out.println("work!");
}
}
package com.proxy.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyFactory implements InvocationHandler{
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
//生成代理对象
@SuppressWarnings("unchecked")
public <T> T newInstance() {
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before...");
Object obj = method.invoke(target, args);
System.out.println("after...");
return obj;
}
}
package com.proxy.jdk;
public class Test {
public static void main(String[] args) {
ProxyFactory pf = new ProxyFactory(new BankEmployee());
Employee e = pf.newInstance();
e.work();
}
}
运行结果:
before…
work!
after…
Java动态代理原理
当我们需要进行性能分析,记录log等操作时,这种方式就很方便,但它底层实现的原理是怎样的呢?以当前比较主流的JDK和Cglib动态代理为例
对于JDK动态代理,主要方式是采用实现相同接口的方式来进行对应业务方法的重写,比如上述的例子,通过Proxy进行代理对象的创建时,实际上是新建了一个代理类,实现我们的业务接口,然后对业务方法进行重写,来达到“动态“的目的
Cglib则是以ASM字节码操作框架作为底层原理的,可以通过直接修改字节码来达到代理的目的
关于他们两个各自的优缺点在稍后会进行对比
手动实现一个动态代理模型
既然上面我们已经清楚JDK动态的原理了,现在就让我们来手动模拟一个代理的模型,在开始之前,先来捋一下我们需要做的事情
1.一个业务接口和一个实现类
2.动态的实现一个对应接口的子类
3.编译该类
4.加载到内存
5.获取一个代理类的对象,完成代理的目的
package com.proxy;
/**
* 业务接口
* @author pc
*/
public interface Work {
public void goWork();
}
package com.proxy;
public class NearWork implements Work{
@Override
public void goWork() {
System.out.println("go work by bike!");
}
}
此处就时模拟JDK动态代理的核心部分
package com.proxy;
import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class MyProxy {
/**
* 获取代理对象
* @param loader
* @param infaces
* @param h
* @return
*/
public static Object newInstance(ClassLoader loader, Class<?>[] infaces, InvocationHandler h) {
//此处为了方便,暂时考虑一个接口的情况,如果要考虑多个接口,需另外处理
Method[] methods = infaces[0].getMethods();
String methodContent = "";
//拼接方法
for(Method method : methods){
methodContent += "public void " + method.getName() + "(){ \r\n" +
"try{ \r\n" +
" Method m = " + infaces[0].getName() + ".class.getMethod(\""+method.getName()+"\");\r\n" +
" h.invoke(this, m, null); \r\n" +
"}catch(Throwable t){} \r\n" +
"} \r\n";
}
//拼接代理类
String str =
"package com.proxy; \r\n" +
"\r\n" +
"import java.lang.reflect.InvocationHandler; \r\n" +
"import java.lang.reflect.Method; \r\n" +
"public class $Proxy implements " + infaces[0].getName() + "{ \r\n " +
" private InvocationHandler h; \r\n"+
"\r\n" +
" public $Proxy(InvocationHandler h){ \r\n" +
" this.h = h; \r\n" +
" } \r\n" +
"\r\n" +
methodContent + "\r\n" +
"\r\n" +
"}";
//创建对应的子类(本质上就是一个java文件)
String dir = System.getProperty("user.dir");
String folder = "/src/main/java";
String fileName = dir + folder + "/com/proxy/$Proxy.java";
File f = new File(fileName);
FileWriter fw = null;
StandardJavaFileManager fileMgr = null;
try{
f.createNewFile();
fw = new FileWriter(f);
fw.write(str);
fw.flush();
//编译 获取到编译器,执行编译任务
//options的作用是指定编译好的class文件存放位置,如果不指定,会放到与java文件同目录下
//编译这一块也可以替换成Runtime.exec("javac ...")来执行java编译指令的形式
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
fileMgr = compiler.getStandardFileManager(null, null, null);
List<String> options = new ArrayList<>(4);
options.add("-d");
options.add(dir + "/target/classes");
Iterable<? extends JavaFileObject> iter = fileMgr.getJavaFileObjects(fileName);
CompilationTask task = compiler.getTask(null, fileMgr, null, options, null, iter);
task.call();
//加载到内存(此时就可以使用了)
ClassLoader classLoader = loader;
Class<?> clazz = classLoader.loadClass("com.proxy.$Proxy");
Constructor<?> c = clazz.getConstructor(InvocationHandler.class);
return c.newInstance(h);
}catch(Exception e){
e.printStackTrace();
}finally {
IoUtil.close(fw, fileMgr);
}
return null;
}
}
package com.proxy;
import java.io.Closeable;
import java.io.IOException;
public class IoUtil {
public static void close(Closeable ...closeables ) {
if(closeables == null) {
return;
}
for (Closeable closeable : closeables) {
if(closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
创建测试类进行测试
package com.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) {
NearWork nw = new NearWork();
Work work = (Work) MyProxy.newInstance(nw.getClass().getClassLoader(),
nw.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before...");
method.invoke(nw, args);
System.out.println("after...");
return null;
}
});
work.goWork();
}
}
测试结果
before…
go work by bike!
after…
至此,我们就完成了一个最简单的动态代理模型(当然,实际要完成一套代理框架,需要考虑的细节还有很多,但最本质的东西是一样的,学到本质,换一种包装也不过是新瓶装旧酒)
结尾
另外,有必要指出几点:
1.在上述编译的过程中,有些同学可能会出现NPE异常,具体表现在通过ToolProvider.getSystemJavaCompiler()获取到的编译器为null,这是因为在我们的IDE编辑器里,运行环境选择的是单独安装的jre,但编译器需要从JDK里获取,所以需要把运行环境改成JDK自带的jre目录
2.编译完后,正常情况下class文件是放在与java文件同一目录下的,但由于我们项目运行时,会到target/classes文件夹下去找class文件,所以在编译时需要指定路径,让它输出到对应文件夹下
3.坊间一直流传着Cglib比JDK动态代理快的传闻,给出的原因是Cglib底层用的ASM框架是基于字节码进行操作的,JDK是基于反射的等等。诚然,在JDK1.6之前,JDK动态代理的性能问题一直是它的硬伤,但随着JDK团队对反射这一块的优化,到JDK1.8时,JDK动态代理的性能已经在一定程度上超过了Cglib,主要得益于新版JVM的优化,比如JIT等,但Cglib还是有它固有的优势,即它不需要有接口也可以,它是基于创建子类来代理的,所以用Cglib代理的类不能用final修饰
4.关于JDK动态代理的硬伤-反射,它慢是有很多原因的,最主要的原因是由于反射是动态进行的,JVM无法对其进行优化,其次在通过class反射创建对象时,会进行一系列的校验,比如参数格式的校验,方法是不是private的校验等等,另外对于参数对象的包装也是导致它慢的原因之一
虽然自JDK1.4后,引入了NativeMethodAccessorImpl与DelegatingMethodAccessorImpl来对多次调用反射(15次,可以通过-Dsun.reflect.inflationThreshold=xxx 配置)的场景进行优化,但其机制决定了反射不会很快
NativeMethodAccessorImpl的反射调用JNI的方法
关于校验那一块对于反射的影响,有人做了一个实验
从图中可以看出当把访问权限设置为允许访问时,确实会影响一些它的性能,虽然只是纳秒级
值得一提的时,多数反射使用的场景是在驱动注册(比如JDBC),XML解析等地方,这些场景一般只会调用反射1次,所以对于性能的影响几乎可以忽略不计。注意不要在循环中调用反射,有些需要new对象的场景,除非确实需要反射来创建,不然都建议直接用构造函数来创建
(关于Java反射这一块,其实是很大的一个部分,限于篇幅,暂不展开来讲,如果有想对这一块了解更多的同学,可以留言,我看情况后面专门抽一篇,捋一捋这部分)
本篇主要是想通过手写动态代理的过程,让大家能对JAVA中动态代理的全部过程有一个清晰的认识,另外还扯了一些其他跟动态代理相关的东西,那么现在让我们来感受一下来自灵魂的拷问吧
Q:SpringAOP了解么,底层原理是什么?
A:…
Q:恩,那么jdk动态代理和cglib的区别是什么?
A:…
Q:恩,不错,你刚刚说cglib在早期比jdk动态代理快,为什么呢?
A:…
Q:既然Jdk动态代理是由于反射才慢的,为什么反射会慢呢?
A:…
Q:为什么JVM不能对其优化,新版JDK做了什么处理让动态代理变快呢?
A:你咋不问为什么井盖是圆的呢?
夜已经很深了,天亮还会远么?