当前位置:   article > 正文

springboot + junit + mockito真香, 可 springboot 是创建 @MockBean/@SpyBean 代理对象的呢?

@mockbean

为了搞懂 mockito 底层是如何通过代理对象走到代理的逻辑的, 我可太难了

测试代码

首先我写了一段测试代码

 @MockBean
private UserService userService;

@Test
public void testGetUserById(){
        Long id=1L;
        User mockUser=new User();
        mockUser.setId(id);
        mockUser.setName("Mock User");
        Mockito.when(userService.findUserById(id)).thenReturn(mockUser);

        User result=userService.findUserById(id);
        System.out.println(result);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

通过调试我发现 userService 对象不是我们的对象, 而是一个新的不知名对象

class cn.gd.cz.hong.sqlite.service.UserService$MockitoMock$219418843

发现这个对象有一个 mock 相关的拦截器

class org.mockito.internal.creation.bytebuddy.MockMethodInterceptor

转念一想, jar 包肯定有 new MockMethodInterceptor 的操作

后来又想 既然在spring 容器中运行, 一般不得有个 PostProcessor

问了下ChatGPT

知道是 MockitoPostProcessor

期间问了 ChatGPT4.0 一些问题, 说的云里雾里, 最后还是源码靠谱的说

MockitoContextCustomizerFactory | 帮我们注入 MockitoContextCustomizer

# Spring Test ContextCustomizerFactories
org.springframework.test.context.ContextCustomizerFactory=\
org.springframework.boot.test.context.ImportsContextCustomizerFactory,\
org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizerFactory,\
org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory,\
org.springframework.boot.test.mock.mockito.MockitoContextCustomizerFactory,\
org.springframework.boot.test.web.client.TestRestTemplateContextCustomizerFactory,\
org.springframework.boot.test.web.reactive.server.WebTestClientContextCustomizerFactory

# Test Execution Listeners
org.springframework.test.context.TestExecutionListener=\
org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener,\
org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.test.web.SpringBootTestRandomPortEnvironmentPostProcessor

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

org.springframework.boot.test.mock.mockito.MockitoContextCustomizerFactory

MockitoContextCustomizer | 注入 MockitoPostProcessor

又是谁帮我们把 MockitoPostProcessor 注入到容器中的

class MockitoContextCustomizer implements ContextCustomizer {

    private final Set<Definition> definitions;

    MockitoContextCustomizer(Set<? extends Definition> definitions) {
        this.definitions = new LinkedHashSet<>(definitions);
    }

    @Override
    public void customizeContext(ConfigurableApplicationContext context,
                                 MergedContextConfiguration mergedContextConfiguration) {
        if (context instanceof BeanDefinitionRegistry) {
            MockitoPostProcessor.register((BeanDefinitionRegistry) context, this.definitions);
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj == null || obj.getClass() != getClass()) {
            return false;
        }
        MockitoContextCustomizer other = (MockitoContextCustomizer) obj;
        return this.definitions.equals(other.definitions);
    }

    @Override
    public int hashCode() {
        return this.definitions.hashCode();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

MockitoContextCustomizer 是一个实现了 ContextCustomizer 接口的类,它用于在 Spring Boot
测试环境中定制和修改应用上下文。MockitoContextCustomizer 主要用于注册 MockitoPostProcessor,以支持 @MockBean
@SpyBean 注解。

MockitoContextCustomizer 的主要部分如下:

  1. 构造函数:接收一个 Definition 集合,将其存储在类的实例变量 definitions 中。

  2. customizeContext 方法:这个方法在 Spring Boot
    测试环境初始化时被调用,用于修改或定制应用上下文。在这个方法中,如果 context 是一个 BeanDefinitionRegistry
    实例,那么将调用 MockitoPostProcessor.register 方法,将 definitions 中的定义注册到 Spring 容器中。

  3. equalshashCode 方法:用于比较两个 MockitoContextCustomizer 实例是否相等。相等的条件是它们的 definitions 相等。

总之,MockitoContextCustomizer 的主要作用是在 Spring Boot 测试环境中注册 MockitoPostProcessor
,以便支持使用 @MockBean@SpyBean 注解创建和注入模拟对象。

@Override
public void customizeContext(ConfigurableApplicationContext context,
        MergedContextConfiguration mergedContextConfiguration){
        if(context instanceof BeanDefinitionRegistry){
        MockitoPostProcessor.register((BeanDefinitionRegistry)context,this.definitions);
        }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

-> MockitoPostProcessor.register

将咱们的 MockitoPostProcessor 加到 Beandefinition

public static void register(BeanDefinitionRegistry registry,Class<?extends MockitoPostProcessor> postProcessor,
        Set<Definition> definitions){
        SpyPostProcessor.register(registry);
        BeanDefinition definition=getOrAddBeanDefinition(registry,postProcessor);
        ValueHolder constructorArg=definition.getConstructorArgumentValues().getIndexedArgumentValue(0,Set.class);
        Set<Definition> existing=(Set<Definition>)constructorArg.getValue();
        if(definitions!=null){
        existing.addAll(definitions);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

MockitoPostProcessor | 注册 @MockBean/@SpyBean

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)throws BeansException{
        Assert.isInstanceOf(BeanDefinitionRegistry.class,beanFactory,
        "@MockBean can only be used on bean factories that implement BeanDefinitionRegistry");
        postProcessBeanFactory(beanFactory,(BeanDefinitionRegistry)beanFactory);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
private void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory,BeanDefinitionRegistry registry){
        beanFactory.registerSingleton(MockitoBeans.class.getName(),this.mockitoBeans);
        DefinitionsParser parser=new DefinitionsParser(this.definitions);
        for(Class<?> configurationClass:getConfigurationClasses(beanFactory)){
        parser.parse(configurationClass);
        }
        Set<Definition> definitions=parser.getDefinitions();
        for(Definition definition:definitions){
        Field field=parser.getField(definition);
        register(beanFactory,registry,definition,field);
        }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

org.springframework.boot.test.mock.mockito.MockitoPostProcessor#register

private void register(ConfigurableListableBeanFactory beanFactory,BeanDefinitionRegistry registry,
        Definition definition,Field field){
        if(definition instanceof MockDefinition){
        registerMock(beanFactory,registry,(MockDefinition)definition,field);
        }
        else if(definition instanceof SpyDefinition){
        registerSpy(beanFactory,registry,(SpyDefinition)definition,field);
        }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注册 MockBean过程如下:

org.springframework.boot.test.mock.mockito.MockDefinition.createMock() -> org.springframework.boot.test.mock.mockito.MockitoPostProcessor.registerMock() -> org.springframework.boot.test.mock.mockito.MockitoPostProcessor.register() -> org.mockito.Mockito.mock() -> org.mockito.internal.MockitoCore.mock() -> org.mockito.internal.util.MockUtil.mock() -> org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker.createMock() -> org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker.createMock()

org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker#createMock

@Override
public<T> T createMock(MockCreationSettings<T> settings,MockHandler handler){
        Class<?extends T> mockedProxyType=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 cce){
        throw new MockitoException(
        join(
        "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.",
        ""),
        cce);
        }catch(org.mockito.creation.instance.InstantiationException e){
        throw new MockitoException(
        "Unable to create mock instance of type '"
        +mockedProxyType.getSuperclass().getSimpleName()
        +"'",
        e);
        }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

org.mockito.internal.creation.bytebuddy.TypeCachingBytecodeGenerator#mockClass

@Override
public<T> Class<T> mockClass(final MockFeatures<T> params){
        lock.readLock().lock();
        try{
        ClassLoader classLoader=params.mockedType.getClassLoader();return(Class<T>)
        typeCache.findOrInsert(
        classLoader,
        new MockitoMockKey(
        params.mockedType,
        params.interfaces,
        params.serializableMode,
        params.stripAnnotations),
        new Callable<Class<?>>(){
@Override
public Class<?> call()throws Exception{
        return bytecodeGenerator.mockClass(params);
        }
        },
        BOOTSTRAP_LOCK);
        }catch(IllegalArgumentException exception){
        Throwable cause=exception.getCause();
        if(cause instanceof RuntimeException){
        throw(RuntimeException)cause;
        }else{
        throw exception;
        }
        }finally{
        lock.readLock().unlock();
        }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

private final Implementation dispatcher = to(DispatcherDefaultingToRealMethod.class);

上面这段代码是关键,

/*
 * Copyright (c) 2016 Mockito contributors
 * This program is made available under the terms of the MIT License.
 */
package org.mockito.internal.creation.bytebuddy;

class SubclassBytecodeGenerator implements BytecodeGenerator {

    private static final String CODEGEN_PACKAGE = "org.mockito.codegen.";

    private final SubclassLoader loader;
    private final ModuleHandler handler;
    private final ByteBuddy byteBuddy;
    private final Random random;
    private final Implementation readReplace;
    private final ElementMatcher<? super MethodDescription> matcher;

    private final Implementation dispatcher = to(DispatcherDefaultingToRealMethod.class);
    private final Implementation hashCode = to(MockMethodInterceptor.ForHashCode.class);
    private final Implementation equals = to(MockMethodInterceptor.ForEquals.class);
    private final Implementation writeReplace = to(MockMethodInterceptor.ForWriteReplace.class);

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
new Callable<Class<?>>(){
@Override
public Class<?> call()throws Exception{
        return bytecodeGenerator.mockClass(params);
        }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这段代码利用 bytebuddy 帮我们动态创建字节码

来到这个

org.mockito.internal.creation.bytebuddy.SubclassBytecodeGenerator#mockClass

/**
 * 使用 ByteBuddy 创建 Mock 类,并使用给定的参数配置 Mock 对象。
 *
 * @param features 包含要为 Mock 对象设置的特征的 MockFeatures 对象
 * @return 创建的 Mock 类的 Class 对象
 */
@Override
public<T> Class<?extends T> mockClass(MockFeatures<T> features){

        // 创建类加载器
        ClassLoader classLoader=
        new MultipleParentClassLoader.Builder()
        .appendMostSpecific(getAllTypes(features.mockedType))
        .appendMostSpecific(features.interfaces)
        .appendMostSpecific(currentThread().getContextClassLoader())
        .appendMostSpecific(MockAccess.class)
        .build();

        // 如果不需要创建新的类加载器,并且 Mock 不是基于 JDK 类型,则尝试在用户运行时包中定义 Mock 类,以允许 Mock 包私有类型和方法。
        // 这还需要我们能够通过重写或显式特权来访问 Mock 类的包,目标包被打开以允许 Mockito 访问。
        boolean localMock=
        classLoader==features.mockedType.getClassLoader()
        &&features.serializableMode!=SerializableMode.ACROSS_CLASSLOADERS
        &&!isComingFromJDK(features.mockedType)
        &&(loader.isDisrespectingOpenness()
        ||handler.isOpened(features.mockedType,MockAccess.class));
        String typeName;
        if(localMock
        ||loader instanceof MultipleParentClassLoader
        &&!isComingFromJDK(features.mockedType)){
        typeName=features.mockedType.getName();
        }else{
        typeName=
        InjectionBase.class.getPackage().getName()
        +"."
        +features.mockedType.getSimpleName();
        }
        String name=
        String.format("%s$%s$%d",typeName,"MockitoMock",Math.abs(random.nextInt()));

        // 调整模块图表以处理 Mock 类
        if(localMock){
        handler.adjustModuleGraph(features.mockedType,MockAccess.class,false,true);
        for(Class<?> iFace:features.interfaces){
        handler.adjustModuleGraph(iFace,features.mockedType,true,false);
        handler.adjustModuleGraph(features.mockedType,iFace,false,true);
        }
        }else{
        boolean exported=handler.isExported(features.mockedType);
        Iterator<Class<?>>it=features.interfaces.iterator();
        while(exported&&it.hasNext()){
        exported=handler.isExported(it.next());
        }
        // 检查是否所有 Mock 的类型都没有限定地导出,以避免生成钩子类型。除非必要,否则我们期望大多数 Mock 类型都符合此条件,这使得这是一个值得进行的性能优化。
        if(exported){
        assertVisibility(features.mockedType);
        for(Class<?> iFace:features.interfaces){
        assertVisibility(iFace);
        }
        }else{
        Class<?> hook=handler.injectionBase(classLoader,typeName);
        assertVisibility(features.mockedType);
        handler.adjustModuleGraph(features.mockedType,hook,true,false);
        for(Class<?> iFace:features.interfaces){
        assertVisibility(iFace);
        handler.adjustModuleGraph(iFace,hook,true,false);
        }
        }
        }

        // 使用 ByteBuddy 构建 Mock 对象
        DynamicType.Builder<T> builder=
        byteBuddy


        .subclass(features.mockedType)
        .name(name.ignoreAlso(isGroovyMethod())
        .annotateType(
        features.stripAnnotations
        ?new Annotation[0]
        :features.mockedType.getAnnotations())
        .implement(new ArrayList<Type>(features.interfaces))
        .method(matcher)
        .intercept(dispatcher)
        .transform(withModifiers(SynchronizationState.PLAIN))
        .attribute(
        features.stripAnnotations
        ?MethodAttributeAppender.NoOp.INSTANCE
        :INCLUDING_RECEIVER)
        .method(isHashCode())
        .intercept(hashCode)
        .method(isEquals())
        .intercept(equals)
        .serialVersionUid(42L)
        .defineField("mockitoInterceptor",MockMethodInterceptor.class,PRIVATE)
        .implement(MockAccess.class)
        .intercept(FieldAccessor.ofBeanProperty());

// 如果 features.serializableMode 为 SerializableMode.ACROSS_CLASSLOADERS,则实现 CrossClassLoaderSerializableMock 接口并拦截 writeReplace 方法
        if(features.serializableMode==SerializableMode.ACROSS_CLASSLOADERS){
        builder=
        builder.implement(CrossClassLoaderSerializableMock.class)
        .intercept(writeReplace);
        }

// 如果有 readReplace 方法,则定义 readObject 方法并拦截它
        if(readReplace!=null){
        builder=
        builder.defineMethod("readObject",void.class,Visibility.PRIVATE)
        .withParameters(ObjectInputStream.class)
        .throwing(ClassNotFoundException.class,IOException.class)
        .intercept(readReplace);
        }

// 忽略包私有类型或方法
        if(name.startsWith(CODEGEN_PACKAGE)||classLoader instanceof MultipleParentClassLoader){
        builder=
        builder.ignoreAlso(
        isPackagePrivate()
        .or(returns(isPackagePrivate()))
        .or(hasParameters(whereAny(hasType(isPackagePrivate())))));
        }

// 创建并加载 Mock 类
        return builder.make()
        .load(
        classLoader,
        loader.resolveStrategy(features.mockedType,classLoader,localMock))
        .getLoaded();
        }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131

该方法创建一个类加载器,并使用 ByteBuddy 创建 Mock 类。这段代码使用注释解释了一系列步骤,包括:

  • 创建类加载器
  • 创建 Mock 类的名称
  • 调整模块图表以处理 Mock 类
  • 使用 ByteBuddy 构建 Mock 对象
  • 如果需要,则实现 CrossClassLoaderSerializableMock 接口并拦截 writeReplace 方法
  • 如果存在 readReplace 方法,则定义 readObject 方法并拦截它
  • 忽略包私有类型或方法
  • 创建并加载 Mock 类
DynamicType.Builder<T> builder=
        byteBuddy
        .subclass(features.mockedType) // 构建子类,被Mock的类型作为父类
        .name(name) // 设置子类名称
        .ignoreAlso(isGroovyMethod()) // 忽略Groovy方法
        .annotateType(
        features.stripAnnotations
        ?new Annotation[0] // 如果 features.stripAnnotations 为 true,则不添加注解
        :features.mockedType.getAnnotations()) // 否则添加 features.mockedType 上的所有注解
        .implement(new ArrayList<Type>(features.interfaces)) // 实现指定的接口
        .method(matcher) // 匹配 Mock 方法的方法匹配器
        .intercept(dispatcher) // 拦截匹配的方法
        .transform(withModifiers(SynchronizationState.PLAIN)) // 将方法修改为“普通”同步模式,而不是 Mockito 的“严格”同步模式
        .attribute(
        features.stripAnnotations
        ?MethodAttributeAppender.NoOp.INSTANCE // 如果 features.stripAnnotations 为 true,则不添加 MethodAttributeAppender 实例
        :INCLUDING_RECEIVER) // 否则添加包含接收器的 MethodAttributeAppender 实例
        .method(isHashCode()) // 重写 hashCode 方法
        .intercept(hashCode) // 拦截 hashCode 方法
        .method(isEquals()) // 重写 equals 方法
        .intercept(equals) // 拦截 equals 方法
        .serialVersionUid(42L) // 添加 serialVersionUID 属性
        .defineField("mockitoInterceptor",MockMethodInterceptor.class,PRIVATE) // 定义一个名为 "mockitoInterceptor" 的私有 MockMethodInterceptor 属性
        .implement(MockAccess.class) // 实现 MockAccess 接口
        .intercept(FieldAccessor.ofBeanProperty()); // 拦截 MockAccess 接口的属性访问器方法
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

byteBuddy 利用构造模式帮我创建一个字节码

包含了一个 MockMethodInterceptor mockitoInterceptor 私有属性

mockitoInterceptor 这个名字是重点, 后面要考

上面的这个类 DispatcherDefaultingToRealMethod 是 MockMethodInterceptor 的内部类

MockMethodInterceptor$DispatcherDefaultingToRealMethod

此中又有一段代码

@FieldValue(“mockitoInterceptor”) MockMethodInterceptor interceptor

这个字段与 byte buff产生的字节码对象中的字段 mockitoInterceptor 对上了

public static class DispatcherDefaultingToRealMethod {

    @SuppressWarnings("unused")
    @RuntimeType
    @BindingPriority(BindingPriority.DEFAULT * 2)
    public static Object interceptSuperCallable(
            @This Object mock,
            @FieldValue("mockitoInterceptor") MockMethodInterceptor interceptor,
            @Origin Method invokedMethod,
            @AllArguments Object[] arguments,
            @SuperCall(serializableProxy = true) Callable<?> superCall)
            throws Throwable {
        if (interceptor == null) {
            return superCall.call();
        }
        return interceptor.doIntercept(
                mock, invokedMethod, arguments, new RealMethod.FromCallable(superCall));
    }

    @SuppressWarnings("unused")
    @RuntimeType
    public static Object interceptAbstract(
            @This Object mock,
            @FieldValue("mockitoInterceptor") MockMethodInterceptor interceptor,
            @StubValue Object stubValue,
            @Origin Method invokedMethod,
            @AllArguments Object[] arguments)
            throws Throwable {
        if (interceptor == null) {
            return stubValue;
        }
        return interceptor.doIntercept(
                mock, invokedMethod, arguments, RealMethod.IsIllegal.INSTANCE);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/366892
推荐阅读
相关标签
  

闽ICP备14008679号