从Mockito源码到字节码增强技术

什么是Mockito

Mockito 是一种 Java Mock 框架,主要是用来做 Mock 测试,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等。

但是,Mockito具体是怎么样创建生成类所对应的mock对象呢,本着能动手就别吵吵的原则,下面就来动手深入源码来进行查看Mockito的实现机制。

Mockito源码阅读

首先我们创建一个Demo类

public class Demo {
    public int add(int a, int b){
        return a+b;
    }
}

然后在单元测试中使用Mockito框架对Demo类进行mock。

    @Test
    void add() {
        Demo mock = Mockito.mock(Demo.class);
    }

接下来打断点进行调试观察。

//Mockito.class
public static <T> T mock(Class<T> classToMock, MockSettings mockSettings) {
    return MOCKITO_CORE.mock(classToMock, mockSettings);
}
我们直接跟踪Mockito.mock方法,到MockitoCore的mock方法
//MockitoCore.class
public <T> T mock(Class<T> typeToMock, MockSettings settings) {
    if (!MockSettingsImpl.class.isInstance(settings)) {
        throw new IllegalArgumentException("Unexpected implementation of '" + settings.getClass().getCanonicalName() + "'\nAt the moment, you cannot provide your own implementations of that class.");
    } else {
        MockSettingsImpl impl = (MockSettingsImpl)MockSettingsImpl.class.cast(settings);
        MockCreationSettings<T> creationSettings = impl.build(typeToMock);
        // 创建mock对象
        T mock = MockUtil.createMock(creationSettings);
        ThreadSafeMockingProgress.mockingProgress().mockingStarted(mock, creationSettings);
        return mock;
    }
}

下面到MockUtil的createMock方法

//MockUtil.class
public static <T> T createMock(MockCreationSettings<T> settings) {
    创建一个MockHandler
    MockHandler mockHandler = MockHandlerFactory.createMockHandler(settings);
    //创建mock对象,这里是调用SubclassByteBuddyMockMaker的createMock
    T mock = mockMaker.createMock(settings, mockHandler);
    Object spiedInstance = settings.getSpiedInstance();
    if (spiedInstance != null) {
        (new LenientCopyTool()).copyToMock(spiedInstance, mock);
    }
    return mock;
}

下面到SubclassByteBuddyMockMaker的createMock方法

//SubclassByteBuddyMockMaker.class
public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
    //生成代理类,调用SubclassBytecodeGenerator的mockClass
    Class<? extends T> mockedProxyType = this.createMockType(settings);
    Instantiator instantiator = Plugins.getInstantiatorProvider().getInstantiator(settings);
    T mockInstance = null;

    try {
        mockInstance = instantiator.newInstance(mockedProxyType);
        MockAccess mockAccess = (MockAccess)mockInstance;
        mockAccess.setMockitoInterceptor(new MockMethodInterceptor(handler, settings));
        return ensureMockIsAssignableToMockedType(settings, mockInstance);
    } catch (ClassCastException var7) {
        throw new MockitoException(StringUtil.join(new Object[]{"ClassCastException occurred while creating the mockito mock :", "  class to mock : " + describeClass(settings.getTypeToMock()), "  created class : " + describeClass(mockedProxyType), "  proxy instance class : " + describeClass(mockInstance), "  instance creation by : " + instantiator.getClass().getSimpleName(), "", "You might experience classloading issues, please ask the mockito mailing-list.", ""}), var7);
    } catch (InstantiationException var8) {
        throw new MockitoException("Unable to create mock instance of type '" + mockedProxyType.getSuperclass().getSimpleName() + "'", var8);
    }
}
public <T> Class<? extends T> createMockType(MockCreationSettings<T> settings) {
    try {
        // 这里通过SubclassBytecodeGenerator来进行创建
        return this.cachingMockBytecodeGenerator.mockClass(MockFeatures.withMockFeatures(settings.getTypeToMock(), settings.getExtraInterfaces(), settings.getSerializableMode(), settings.isStripAnnotations()));
    } catch (Exception var3) {
        throw this.prettifyFailure(settings, var3);
    }
}

走到这里终于看到了希望,Mockito底层是使用Buddy框架进行实现。

// SubclassBytecodeGenerator.clsss
public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
    DynamicType.Builder<T> builder = 
             this.byteBuddy
                 .subclass(features.mockedType)    //生成Demo的子类
                 .name(this.nameFor(features.mockedType)) // 生成类名com.wsy.mockito.Demo$MockitoMock$1642519669
                 .ignoreAlso(isGroovyMethod())
                 .annotateType(features.stripAnnotations ? new Annotation[0] : features.mockedType.getAnnotations())
                 .implement(new ArrayList(features.interfaces))
                 .method(this.matcher)    //MockMethodInterceptor拦截器
                 .intercept(MethodDelegation.to(MockMethodInterceptor.DispatcherDefaultingToRealMethod.class))
                 .transform(ForMethod.withModifiers(new ModifierContributor.ForMethod[]{SynchronizationState.PLAIN}))
                 .attribute((MethodAttributeAppender.Factory)(features.stripAnnotations ? NoOp.INSTANCE : ForInstrumentedMethod.INCLUDING_RECEIVER))
                 .method(ElementMatchers.isHashCode())
                 .intercept(MethodDelegation.to(MockMethodInterceptor.ForHashCode.class))
                 .method(ElementMatchers.isEquals())
                 .intercept(MethodDelegation.to(MockMethodInterceptor.ForEquals.class))
                 .serialVersionUid(42L)
                 .defineField("mockitoInterceptor", MockMethodInterceptor.class, new ModifierContributor.ForField[]{Visibility.PRIVATE})
                 .implement(new Type[]{MockAccess.class})
                 .intercept(FieldAccessor.ofBeanProperty());
    if (features.serializableMode == SerializableMode.ACROSS_CLASSLOADERS) {
        builder = ((DynamicType.Builder)builder).implement(new Type[]{ByteBuddyCrossClassLoaderSerializationSupport.CrossClassLoaderSerializableMock.class}).intercept(MethodDelegation.to(MockMethodInterceptor.ForWriteReplace.class));
    }

    if (this.readReplace != null) {
        builder = ((DynamicType.Builder)builder).defineMethod("readObject", Void.TYPE, new ModifierContributor.ForMethod[]{Visibility.PRIVATE}).withParameters(new Type[]{ObjectInputStream.class}).throwing(new Type[]{ClassNotFoundException.class, IOException.class}).intercept(this.readReplace);
    }

    ClassLoader classLoader = (new MultipleParentClassLoader.Builder())
            .append(new Class[]{features.mockedType})
            .append(features.interfaces)
            .append(new ClassLoader[]{Thread.currentThread().getContextClassLoader()})
            .append(new Class[]{MockAccess.class, MockMethodInterceptor.DispatcherDefaultingToRealMethod.class})
            .append(new Class[]{MockMethodInterceptor.class, MockMethodInterceptor.ForHashCode.class, MockMethodInterceptor.ForEquals.class})
            .build(MockMethodInterceptor.class.getClassLoader());
    if (classLoader != features.mockedType.getClassLoader()) {
        assertVisibility(features.mockedType);
        Iterator var4 = features.interfaces.iterator();

        while(var4.hasNext()) {
            Class<?> iFace = (Class)var4.next();
            assertVisibility(iFace);
        }

        builder = ((DynamicType.Builder)builder).ignoreAlso(ElementMatchers.isPackagePrivate().or(ElementMatchers.returns(ElementMatchers.isPackagePrivate())).or(ElementMatchers.hasParameters(ElementMatchers.whereAny(ElementMatchers.hasType(ElementMatchers.isPackagePrivate())))));
    }

    return ((DynamicType.Builder)builder).make().load(classLoader, this.loader.getStrategy(features.mockedType)).getLoaded();
}

上面就是生成代理类的关键代码。我看可以看到Mockito使用的是ByteBuddy这个框架,它并不需要编译器的帮助,而是直接生成class,然后使用ClassLoader来进行加载,GitHub地址为:https://github.com/raphw/byte-buddy。
这里使用的是ByteBuddy.subclass()方法。这种方式比较好理解,就是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码。
通过调试可以看到ByteBuddy.subclass()方法的入参features.mockedType的值就是我们最初创建的Demo类

这里就是所指定的类名也是"com.wsy.mockito.Demo"

如果我们使用过其他字节码生成框架,如常见的Cglib,ASM或Javassist,就可以大概猜到这里做了什么事情。
这里很重要的一点就是把MockMethodInterceptor作为代理类的拦截器。最后就是生成字节码然后动态加载到JVM中。

字节码增强技术

这里简单的介绍一下。字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。Spring中AOP的底层实现就是基于字节码增强技术。主要有下面常用的几类。

ASM

ASM是修改字节码的最底层方案,可以直接生成、修改、保存二进制class文件,直接使用ASM非常困难,需要在理解汇编等知识的基础上,手撸字节码,不过大部分高级字节码生成工具都会依赖ASM,包括CGLIB、ByteBuddy等。

CGLIB

cglib是一种动态生成字节码的高级库,被Spring大量使用,底层就使用ASM。

ByteBuddy

一种高级字节码生成方案,方法调用性能优秀。属于一个更高层次操作字节码的工具包。
ByteBuddy主要的目标是生成执行时间快的代码,但底层还是采用了ASM。

Javassist

能够在运行时定义、编译新类,并在JVM加载时修改类文件。相比ASM的缺点就是臃肿,优点就是生成新字节码非常方便,直接拼java源码就行了,也就是说,库里面自带了一个java编译器,同时也可以扩展类。javassist 内部结构是利用hashmap来区分class,就不需要整那些ASM结构树啥的了,总体和CGLIB在同一个层次级别上

ByteBuddy框架

Byte Buddy是一个JVM的运行时代码生成器,你可以利用它创建任何类,且不像JDK动态代理那样强制实现一个接口。Byte Buddy还提供了简单的API,便于手工、通过Java Agent,或者在构建期间修改字节码。

字节码文件

Java字节码是众多字节码增强技术的知识基础。Java语言写出的源代码首先需要编译成class文件,即字节码文件,然后被JVM加载并运行,每个class文件具有如下固定的数据格式。

ClassFile {
    u4             magic;           // 魔数,固定为0xCAFEBABE
    u2             minor_version;   // 次版本
    u2             major_version;   // 主版本,常见版本:52对应1.8,51对应1.7,其他依次类推
    u2             constant_pool_count;                     // 常量池个数
    cp_info        constant_pool[constant_pool_count-1];    // 常量池定义
    u2             access_flags;    // 访问标志:ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT等
    u2             this_class;      // 类索引
    u2             super_class;     // 父类索引
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

class文件本质上是一个字节码流,每个字节码所处的位置代表着一定的指令和含义。
如何对class文件中定义的指令和字节码进行解读、增强定义、编排,这是字节码增强技术所要完成的事情。

Demo:

下面是采用ByteBuddy进行字节码增强的一个简单的例子:

Class<?> dynamicType = new ByteBuddy() 
    // 指定父类 
    .subclass(Object.class) 
    // 根据名称来匹配需要拦截的方法 
    .method(ElementMatchers.named("toString")) 
    // 拦截方法调用,返回固定值 
    .intercept(FixedValue.value("Hello World!")) 
    // 产生字节码 
    .make() 
    // 加载类 
    .load(getClass()
    // 获得Class对象 .getLoaded(); 
    .getClassLoader()) 
    
assertThat(dynamicType.newInstance().toString(), is("Hello World!"));

附:

JavaAgent 和字节码增强技术
Mockito的使用及实现原理
mockito简介及源码分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值