原创:微信公众号 码农参上,欢迎分享,转载请保留出处。

前几天的时候,交流群里的小伙伴抛出了一个问题,为什么JDK的动态代理一定要基于接口实现呢?

好的安排,其实要想弄懂这个问题还是需要一些关于代理和反射的底层知识的,我们今天就盘一盘这个问题,走你~

一个简单的例子

在分析原因之前,我们先完整的看一下实现jdk动态代理需要几个步骤,首先需要定义一个接口:

public interface Worker {
void work();
}

再写一个基于这个接口的实现类:

public class Programmer implements Worker {
@Override
public void work() {
System.out.println("coding...");
}
}

自定义一个Handler,实现InvocationHandler接口,通过重写内部的invoke方法实现逻辑增强。其实这个InvocationHandler可以使用匿名内部类的形式定义,这里为了结构清晰拿出来单独声明。

public class WorkHandler implements InvocationHandler {
private Object target;
WorkHandler(Object target){
this.target = target;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("work")) {
System.out.println("before work...");
Object result = method.invoke(target, args);
System.out.println("after work...");
return result;
}
return method.invoke(target, args);
}
}

main方法中进行测试,使用Proxy类的静态方法newProxyInstance生成一个代理对象并调用方法:

public static void main(String[] args) {
Programmer programmer = new Programmer();
Worker worker = (Worker) Proxy.newProxyInstance(
programmer.getClass().getClassLoader(),
programmer.getClass().getInterfaces(),
new WorkHandler(programmer));
worker.work();
}

执行上面的代码,输出:

before work...
coding...
after work...

可以看到,执行了方法逻辑的增强,到这,一个简单的动态代理过程就实现了,下面我们分析一下源码。

Proxy源码解析

既然是一个代理的过程,那么肯定存在原生对象代理对象之分,下面我们查看源码中是如何动态的创建代理对象的过程。上面例子中,创建代理对象调用的是Proxy类的静态方法newProxyInstance,查看一下源码:

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException{
Objects.requireNonNull(h); final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
} /*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs); /*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
} final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
}//省略catch
}

概括一下上面代码中重点部分:

  • checkProxyAccess方法中,进行参数验证
  • getProxyClass0方法中,生成一个代理类Class或者寻找已生成过的代理类的缓存
  • 通过getConstructor方法,获取生成的代理类的构造方法
  • 通过newInstance方法,生成实例对象,也就是最终的代理对象

上面这个过程中,获取构造方法和生成对象都是直接利用的反射,而需要重点看看的是生成代理类的方法getProxyClass0

private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
} // If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}

注释写的非常清晰,如果缓存中已经存在了就直接从缓存中取,这里的proxyClassCache是一个WeakCache类型,如果缓存中目标classLoader和接口数组对应的类已经存在,那么返回缓存的副本。如果没有就使用ProxyClassFactory去生成Class对象。中间的调用流程可以省略,最终实际调用了ProxyClassFactoryapply方法生成Class。在apply方法中,主要做了下面3件事。

  • 首先,根据规则生成文件名:
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

如果接口被定义为public公有,那么默认会使用com.sun.proxy作为包名,类名是$Proxy加上一个自增的整数值,初始时是0,因此生成的文件名是$Proxy0

如果是非公有接口,那么会使用和被代理类一样的包名,可以写一个private接口的例子进行一下测试。

package com.hydra.test.face;
public class InnerTest {
private interface InnerInterface {
void run();
} class InnerClazz implements InnerInterface {
@Override
public void run() {
System.out.println("go");
}
}
}

这时生成的代理类的包名为com.hydra.test.face,与被代理类相同:

  • 然后,利用ProxyGenerator.generateProxyClass方法生成代理的字节码数组:
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);

generateProxyClass方法中,有一个重要的参数会发挥作用:

private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));

如果这个属性被配置为true,那么会把字节码存储到硬盘上的class文件中,否则不会保存临时的字节码文件。

  • 最后,调用本地方法defineClass0生成Class对象:
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);

返回代理类的Class后的流程我们在前面就已经介绍过了,先获得构造方法,再使用构造方法反射的方式创建代理对象。

神秘的代理对象

创建代理对象流程的源码分析完了,我们可以先通过debug来看看上面生成的这个代理对象究竟是个什么:

和源码中看到的规则一样,是一个Class为$Proxy0的神秘对象,再看一下代理对象的Class的详细信息:

类的全限定名是com.sun.proxy.$Proxy0,在上面我们提到过,这个类是在运行过程中动态生成的,并且程序执行完成后,会自动删除掉class文件。如果想要保留这个临时文件不被删除,就要修改我们上面提到的参数,具体操作起来有两种方式,第一种是在启动VM参数中加入:

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

第二种是在代码中加入下面这一句,注意要加在生成动态代理对象之前:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

使用了上面两种方式中的任意一种后,就可以保存下来临时的字节码文件了,需要注意这个文件生成的位置,并不是在target目录下,而是生成在项目目录下的com\sun\proxy中,正好和默认生成的包名对应。

拿到字节码文件后,就可以使用反编译工具来反编译它了,这里使用jad在cmd下一条命令直接搞定:

jad -s java $Proxy0.class

看一下反编译后$Proxy0.java文件的内容,下面的代码中,我只保留了核心部分,省略了无关紧要的equalstoStringhashCode方法的定义。

public final class $Proxy0 extends Proxy implements Worker{
public $Proxy0(InvocationHandler invocationhandler){
super(invocationhandler);
} public final void work(){
try{
super.h.invoke(this, m3, null);
return;
}catch(Error _ex) { }
catch(Throwable throwable){
throw new UndeclaredThrowableException(throwable);
}
} private static Method m3;
static {
try{
m3 = Class.forName("com.hydra.test.Worker").getMethod("work", new Class[0]);
//省略其他Method
}//省略catch
}
}

这个临时生成的代理类$Proxy0中主要做了下面的几件事:

  • 在这个类的静态代码块中,通过反射初始化了多个静态方法Method变量,除了接口中的方法还有equalstoStringhashCode这三个方法
  • 继承父类Proxy,实例化的过程中会调用父类的构造方法,构造方法中传入的invocationHandler对象实际上就是我们自定义的WorkHandler的实例
  • 实现了自定义的接口Worker,并重写了work方法,方法内调用了InvocationHandlerinvoke方法,也就是实际上调用了WorkHandlerinvoke方法
  • 省略的equalstoStringhashCode方法实现也一样,都是调用super.h.invoke()方法

到这里,整体的流程就分析完了,我们可以用一张图来简要总结上面的过程:

为什么要有接口?

通过上面的分析,我们已经知道了代理对象是如何生成的了,那么回到开头的问题,为什么jdk的动态代理一定要基于接口呢?

其实如果不看上面的分析,我们也应该知道,要扩展一个类有常见的两种方式,继承父类或实现接口。这两种方式都允许我们对方法的逻辑进行增强,但现在不是由我们自己来重写方法,而是要想办法让jvm去调用InvocationHandler中的invoke方法,也就是说代理类需要和两个东西关联在一起:

  • 被代理类
  • InvocationHandler

而jdk处理这个问题的方式是选择继承父类Proxy,并把InvocationHandler存在父类的对象中:

public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}
//...
}

通过父类Proxy的构造方法,保存了创建代理对象过程中传进来的InvocationHandler的实例,使用protected修饰保证了它可以在子类中被访问和使用。但是同时,因为java是单继承的,因此在继承了Proxy后,只能通过实现目标接口的方式来实现方法的扩展,达到我们增强目标方法逻辑的目的。

扯点别的

其实看完源码、弄明白代理对象生成的流程后,我们还可以用另一种方法实现动态代理:

public static void main(String[] args) throws Exception {
Class<?> proxyClass = Proxy.getProxyClass(Test3.class.getClassLoader(), Worker.class);
Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
InvocationHandler workHandler = new WorkHandler(new Programmer());
Worker worker = (Worker) constructor.newInstance(workHandler);
worker.work();
}

运行结果与之前相同,这种写法其实就是抽出了我们前面介绍的几个核心方法,中间省略了一些参数的校验过程,这种方式可以帮助大家熟悉jdk动态代理原理,但是在使用过程中还是建议大家使用标准方式,相对更加安全规范。

总结

本文从源码以及实验的角度,分析了jdk动态代理生成代理对象的流程,通过代理类的实现原理分析了为什么jdk动态代理一定要基于接口实现。总的来说,jdk动态代理的应用还是非常广泛的,例如在Spring、Mybatis以及Feign等很多框架中动态代理都被大量的使用,可以说学好jdk动态代理,对于我们阅读这些框架的底层源码还是很有帮助的。

作者简介,码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。个人微信DrHydra9,欢迎添加好友,进一步交流。

JDK动态代理为什么必须要基于接口?的更多相关文章

  1. SpringBoot27 JDK动态代理详解、获取指定的类类型、动态注册Bean、接口调用框架

    1 JDK动态代理详解 静态代理.JDK动态代理.Cglib动态代理的简单实现方式和区别请参见我的另外一篇博文. 1.1 JDK代理的基本步骤 >通过实现InvocationHandler接口来 ...

  2. jdk动态代理与cglib代理、spring aop代理实现原理

    原创声明:本博客来源与本人另一博客[http://blog.csdn.net/liaohaojian/article/details/63683317]原创作品,绝非他处摘取 代理(proxy)的定义 ...

  3. jdk动态代理与cglib代理、spring aop代理实现原理解析

    原创声明:本博客来源为本人原创作品,绝非他处摘取,转摘请联系博主 代理(proxy)的定义:为某对象提供代理服务,拥有操作代理对象的功能,在某些情况下,当客户不想或者不能直接引用另一个对象,而代理对象 ...

  4. 何为代理?jdk动态代理与cglib代理、spring Aop代理原理浅析

    原创声明:本博客来源为本人原创作品,绝非他处摘取,转摘请联系博主 代理(proxy)的定义:为某对象提供代理服务,拥有操作代理对象的功能,在某些情况下,当客户不想或者不能直接引用另一个对象,而代理对象 ...

  5. 代理模式(静态代理、JDK动态代理原理分析、CGLIB动态代理)

    代理模式 代理模式是设计模式之一,为一个对象提供一个替身或者占位符以控制对这个对象的访问,它给目标对象提供一个代理对象,由代理对象控制对目标对象的访问. 那么为什么要使用代理模式呢? 1.隔离,客户端 ...

  6. JDK动态代理与CGLib动态代理相关问题

    导读: 1.JDK动态代理原理是什么?为什么不支持类的代理? 2.JDK动态代理实例 3.CGLib代理原理是什么? 4.CGLib代理实例 5.JDK动态代理与CGLib代理的区别是什么? 6.总结 ...

  7. jdk动态代理与cglib代理、spring Aop代理原理-代理使用浅析

    原创声明:本博客来源为本人原创作品,绝非他处摘取,转摘请联系博主 代理(proxy)的定义:为某对象提供代理服务,拥有操作代理对象的功能,在某些情况下,当客户不想或者不能直接引用另一个对象,而代理对象 ...

  8. java学习笔记(中级篇)—JDK动态代理

    一.什么是代理模式 相信大家都知道代理商这个概念,在商业中,代理商无处不在.假设你要去买东西,你不可能去找真正的厂家去买,也不可能直接跟厂家提出需求,代理商就是这中间的一桥梁,连接买家和厂商.你要买或 ...

  9. 静态代理、jdk动态代理、cglib动态代理

    一.静态代理 Subject:抽象主题角色,抽象主题类可以是抽象类,也可以是接口,是一个最普通的业务类型定义,无特殊要求. RealSubject:具体主题角色,也叫被委托角色.被代理角色.是业务逻辑 ...

随机推荐

  1. ADD software version display

    ADD software version display ADD software version display1. Problem Description2. Analysis3. Solutio ...

  2. 创建app子应用,配置数据库,编写模型,进行数据迁移

    文章目录 web开发django模型 1.创建app子应用 2.配置子应用 3.使用 4.配置子应用管理自已的路由 django数据库开发思维与ORM 1.创建数据库 2.配置数据库 3.安装pymy ...

  3. Java基础-JNI入门示例

    1.JNI是什么? JNI(Java Native Interface) Java本地接口,又叫Java原生接口.它允许Java调用C/C++的代码,同时也允许在C/C++中调用Java的代码. 可以 ...

  4. 基础概念(2):怎么用cc来编译?

    怎么用cc来编译? 总结卡片: cc的使用可以很简单,指定要转换的程序文件就可以了,比如:cc hello.c. 按cc的规则(我这里是clang-llvm),程序文件以.c或.cpp为后缀. cc有 ...

  5. Anchor CMS 0.12.7 跨站请求伪造漏洞(CVE-2020-23342)

    这个漏洞复现相对来说很简单,而且这个Anchor CMS也十分适合新手训练代码审计能力.里面是一个php框架的轻量级设计,通过路由实现的传递参数. 0x00 漏洞介绍 Anchor(CMS)是一款优秀 ...

  6. Django 优化杂谈

    Django 优化杂谈 Apr 21 2017 总结下最近看过的一些文章,然后想到的一些优化点,整理一下. 数据库连接池 http://mt.dbanotes.net/arch/instagram.h ...

  7. mysql主从模型下如果保证主误删除数据,尽可能避免数据丢失方案

  8. elasticsearch算法之词项相似度算法(二)

    六.莱文斯坦编辑距离 前边的几种距离计算方法都是针对相同长度的词项,莱文斯坦编辑距离可以计算两个长度不同的单词之间的距离:莱文斯坦编辑距离是通过添加.删除.或者将一个字符替换为另外一个字符所需的最小编 ...

  9. Android Studio如何查看自己设计的数据库

    首先点击左上角进入Device File Explorer 进入后 点击data-data 找到你的项目名称 进入后点击你建立的数据库 一步步按照提示进行操作,即可显示你的表

  10. JavaScripts调用摄像头【MediaDevices.getUserMedia()】

    h5调用摄像头(允许自定义界面)[MediaDevices.getUserMedia()] <!DOCTYPE html> <html lang="en"> ...