JDK动态代理[4]----ProxyGenerator生成代理类的字节码文件解析
通过前面几篇的分析,我们知道代理类是通过Proxy类的ProxyClassFactory工厂生成的,这个工厂类会去调用ProxyGenerator类的generateProxyClass()方法来生成代理类的字节码。ProxyGenerator这个类存放在sun.misc包下,我们可以通过OpenJDK源码来找到这个类,该类的generateProxyClass()静态方法的核心内容就是去调用generateClassFile()实例方法来生成Class文件。我们直接来看generateClassFile()这个方法内部做了些什么。
private byte[] generateClassFile() {
//第一步, 将所有的方法组装成ProxyMethod对象
//首先为代理类生成toString, hashCode, equals等代理方法
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
//遍历每一个接口的每一个方法, 并且为其生成ProxyMethod对象
for (int i = 0; i < interfaces.length; i++) {
Method[] methods = interfaces[i].getMethods();
for (int j = 0; j < methods.length; j++) {
addProxyMethod(methods[j], interfaces[i]);
}
}
//对于具有相同签名的代理方法, 检验方法的返回值是否兼容
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
} //第二步, 组装要生成的class文件的所有的字段信息和方法信息
try {
//添加构造器方法
methods.add(generateConstructor());
//遍历缓存中的代理方法
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {
//添加代理类的静态字段, 例如:private static Method m1;
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC));
//添加代理类的代理方法
methods.add(pm.generateMethod());
}
}
//添加代理类的静态字段初始化方法
methods.add(generateStaticInitializer());
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception");
} //验证方法和字段集合不能大于65535
if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
} //第三步, 写入最终的class文件
//验证常量池中存在代理类的全限定名
cp.getClass(dotToSlash(className));
//验证常量池中存在代理类父类的全限定名, 父类名为:"java/lang/reflect/Proxy"
cp.getClass(superclassName);
//验证常量池存在代理类接口的全限定名
for (int i = 0; i < interfaces.length; i++) {
cp.getClass(dotToSlash(interfaces[i].getName()));
}
//接下来要开始写入文件了,设置常量池只读
cp.setReadOnly(); ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
try {
//1.写入魔数
dout.writeInt(0xCAFEBABE);
//2.写入次版本号
dout.writeShort(CLASSFILE_MINOR_VERSION);
//3.写入主版本号
dout.writeShort(CLASSFILE_MAJOR_VERSION);
//4.写入常量池
cp.write(dout);
//5.写入访问修饰符
dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
//6.写入类索引
dout.writeShort(cp.getClass(dotToSlash(className)));
//7.写入父类索引, 生成的代理类都继承自Proxy
dout.writeShort(cp.getClass(superclassName));
//8.写入接口计数值
dout.writeShort(interfaces.length);
//9.写入接口集合
for (int i = 0; i < interfaces.length; i++) {
dout.writeShort(cp.getClass(dotToSlash(interfaces[i].getName())));
}
//10.写入字段计数值
dout.writeShort(fields.size());
//11.写入字段集合
for (FieldInfo f : fields) {
f.write(dout);
}
//12.写入方法计数值
dout.writeShort(methods.size());
//13.写入方法集合
for (MethodInfo m : methods) {
m.write(dout);
}
//14.写入属性计数值, 代理类class文件没有属性所以为0
dout.writeShort(0);
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception");
}
//转换成二进制数组输出
return bout.toByteArray();
}
可以看到generateClassFile()方法是按照Class文件结构进行动态拼接的。什么是Class文件呢?在这里我们先要说明下,我们平时编写的Java文件是以.java结尾的,在编写好了之后通过编译器进行编译会生成.class文件,这个.class文件就是Class文件。Java程序的执行只依赖于Class文件,和Java文件是没有关系的。这个Class文件描述了一个类的信息,当我们需要使用到一个类时,Java虚拟机就会提前去加载这个类的Class文件并进行初始化和相关的检验工作,Java虚拟机能够保证在你使用到这个类之前就会完成这些工作,我们只需要安心的去使用它就好了,而不必关心Java虚拟机是怎样加载它的。当然,Class文件并不一定非得通过编译Java文件而来,你甚至可以直接通过文本编辑器来编写Class文件。在这里,JDK动态代理就是通过程序来动态生成Class文件的。我们再次回到上面的代码中,可以看到,生成Class文件主要分为三步:
第一步:收集所有要生成的代理方法,将其包装成ProxyMethod对象并注册到Map集合中。
第二步:收集所有要为Class文件生成的字段信息和方法信息。
第三步:完成了上面的工作后,开始组装Class文件。
我们知道一个类的核心部分就是它的字段和方法。我们重点聚焦第二步,看看它为代理类生成了哪些字段和方法。在第二步中,按顺序做了下面四件事。
1.为代理类生成一个带参构造器,传入InvocationHandler实例的引用并调用父类的带参构造器。
2.遍历代理方法Map集合,为每个代理方法生成对应的Method类型静态域,并将其添加到fields集合中。
3.遍历代理方法Map集合,为每个代理方法生成对应的MethodInfo对象,并将其添加到methods集合中。
4.为代理类生成静态初始化方法,该静态初始化方法主要是将每个代理方法的引用赋值给对应的静态字段。
通过以上分析,我们可以大致知道JDK动态代理最终会为我们生成如下结构的代理类:
public class Proxy0 extends Proxy implements UserDao { //第一步, 生成构造器
protected Proxy0(InvocationHandler h) {
super(h);
} //第二步, 生成静态域
private static Method m1; //hashCode方法
private static Method m2; //equals方法
private static Method m3; //toString方法
private static Method m4; //... //第三步, 生成代理方法
@Override
public int hashCode() {
try {
return (int) h.invoke(this, m1, null);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
} @Override
public boolean equals(Object obj) {
try {
Object[] args = new Object[] {obj};
return (boolean) h.invoke(this, m2, args);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
} @Override
public String toString() {
try {
return (String) h.invoke(this, m3, null);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
} @Override
public void save(User user) {
try {
//构造参数数组, 如果有多个参数往后面添加就行了
Object[] args = new Object[] {user};
h.invoke(this, m4, args);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
} //第四步, 生成静态初始化方法
static {
try {
Class c1 = Class.forName(Object.class.getName());
Class c2 = Class.forName(UserDao.class.getName());
m1 = c1.getMethod("hashCode", null);
m2 = c1.getMethod("equals", new Class[]{Object.class});
m3 = c1.getMethod("toString", null);
m4 = c2.getMethod("save", new Class[]{User.class});
//...
} catch (Exception e) {
e.printStackTrace();
}
} }
至此,经过层层分析,深入探究JDK源码,我们还原了动态生成的代理类的本来面目,之前心中存在的一些疑问也随之得到了很好的解释
1.代理类默认继承Porxy类,因为Java中只支持单继承,所以JDK动态代理只能去实现接口。
2.代理方法都会去调用InvocationHandler的invoke()方法,因此我们需要重写InvocationHandler的invoke()方法。
3.调用invoke()方法时会传入代理实例本身,目标方法和目标方法参数。解释了invoke()方法的参数是怎样来的。
使用刚刚构造出来的Proxy0作为代理类再次进行测试,可以看到最终的结果与使用JDK动态生成的代理类的效果是一样的。再次验证了我们的分析是可靠且准确的。至此,JDK动态代理系列文章宣告结束。通过本系列的分析,笔者解决了心中长久以来的疑惑,相信读者们对JDK动态代理的理解也更深了一步。但是纸上得来终觉浅,想要更好的掌握JDK动态代理技术,读者可参照本系列文章自行查阅JDK源码,也可与笔者交流学习心得,指出笔者分析不当的地方,共同学习,共同进步。
JDK动态代理[4]----ProxyGenerator生成代理类的字节码文件解析的更多相关文章
- java中三种方式获得类的字节码文件对象
package get_class_method; public class ReflectDemo { /** * @param args */ public static void main(St ...
- 命令行中运行Java字节码文件提示找不到或无法加载主类的问题
测试类在命令行操作,编译通过,运行时,提示 错误: 找不到或无法加载主类 java类 package com.company.schoolExercise; public class test7_3_ ...
- Spring AOP源码分析(三):基于JDK动态代理和CGLIB创建代理对象的实现原理
AOP代理对象的创建 AOP相关的代理对象的创建主要在applyBeanPostProcessorsBeforeInstantiation方法实现: protected Object applyBea ...
- 【java】查看Java字节码文件内容的方法+使用javap找不到类 解决方法
研究synchronized底层实现,涉及到查看java字节码的需要 前提是,你的PC已经成功安装了JDK并别配置了环境变量. ==========查看方法========= 一.javap查看简约字 ...
- JDK动态代理深入理解分析并手写简易JDK动态代理(上)
原文同步发表至个人博客[夜月归途] 原文链接:http://www.guitu18.com/se/java/2019-01-03/27.html 作者:夜月归途 出处:http://www.guitu ...
- JDK动态代理实现机制
=========================================== 原文链接: JDK动态代理实现机制 转载请注明出处! =========================== ...
- 代理模式(静态代理、JDK动态代理原理分析、CGLIB动态代理)
代理模式 代理模式是设计模式之一,为一个对象提供一个替身或者占位符以控制对这个对象的访问,它给目标对象提供一个代理对象,由代理对象控制对目标对象的访问. 那么为什么要使用代理模式呢? 1.隔离,客户端 ...
- JDK动态代理详解
JDK动态代理是代理模式的一种,且只能代理接口.spring也有动态代理,称为CGLib,现在主要来看一下JDK动态代理是如何实现的? 一.介绍 JDK动态代理是有JDK提供的工具类Proxy实现的, ...
- Java代理:静态代理、JDK动态代理和CGLIB动态代理
代理模式(英语:Proxy Pattern)是程序设计中的一种设计模式.所谓的代理者是指一个类别可以作为其它东西的接口.代理者可以作任何东西的接口:网络连接.存储器中的大对象.文件或其它昂贵或无法复制 ...
随机推荐
- 51Nod 1084 矩阵取数问题 V2 双线程DP 滚动数组优化
基准时间限制:2 秒 空间限制:131072 KB 一个M*N矩阵中有不同的正整数,经过这个格子,就能获得相应价值的奖励,先从左上走到右下,再从右下走到左上.第1遍时只能向下和向右走,第2遍时只能向 ...
- CCF-201503-3-节日
问题描述 试题编号: 201503-3 试题名称: 节日 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 有一类节日的日期并不是固定的,而是以"a月的第b个星期c&q ...
- CMMI-4中19个PA的大致描述
组织过程资产库下面有组织级标准过程库, 这个库里一共有19各PA(就是标准过程啦) PA的英文是Process Area CM(配置管理过程,英文是Configuration Manage ...
- Centos7 下安装Docke
为什么 要用centos7呢. 现在哪个企业用的是centos7呀.不都是老版本么. 对咱们是新技术.所以只有新系统才可以用.因为docker要求服务CentOS6以上,kernel 版本必须2.6. ...
- Hibernate框架入门
导读 本文主要介绍hibernate的入门,主要包括以下内容:hibernate介绍.hibernate环境搭建.hibernate简单测试.测试涉及的api详解. 一.hibernate介绍 JDB ...
- Java读取打印机自定义纸张.
打印出现截断? 对于自定义纸张打印, 一定要先在打印机配置那边添加, 不然会出现截断. 例如打印1000*500, 出来是正常的, 打印216*139, 出现了截断. 因为java默认的打印, 会从打 ...
- 求前n个素数(C++)
输入一个输n,输出前n个素数. #include<iostream> #include <math.h> using namespace std; class Sushu { ...
- 史上最强学生管理系统之IO版
既上一博发布的ArrayList版本之后,新一版的IO版又来了,其实只是在上一个版本里面添加了IO流的内容,将存入更改的信息更新到了文件中而已,这个版本网上仍然很多,本人只是在某些方面稍加修改,因为自 ...
- Winform开发框架中工作流模块之申请单草稿处理
在我们开发工作流模块的时候,有时候填写申请单过程中,暂时不想提交审批,那么可以暂存为草稿,以供下次继续填写或者提交处理,那么这个草稿的功能是比较实用的,否则对于一些填写内容比较多的申请单,每次要重填写 ...
- 用keras做SQL注入攻击的判断
本文是通过深度学习框架keras来做SQL注入特征识别, 不过虽然用了keras,但是大部分还是普通的神经网络,只是外加了一些规则化.dropout层(随着深度学习出现的层). 基本思路就是喂入一堆数 ...