转载

JVMTI Agent 工作原理及核心源码分析

前一节讲述了基于JVMTI如何实现Agent,还有一种是基于Java Instrument API实现Agent,可以在Java代码层面编写Agent代码,而非基于C++/C的代码,具体使用可参考 《Java Instrument 功能使用及原理》 :

-javaagent :为开头的默认为instrument的agent;

那么以上这两种Agent实现方式,又是在JVMTI源码中如何运行工作呢?

1 初始化 Agent

在JVM启动时,会读取JVM命令行参数 -agentlib -agentpath -javaagent并构建了Agent Library链表初始化 Agent 代码如下

if (match_option(option, "-agentlib:", &tail) || (is_absolute_path = match_option(option, "-agentpath:", &tail))) {  
  if(tail != NULL) {  
    const char* pos = strchr(tail, '=');  
    size_t len = (pos == NULL) ? strlen(tail) : pos - tail;  
    char* name = strncpy(NEW_C_HEAP_ARRAY(char, len + 1), tail, len);  
    name[len] = '/0';  
    char *options = NULL;  
    if(pos != NULL) {  
      options = strcpy(NEW_C_HEAP_ARRAY(char, strlen(pos + 1) + 1), pos + 1);  
    }  
    if ((strcmp(name, "hprof") == 0) || (strcmp(name, "jdwp") == 0)) {  
      warning("profiling and debugging agents are not supported with Kernel VM");    
    } else  if
      // JVMTI_KERNEL 构建Agent Library链表 
      add_init_agent(name, options, is_absolute_path);  
    } 
} else if (match_option(option, "-javaagent:", &tail)) {
  // -javaagent   
  if(tail != NULL) {  
    char *options = strcpy(NEW_C_HEAP_ARRAY(char, strlen(tail) + 1), tail);  
    // 构建Agent Library链表
    add_init_agent("instrument", options, false);  
  }  
  // -Xnoclassgc  
}

2 加载Agent链接库

在启动JVM create_vm时,对agent链表中的每个agent库, 加载所指定的动态库, 并调用里面的Agent_OnLoad方法 ,比如: 对于java instrument agent加载就是对libinstrument的动态库instrument.so加载

// Create agents for -agentlib:  -agentpath:  and converted -Xrun  
void Threads::create_vm_init_agents() {  
  extern struct JavaVM_ main_vm;  
  AgentLibrary* agent;  
  JvmtiExport::enter_onload_phase();  
  for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {  
    OnLoadEntry_t  on_load_entry = lookup_agent_on_load(agent);    
    if (on_load_entry != NULL) {  
      // 调用 Agent_OnLoad 函数  
      jint err = (*on_load_entry)(&main_vm, agent->options(), NULL);  
      if (err != JNI_OK) {  
        vm_exit_during_initialization("agent library failed to init", agent->name());  
      }  
    } else {  
      vm_exit_during_initialization("Could not find Agent_OnLoad function in the agent library", agent->name());  
    }  
  }  
  JvmtiExport::enter_primordial_phase();  
}

3 创建 Instrument JPLISAgent

在方法Agent_OnLoad中创建一个新的 JPLISAgent(Java Programming Language Instrumentation Services Agent),初始化了类和包里的配置文件,并且同时从Vm环境中获取了 jvmtiEnv 的环境

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *tail, void * reserved) {  
    JPLISInitializationError initerror  = JPLIS_INIT_ERROR_NONE;  
    jint                     result     = JNI_OK;  
    JPLISAgent *             agent      = NULL;
    // 创建一个新的JPLISAgent对象  
    initerror = createNewJPLISAgent(vm, &agent);  
    if ( initerror == JPLIS_INIT_ERROR_NONE ) {  
        if (parseArgumentTail(tail, &jarfile, &options) != 0) {  
            fprintf(stderr, "-javaagent: memory allocation failure./n");  
            return JNI_ERR;  
        }  
        attributes = readAttributes(jarfile);  
        if (attributes == NULL) {  
            fprintf(stderr, "Error opening zip file or JAR manifest missing : %s/n", jarfile);  
            free(jarfile);  
            if (options != NULL) free(options);  
            return JNI_ERR;  
        }  
        premainClass = getAttribute(attributes, "Premain-Class");  
        if (premainClass == NULL) {  
            fprintf(stderr, "Failed to find Premain-Class manifest attribute in %s/n", jarfile);  
            free(jarfile);  
            if (options != NULL) free(options);  
            freeAttributes(attributes);  
            return JNI_ERR;  
        }  
        /* 
         * Add to the jarfile 把jar文件追加到agent的classpath中。
         */  
        appendClassPath(agent, jarfile);  
        ……  
}

在代码中,可以看到在 读取jar的配置文件MANIFEST里Premain-Class,并且把jar文件追加到agent的class path中

4 JVMTI 回调接口

以下是JVMTI的一些回调接口,通过这些回调接口设置回调函数指针:

typedef struct {
    /* 50 : VM Initialization Event */  
    jvmtiEventVMInit VMInit;   
    /* 51 : VM Death Event */  
    jvmtiEventVMDeath VMDeath; 
    /* 52 : Thread Start */  
    jvmtiEventThreadStart ThreadStart;
    /* 53 : Thread End */  
    jvmtiEventThreadEnd ThreadEnd;  
    /* 54 : Class File Load Hook */  
    jvmtiEventClassFileLoadHook ClassFileLoadHook;
    /* 55 : Class Load */  
    jvmtiEventClassLoad ClassLoad; 
    /* 56 : Class Prepare */  
    jvmtiEventClassPrepare ClassPrepare;
    /* 57 : VM Start Event */  
    jvmtiEventVMStart VMStart;
    /* 58 : Exception */  
    jvmtiEventException Exception;
    /* 59 : Exception Catch */  
    jvmtiEventExceptionCatch ExceptionCatch; 
    /* 60 : Single Step */  
    jvmtiEventSingleStep SingleStep;
    /* 61 : Frame Pop */  
    jvmtiEventFramePop FramePop;
    /* 62 : Breakpoint */  
    jvmtiEventBreakpoint Breakpoint; 
    /* 63 : Field Access */  
    jvmtiEventFieldAccess FieldAccess;
    /* 64 : Field Modification */  
    jvmtiEventFieldModification FieldModification; 
    /* 65 : Method Entry */  
    jvmtiEventMethodEntry MethodEntry;
    /* 66 : Method Exit */  
    jvmtiEventMethodExit MethodExit;
    /* 67 : Native Method Bind */  
    jvmtiEventNativeMethodBind NativeMethodBind;
    /* 68 : Compiled Method Load */  
    jvmtiEventCompiledMethodLoad CompiledMethodLoad;
    /* 69 : Compiled Method Unload */  
    jvmtiEventCompiledMethodUnload CompiledMethodUnload; 
    /* 70 : Dynamic Code Generated */  
    jvmtiEventDynamicCodeGenerated DynamicCodeGenerated; 
    /* 71 : Data Dump Request */  
    jvmtiEventDataDumpRequest DataDumpRequest;
    /* 72 */  
    jvmtiEventReserved reserved72;
    /* 73 : Monitor Wait */  
    jvmtiEventMonitorWait MonitorWait;
    /* 74 : Monitor Waited */  
    jvmtiEventMonitorWaited MonitorWaited;
    /* 75 : Monitor Contended Enter */  
    jvmtiEventMonitorContendedEnter MonitorContendedEnter;
    /* 76 : Monitor Contended Entered */  
    jvmtiEventMonitorContendedEntered MonitorContendedEntered;
    /* 77 */  
    jvmtiEventReserved reserved77;
    /* 78 */  
    jvmtiEventReserved reserved78; 
    /* 79 */  
    jvmtiEventReserved reserved79; 
    /* 80 : Resource Exhausted */  
    jvmtiEventResourceExhausted ResourceExhausted;
    /* 81 : Garbage Collection Start */  
    jvmtiEventGarbageCollectionStart GarbageCollectionStart;
    /* 82 : Garbage Collection Finish */  
    jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;
    /* 83 : Object Free */  
    jvmtiEventObjectFree ObjectFree;
    /* 84 : VM Object Allocation */  
    jvmtiEventVMObjectAlloc VMObjectAlloc;  
} jvmtiEventCallbacks;

4.1 执行jvmtiEventVMInit的回调函数

虚拟机在创建create_vm的时候,初始化了JVMTI环境后会执行JvmtiExport::post_vm_initialized(); 方法,代码如下:

void JvmtiExport::post_vm_initialized() {  
  EVT_TRIG_TRACE(JVMTI_EVENT_VM_INIT, ("JVMTI Trg VM init event triggered" ));  
  // can now enable events  
  JvmtiEventController::vm_init();  
  JvmtiEnvIterator it;  
  for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {  
    if (env->is_enabled(JVMTI_EVENT_VM_INIT)) {  
      EVT_TRACE(JVMTI_EVENT_VM_INIT, ("JVMTI Evt VM init event sent" ));  
      JavaThread *thread  = JavaThread::current();  
      JvmtiThreadEventMark jem(thread);  
      JvmtiJavaThreadEventTransition jet(thread);  
      jvmtiEventVMInit callback = env->callbacks()->VMInit;  
      if (callback != NULL) {
         // 调用了VMInit的回调函数  
         (*callback)(env->jvmti_external(), jem.jni_env(), jem.jni_thread());  
      }  
    }  
  }  
}

4.2 执行jvmtiEventClassFileLoadHook的回调函数

钩子方法是jvmtiEventClassFileLoadHook的回调方法, 代码在classFileParser的文件中

instanceKlassHandle ClassFileParser::parseClassFile(symbolHandle name, Handle class_loader,Handle protection_domain, KlassHandle host_klass, GrowableArray<Handle>* cp_patches, symbolHandle& parsed_name,bool verify, TRAPS) {  
  ……  
  if (JvmtiExport::should_post_class_file_load_hook()) {  
     unsigned char* ptr = cfs->buffer();  
     unsigned char* end_ptr = cfs->buffer() + cfs->length();  
     JvmtiExport::post_class_file_load_hook(name, class_loader, protection_domain,  
                                           &ptr, &end_ptr,  
                                           &cached_class_file_bytes,  
                                           &cached_class_file_length);  
     if (ptr != cfs->buffer()) {  
        // JVMTI agent has modified class file data.  
        // Set new class file stream using JVMTI agent modified  
        // class file data.  
        cfs = new ClassFileStream(ptr, end_ptr - ptr, cfs->source());  
        set_stream(cfs);  
     }  
  }  
  …  
}

在jvmtiexport::post_class_file_load_hook函数最后 调用了post_to_env()函数

void post_to_env(JvmtiEnv* env, bool caching_needed) {  
   unsigned char *new_data = NULL;  
   jint new_len = 0;  
   JvmtiClassFileLoadEventMark jem(_thread, _h_name, _class_loader,  
                                    _h_protection_domain,  
                                    _h_class_being_redefined);  
  
   JvmtiJavaThreadEventTransition jet(_thread);  
  
   JNIEnv* jni_env =  (JvmtiEnv::get_phase() == JVMTI_PHASE_PRIMORDIAL)? NULL : jem.jni_env();  
  
   jvmtiEventClassFileLoadHook callback = env->callbacks()->ClassFileLoadHook;  
  
   if (callback != NULL) {  
      (*callback)(env->jvmti_external(), jni_env,  
            jem.class_being_redefined(),  
            jem.jloader(), jem.class_name(),  
            jem.protection_domain(),  
            _curr_len, _curr_data,  
            &new_len, &new_data);
   }
   ......  
}

函数中调用了jvmtiEventClassFileLoadHook的回调函数,也就是刚才在结构体中定义的jvmtiEventCallbacks。

4.3 注册钩子函数jvmtiEventClassFileLoadHook

如上,那么钩子函数jvmtiEventClassFileLoadHook是何时注册的, 回到刚才创建新的JPLISAgent代码中

JPLISInitializationError  createNewJPLISAgent(JavaVM * vm, JPLISAgent **agent_ptr) {  
    JPLISInitializationError initerror       = JPLIS_INIT_ERROR_NONE;  
    jvmtiEnv *               jvmtienv        = NULL;  
    jint                     jnierror        = JNI_OK;  
    *agent_ptr = NULL;  
    jnierror = (*vm)->GetEnv(vm,(void **) &jvmtienv,JVMTI_VERSION);  
    if (jnierror != JNI_OK) {  
        initerror = JPLIS_INIT_ERROR_CANNOT_CREATE_NATIVE_AGENT;  
    } else {  
        JPLISAgent * agent = allocateJPLISAgent(jvmtienv);  
        if (agent == NULL) {  
            initerror = JPLIS_INIT_ERROR_ALLOCATION_FAILURE;  
        } else {  
            initerror = initializeJPLISAgent(agent, vm, jvmtienv);  
            if (initerror == JPLIS_INIT_ERROR_NONE) {  
                *agent_ptr = agent;  
            } else {  
                deallocateJPLISAgent(jvmtienv, agent);  
            }  
        }  
        /* don't leak envs */  
        if ( initerror != JPLIS_INIT_ERROR_NONE ) {  
            jvmtiError jvmtierror = (*jvmtienv)->DisposeEnvironment(jvmtienv);  
            jplis_assert(jvmtierror == JVMTI_ERROR_NONE);  
        }  
    }  
    return initerror;  
}  

函数initializeJPLISAgent初始化了JPLISAgent:

JPLISInitializationError initializeJPLISAgent(   JPLISAgent *    agent,JavaVM *        vm,jvmtiEnv *      jvmtienv) {  
   ……  
    checkCapabilities(agent);  
    jvmtierror == (*jvmtienv)->GetPhase(jvmtienv, &phase);  
    jplis_assert(jvmtierror == JVMTI_ERROR_NONE);  
    if (phase == JVMTI_PHASE_LIVE) {  
        return JPLIS_INIT_ERROR_NONE;  
    }  
    /* now turn on the VMInit event */  
    if ( jvmtierror == JVMTI_ERROR_NONE ) {  
        jvmtiEventCallbacks callbacks;  
        memset(&callbacks, 0, sizeof(callbacks));  
        callbacks.VMInit = &eventHandlerVMInit;  
        jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv, &callbacks,  sizeof(callbacks));  
  
        jplis_assert(jvmtierror == JVMTI_ERROR_NONE);  
    }  
  ……  
}

JPLISAgent里首先 注册了一个VMInit的初始化函数eventHandlerVMInit,跟踪eventHandlerVMInit函数

void JNICALL eventHandlerVMInit( jvmtiEnv * jvmtienv,  
                    JNIEnv * jnienv,  
                    jthread thread) {  
    JPLISEnvironment * environment  = NULL;  
    jboolean success = JNI_FALSE;  
  
    environment = getJPLISEnvironment(jvmtienv);  
  
    /* process the premain calls on the all the JPL agents */  
    if ( environment != NULL ) {  
        jthrowable outstandingException = preserveThrowable(jnienv);  
        success = processJavaStart( environment->mAgent,  
                                    jnienv);  
        restoreThrowable(jnienv, outstandingException);  
    }  
  
    /* if we fail to start cleanly, bring down the JVM */  
    if ( !success ) {  
        abortJVM(jnienv, JPLIS_ERRORMESSAGE_CANNOTSTART);  
    }  
}

在processJavaStart里:

jboolean processJavaStart(JPLISAgent * agent, JNIEnv * jnienv) {  
    jboolean    result;  
    result = initializeFallbackError(jnienv);  
    jplis_assert(result);  
    if ( result ) {  
        result = createInstrumentationImpl(jnienv, agent);  
        jplis_assert(result);  
    }  
    if ( result ) {  
        result = setLivePhaseEventHandlers(agent);  
        jplis_assert(result);  
    }  
    if ( result ) {  
        result = startJavaAgent(agent, jnienv, agent->mAgentClassName, agent->mOptionsString,agent->mPremainCaller);  
    }  
    if ( result ) {  
        deallocateCommandLineData(agent);  
    }  
    return result;  
}

在setLivePhaseEventHandler函数中注册了,代码如下:

callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;

5 JPLISAgent结构体

struct _JPLISAgent {  
    JavaVM *                mJVM;                   /* handle to the JVM */  
    JPLISEnvironment        mNormalEnvironment;     /* for every thing but retransform stuff */  
    JPLISEnvironment        mRetransformEnvironment;/* for retransform stuff only */  
    jobject                 mInstrumentationImpl;   /* handle to the Instrumentation instance */  
    jmethodID               mPremainCaller;         /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */  
    jmethodID               mAgentmainCaller;       /* method on the InstrumentationImpl for agents loaded via attach mechanism */  
    jmethodID               mTransform;             /* method on the InstrumentationImpl that does the class file transform */  
    jboolean                mRedefineAvailable;     /* cached answer to "does this agent support redefine" */  
    jboolean                mRedefineAdded;         /* indicates if can_redefine_classes capability has been added */  
    jboolean                mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */  
    jboolean                mNativeMethodPrefixAdded;     /* indicates if can_set_native_method_prefix capability has been added */  
    char const *            mAgentClassName;        /* agent class name */  
    char const *            mOptionsString;         /* -javaagent options string */  
};  
struct _JPLISEnvironment {  
    jvmtiEnv *              mJVMTIEnv;              /* the JVM TI environment */  
    JPLISAgent *            mAgent;                 /* corresponding agent */  
    jboolean                mIsRetransformer;       /* indicates if special environment */  
};
  1. mNormalEnvironment :agent环境;
  2. mRetransformEnvironment :retransform环境;
  3. mInstrumentationImpl :sun自己提供的instrument对象;
  4. mPremainCallersun.instrument.InstrumentationImpl.loadClassAndCallPremain 方法,agent启动时加载会被调用该方法;
  5. mAgentmainCallersun.instrument.InstrumentationImpl.loadClassAndCallAgentmain 方法,agent attach动态加载agent的时会被调用该方法;
  6. mTransformsun.instrument.InstrumentationImpl.transform 方法;
  7. mAgentClassName :javaagent的MANIFEST.MF里指定的 Agent-Class
  8. mOptionsString :agent初始参数;
  9. mRedefineAvailable :MANIFEST.MF里的参数 Can-Redefine-Classes:true
  10. mNativeMethodPrefixAvailable :MANIFEST.MF里的参数 Can-Set-Native-Method-Prefix:true
  11. mIsRetransformer :MANIFEST.MF里的参数 Can-Retransform-Classes:true

在startJavaAgent的方法中调用了启动JPLISAgent的方式,我们来看invokeJavaAgentMainMethod:

jboolean invokeJavaAgentMainMethod(JNIEnv * jnienv,  
                           jobject instrumentationImpl,  
                           jmethodID mainCallingMethod,  
                           jstring className,  
                           jstring optionsString) {  
    jboolean errorOutstanding = JNI_FALSE;  
    jplis_assert(mainCallingMethod != NULL);  
    if (mainCallingMethod != NULL ) {  
        (*jnienv)->CallVoidMethod(jnienv,  
                         instrumentationImpl,  
                         mainCallingMethod,  
                         className,  
                         optionsString);  
        errorOutstanding = checkForThrowable(jnienv);  
        if ( errorOutstanding ) {  
            logThrowable(jnienv);  
        }  
        checkForAndClearThrowable(jnienv);  
    }  
    return !errorOutstanding;  
}

在函数里,实际上是调用java类sun.instrument.InstrumentationImpl 类里的方法loadClassAndCallPremain。

private void loadClassAndCallPremain(String var1, String var2) throws Throwable {
        this.loadClassAndStartAgent(var1, "premain", var2);
    }

    private void loadClassAndCallAgentmain(String var1, String var2) throws Throwable {
        this.loadClassAndStartAgent(var1, "agentmain", var2);
    }

继续查看Java的sun.instrument.InstrumentationImpl类的方法loadClassAndStartAgent:

private void loadClassAndStartAgent(String classname,  
                            String methodname,  
                            String optionsString) throws Throwable {  
        ...  
        try {  
            m = javaAgentClass.getDeclaredMethod( methodname,  
                                 new Class<?>[] {  
                                     String.class,  
                                     java.lang.instrument.Instrumentation.class  
                                 }  
                               );  
            twoArgAgent = true;  
        } catch (NoSuchMethodException x) {  
            // remember the NoSuchMethodException  
            firstExc = x;  
        }  
  
        if (m == null) {  
            // now try the declared 1-arg method  
            try {  
                m = javaAgentClass.getDeclaredMethod(methodname, new Class<?>[] { String.class });  
            } catch (NoSuchMethodException x) {  
                // ignore this exception because we'll try  
                // two arg inheritance next  
            }  
        }  
  
        if (m == null) {  
            // now try the inherited 2-arg method  
            try {  
                m = javaAgentClass.getMethod( methodname,  
                                 new Class<?>[] {  
                                     String.class,  
                                     java.lang.instrument.Instrumentation.class  
                                 }  
                               );  
                twoArgAgent = true;  
            } catch (NoSuchMethodException x) {  
                // ignore this exception because we'll try  
                // one arg inheritance next  
            }  
        }  
  
        if (m == null) {  
            // finally try the inherited 1-arg method  
            try {  
                m = javaAgentClass.getMethod(methodname, new Class<?>[] { String.class });  
            } catch (NoSuchMethodException x) {  
                // none of the methods exists so we throw the  
                // first NoSuchMethodException as per 5.0  
                throw firstExc;  
            }  
        }  
  
        // the premain method should not be required to be public,  
        // make it accessible so we can call it  
        // Note: The spec says the following:  
        //     The agent class must implement a public static premain method...  
        setAccessible(m, true);  
  
        // invoke the 1 or 2-arg method  
        if (twoArgAgent) {  
            m.invoke(null, new Object[] { optionsString, this });  
        } else {  
            m.invoke(null, new Object[] { optionsString });  
        }  
  
        // don't let others access a non-public premain method  
        setAccessible(m, false);  
    } 

在InstrumentationImpl的类中初始化了我们自定义的Transformer的premain方法:

public class MyInjectTransformer  implements ClassFileTransformer{  
    public static void premain(String options, Instrumentation ins) {  
        ins.addTransformer(new SQLInjectTransformer());  
    }  
      
    @Override  
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,  
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {  
        return null;  
    }  
      
}

钩子函数eventHandlerClassFileLoadHook:

void JNICALL eventHandlerClassFileLoadHook(  jvmtiEnv *              jvmtienv,  
                                JNIEnv *                jnienv,  
                                jclass                  class_being_redefined,  
                                jobject                 loader,  
                                const char*             name,  
                                jobject                 protectionDomain,  
                                jint                    class_data_len,  
                                const unsigned char*    class_data,  
                                jint*                   new_class_data_len,  
                                unsigned char**         new_class_data) {  
    JPLISEnvironment * environment  = NULL;  
  
    environment = getJPLISEnvironment(jvmtienv);  
  
    /* if something is internally inconsistent (no agent), just silently return without touching the buffer */  
    if ( environment != NULL ) {  
        jthrowable outstandingException = preserveThrowable(jnienv);  
        transformClassFile(environment->mAgent,  
                            jnienv,  
                            loader,  
                            name,  
                            class_being_redefined,  
                            protectionDomain,  
                            class_data_len,  
                            class_data,  
                            new_class_data_len,  
                            new_class_data,  
                            environment->mIsRetransformer);  
        restoreThrowable(jnienv, outstandingException);  
    }  
}

重要的是transformClassFile函数,看看它究竟做了啥事情:

transformedBufferObject = (*jnienv)->CallObjectMethod(  
                                                jnienv,  
                                                agent->mInstrumentationImpl,  
                                                agent->mTransform,  
                                                loaderObject,  
                                                classNameStringObject,  
                                                classBeingRedefined,  
                                                protectionDomain,  
                                                classFileBufferObject,  
                                                is_retransformer);

也就是调用了InstrumentationImpl里的transform方法, 在InstrumentationImpl类里通过TransformerManager的transform的方法最终调用我们自定义的MyTransformer的类的transform方法

private byte[] transform(ClassLoader var1, String var2, Class var3, ProtectionDomain var4, byte[] var5, boolean var6) {
        TransformerManager var7 = var6 ? this.mRetransfomableTransformerManager : this.mTransformerManager;
        return var7 == null ? null : var7.transform(var1, var2, var3, var4, var5);
    }
原文  https://juejin.im/post/5b0925ec51882538aa1ee248
正文到此结束
Loading...