title: JMockit原理剖析 tags:
- JMockit
- ASM
- instrumentation
- java
- test categories: 测试 date: 2017-09-22 14:40:05
上篇文章描述了Jmockit的最基本的也是最方便也是最神奇的mock神技单元测试JMockit使用
本篇大概就其原理好好说道。
背景知识
Instrumentation知识算是java5之后一个激动人心的功能。
对于部分年纪较大的开发者大概会见过在程序的jvm参数配置如下
-javaagent:D:\resin-pro-3.1.12\lib\aspectjweaver-1.7.0.jar
-javaagent:D:\resin-pro-3.1.12\lib\spring-instrument-3.1.2.RELEASE.jar
复制代码
多年前笔者也曾经接触过在jvm启动参数中配置javaagent 关于aspectjweaver为何需要配置javaagent也是一直心有疑虑
到了javase6之后java又增加了新的动态代理(这也是当前java界比较流行的自动探针apm技术的原理,不需要修改代码完成监控)
从java5的技术称之为premain 而java6的新技术称之为agentmain
java中main函数想必所有的开发人员都相当熟悉,关于premain和agentmain也顾名思义可以理解
分析
首先查看jmockit的manifest文件
Manifest-Version: 1.0
Premain-Class: mockit.internal.startup.Startup
Archiver-Version: Plexus Archiver
Built-By: PC11
Agent-Class: mockit.internal.startup.Startup
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.2.1
Build-Jdk: 1.8.0_20
复制代码
从代码中可以分析其支持两种Instrumentation
指定启动class为mockit.internal.startup.Startup
代码
基本上我们都是通过直接创建MockUp的子类来实现相关的Mock操作的。
因此先分析一下
protected MockUp()
{
validateMockingAllowed();
MockUp<?> previousMockUp = findPreviouslyMockedClassIfMockUpAlreadyApplied();
if (previousMockUp != null) {
mockedType = previousMockUp.mockedType;
mockedClass = previousMockUp.mockedClass;
return;
}
mockedType = validateTypeToMock();
if (mockedType instanceof Class<?>) {
@SuppressWarnings("unchecked") Class<T> classToMock = (Class<T>) mockedType;
mockedClass = redefineClassOrImplementInterface(classToMock);
}
else if (mockedType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) mockedType;
@SuppressWarnings("unchecked") Class<T> classToMock = (Class<T>) parameterizedType.getRawType();
mockedClass = redefineClassOrImplementInterface(classToMock);
}
else {
Type[] typesToMock = ((TypeVariable<?>) mockedType).getBounds();
if (typesToMock.length > 1) {
mockedClass = new MockedImplementationClass<T>(this).createImplementation(typesToMock);
}
else {
mockedClass = new CaptureOfMockedUpImplementations(this, typesToMock[0]).apply();
}
}
}
复制代码
可以看到对应Mock类型支持Class ParameterizedType(泛型类比如List)
我们从最基本的Class开始分析
private Class<T> redefineClassOrImplementInterface(@NotNull Class<T> classToMock)
{
if (classToMock.isInterface()) {
return createInstanceOfMockedImplementationClass(classToMock, mockedType);
}
Class<T> realClass = classToMock;
if (isAbstract(classToMock.getModifiers()) && classToMock.getClassLoader() != null) {
classToMock = generateConcreteSubclass(classToMock);
}
classesToRestore = redefineMethods(realClass, classToMock, mockedType);
return classToMock;
}
复制代码
很明显其区分了interface和普通类型(对于class还处理了是否是抽象类)
对于List来说实质上是一个interface 我们继续向下看
public Class<T> createImplementation(@NotNull Class<T> interfaceToBeMocked, @Nullable Type typeToMock)
{
createImplementation(interfaceToBeMocked);
byte[] generatedBytecode = implementationClass == null ? null : implementationClass.getGeneratedBytecode();
MockClassSetup mockClassSetup = new MockClassSetup(generatedClass, typeToMock, mockUpInstance, generatedBytecode);
mockClassSetup.redefineMethodsInGeneratedClass();
return generatedClass;
}
Class<T> createImplementation(@NotNull Class<T> interfaceToBeMocked)
{
if (isPublic(interfaceToBeMocked.getModifiers())) {
generateImplementationForPublicInterface(interfaceToBeMocked);
}
else {
//noinspection unchecked
generatedClass = (Class<T>) Proxy.getProxyClass(interfaceToBeMocked.getClassLoader(), interfaceToBeMocked);
}
return generatedClass;
}
private void generateImplementationForPublicInterface(@NotNull Class<T> interfaceToBeMocked)
{
implementationClass = new ImplementationClass<T>(interfaceToBeMocked) {
@NotNull @Override
protected ClassVisitor createMethodBodyGenerator(@NotNull ClassReader typeReader)
{
return new InterfaceImplementationGenerator(typeReader, generatedClassName);
}
};
generatedClass = implementationClass.generateClass();
}
复制代码
从上述代码可以看出基本上使用了InterfaceImplementationGenerator作为接口实现类的生成。
package mockit.internal.mockups;
import mockit.external.asm.*;
import mockit.internal.classGeneration.*;
import static mockit.external.asm.Opcodes.*;
import org.jetbrains.annotations.*;
public final class InterfaceImplementationGenerator extends BaseImplementationGenerator
{
public InterfaceImplementationGenerator(@NotNull ClassReader classReader, @NotNull String implementationClassName)
{
super(classReader, implementationClassName);
}
@Override
protected void generateMethodBody(
int access, @NotNull String name, @NotNull String desc, @Nullable String signature, @Nullable String[] exceptions)
{
mw = cw.visitMethod(ACC_PUBLIC, name, desc, signature, exceptions);
generateEmptyImplementation(desc);
}
}
复制代码
关注到ClassWriter类的存在(asm)但是并没有引用asm的jar啊
从包的结构来看 应该是jmockito在打包的时候将asm的源码打包进去到了external(此处顺带吐槽一下百世的XingNg,将CXF打包进去)
关于asm各位有兴趣可以到网上自己学习相关(要求一些字节码和操作数相关知识,api较为底层)
查看上述generateEmptyImplementation 方法
@Override
protected void generateMethodBody(
int access, @NotNull String name, @NotNull String desc, @Nullable String signature, @Nullable String[] exceptions)
{
mw = cw.visitMethod(ACC_PUBLIC, name, desc, signature, exceptions);
generateEmptyImplementation(desc);
}
protected final void generateEmptyImplementation(@NotNull String desc)
{
Type returnType = Type.getReturnType(desc);
pushDefaultValueForType(returnType);
mw.visitInsn(returnType.getOpcode(IRETURN));
mw.visitMaxs(1, 0);
}
private void pushDefaultValueForType(@NotNull Type type)
{
switch (type.getSort()) {
case Type.VOID: break;
case Type.BOOLEAN:
case Type.CHAR:
case Type.BYTE:
case Type.SHORT:
case Type.INT: mw.visitInsn(ICONST_0); break;
case Type.LONG: mw.visitInsn(LCONST_0); break;
case Type.FLOAT: mw.visitInsn(FCONST_0); break;
case Type.DOUBLE: mw.visitInsn(DCONST_0); break;
case Type.ARRAY: generateCreationOfEmptyArray(type); break;
default: mw.visitInsn(ACONST_NULL);
}
}
复制代码
这边涉及到一些asm的底层api asm基本通过访问者模式来实现大量的通过底层常量池来完成 比如boolean char byte short int均返回了ICONST_0对象直接返回了ACONST_NULL 当然还需要计算栈空间大小visitMax
那么具体实现是如何被替换进了接口实现呢?
@NotNull
public Class<T> createImplementation(@NotNull Class<T> interfaceToBeMocked, @Nullable Type typeToMock)
{
createImplementation(interfaceToBeMocked);
byte[] generatedBytecode = implementationClass == null ? null : implementationClass.getGeneratedBytecode();
MockClassSetup mockClassSetup = new MockClassSetup(generatedClass, typeToMock, mockUpInstance, generatedBytecode);
mockClassSetup.redefineMethodsInGeneratedClass();
return generatedClass;
}
复制代码
很明显此处mockUpInstance是从上文中通过MockUp的子类的实例
通过MockClassSetup完成了类的重定义
public void redefineMethodsInGeneratedClass()
{
byte[] modifiedClassFile = modifyRealClass(realClass);
validateThatAllMockMethodsWereApplied();
if (modifiedClassFile != null) {
applyClassModifications(realClass, modifiedClassFile);
}
}
复制代码
我们来粗略查看一下MockClassSetup的实现
private MockClassSetup(
@NotNull Class<?> realClass, @NotNull Class<?> classToMock, @Nullable Type mockedType, @NotNull MockUp<?> mockUp,
@Nullable byte[] realClassCode)
{
this.realClass = classToMock;
mockMethods = new MockMethods(realClass, mockedType);
this.mockUp = mockUp;
forStartupMock = Startup.initializing;
rcReader = realClassCode == null ? null : new ClassReader(realClassCode);
Class<?> mockUpClass = mockUp.getClass();
new MockMethodCollector(mockMethods).collectMockMethods(mockUpClass);
mockMethods.registerMockStates(mockUp, forStartupMock);
if (forStartupMock) {
TestRun.getMockClasses().addMock(mockMethods.getMockClassInternalName(), mockUp);
}
else {
TestRun.getMockClasses().addMock(mockUp);
}
}
复制代码
最重要的细节出现了mockMethods 这个应当是我们目前最重要的关键词。当对应的method呗Mock注解修饰会直接覆盖既有的实现
MockMethods实现如下
MockMethods(@NotNull Class<?> realClass, @Nullable Type mockedType)
{
this.realClass = realClass;
if (mockedType == null || realClass == mockedType) {
mockedTypeIsAClass = true;
}
else {
Class<?> mockedClass = Utilities.getClassType(mockedType);
mockedTypeIsAClass = !mockedClass.isInterface();
}
reentrantRealClass = mockedTypeIsAClass && MockingBridge.instanceOfClassThatParticipatesInClassLoading(realClass);
methods = new ArrayList<MockMethod>();
typeParametersToTypeArguments = new GenericTypeReflection(realClass, mockedType);
mockClassInternalName = "";
}
复制代码
将对应的方法放入methods属性中
那么找到对应放入methods的地方
return new MethodVisitor()
{
@Override
@Nullable public AnnotationVisitor visitAnnotation(String desc, boolean visible)
{
if ("Lmockit/Mock;".equals(desc)) {
MockMethods.MockMethod mockMethod =
mockMethods.addMethod(collectingFromSuperClass, access, methodName, methodDesc);
if (mockMethod != null) {
return new MockAnnotationVisitor(mockMethod);
}
}
return null;
}
复制代码
必须是mockit.Mock注解修饰的方法会被放入到methods list中。那么在下文中使用modifyRealClass将会替换对应的方法
@Nullable
private byte[] modifyRealClass(@NotNull Class<?> classToModify)
{
if (rcReader == null) {
if (!HOTSPOT_VM && classToModify == System.class) {
return null;
}
rcReader = createClassReaderForRealClass(classToModify);
}
MockupsModifier modifier = new MockupsModifier(rcReader, classToModify, mockUp, mockMethods);
rcReader.accept(modifier, SKIP_FRAMES);
return modifier.wasModified() ? modifier.toByteArray() : null;
}
复制代码
MockupsModifier可以望文生义,确实在此处重新覆盖了相关实现。这样获得新的接口的实例中就会存有在MockUp中同签名(不包括static)的实现了。
如上是一个简单的关于接口利用asm实现mock过程的分析
同样的道理当使用具体实现类(非接口)此时由于对应的bytecode已经被修改 后面通过任意方式new或者其他方式都可以完成字节码的替换(也就完成了aop的功能)
目前主流的无打桩apm基本通过类似原理来实现 比如 透视宝