JMockit原理剖析


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基本通过类似原理来实现 比如 透视宝

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值