日志

java动态代理——jvm指令集基本概念和方法字节码结构的进一步探究及proxy源码分析四

 来源    2020-08-02    0  

前文地址

https://www.cnblogs.com/tera/p/13336627.html

本系列文章主要是博主在学习spring aop的过程中了解到其使用了java动态代理,本着究根问底的态度,于是对java动态代理的本质原理做了一些研究,于是便有了这个系列的文章

上一篇文章详细分析了class字节码结构中的field_info和method_info,以及对应的Proxy的源码。本文将会更详细的分析method_info中的方法执行体部分

因为方法的字节码涉及到了jvm的操作指令,因此我们先做一个基础性的了解

原文地址:https://dzone.com/articles/introduction-to-java-bytecode
jvm指令文档:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
文中开始介绍的堆、栈、方法区等概念这里就不详细描述了,主要看它后面对一些简单方法的字节码的解析
首先我们定义一个简单的类

public class Test {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = a + b;
    }
}

编译生成Test.class

javac Test.java

查看字节码结构

javap -v Test.class

我们关注其中的main方法部分

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: istore_3
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 4
        line 6: 8

其中的Code正是方法的执行体,下面按照顺序图解具体操作

iconst_1:将常量1压入操作栈

istore_1:弹出栈顶的操作数,存入栈的本地变量数组的索引1,也就是变量a

 iconst_2:将常量2压入操作栈

 istore_2:弹出栈顶的操作数,存入栈的本地变量数组的索引2,也就是变量b

 

 iload_1:从本地变量索引1种读取值,并压入操作栈

iload_2:从本地变量索引2种读取值,并压入操作栈

 iadd:弹出栈顶的2个操作数,相加后将结果压入操作栈

 istore_3:弹出栈顶的操作数,存入栈的本地变量数组的索引3,也就是变量c

 return:从方法返回

如果我们在类中定义一个方法

public class Test {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = calc(a, b);
    }
    static int calc(int a, int b) {
        return (int) Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
    }
}

得到的字节码如下,这次我把部分Constant pool也展示在下面

Constant pool:
   #1 = Methodref          #8.#19         // java/lang/Object."<init>":()V
   #2 = Methodref          #7.#20         // Test.calc:(II)I
   #3 = Double             2.0d
   #5 = Methodref          #21.#22        // java/lang/Math.pow:(DD)D
   #6 = Methodref          #21.#23        // java/lang/Math.sqrt:(D)D
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: invokestatic  #2                  // Method calc:(II)I
         9: istore_3
        10: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 4
        line 6: 10

  static int calc(int, int);
    descriptor: (II)I
    flags: ACC_STATIC
    Code:
      stack=6, locals=2, args_size=2
         0: iload_0
         1: i2d
         2: ldc2_w        #3                  // double 2.0d
         5: invokestatic  #5                  // Method java/lang/Math.pow:(DD)D
         8: iload_1
         9: i2d
        10: ldc2_w        #3                  // double 2.0d
        13: invokestatic  #5                  // Method java/lang/Math.pow:(DD)D
        16: dadd
        17: invokestatic  #6                  // Method java/lang/Math.sqrt:(D)D
        20: d2i
        21: ireturn
      LineNumberTable:
        line 8: 0

这里我们主要看一下一些新出现的操作指令
在main方法中,编号6
invokestatic #2:调用静态方法,方法在Constant Pool中索引为2,表示Test.calc方法(这里特别注意,调用的方法目标必须是常量池中的一个有效索引)
在cacl方法中
i2d将int类型的转换成double类型的
ldc2_w:将long型或者double型(思考一下为何是这2种类型放在同一个操作指令中)从静态池中压入栈
dadd:将double相加
d2i:将double类型转换成int类型
ireturn:返回一个int

将上面的jvm指令结合java代码,就可以初步理解每一行java代码究竟是如何被jvm执行的了

接下去我们可以通过Proxy的代码结合实际来看看

方法还是generateClassFile()
在上一篇文章的第三部分字节与方法字节码的写入中,有提到

这里的第一行,正是写入构造器的字节码,这一部分因为涉及到jvm的执行指令,我们放到下篇文章再详细看,所以这里先跳过

this.methods.add(this.generateConstructor());

此时我们就可以详细看下generateConstructor方法究竟干了什么

private ProxyGenerator.MethodInfo generateConstructor() throws IOException {
    ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
    DataOutputStream var2 = new DataOutputStream(var1.code);
    this.code_aload(0, var2);
    this.code_aload(1, var2);
    var2.writeByte(183);
    var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));
    var2.writeByte(177);
    var1.maxStack = 10;
    var1.maxLocals = 2;
    var1.declaredExceptions = new short[0];
    return var1;
}

接下一行一行分析

初始化MethodInfo对象,3个参数分别是,方法名、方法描述、access_flag,1表示public(参见Modifier.java)

因为是构造函数,所以方法名为<init>

方法的描述表示,该方法获取一个java.lang.reflect.InvocationHandler类型的参数,返回值为V(表示void)

方法的access_flag为1,表示public

ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);

写入aload_0和aload_1操作指令

this.code_aload(0, var2);
this.code_aload(1, var2);

写入183号操作指令,查文档得:invokespecial

调用实例方法,特别用来处理父类的构造函数

var2.writeByte(183);

写入需要调用的方法名和方法的参数

注意,这里的方法是通过this.cp.getMethodRef方法得到的,也就是说,这里写入的最终数据,其实是一个符合该方法描述的常量池中的一个有效索引(这部分知识可以参看之前的3篇文章)

var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));

写入177号指令,查文档得:return

返回void

var2.writeByte(177);

和上一篇文章中提到的一样,最后还需要写入栈深和本地变量数量,以及方法会抛出的异常数量,因为构造函数不主动抛出异常,所以异常数量直接为0

var1.maxStack = 10;
var1.maxLocals = 2;
var1.declaredExceptions = new short[0];

到此,一个构造函数的结构就完成了

此时我们总结一下该构造函数的结构

aload_0;
aload_1;
invokespecial  #x  //这里x对应Constant pool中构造函数的编号
return;

验证一下,我们建立一个类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Test extends Proxy {
    protected TestClass(InvocationHandler h) {
        super(h);
    }
}

查看其字节码

protected Test(java.lang.reflect.InvocationHandler);
    descriptor: (Ljava/lang/reflect/InvocationHandler;)V
    flags: ACC_PROTECTED
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #1                  // Method java/lang/reflect/Proxy."<init>":(Ljava/lang/reflect/InvocationHandler;)V
         5: return
      LineNumberTable:
        line 6: 0
        line 7: 5

正和我们之前总结的一模一样

结合之前的一些jvm指令的基本描述,我们就可以对method_info的正题结构有了更深入的了解

本文中我们初步了解了方法执行体Code的结构,jvm指令的基本概念,那么在下一篇文章中,我们将会继续探究Proxy的最核心的部分,代理方法的实际实现

相关文章
JVM插码之四:Java动态代理机制的对比(JDK 和CGLIB,Javassist,ASM)
日志一.class文件简介及加载      Java编译器编译好Java文件之后,产生.class 文件在磁盘中.这种class文件是二进制文件,内容是只有JVM虚拟机能够识别的机器码.JVM虚拟机读取字 ...
1
[Spring学习笔记 4 ] AOP 概念原理以及java动态代理
日志一.Spring IoC容器补充(1) Spring IoC容器,DI(依赖注入): 注入的方式:设值方法注入setter(属性注入)/构造子注入(构造函数传入依赖的对象)/字段注入Field(注解) ...
1
java – 动态代理 – 创建新代理实例时的类加载器参数
问答我想知道在创建动态代理实例时何时调用newProxyInstance方法,ClassLoader参数究竟是什么? public static Object newProxyInstance(Class ...
1
Java动态代理的性能成本
问答许多现代框架(Spring,Hibernate)使用Java动态代理提供了非常好的动态行为,但是与之相关的性能成本是多少? Sun JVM有公共基准测试吗?::几个指针: > Debunking ...
1
java动态代理与常规代理的有用性
问答我需要一些建议,动态代理将被证明比常规代理更有用. 我已经花了很多精力学习如何有效地使用动态代理.在这个问题中,搁置像AspectJ这样的框架可以基本上执行我们用动态代理实现的一切,或者CGLIB可以 ...
java – 动态代理和检查异常
问答如何使我的动态代理抛出检查异常? 我需要一个接口的透明包装器,有时会抛出诸如IOException这样的检查异常.没有第三方AOP可能没有写我自己的代理?用手修改界面的所有20种方法也不是一个选择.: ...
Java动态代理问题
问答1.动态代理实例是否为目标类的子类? java doc说代理实例实现了"接口列表",没有提到子类化,但是通过调试,我看到代理实例确实继承了目标类的属性."接口列表&quo ...
1
Java动态代理 – 如何引用具体类
问答我有一个与java中的动态代理有关的问题. 假设我有一个名为Foo的接口,方法为execute,类FooImpl实现了Foo. 当我为Foo创建一个代理时,我有类似的东西: Foo f = (Foo) ...
1
没有目标对象的Java动态代理?
问答奇怪的问题-- 如何在没有实际拥有目标对象的情况下使用动态代理时,如何使用Java的调用拦截器? 例如,我想创建一个超级对象,它可以代表在运行时指定的十几个接口,而不一定需要一个实现其中任何接口的对象 ...
2
Java 动态代理机制分析及扩展--转
日志http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/#icomments http://www.ibm.com/developerworks/c ...
1
java基础(十八)----- java动态代理原理源码解析
日志关于Java中的动态代理,我们首先需要了解的是一种常用的设计模式--代理模式,而对于代理,根据创建代理类的时间点,又可以分为静态代理和动态代理. 静态代理 1.静态代理 静态代理:由程序员创建或特定工 ...
1
JAVA设计模式-动态代理(Proxy)源码分析
日志在文章:JAVA设计模式-动态代理(Proxy)示例及说明中,为动态代理设计模式举了一个小小的例子,那么这篇文章就来分析一下源码的实现. 一,Proxy.newProxyInstance方法 @Cal ...
1
Java动态代理实现方法
日志import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflec ...
1
JAVA 动态代理学习记录
日志打算用JAVA实现一个简单的RPC框架,看完RPC参考代码之后,感觉RPC的实现主要用到了两个方面的JAVA知识:网络通信和动态代理.因此,先补补动态代理的知识.---多看看代码中写的注释 参考:Ja ...
1
AOP与JAVA动态代理
日志1.AOP的各种实现 AOP就是面向切面编程,我们可以从以下几个层面来实现AOP 在编译期修改源代码 在运行期字节码加载前修改字节码 在运行期字节码加载后动态创建代理类的字节码 2.AOP各种实现机制 ...
1
深入理解 Java 动态代理机制
日志Java 有两种代理方式,一种是静态代理,另一种是动态代理.对于静态代理,其实就是通过依赖注入,对对象进行封装,不让外部知道实现的细节.很多 API 就是通过这种形式来封装的. 代理模式结构图(图片来 ...
1
java动态代理浅析
日志最近在公司看到了mybatis与spring整合中MapperScannerConfigurer的使用,该类通过反向代理自动生成基于接口的动态代理类. 于是想起了java的动态代理,然后就有了这篇文章 ...
1
Java动态代理的实现机制
日志一.概述 代理是一种设计模式,其目的是为其他对象提供一个代理以控制对某个对象的访问,代理类负责为委托类预处理消息,过滤消息并转发消息以及进行消息被委托类执行后的后续处理.为了保持行为的一致性,代理类和 ...
1
彻底理解JAVA动态代理
日志代理设计模式 定义:为其他对象提供一种代理以控制对这个对象的访问. 代理模式的结构如下图所示. 动态代理使用 java动态代理机制以巧妙的方式实现了代理模式的设计理念. 代理模式示例代码 public ...
Java 动态代理
日志1 What is that?    代理:本来应该自己做的事情,却请了别人来做,被请的人就是代理对象.     动态代理: 在程序运行过程中产生的这个对象               而程序运行过程 ...
1