java 关机小程序 java小程序运行过程_字节码


在上一篇文章(清香白莲:一文带你玩转JMockit))我们介绍了Jmockit的各种强大Mock功能,可以Mock类、对象、方法与静态属性等,以辅佐单元测试。本文将介绍其基本原理,以减少在使用过程中踩到的坑。

JMockit是在加载并执行字节码的过程中做了手脚,对原方法的字节码做了修改与调包,从而达到Mock的目的的,我们先介绍Java程序是如何运行的,然后介绍如何修改字节码,再介绍如何在运行时做的调包。

Java程序如何运行的

我们知道Java程序的执行过程分两步:

  • 先将java代码(*.java文件)用javac命令编译成字节码(*.class文件);
  • 然后通过java命令让java虚拟机加载字节码,对字节码进行解释执行。

大致流程图如下。


java 关机小程序 java小程序运行过程_java 关机小程序_02

java程序执行流程


因为JVM实际运行的是编译后的字节码,所以也有人说java是解释性语言或者脚本语言。

比如下面的Hello World程序


package


编译后的字节码为


java 关机小程序 java小程序运行过程_java_03


JVM规范详细规定了class文件的格式,包括常量池、字段表、方法表和属性表等内容在字节码中的偏移量、长度等信息。如果你愿意,完全是可以从上面这种16进制的字节码中解析出这个类的全部信息,并对其进行修改,并且修改后还要重新计算class 文件的校验码以通过 JVM安全机制校验。确实有一些对class文件格式烂熟于心的程序员,尤其时JVM开发者就这么干过,从而在不改变java代码的前提下,改变其运行时的行为。

好在class文件的格式是固定的,现在已经有不少类库可以直接将class文件即字节码解析为一个结构化的数据,调用者可以在这个结构化的数据上修改class文件的各个部分,并生成修改后的字节码,使得修改字节码的任务轻松了许多。

而ASM就是这样的一种库,其具有体积小、速度快等优点,具有广泛应用,如Groovy、Cobertura、JDBCPersistence与AspectJ等。

ASM

什么是ASM

ASM的官方定义如下

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

说人话就是:ASM是一个通用的 Java 字节码操控和分析框架。 它可以用于修改已有的类也可以直接生成类。ASM 提供了一些常用的字节码转换和分析算法,从中可以构建自定义的复杂转换和源码分析工具。ASM提供了与其他 Java 字节码框架类似的方法,但是更注重性能。因为它被设计和实现成尽可能小和快,所以非常适用于动态系统(当然也可以用于静态的方式,例如在编译器中)。

ASM原理

在ASM 中,有一个 ClassReader类可以对字节码进行解析,得到一个树状的数据结构以表示字节码。ClassReader类有一个accept方法,这个方法接受一个实现了 ClassVisitor接口的类的实例,然后依次调用 ClassVisitor接口的各个visit方法实现对字节码的修改。这个过程其实是采用了Visitor模式,一群ClassVisitor对字节码树进行修改。

各个 ClassVisitor通过职责链 (Chain-of-responsibility) 模式装配在一起,传给accept方法的是职责链顶端的 实现了 ClassVisitor接口的类(ClassAdaptor)的实例。

ClassAdaptor类实现了 ClassVisitor接口所定义的所有函数,其构造器中需要传入一个实现了 ClassVisitor接口的类的实例,作为职责链中的下一个Visitor,每个ClassAdaptor的每个visit方法的默认实现是直接委派给这个实例。

ClassVisitor接口主要有两类visit方法

  • visitMethod:用于修改方法的行为,返回一个实现 MethordVisitor接口的实例;
  • visitField:用于修改属性,返回一个实现 FieldVisitor接口的实例。

ASM用法

假设我们需要在前面的HelloWorlder实例的say方法执行时,先调用下列HelloASMer类的hello方法。


package


首先,从ClassAdapter派生一个子类AddASMHelloWorlderClassAdapter,并覆盖其visitMethod,用于修改HelloWorlder类的say方法的字节码。


import


然后我们定义一个MethodAdapter的子类AddASMHelloWorlderMethodAdapter,复写其visitCode方法,实现对HelloASMer.hello()的调用。ClassReader读到每个方法的首部时调用 visitCode(),如果是say方法就会插入对HelloASMer.hello()的调用。


package


最后定义ClassReader,集成上面定义的ClassAdapter与用于输出字节码的ClassWriter构建职责链,并传入ClassReader的accept方法,生成修改后的字节码。


package


最后我们测试下


package


输出结果如下


'm ASM
Hello World!


那么我们看下ASM在JMockit中是如何使用的。

JMockit中是如何被使用ASM的

我们看一下JMockit里面MockUp类的带类型参数的构造器


protected


不难看出,对于普通类型是通过redefineMethods来Mock方法的,我们f往下翻,看看这个redefineMethods到底做了什么。


private


这一层只是简单创建了个MockClassSetup实例并调用了其redefineMethods方法,下一层也没什么只是调用了redefineMethodsInClassHierarchy方法并做了验证,我们看看redefineMethodsInClassHierarchy方法


private


这一层调用的结果主要是通过modifyRealClass方法获得,我们看看modifyRealClassf方法内部,是不是发现了什么了。


private


rcReader 就是一个ASM中的ClassReader对象,其accept方法接受的实例modifier属于MockupsModifier类型,MockupsModifier继承自BaseClassModifier,而BaseClassModifier是实现了ClassVisitori接口的。

顺便说一句,JMockit项目里面y是找不到asm的jar包的,pom里也找不到相应的依赖,它是直接把asm的源码放到了自己的项目里,这点比较鸡贼。

好了,我们知道了JMockit是通过ASM来修改原始类的字节码的,那么这些修改后的字节码是如何在运行使顶替原始类的字节码的呢?这就要轮到instrumentation上场了。

instrumentation

什么是instrumentation

什么是instrumentation了?其实它是一个接口,其官方定义为:

This class provides services needed to instrument Java programming language code. Instrumentation is the addition of byte-codes to methods for the purpose of gathering data to be utilized by tools. Since the changes are purely additive, these tools do not modify application state or behavior. Examples of such benign tools include monitoring agents, profilers, coverage analyzers, and event loggers.

谷歌翻译结果为:这个类为JVM上运行时的程序提供测量手段。很多工具通过Instrumenation 修改方法字节码 实现收集数据目的。这些通过Instrumentaion搜集数据的工具不会改变程序的状态和行为。这些良好的工具包括 monitoring agents , ,profilers, coverage analyzers, 和 event loggers。

instrumentation使用方式

通过上面的定义我们可以看出, instrumentation只是一个与虚拟机上运行的程序进行交互的接口,通过instrumentation的方法可以获取程序的运行时信息或者修改程序的运行时行为,instrumentation接口有哪些方法可以从其API文档查到。

实际我们需要完成的运行时监控或者修改行为的业务逻辑是在agent类中实现的,而agent类是以Jar 包形式部署的,在Jar包的manifest中需要指定哪个类作为agent类。

有两种方式可以指定agent类,在开发时就需要事先确认好采用那种方式,因为它们获取instrumentation接口实例的方式不一样。

  • premain: JVM启动时通过启动参数指定agent类,这种方式下Instrumentation的接口实例是通过agent类的premain方法传入的。
  • agentmain: JVM启动完成后,另运行一个程序将agent类attach到前面的进程。这种方式下Instrumention实例通过agent类中的的agentmain方法传入。

我们通过两个例子看下这两种方式是如何使用的

premain

premain是在JVM启动时指定agent,在java5的时候就有了,现在用的不多了,更多用后面介绍的agentmain技术,一些年纪较大(混得不好)的开发者可能见过。

还是以前面的HelloWorlder类为例,我们希望在调用其say方法前打印出“I'm instrumentation”。先编写agent类MyAgent,需要有一个premain方法用于接受Instrumentation接口实例。


public


还需要实现ClassFileTransformer接口,用于实现我们需要的打印操作,这个类的实例作为Instrumentation.addTransformer方法的参数被传入。需要注意的是className的路径是以"/"分割的,而不是点。


package


在META-INF 中添加MANIFEST.MF ,在清单中添加agent类,以及依赖包javassist-3.15.0-GA.jar的位置。


Class-Path: javassist-3.15.0-GA.jar
Premain-Class: com.alibaba.taobao.algorithm.MyAgent


将上面代码与文件一起编译并打成jar打包myagent.jar。

编写主类,其实就是创建HelloWorlder实例并调用其say方法,可以复用前面的ASMTest类,并将相关代码编译打包进test.jar。运行下面命令即可实现我们的打印需求。


java -javaagent:myagent.jar -cp test.jar com.alibaba.taobao.algorithm.ASMTest


如果使用idea开发,只需要在运行ASMTest的main方法的VM options配置项里面加上就可以了。


-javaagent:${yourPath}/myagent.jar


agentmain

agentmain是java6新增的功能,基于其动态代理技术实现,有了它就不需要在主陈旭启动时指定agent了,主程序跟agent完全解耦。agent只需要知道主程序的进程id就可以attach上去进行监控或其他操作。

假设现在我们的需求是在JVM运行后查看JVM加载了哪些类,同样我们先要有个agent类,并且它需要有agentmain方法,通过调用Instrumentation接口的getAllLoadedClasses可以获取当前JVM中加载的所有类。


package


为了演示方便再写一个空跑1小时的主程序


package


跟前面类似,修改修改MANIFEST.MF并编译打成jar包attatch-agent-test.jar


Agent-Class: com.alibaba.taobao.algorithm.PostAgent


编写attach程序


public


运行LongTestMain,通过jps 拿到其进程 id,再将该id作为参数运行上面的attach程序即可在控制台上看到打印出的所有类名。

JMockit中是如何使用Instrumentation的

在一文带你玩转JMockit指出在用JMockit辅佐单元测试时需要配置下列maven插件,其实就是在运行测试代码时,添加-javaagent参数指定agent类所在的jar包,显然是用到了premain模式来获取Instrumentation接口实例。


<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-surefire-plugin</artifactId>
   <version>2.4.3</version>
   <configuration>
      <argLine>-javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar</argLine>
      <useSystemClassLoader>true</useSystemClassLoader>
    </configuration>
</plugin>


我们再看看JMockit的MANIFEST文件,可以发现它同时使用了premain和agentmain,且agent类都是mockit.internal.startup.Startup


Manifest-Version: 1.0
Premain-Class: mockit.internal.startup.Startup
Archiver-Version: Plexus Archiver
Built-By: usuario
Agent-Class: mockit.internal.startup.Startup
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.2.5
Build-Jdk: 1.8.0_31


我们来看看Startup 类的内容,果不其然里面有premain与agentmain方法,主要都是调用initialize方法


public


再看看initialize方法


private


很明显,这里调用了Instrumentation 接口的addTransformer方法,实现了运行时对字节码的修改。CachedClassfiles与ExpectationsTransformer均继承自ClassFileTransformer,分别对应基于状态(MockUp)与基于行为(Expectations)的Mock方式对字节码的修改。

结论

总结成一句话就是:JMockit通过ASM来修改原类的编译结果,得到新的字节码,然后在运行时,使用agent类Startup通过Instrumentation 接口对Mock部分的字节码进行调包,这就神不知鬼不觉的达到了Mock的目的。


java 关机小程序 java小程序运行过程_JVM_04

JMockit原理