java动态代理——字段和方法字节码的基础结构及Proxy源码分析三
前文地址:https://www.cnblogs.com/tera/p/13280547.html
本系列文章主要是博主在学习spring aop的过程中了解到其使用了java动态代理,本着究根问底的态度,于是对java动态代理的本质原理做了一些研究,于是便有了这个系列的文章
接上文,我们对class字节的结构有了一个整体的了解,并对Proxy的代码做了相应的解析,本文将继续详细看看字段和方法的结构
我们还是回到方法的入口
ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
final byte[] var4 = var3.generateClassFile();
进入generateClassFile()方法
第一部分,Object方法的预处理
this.addProxyMethod(hashCodeMethod, Object.class);
this.addProxyMethod(equalsMethod, Object.class);
this.addProxyMethod(toStringMethod, Object.class);
首先无论是什么类,都是继承自Object的,因此Object中的方法是一定需要的
注意,这里addProxyMethod并非直接写字节码了,而是做了一些预处理
我们先看下3个方法中的第一个参数是个啥
在静态构造函数中,可以看到的确就是Object的3个方法
static {
try {
hashCodeMethod = Object.class.getMethod("hashCode");
equalsMethod = Object.class.getMethod("equals", Object.class);
toStringMethod = Object.class.getMethod("toString");
} catch (NoSuchMethodException var1) {
throw new NoSuchMethodError(var1.getMessage());
}
}
我们进入addProxyMethod方法,这里对变量名做了一个可读性处理
String methodName = method.getName();
Class[] paramTypes = method.getParameterTypes();
Class returnType = method.getReturnType();
Class[] exceptionTypes = method.getExceptionTypes();
String cacheKey = methodName + getParameterDescriptors(paramTypes);
Object cache = (List)this.proxyMethods.get(cacheKey);
...
((List) cache).add(new ProxyGenerator.ProxyMethod(methodName, paramTypes, returnType, exceptionTypes, targetClass));
概括而言,就是根据方法的各个要素生成一个ProxyMethod对象,然后将其加入一个缓存List中
接着我们进入ProxyMethod的构造函数查看
private ProxyMethod(String var2, Class<?>[] var3, Class<?> var4, Class<?>[] var5, Class<?> var6) {
this.methodName = var2;
this.parameterTypes = var3;
this.returnType = var4;
this.exceptionTypes = var5;
this.fromClass = var6;
this.methodFieldName = "m" + ProxyGenerator.this.proxyMethodCount++;
}
值得注意的是,在ProxyMethod的构造函数中有2个字段,在后面会有用到
一个是methodName,表示方法名
另外一个是以m+递增数字的methodFieldName,表示该方法在最终生成的类中的Method类型的字段的名称
第二部分,接口方法的预处理
Class[] interfaces = this.interfaces;
int interfaceLength = interfaces.length; int i;
Class clazz;
for(i = 0; i < interfaceLength; ++i) {
clazz = interfaces[i];
Method[] methods = clazz.getMethods();
int methodLength = methods.length; for(int j = 0; j < methodLength; ++j) {
Method m = methods[j];
this.addProxyMethod(m, clazz);
}
}
既然生成的类实现了传入的接口,因此循环接口,将接口的方法要素添加到proxyMethods中,和之前处理Object的方法一样
第三部分,字段和方法的字节码写入
Iterator iterator;
try {
this.methods.add(this.generateConstructor());
iterator = this.proxyMethods.values().iterator();
while(iterator.hasNext()) {
list = (List) iterator.next();
listIterator = list.iterator(); while(listIterator.hasNext()) {
ProxyGenerator.ProxyMethod proxyMethod = (ProxyGenerator.ProxyMethod) listIterator.next();
this.fields.add(new ProxyGenerator.FieldInfo(proxyMethod.methodFieldName, "Ljava/lang/reflect/Method;", 10));
this.methods.add(proxyMethod.generateMethod());
}
} this.methods.add(this.generateStaticInitializer());
} catch (IOException var10) {
throw new InternalError("unexpected I/O Exception", var10);
}
这里的第一行,正是写入构造器的字节码,这一部分因为涉及到jvm的执行指令,我们放到下篇文章再详细看,所以这里先跳过
this.methods.add(this.generateConstructor());
直接看后面的while循环,就是遍历之前我们添加的Object和接口定义的方法,然后生成相应的字段字节码和方法字节码
while(listIterator.hasNext()) {
ProxyGenerator.ProxyMethod proxyMethod = (ProxyGenerator.ProxyMethod) listIterator.next();
this.fields.add(new ProxyGenerator.FieldInfo(proxyMethod.methodFieldName, "Ljava/lang/reflect/Method;", 10));
this.methods.add(proxyMethod.generateMethod());
}
下面先详细看看字段字节码的细节
第四部分,字段字节码
this.fields.add(new ProxyGenerator.FieldInfo(proxyMethod.methodFieldName, "Ljava/lang/reflect/Method;", 10));
FieldInfo构造函数中
第一个参数proxyMethod.methodFieldName是我们在之前提到的m+递增数字生成的methodFieldName
第二个参数是类型描述
第三个参数是accessFlag,10表示private static (Modifier.PRIVATE | Modifier.STATIC)
进入构造函数看一下
public FieldInfo(String var2, String var3, int var4) {
this.name = var2;
this.descriptor = var3;
this.accessFlags = var4;
ProxyGenerator.this.cp.getUtf8(var2);
ProxyGenerator.this.cp.getUtf8(var3);
}
回想上一篇文章中的field_info类型(忽略attributes)
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
}
this.name、this.descriptor、this.accessFlags正好和field_info中的结构一一对应
同时,由于name_index和descriptor_index都是常量池中的一个索引,因此需要将其写入常量池
这里的cp就是指Constant pool,把methodFieldName和descriptor写入到静态池
ProxyGenerator.this.cp.getUtf8(var2);
ProxyGenerator.this.cp.getUtf8(var3);
之后我们可以直接看,FieldInfo中的write方法,这就是最后写入的字节的方法
public void write(DataOutputStream var1) throws IOException {
var1.writeShort(this.accessFlags);
var1.writeShort(ProxyGenerator.this.cp.getUtf8(this.name));
var1.writeShort(ProxyGenerator.this.cp.getUtf8(this.descriptor));
var1.writeShort(0);
}
对照之前的field_info
第一个写入access_flags
接着写入name_index和descriptor_index,值都是索引
最后因为attribute数量是0,因此直接写0
此时一个完整的字段结构就写入完毕了
接着我们回头查看ProxyGenerator.this.cp.getUtf8方法,看看索引是如何确定的
public short getUtf8(String var1) {
if (var1 == null) {
throw new NullPointerException();
} else {
return this.getValue(var1);
}
}
接续查看getValue方法
private short getValue(Object var1) {
Short var2 = (Short)this.map.get(var1);
if (var2 != null) {
return var2;
} else if (this.readOnly) {
throw new InternalError("late constant pool addition: " + var1);
} else {
short var3 = this.addEntry(new ProxyGenerator.ConstantPool.ValueEntry(var1));
this.map.put(var1, new Short(var3));
return var3;
}
}
这里用map做了一个缓存,key就是需要写入的字段,value就是索引值,如果命中了map,则直接返回value
如果没有命中缓存,则需要addEntry
查看addEntry方法
private short addEntry(ProxyGenerator.ConstantPool.Entry var1) {
this.pool.add(var1);
if (this.pool.size() >= 65535) {
throw new IllegalArgumentException("constant pool size limit exceeded");
} else {
return (short)this.pool.size();
}
}
即将生成的entry添加入pool,并返回当前pool的大小,也就是该常量在池中的索引
回想一下cp的结构,其中cp数量是count+1,cp数组有效索引是从1开始的,因此这里直接返回pool的size,而不是size-1
因此
ProxyGenerator.this.cp.getUtf8()方法做了2件事情
1.将值写入常量池
2.返回该值在常量池中的索引
到这里,字段的相关内容就结束了,接下去我们查看方法的字节码
第五部分,方法字节码
先看之前while循环中的代码
this.methods.add(proxyMethod.generateMethod());
查看generateMethod方法
因为方法的结构体其实包含两个大部分,第一部分是和field_info一样的基础属性,第二部分是方法的执行体,所以后面会单独有文章介绍方法的执行体是怎么写入的,这里我们先关注方法的基本结构
String var1 = ProxyGenerator.getMethodDescriptor(this.parameterTypes, this.returnType);
ProxyGenerator.MethodInfo var2 = ProxyGenerator.this.new MethodInfo(this.methodName, var1, 17);
这里第一行是获取方法的描述,类似于 ()V 描述方法的参数和返回参数,这里()V表示获取0个参数,返回为void的方法
第二行就生成一个MethodInfo对象,查看其构造函数
public MethodInfo(String var2, String var3, int var4) {
this.name = var2;
this.descriptor = var3;
this.accessFlags = var4;
ProxyGenerator.this.cp.getUtf8(var2);
ProxyGenerator.this.cp.getUtf8(var3);
ProxyGenerator.this.cp.getUtf8("Code");
ProxyGenerator.this.cp.getUtf8("Exceptions");
}
同样回顾第二篇中的method_info
method_info {
u2 access_flags;//access_flag
u2 name_index;//常量池中的一个有效索引,必须是Utf8类型(表示方法或字段的名字)
u2 descriptor_index;//常量池中的一个有效索引,必须是Utf8类型(表示方法的描述)
u2 attributes_count;//属性数量
attribute_info attributes[attributes_count];//属性的具体内容
}
和field_info不同,除了基础的access_flags、name_index、descriptor_index外,MethodInfo的构造函数还写入了2个额外的常量池对象:Code和Exceptions,表示2种attributes
Code表示执行代码
Exceptions表示方法会抛出的异常
同样,我们接着就查看MethodInfo中的write方法
写入access_flags、name_index、descriptor_index
var1.writeShort(this.accessFlags);
var1.writeShort(ProxyGenerator.this.cp.getUtf8(this.name));
var1.writeShort(ProxyGenerator.this.cp.getUtf8(this.descriptor));
写入属性的数量
var1.writeShort(2);
此时我们就需要看下attributes的基础结构了
attribute_info {
u2 attribute_name_index;//名字在常量池的索引
u4 attribute_length;//attribute的字节长度
u1 info[attribute_length];//attribute的实际数据
}
这里我们就先了解2种具体的attribute,一个是Code,一个是Exception,正是之前在构造函数中看到的
Code的结构
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
此时我们对应着代码来看
首先写入attribute_name_index
var1.writeShort(ProxyGenerator.this.cp.getUtf8("Code"));
写入数据长度attribute_length,这里的12和8会在本文后面解释
var1.writeInt(12 + this.code.size() + 8 * this.exceptionTable.size());
写入栈深max_stack和max_locals本地变量数量,这2个值在下一篇文章的generateMethod()方法详细介绍中涉及到,这里就先不展开了
var1.writeShort(this.maxStack);
var1.writeShort(this.maxLocals);
写入方法执行体字节的长度code_length和方法执行体具体字节code[code_length],这2部分也会在generateMethod()方法详细介绍中涉及到,这里就先不展开了
var1.writeInt(this.code.size());
this.code.writeTo(var1);
此时我们看到写入max_stack、max_locals、code_length时,字段的类型分别是short、short、integer,加起共8个字节
写入方法会抛出的异常数量exception_table_length
var1.writeShort(this.exceptionTable.size());
这个时候exception_table_length是一个short类型,加上之前的8个字节,一共是10个字节
写入异常的具体结构
Iterator var2 = this.exceptionTable.iterator();
while(var2.hasNext()) {
ProxyGenerator.ExceptionTableEntry var3 = (ProxyGenerator.ExceptionTableEntry)var2.next();
var1.writeShort(var3.startPc);
var1.writeShort(var3.endPc);
var1.writeShort(var3.handlerPc);
var1.writeShort(var3.catchType);
}
每一个异常都有4个字段,start_pc、end_pc、handler_pc、catch_type,都是short类型,因此一个Exception就会有8个字节,这个8正对应了上面attribute_length中的8
最后写入attributes自身的attributes_count,因为没有,所以直接写0
var1.writeShort(0);
这个数量是一个short类型,加上之前累积的10个字节,一共12个字节,对应了attribute_length中的12
接下去看Exception
Exception结构
Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}
这个结构相对就简单了很多,下面对应代码来看
先写入常量池的索引attribute_name_index
var1.writeShort(ProxyGenerator.this.cp.getUtf8("Exceptions"));
写入attribute长度attribute_length,这里的2个2也在后面解释,不过我想大家自己也能想到分别代表什么了吧
var1.writeInt(2 + 2 * this.declaredExceptions.length);
写入异常数量number_of_exceptions,类型是short,对应了第一个2
var1.writeShort(this.declaredExceptions.length);
写入具体的异常在常量池中的索引,每一个数据都是一个short,对应了第二个2
var1.writeShort(this.declaredExceptions.length);
short[] var6 = this.declaredExceptions;
int var7 = var6.length; for(int var4 = 0; var4 < var7; ++var4) {
short var5 = var6[var4];
var1.writeShort(var5);
}
以上,字段和方法的写入就基本解析就完成了
之后将探究generateMethod()方法最复杂的执行体内容
java动态代理——字段和方法字节码的基础结构及Proxy源码分析三的更多相关文章
- java动态代理——代理方法的假设和验证及Proxy源码分析五
前文地址 https://www.cnblogs.com/tera/p/13419025.html 本系列文章主要是博主在学习spring aop的过程中了解到其使用了java动态代理,本着究根问底的 ...
- Java动态代理的实现方法
AOP的拦截功能是由java中的动态代理来实现的.说白了,就是在目标类的基础上增加切面逻辑,生成增强的目标类(该切面逻辑或者在目标类函数执行之前,或者目标类函数执行之后,或者在目标类函数抛出异常时候执 ...
- 大厂高级工程师面试必问系列:Java动态代理机制和实现原理详解
代理模式 Java动态代理运用了设计模式中常用的代理模式 代理模式: 目的就是为其他对象提供一个代理用来控制对某个真实对象的访问 代理类的作用: 为委托类预处理消息 过滤消息并转发消息 进行消息被委托 ...
- java动态代理——jvm指令集基本概念和方法字节码结构的进一步探究及proxy源码分析四
前文地址 https://www.cnblogs.com/tera/p/13336627.html 本系列文章主要是博主在学习spring aop的过程中了解到其使用了java动态代理,本着究根问底的 ...
- java动态代理基本原理及proxy源码分析一
本系列文章主要是博主在学习spring aop的过程中了解到其使用了java动态代理,本着究根问底的态度,于是对java动态代理的本质原理做了一些研究,于是便有了这个系列的文章 为了尽快进入正题,这里 ...
- java基础(十八)----- java动态代理原理源码解析
关于Java中的动态代理,我们首先需要了解的是一种常用的设计模式--代理模式,而对于代理,根据创建代理类的时间点,又可以分为静态代理和动态代理. 静态代理 1.静态代理 静态代理:由程序员创建或特定工 ...
- java 动态代理深度学习(Proxy,InvocationHandler),含$Proxy0源码
java 动态代理深度学习, 一.相关类及其方法: java.lang.reflect.Proxy,Proxy 提供用于创建动态代理类和实例的静态方法.newProxyInstance()返回一个指定 ...
- JAVA设计模式-动态代理(Proxy)源码分析
在文章:JAVA设计模式-动态代理(Proxy)示例及说明中,为动态代理设计模式举了一个小小的例子,那么这篇文章就来分析一下源码的实现. 一,Proxy.newProxyInstance方法 @Cal ...
- 初看Mybatis 源码 (二) Java动态代理类
先抛出一个问题,用过Mybatis的都知道,我们只需要定义一个Dao的接口,在里面写上一些CRUD相关操作,然后配置一下sql映射文件,就可以达到调用接口中的方法,然后执行sql语句的效果,为什么呢? ...
随机推荐
- 初至cnblogs —— 博客搬迁
感觉写博客是一种总结.分享知识的有效方式,于是打算坚持通过博客这一载体来提升自己. 最初通过 Hexo + GitHub Page 来搭建个人博客,但是通过这种方式搭建的博客基本没有访问量.个人感觉没 ...
- 使用Spring Cache集成Redis
SpringBoot 是为了简化 Spring 应用的创建.运行.调试.部署等一系列问题而诞生的产物,自动装配的特性让我们可以更好的关注业务本身而不是外部的XML配置,我们只需遵循规范,引入相关的依赖 ...
- Layui的分页模块在网站中的应用
制作网站的时候,有时候我们常常会被一些要求复杂的分页给困住,最后要么就是写一个简单的分页,要么就做成瀑布流的形式. 有了Layui之后,我认为开发人员多了一个选择,那就是尝试用Layui内置的分页模块 ...
- 轻松让HTML5可以显示桌面通知Notification非常实用
使用Notification的流程 1.检查浏览器是否支持Notification2.检查浏览器的通知权限3.如果权限不够则申请获取权限4.创建消息通知5.展示消息通知 Notification AP ...
- UiAutomator源码学习(1)-- UiDevice
UiDevice提供对设备状态信息的访问. 也可以使用此类来模拟设备上的用户操作,例如按键盘或按Home和Menu按钮.UiDevice类的完整源码 UiDevice.java 废话不多说,我们首先根 ...
- 「疫期集训day0」启程
看了看几乎所有学长都是写的博客,所以写的博客 由于是第一回集训,考得都是老题(虽然有些还不会) 感受1:我调试好蒻呃,调试巨蒻,T1lis模板5分切,结果T2T3T4调了将近了两个小时,先是T2路径输 ...
- Jmeter系列(40)- 详解 Jmeter CLI 模式
如果你想从头学习Jmeter,可以看看这个系列的文章哦 https://www.cnblogs.com/poloyy/category/1746599.html 什么是 CLI 模式 CLI = Co ...
- python 并发专题(十一):基础部分补充(三)线程
1. 背景 理论上来说:单个进程的多线程可以利用多核. 但是,开发Cpython解释器的程序员,给进入解释器的线程加了锁. 2. 加锁的原因: 当时都是单核时代,而且cpu价格非常贵. 如果不加全局解 ...
- 你应该知道的ip地址相关知识
IP地址是一个网卡在网络世界里的通讯地址,相当于我们家里的门牌号码.这样类比的话,很显然ip地址是唯一的.在windows系统中,我们可以使用 ipconfig 命令查看本机的ip地址相关信息: 图中 ...
- 将python3打包成为exe可执行文件(pyinstaller)
我们工作中可能会遇到,客户需要一个爬虫或者其他什么功能的python脚本. 这个时候,如果我们直接把我们的python脚本发给客户,会有两个问题: 1.客户的电脑或者服务器可能并没有安装python环 ...