您的位置:首页 > 编程语言 > Java开发

深入理解Java动态代理及手动实现

2018-07-28 15:41 941 查看

前言

文章目录如下,便于快速索引

一、什么叫代理?

二、什么叫动态代理?

三、动态代理有什么优势?

四、动态代理的JDK实现原理

4.1核心类/接口

4.2 代理类$Proxy0解析

4.3 动态代理的经典使用

五、手写代码模拟JDK动态代理

六、参考资料

先将自己总结的Java动态代理UML图放在前面,用相同的颜色代表同一个或本质上相同的类与方法,便于大家理解。接口UserService是我们自己定义的接口——接口中有方法execute();被代理类是实现了该接口的具体类;代理类则是存在于内存中,也实现了UserService接口的类,在内存中,该类名为$Proxy0。



下面进入正文,今天想跟大家分享一下我自己在学习Java动态代理过程中的理解与收获,最后用自己的代码模拟实现JDK的动态代理。

一、什么叫代理?

这个概念不是我想表达的重点,所以这里我引用别人一篇博文的例子

“动态代理技术就是用来产生一个对象的代理对象的。在开发中为什么需要为一个对象产生代理对象呢?
举一个现实生活中的例子:歌星或者明星都有一个自己的经纪人,这个经纪人就是他们的代理人,当我们需要找明星表演时,不能直接找到该明星,只能是找明星的代理人。比如刘德华在现实生活中非常有名,会唱歌,会跳舞,会拍戏,刘德华在没有出名之前,我们可以直接找他唱歌,跳舞,拍戏,刘德华出名之后,他干的第一件事就是找一个经纪人,这个经纪人就是刘德华的代理人(代理),当我们需要找刘德华表演时,不能直接找到刘德华了(刘德华说,你找我代理人商谈具体事宜吧!),只能是找刘德华的代理人,因此刘德华这个代理人存在的价值就是拦截我们对刘德华的直接访问!
这个现实中的例子和我们在开发中是一样的,我们在开发中之所以要产生一个对象的代理对象,主要用于拦截对真实业务对象的访问。那么代理对象应该具有什么方法呢?代理对象应该具有和目标对象相同的方法

所以在这里明确代理对象的两个概念:

1、代理对象存在的价值主要用于拦截对真实业务对象的访问。(事务的开启与关闭)

2、代理对象应该具有和目标对象(真实业务对象)相同的方法。(要求实现同一接口)

刘德华(真实业务对象)会唱歌,会跳舞,会拍戏,我们现在不能直接找他唱歌,跳舞,拍戏了,只能找他的代理人(代理对象)唱歌,跳舞,拍戏,一个人要想成为刘德华的代理人,那么他必须具有和刘德华一样的行为(会唱歌,会跳舞,会拍戏),刘德华有什么方法,他(代理人)就要有什么方法,我们找刘德华的代理人唱歌,跳舞,拍戏,但是代理人不是真的懂得唱歌,跳舞,拍戏的,真正懂得唱歌,跳舞,拍戏的是刘德华,在现实中的例子就是我们要找刘德华唱歌,跳舞,拍戏,那么只能先找他的经纪人,交钱给他的经纪人,然后经纪人再让刘德华去唱歌,跳舞,拍戏。”

二、什么叫动态代理?

代理类在程序运行时创建的代理方式被成为动态代理。 也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。

三、动态代理有什么优势?

通过使用代理,通常有两个优点:

优点一:可以隐藏被代理类的实现;

优点二:可以实现客户与被代理类间的解耦,在不修改被代理类代码的情况下能够做一些额外的处理。

四、动态代理的JDK实现原理

在java的动态代理机制中,有两个重要的类和接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的。当然,我还想带各位深入了解一下存在于JVM中神秘的动态代理类——$Proxy0。最后再给出java动态代理的经典使用流程。

4.1 核心类/接口

4.1.1 java.lang.reflect.Proxy类

Proxy类提供了用于创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。

public class Proxy extends Object implements Serializable

(1)Proxy的主要静态变量

// 映射表:用于维护类装载器对象到其对应的代理类缓存
private static Map loaderToCache = new WeakHashMap();

// 标记:用于标记一个动态代理类正在被创建中
private static Object pendingGenerationMarker = new Object();

// 同步表:记录已经被创建的动态代理类类型,主要被方法 isProxyClass 进行相关的判断
private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap());

// 关联的调用处理器引用
protected InvocationHandler h;

(2)Proxy的构造方法

// 由于 Proxy 内部从不直接调用构造函数,所以 private 类型意味着禁止任何调用
private Proxy() {}

// 由于 Proxy 内部从不直接调用构造函数,所以 protected 意味着只有子类可以调用
protected Proxy(InvocationHandler h) {this.h = h;}

(3)Proxy静态方法newProxyInstance

public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException {

// 检查 h 不为空,否则抛异常
if (h == null) {
throw new NullPointerException();
}

// 获得与制定类装载器和一组接口相关的代理类类型对象
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, interfaces);

// 通过反射获取构造函数对象并生成代理类实例
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
SecurityManager sm = System.getSecurityManager();
if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {
// create proxy instance with doPrivilege as the proxy class may
// implement non-public interfaces that requires a special permission
return AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
return newInstance(cons, ih);
}
});
} else {
return newInstance(cons, ih);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString());
}
}

private static Object newInstance(Constructor<?> cons, InvocationHandler h) {
try {
return cons.newInstance(new Object[] {h} );
} catch (IllegalAccessException e) {
throw new InternalError(e.toString());
} catch (InstantiationException e) {
throw new InternalError(e.toString());
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString());
}
}
}

这个方法的作用就是得到一个动态的代理对象,其接收三个参数,我们来看看这三个参数所代表的含义。

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

//loader:  一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载
//interfaces:  一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
//h:  一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上

官方JDK文档给出了使用该方法创建一个动态代理类的模板:

Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class[] { Foo.class },
handler);

动态代理真正的关键是在 getProxyClass0 方法。这个我们在后面手动实现模拟JDK动态代码的时候可以看到。

4.1.2 java.lang.reflect.InvocationHandler接口

每一个动态代理类中都有一个实现了InvocationHandler这个接口(代码中的中介)的实例handler类(即前言中的MyInvocationHandler类),当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke(对方法的增强就写在这里面) 方法来进行调用。

import java.lang.reflect.Method;

public interface MyInvocationHandler {

Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

}

我们看到这个方法一共接受三个参数,那么这三个参数分别代表什么呢?

Object invoke(Object proxy, Method method, Object[] args) throws Throwable
//proxy:  内存中的代理实例 $proxy0
//method:  内存代理实例中class.forName("被代理类").getMethod("目标方法") 即被代理的类的方法对象
//args:  指代的是调用真实对象某个方法时接受的参数

我们怎么确认上面几个参数就是这个意思呢?那就看看下面这节吧~

4.2 代理类$Proxy0解析

(1)为什么内存中的动态代理类叫做$Proxy0?



这个可以通过断点查看到~

(2)怎么拿到动态代理类的字节码文件?

public static void createProxyFile() throws IOException {
byte[] generateProxyClass = ProxyGenerator.generateProxyClass("$Proxy0", new Class<?>[]{UserService.class});

FileOutputStream outputStream = new FileOutputStream("$Proxy0.class");
outputStream.write(generateProxyClass);
outputStream.close();
}

在4.3 (5)中最终输出动态代理类执行结果后可以调用上面的方法,即可得到字节码文件

(3)动态代理类$Proxy0字节码文件解析



很清楚,动态代理类实现了UserService接口,继承了Proxy类

首先我们看左边为动态代理类的代码结构。

构造方法

//$Proxy类
public $Proxy0(InvocationHandler var1) throws  {
super(var1);
}
//父类Proxy
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}

可以看到动态代理类包含一个有参构造方法,内部调用了父类方法,其实也就是完成了调用处理器 InvocationHandler的实例化。该构造方法大家可得注意,在4.3(6)中提到的动态代理创建流程第3步,使用JAVA反射机制获取动态代理对象时:

Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});

就是为了调用本构造方法。

Object方法

大家可以看到动态代理类有m0~m3四个方法,这四个方法分别是什么呢?

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("proxy.test.UserService").getMethod("execute");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}

可以看到,通过java反射机制,除了m3是UserService接口方法的实现以外,其他方法都是Object的方法。那这些方法有什么特点呢?

public final boolean equals(Object var1) throws  {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

其实核心就一句话 h.invoke() 也就是说动态代理类中的方法都使用了java反射机制去调用调用处理器 InvocationHandler中的方法。

接口方法

接口方法与Object方法一样,内部就一句话。我们注意到,invoke方法传入3个参数,这个invoke方法也就是4.1.2中我们提到的InvocationHandler接口的 invoke方法,那理解3个参数的意义也就很简单了。

参数1传入的为this——即$Proxy0本身,所以是内存中的动态代理对象

参数2传入的为m3——也就是proxy.test.UserService中名为execute的方法,即接口中的方法。而这也完全证实了之前在“一、什么是代理?”部分提到的第二个特点——代理对象应该具有和目标对象(真实业务对象)相同的方法。(要求实现同一接口)

参数3传入的为null——因为execute方法没有参数,所以为空。

public final String execute() throws  {
try {
return (String)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}


4.3 动态代理的经典使用

(1)理解UML图



再把本图放上来暖场~JAVA中动态代理的使用基本就是如上图所示。下图是我们程序的目录结构,下面我们用代码来实现。



(2)定义对象的行为接口UserService

package proxy.test;

public interface UserService {

public String execute() throws Throwable ;
}

(3)定义目标业务对象类UserServiceImpl

package proxy.test;

public class UserServiceImpl implements UserService{
@Override
public String execute() throws Throwable {
System.out.println("step 2 执行方法啦!!");
return "step 2 执行方法啦!!";
}
}

(4)自定义“调用处理程序”——MyInvocationHandler

package proxy.test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler {

private UserService userService;

public MyInvocationHandler(UserService userService) {
this.userService = userService;
}

/*
* @proxy 内存中的代理实例 $proxy0
* @method 内存代理实例中class.forName("被代理类").getMethod("目标方法") 即被代理的类的方法对象
* @args 方法参数
* */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();   //事务开启

method.invoke(userService, args);

after();    //事务关闭

return "成功了";
}

private void before() {
System.out.println("事务开启!");
}

private void after() {
System.out.println("事务关闭");
}
}

动态代理最具魅力所在——也就是上面代码中事务开启和关闭部分,总结起来就是:实现了方法的增强,让你可以在不修改源码的情况下,增强一些方法,在方法执行前后做任何你想做的事情(甚至根本不去执行这个方法),因为在InvocationHandler的invoke方法中,你可以直接获取正在调用方法对应的Method对象,具体应用的话,比如可以添加调用日志,做事务控制等。

(5)代理类生成并测试代码

package proxy.test;
import java.lang.reflect.Proxy;

public class MyTest {

public static void main(String[] args) throws Throwable {
System.out.println("---------------JDK动态代理----------------");
UserService userService = (UserService) Proxy.newProxyInstance(MyTest.class.getClassLoader(),
new Class<?>[]{UserService.class},
new MyInvocationHandler(new UserServiceImpl()));

userService.execute();
}
}

上面的代码也就是4.1.1(3)中提到的JDK文档给出的创建代理类方式。通过强制类型转换并执行相应方法,得到输出如下:



(6)动态代理创建流程总结

动态代理的创建是基于java反射的,一个典型的动态代理创建对象过程可分为以下四个步骤:

1.通过实现InvocationHandler接口创建自己的调用处理器

InvocationHandler handler = new InvocationHandlerImpl(...);

2.通过为Proxy类指定ClassLoader对象和一组interface创建动态代理类

Class clazz = Proxy.getProxyClass(classLoader,new Class[]{...});

3.通过反射机制获取动态代理类$Proxy0的构造函数,其参数类型是调用处理器接口类型

Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});

4.通过构造函数创建代理类实例,此时需将调用处理器对象作为参数被传入

Interface Proxy = (Interface)constructor.newInstance(new Object[] (handler));

为了简化对象创建过程,Proxy类中的newInstance方法封装了2~4,只需两步即可完成代理对象的创建。生成的动态代理类$Proxy0继承Proxy类实现UserService接口,实现的UserService的方法实际调用调用处理器的invoke方法,而invoke方法利用反射调用的是被代理对象的的方法(Object result=method.invoke(proxied,args))。

(7)JDK动态代理的不足

诚然,Proxy已经设计得非常优美,但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。摆脱这个遗憾就得依靠CGLIB(此处等待下文)。

五、手写代码模拟JDK动态代理

5.1 原理解析

手撸代码模拟JDK动态代理,其实也就是对java.lang.reflect.Proxy类的功能进行模拟。其步骤主要有以下四步:

(1)创建代理类的源码;

拿到被代理类(如UserServiceImpl)实现的接口类对象(如UserService.class),遍历里面的方法(如execute()方法),以字符串的形式拼凑出代理类源码(动态代理类与被代理类实现同一接口在此体现),将代理类的源码写到本地java文件

(2)将源码进行编译成字节码;

读取源码,编译java文件,得到.class字节码文件(的路径)

(3)将字节码加载到内存;

(4)实例化代理类对象并返回给调用者。

其中步骤(1)、(2)、(3)就是我们自定义的Proxy类所要完成的功能,类的结构如下图;步骤(4)是我们功能代码/测试代码要实现的。下面我们对每一步进行解析。



5.2 创建代理类的源码

项目实现源码已经上传,欢迎点击下载~

(1)使用字符串拼凑动态代理对象的java源码

//用字符串的形式拼凑出内存里的代理类
static String rt = "\r\n";
private static String get$Proxy0(Class<?> interfaces) {

Method[] methods = interfaces.getMethods();

String proxyClass = "package proxy;" + rt
+ "import java.lang.reflect.Method;" + rt
+ "public class $Proxy0 implements " + interfaces.getName() + "{"
+ rt + "MyInvocationHandler h;" + rt
+ "public $Proxy0(MyInvocationHandler h) {" + rt
+ "this.h = h;" + rt + "}" + getMethodString(methods, interfaces)
+ rt + "}";
return proxyClass;
}

private static String getMethodString(Method[] methods, Class<?> interfaces) {
String proxyMethod = "";

for (Method method : methods) {
proxyMethod += "public String " + method.getName()
+ "() throws Throwable {" + rt + "Method md = "
+ interfaces.getName() + ".class.getMethod(\"" + method.getName()
+ "\",new Class[]{});" + rt
+ "return (String)this.h.invoke(this, md, null);" + rt + "}" + rt;
}

return proxyMethod;
}

上面这段代码所模拟的具体代码实现在JDK中是在jar包中的,其得到的结果就是生成了$Proxy0.java

package proxy;
import java.lang.reflect.Method;

public class $Proxy0 implements proxy.test.UserService{
MyInvocationHandler h;
public $Proxy0(MyInvocationHandler h) {
this.h = h;
}

public String execute() throws Throwable {
Method md = proxy.test.UserService.class.getMethod("execute",new Class[]{});
return (String)this.h.invoke(this, md, null);
}
}

(2)将源码写入本地文件

private static void outputFile(String proxyClass, String path) {
File f = new File(path);
try {
FileWriter fw = new FileWriter(f);
fw.write(proxyClass);
fw.flush();
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}


5.3 将源码进行编译成字节码

private static void compileJavaFile(String fileName) {
try {
//获得当前系统中的编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//获得文件管理者
StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> fileObjects = manager.getJavaFileObjects(fileName);
//编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, fileObjects);
//开始编译,执行完可在当前目录下看到.class文件
task.call();
//关闭文件管理者
manager.close();
} catch (IOException e) {
e.printStackTrace();
}
}

这段代码用的都是JAVA自带的编译工具,不做过多解释。

5.4 将字节码加载到内存

private static Object loadClassToJvm(MyInvocationHandler h) {
try {
//使用自定义类加载器
MyClassLoader loader = new MyClassLoader("C:\\HailinLuo\\CODING\\JAVA\\JavaProgramming\\src\\proxy");
//得到动态代理类的反射对象
Class<?> $Proxy0 = loader.findClass("$Proxy0");
//通过反射机制获取动态代理类$Proxy0的构造函数,其参数类型是调用处理器接口类型
Constructor<?> constructors = $Proxy0.getConstructor(MyInvocationHandler.class);
//通过构造函数创建动态代理类实例
return constructors.newInstance(h);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
return null;
}

这里使用了自定义的类加载器MyClassLoader

package proxy;

import java.io.*;

public class MyClassLoader extends ClassLoader {
File dir;
//把文件路径用构造函数传进来
public MyClassLoader(String path) {
dir = new File(path);
}
/*
* 本方法就是去加载对应的字节码文件
* */
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//如果文件路径可用
if (dir != null) {
File clazzFile = new File(dir, name + ".class");
//如果字节码文件存在
if (clazzFile.exists()) {
//把字节码文件加载到VM
try {
//文件流对接class文件
FileInputStream inputStream = new FileInputStream(clazzFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
//将class文件读取到buffer中
while ((len = inputStream.read(buffer)) != -1) {
//将buffer中的内容读取到baos中的buffer
baos.write(buffer, 0, len);
}
//将buffer中的字节读到内存加载为class
return defineClass("proxy." + name, baos.toByteArray(), 0, baos.size());
} catch (IOException e) {
e.printStackTrace();
}
}
}
return super.findClass(name);
}
}

也可以使用URLClassLoader 加载,如下所示

//load到内存
URL[] urls = new URL[]{new URL("C:\\HailinLuo\\CODING\\JAVA\\JavaProgramming\\src\\proxy")};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class cls = urlClassLoader.loadClass("proxy.$Proxy0");


5.5 实例化代理类对象并返回给调用者

UserService service = (UserService) MyProxy.newProxyInstance(MyTest.class.getClassLoader(),
UserService.class,
new MyInvocationHandlerImpl(new UserServiceImpl()));

service.execute();

没什么好说的。。。

5.6 输出结果



如上图所示,下方红框所示输出结果与JDK动态代理效果一致。说明我们的模拟是成功的!而上面那个红框中的$Proxy0.java和$Proxy0.class则是5.2(生成源码)与5.3(编译为字节码)执行的结果。

OK,分享就到这里~

六、参考资料

项目源码已经上传,欢迎点击下载~
http://www.jb51.net/article/86531.htm https://blog.csdn.net/pangqiandou/article/details/52964066 https://blog.csdn.net/scplove/article/details/52451899 https://www.jianshu.com/p/dbce090d5c3e?1487292535486 https://blog.csdn.net/ljt2724960661/article/details/52507314
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java JDK