专栏首页7DGroup性能工具之JMeter5.0核心类StandardJMeterEngine源码分析

性能工具之JMeter5.0核心类StandardJMeterEngine源码分析

概述

JMeter 默认单机压测引擎,运行 JMeter 测试,直接用于本地 GUI 和非 GUI 调用,或者RemoteJMeterEngineImpl 在服务器模式下运行时启动。

API地址:https://jmeter.apache.org/api/org/apache/jmeter/engine/StandardJMeterEngine.html

工程位置

逻辑关系

简要解读:

  • HashTree是依赖的数据结构;
  • SearchByClass 用来查找 HashTree 中的所有节点,并把节点实例化为真正的对象,例如图中TestPlan/ThreadGroup/JavaSampler/ResultCollector 在 HashTree 中本来都是只是配置,全部通过 SearchByClass 实例化的;
  • 实例化出来的对象如果是 TestStateListener 类型,则会在有生命周期的函数回调,测试前调 testStarted,结束掉 testEnded, 比如 ResultCollector是该类型的一种,在结束的时候回调 testEnded 方法完成 report 的写入;
  • PreCompiler 用来解析 Arguments, 把 TestPlan 节点中配置的参数作为JMeterVariables 加入到测试线程上线文中;
  • ThreadGroup 用来用来管理一组线程,包括线程的个数/启动/关闭等;StopTest 作为其内部类对外不可见,作为一个 Runnable,作用是异步停止测试,stopTest方法也是通过该内部类实现的。

主要变量

注意关键字 volatile

// 灵魂级变量,注意关键字volatile
private static volatile StandardJMeterEngine engine;

构造函数

有两种构造函数,带参和不带参

// 不带参构造函数
public StandardJMeterEngine() {
    this(null);
}

// 带参构造函数
public StandardJMeterEngine(String host) {
    this.host = host;
    // Hack to allow external control
    initSingletonEngine(this);
}

主要方法

askThreadsToStop

清洁关闭,即等待当前运行的采样器结束

/**
     * Clean shutdown ie, wait for end of current running samplers
     */
    public void askThreadsToStop() {
        if (engine != null) { // Will be null if StopTest thread has started
            engine.stopTest(false);
        }
    }

reset

JMeterEngine 如果运行则停止

// 重置。在StandardJMeterEngine中就是直接调用stopTest(true).
    @Override
    public void reset() {
        if (running) {
            stopTest();
        }
    }

configure(HashTree testTree)

配置引擎,HashTree 是 JMeter 执行测试依赖的数据结构,configure 在执行测试之前进行配置测试数据。

// HashTree是JMeter执行测试依赖的数据结构,configure在执行测试之前进行配置测试数据
    // 从HashTree中解析出TestPlan, 获取TestPlan的serialized和tearDownOnShutdown并保存为local属性,同时把整个HashTree也保存到local。
    // StandardJMeterEngine依赖线程组ThreadGroup, 一个测试中可能会有多个线程组,如果serialized为true,则StandardJMeterEngine会串行的去执行这些线程组,每启动一个ThreadGroup主线程都会等它结束;否则就并行执行所有的线程组。
    // tearDownOnShutdown与PostThreadGroup配合使用的,这个Special Thread Group专门用来做清理工作

    @Override
    public void configure(HashTree testTree) {
        // Is testplan serialised?
        SearchByClass<TestPlan> testPlan = new SearchByClass<>(TestPlan.class);
        testTree.traverse(testPlan);
        Object[] plan = testPlan.getSearchResults().toArray();
        if (plan.length == 0) {
            throw new IllegalStateException("Could not find the TestPlan class!");
        }
        TestPlan tp = (TestPlan) plan[0];
        serialized = tp.isSerialized();
        tearDownOnShutdown = tp.isTearDownOnShutdown();
        active = true;
        test = testTree;
    }

exit

远程退出由 RemoteJMeterEngineImpl.rexit() 和notifyTestListenersOfEnd() 调用 iff exitAfterTest 为 true; 反过来,run( ) 方法调用,也调用 StopTest 类

/** 
     * Remote exit
     * Called by RemoteJMeterEngineImpl.rexit()
     * and by notifyTestListenersOfEnd() iff exitAfterTest is true;
     * in turn that is called by the run() method and the StopTest class
     * also called
     *
     * 是为Remote Test准备的
     * 如果当前的测试是从一个客户端的JMeter执行远程JMeterEngine的remote samples,则应该调用该exit()方法来关闭远程的测试
     * 被RemoteJMeterEngineImpl.rexit()调用和exitAfterTest为真时被notifyTestListenersOfEnd()调用
     */
    @Override
    public void exit() {
        ClientJMeterEngine.tidyRMI(log); // This should be enough to allow server to exit.
        if (REMOTE_SYSTEM_EXIT) { // default is false
            log.warn("About to run System.exit(0) on {}", host);
            // Needs to be run in a separate thread to allow RMI call to return OK
            Thread t = new Thread() {
                @Override
                public void run() {
                    pause(1000); // Allow RMI to complete
                    log.info("Bye from {}", host);
                    System.out.println("Bye from "+host); // NOSONAR Intentional
                    System.exit(0); // NOSONAR Intentional
                }
            };
            t.start();
        }
    }

isActive

isActive 在测试中 JMeterEngine 返回值:

boolean 用于显示引擎是否处于活动状态的标志(在测试运行时为true)。在测试结束时设置为 false。

/**
     *  引擎是否有效的标识,在测试结束时设为false
     *  在confgiure()的时候设该值为true,在执行完测试(指的是该JMeterEngine所有ThreadGroup)之后设置为false。
     *  如果active==true,则说明该JMeterEngine已经配置完测试并且还没执行完,我们不能再进行configure或者runTest了;
     *  若active == false, 则该JMeterEngine是空闲的,我们可以重新配置HashTree,执行新的测试.
     *
     * @return
     */

    @Override
    public boolean isActive() {
        return active;
    }

engine

操作 engine,initSingletonEngine()、initSingletonEngine()、stopEngineNow()、stopEngine

/**
     * Set the shared engine
     * 操作 engine,initSingletonEngine()、initSingletonEngine()、stopEngineNow()、stopEngine
     * @param standardJMeterEngine 
     */
    private static void initSingletonEngine(StandardJMeterEngine standardJMeterEngine) {
        StandardJMeterEngine.engine = standardJMeterEngine; 
    }

    public static void stopEngineNow() {
        if (engine != null) {// May be null if called from Unit test
            engine.stopTest(true);
        }
    }

    public static void stopEngine() {
        if (engine != null) { // May be null if called from Unit test
            engine.stopTest(false);
        }
    }

run

run(),启动测试。

JMeterContextService 清零:numberOfActiveThreads=0, 重置 testStart时间

JMeterContextService.startTest();

JMeterContextService.startTest():

/**
     * Method is called by the JMeterEngine class when a test run is started.
     * Zeroes numberOfActiveThreads.
     * Saves current time in a field and in the JMeter property "TESTSTART.MS"
     */
    public static synchronized void startTest() {
        if (testStart == 0) {
            numberOfActiveThreads = 0;
            testStart = System.currentTimeMillis();
            JMeterUtils.setProperty("TESTSTART.MS",Long.toString(testStart));// $NON-NLS-1$
        }
    }

PreCompiler the Tashree,见上面的简要解读

try {
    PreCompiler compiler = new PreCompiler();
    test.traverse(compiler);
} catch (RuntimeException e) {
    log.error("Error occurred compiling the tree:", e);
    JMeterUtils.reportErrorToUser("Error occurred compiling the tree: - see log file", e);
    return; // no point continuing
}

利用 SearchByClass 解析所有 TestStateListener 加入到 testList 中

SearchByClass<TestStateListener> testListeners = new SearchByClass<>(TestStateListener.class); // TL-S&E
test.traverse(testListeners);
// Merge in any additional test listeners
// currently only used by the function parser
testListeners.getSearchResults().addAll(testList);
  • 触发上一步中解析的 testListener 的 testStarted 方法:ResultCollector 会递增 instanceCount,初始化 fileOutput;TestPlan 会设置 FileServer 的basedir,添加 classpath; JavaSampler 会初始化真正要跑的AbstractJavaSamplerClient 类;
  • 利用 SearchByClass 解析所有 ThreadGroup(包括SetupThreadGroup,ThreadGroup, PostThreadGroup)
notifyTestListenersOfStart(testListeners);

private void notifyTestListenersOfStart(SearchByClass<TestStateListener> testListeners) {
    for (TestStateListener tl : testListeners.getSearchResults()) {
        if (tl instanceof TestBean) {
            TestBeanHelper.prepare((TestElement) tl);
        }
        if (host == null) {
            tl.testStarted();
        } else {
            tl.testStarted(host);
        }
    }
}

实例化一个 ListenerNotifier 实例,用来通知事件发生

ListenerNotifier notifier = new ListenerNotifier();

启动所有 SetupThreadGroup (一般情况下没有 SetupThreadGroup )并等待到都结束

if (setupIter.hasNext()) {
    log.info("Starting setUp thread groups");
    while (running && setupIter.hasNext()) {// for each setup thread group
        AbstractThreadGroup group = setupIter.next();
        groupCount++;
        String groupName = group.getName();
        log.info("Starting setUp ThreadGroup: " + groupCount + " : " + groupName);
        startThreadGroup(group, groupCount, setupSearcher, testLevelElements, notifier);
        if (serialized && setupIter.hasNext()) {
            log.info("Waiting for setup thread group: " + groupName
                    + " to finish before starting next setup group");
            group.waitThreadsStopped();
        }
    }
    log.info("Waiting for all setup thread groups to exit");
    // wait for all Setup Threads To Exit
    waitThreadsStopped();
    log.info("All Setup Threads have ended");
    groupCount = 0;
    JMeterContextService.clearTotalThreads();
}

进行一次 gc 后 开始跑真正的测试,即启动所有的 ThreadGroup,这里会检查 serialized 属性,用来判断是否这些 ThreadGroup 串行执行

JMeterUtils.helpGC();

等待所有的ThreadGroup结束

while (running && iter.hasNext()) {// for each thread group
    AbstractThreadGroup group = iter.next();
    // ignore Setup and Post here. We could have filtered the searcher.
    // but then
    // future Thread Group objects wouldn't execute.
    if (group instanceof SetupThreadGroup || group instanceof PostThreadGroup) {
        continue;
    }
    groupCount++;
    String groupName = group.getName();
    log.info("Starting ThreadGroup: " + groupCount + " : " + groupName);
    startThreadGroup(group, groupCount, searcher, testLevelElements, notifier);
    if (serialized && iter.hasNext()) {
        log.info("Waiting for thread group: " + groupName + " to finish before starting next group");
        group.waitThreadsStopped();
    }
} // end of thread groups
if (groupCount == 0) { // No TGs found
    log.info("No enabled thread groups found");
} else {
    if (running) {
        log.info("All thread groups have been started");
    } else {
        log.info("Test stopped - no more thread groups will be started");
    }
}

// wait for all Test Threads To Exit
waitThreadsStopped();

若有 PostThreadGroup(一般没有),执行所有的 PostThreadGroup 并等待至所有 PostThreadGroup 结束

if (postIter.hasNext()) {
    groupCount = 0;
    JMeterContextService.clearTotalThreads();
    log.info("Starting tearDown thread groups");
    if (mainGroups && !running) { // i.e. shutdown/stopped during main
                                    // thread groups
        running = shutdown && tearDownOnShutdown; // re-enable for
                                                    // tearDown if
                                                    // necessary
    }
    while (running && postIter.hasNext()) {// for each setup thread
                                            // group
        AbstractThreadGroup group = postIter.next();
        groupCount++;
        String groupName = group.getName();
        log.info("Starting tearDown ThreadGroup: " + groupCount + " : " + groupName);
        startThreadGroup(group, groupCount, postSearcher, testLevelElements, notifier);
        if (serialized && postIter.hasNext()) {
            log.info("Waiting for post thread group: " + groupName
                    + " to finish before starting next post group");
            group.waitThreadsStopped();
        }
    }
    waitThreadsStopped(); // wait for Post threads to stop
}

触发第三步中解析的 testListener 的 testEnded 方法:JavaSampler 会调用真正跑的 AbstractJavaSamplerClient 的 teardownTest 方法,可以打印该 JavaSamplerClient 测试总共花费的时间;

  • ResultCollector 用来将测试结果写如文件生成;
  • reportTestPlan 用来关闭文件。
notifyTestListenersOfEnd(testListeners);
JMeterContextService.endTest();

startThreadGroup

启动线程组,run 方法中调用

private void startThreadGroup(AbstractThreadGroup group, int groupCount, SearchByClass<?> searcher,
        List<?> testLevelElements, ListenerNotifier notifier) {
    try {
        int numThreads = group.getNumThreads();
        JMeterContextService.addTotalThreads(numThreads);
        boolean onErrorStopTest = group.getOnErrorStopTest();
        boolean onErrorStopTestNow = group.getOnErrorStopTestNow();
        boolean onErrorStopThread = group.getOnErrorStopThread();
        boolean onErrorStartNextLoop = group.getOnErrorStartNextLoop();
        String groupName = group.getName();
        log.info("Starting " + numThreads + " threads for group " + groupName + ".");

        if (onErrorStopTest) {
            log.info("Test will stop on error");
        } else if (onErrorStopTestNow) {
            log.info("Test will stop abruptly on error");
        } else if (onErrorStopThread) {
            log.info("Thread will stop on error");
        } else if (onErrorStartNextLoop) {
            log.info("Thread will start next loop on error");
        } else {
            log.info("Thread will continue on error");
        }
        ListedHashTree threadGroupTree = (ListedHashTree) searcher.getSubTree(group);
        threadGroupTree.add(group, testLevelElements);

        groups.add(group);
        group.start(groupCount, notifier, threadGroupTree, this);
    } catch (JMeterStopTestException ex) { // NOSONAR Reported by log
        JMeterUtils.reportErrorToUser("Error occurred starting thread group :" + group.getName()
                + ", error message:" + ex.getMessage() + ", \r\nsee log file for more details", ex);
        return; // no point continuing
    }
}

waitThreadsStopped

等待线程停止,run 方法中调用

/**
 * Wait for Group Threads to stop
 */
private void waitThreadsStopped() {
    // ConcurrentHashMap does not need synch. here
    for (AbstractThreadGroup threadGroup : groups) {
        threadGroup.waitThreadsStopped();
    }
}

/**
 * Wait for all Group Threads to stop
 */
@Override
public void waitThreadsStopped() {
    if (delayedStartup) {
        waitThreadStopped(threadStarter);
    }
    for (Thread t : allThreads.values()) {
        waitThreadStopped(t);
    }
}

/**
 * Wait for thread to stop
 * @param thread Thread
 */
private void waitThreadStopped(Thread thread) {
    if (thread != null) {
        while (thread.isAlive()) {
            try {
                thread.join(WAIT_TO_DIE);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

removeThreadGroups

移除线程组,在 run 方法里调用

private void removeThreadGroups(List<?> elements) {
        Iterator<?> iter = elements.iterator();
        while (iter.hasNext()) { // Can't use for loop here because we remove elements
            Object item = iter.next();
            if (item instanceof AbstractThreadGroup || !(item instanceof TestElement)) {
                iter.remove();
            }
        }
    }

runTest

runTest( ),调用该方法用来执行测试,启动一个线程并触发它的run()方法,若报异常则调用stopTest(),抛出 JMeterEngineException。

// 调用该方法用来执行测试,启动一个线程并触发它的run()方法,若报异常则调用stopTest(),抛出JMeterEngineException
    @Override
    public void runTest() throws JMeterEngineException {
        if (host != null){
            long now=System.currentTimeMillis();
            System.out.println("Starting the test on host " + host + " @ "+new Date(now)+" ("+now+")"); // NOSONAR Intentional
        }
        try {
            Thread runningThread = new Thread(this, "StandardJMeterEngine");
            // 启动一个线程并触发它的run()方法
            runningThread.start();
        } catch (Exception err) {
            stopTest();
            throw new JMeterEngineException(err);
        }
    }

stopThread

根据 threadName 停止线程的执行:分两种情况立即停止和非立即停止,根据第二个参数的值决定

//根据threadName停止线程的执行:分两种情况立即停止和非立即停止,根据第二个参数的值决定
 public static boolean stopThread(String threadName) {
        return stopThread(threadName, false);
    }

    public static boolean stopThreadNow(String threadName) {
        return stopThread(threadName, true);
    }

    private static boolean stopThread(String threadName, boolean now) {
        if (engine == null) {
            return false;// e.g. not yet started
        }
        boolean wasStopped = false;
        // ConcurrentHashMap does not need synch. here
        for (AbstractThreadGroup threadGroup : engine.groups) {
            wasStopped = wasStopped || threadGroup.stopThread(threadName, now);
        }
        return wasStopped;
    }

ThreadGroup.stopThread调用及具体实现代码如下:

/**
     * Stop thread called threadName:
     * <ol>
     *  <li>stop JMeter thread</li>
     *  <li>interrupt JMeter thread</li>
     *  <li>interrupt underlying thread</li>
     * </ol>
     * @param threadName String thread name
     * @param now boolean for stop
     * @return true if thread stopped
     */
    @Override
    public boolean stopThread(String threadName, boolean now) {
        for (Entry<JMeterThread, Thread> threadEntry : allThreads.entrySet()) {
            JMeterThread jMeterThread = threadEntry.getKey();
            if (jMeterThread.getThreadName().equals(threadName)) {
                stopThread(jMeterThread, threadEntry.getValue(), now);
                return true;
            }
        }
        return false;
    }

    /**
     * Hard Stop JMeterThread thrd and interrupt JVM Thread if interrupt is true
     * @param jmeterThread {@link JMeterThread}
     * @param jvmThread {@link Thread}
     * @param interrupt Interrupt thread or not
     */
    private void stopThread(JMeterThread jmeterThread, Thread jvmThread, boolean interrupt) {
        jmeterThread.stop();
        jmeterThread.interrupt(); // interrupt sampler if possible
        if (interrupt && jvmThread != null) { // Bug 49734
            jvmThread.interrupt(); // also interrupt JVM thread
        }
    }

stopTest

stopTest(boolean now)

测试,若 now 为 true 则停止动作立即执行;若为 false 则停止动作缓刑,它会等待当前正在执行的测试至少执行完一个 iteration。

// 停止测试,若now为true则停止动作立即执行;若为false则停止动作缓刑,它会等待当前正在执行的测试至少执行完一个iteration。
    @Override
    public synchronized void stopTest(boolean now) {
        Thread stopThread = new Thread(new StopTest(now));
        stopThread.start();
    }

stopTest()

立即停止执行测试

/**
     * Stop Test Now
     */
    @Override
    public synchronized void stopTest() {
        stopTest(true);
    }

notifyTestListenersOfStart

测试开始通知监听

private void notifyTestListenersOfStart(SearchByClass<TestStateListener> testListeners) {
        for (TestStateListener tl : testListeners.getSearchResults()) {
            if (tl instanceof TestBean) {
                TestBeanHelper.prepare((TestElement) tl);
            }
            if (host == null) {
                tl.testStarted();
            } else {
                tl.testStarted(host);
            }
        }
    }

介绍本方法需要了解下 TestStateListener 接口

package org.apache.jmeter.testelement;

/**
 * @since 2.8
 */
public interface TestStateListener {

    /**
     * <p>
     * Called just before the start of the test from the main engine thread.
     *
     * This is before the test elements are cloned.
     *
     * Note that not all the test
     * variables will have been set up at this point.
     * </p>
     *
     * <p>
     * <b>
     * N.B. testStarted() and testEnded() are called from different threads.
     * </b>
     * </p>
     * @see org.apache.jmeter.engine.StandardJMeterEngine#run()
     *
     */
    void testStarted();

    /**
     * <p>
     * Called just before the start of the test from the main engine thread.
     *
     * This is before the test elements are cloned.
     *
     * Note that not all the test
     * variables will have been set up at this point.
     * </p>
     *
     * <p>
     * <b>
     * N.B. testStarted() and testEnded() are called from different threads.
     * </b>
     * </p>
     * @see org.apache.jmeter.engine.StandardJMeterEngine#run()
     * @param host name of host
     */
    void testStarted(String host);

    /**
     * <p>
     * Called once for all threads after the end of a test.
     *
     * This will use the same element instances as at the start of the test.
     * </p>
     *
     * <p>
     * <b>
     * N.B. testStarted() and testEnded() are called from different threads.
     * </b>
     * </p>
     * @see org.apache.jmeter.engine.StandardJMeterEngine#stopTest()
     *
     */
    void testEnded();

    /**
     * <p>
     * Called once for all threads after the end of a test.
     *
     * This will use the same element instances as at the start of the test.
     * </p>
     *
     * <p>
     * <b>
     * N.B. testStarted() and testEnded() are called from different threads.
     * </b>
     * </p>
     * @see org.apache.jmeter.engine.StandardJMeterEngine#stopTest()
     * @param host name of host
     *
     */

    void testEnded(String host);

}
  • testStarted:在测试开始之前调用
  • testEnded:在所有线程测试结束时调用一次

notifyTestListenersOfEnd

测试结束通知监听

private void notifyTestListenersOfEnd(SearchByClass<TestStateListener> testListeners) {
        log.info("Notifying test listeners of end of test");
        for (TestStateListener tl : testListeners.getSearchResults()) {
            try {
                if (host == null) {
                    tl.testEnded();
                } else {
                    tl.testEnded(host);
                }
            } catch (Exception e) {
                log.warn("Error encountered during shutdown of "+tl.toString(),e);
            }
        }
        if (host != null) {
            log.info("Test has ended on host {} ", host);
            long now=System.currentTimeMillis();
            System.out.println("Finished the test on host " + host + " @ "+new Date(now)+" ("+now+")" // NOSONAR Intentional
                    +(EXIT_AFTER_TEST ? " - exit requested." : ""));
            if (EXIT_AFTER_TEST){
                exit();
            }
        }
        active=false;
    }

单机执行

// 加载jmx文件
FileServer.getFileServer().setBaseForScript(jmxFile);
// 设置jmx脚本文件的工作目录
HashTree jmxTree = SaveService.loadTree(jmxFile);
// 去掉没用的节点元素,替换掉可以替换的控制器
JMeter.convertSubTree(jmxTree);

// 初始化默认的压测引擎
JMeterEngine engine = new StandardJMeterEngine();
engine.configure(jmxTree);
engine.runTest();

分布式执行

// 分布式执行脚本,StringTokenizer是为了初始化hosts参数
// DistributedRunner本质上还是StandardJMeterEngine来执行的压测,使用的是rmi的协议实现的分布式压测。
java.util.StringTokenizer st = new java.util.StringTokenizer(remoteHostsString, ",");//$NON-NLS-1$
List<String> hosts = new LinkedList<>();
while (st.hasMoreElements()) {
   hosts.add((String) st.nextElement());
}

DistributedRunner distributedRunner=new DistributedRunner(this.remoteProps);
distributedRunner.setStdout(System.out); // NOSONAR
distributedRunner.setStdErr(System.err); // NOSONAR
distributedRunner.init(hosts, clonedTree);
engines.addAll(distributedRunner.getEngines());
distributedRunner.start();

StringTokenizer 是为了初始化hosts参数使用的。 DistributedRunner 本质上还是 StandardJMeterEngine 来执行的压测,使用的是 RMI 的协议实现的分布式压测。

参考资料:

https://blog.csdn.net/yue530tomtom/article/details/78055365 https://jmeter.apache.org/api/org/apache/jmeter/engine/StandardJMeterEngine.html

本文分享自微信公众号 - 7DGroup(Zee_7DGroup),作者:左泽位

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-02-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 性能环境之Jenkins+Maven自动化部署SpringBoot压测环境(实战篇)

    Jenkins是目前最流行的开源CI(持续集成)工具,广泛用于项目开发,部署和自动化等。 本文将带着大家一起完成在阿里云Centos 7服务器间完成 Jenki...

    高楼Zee
  • 走进Java接口测试之持久层框架Spring-data-jpa

    在接口测试中把 Case存储至数据库中,是比较常见的“数据驱动”做法。而在实际的接口测试用例开发中,对数据库的操作无非就是“增删改查”。就为最普遍的单表操作而言...

    高楼Zee
  • 性能工具之linux常见日志统计分析命令

    在上文中性能工具之linux三剑客awk、grep、sed详解,我们已经详细介绍 linux 三剑客的基本使用,接下来我们看看具体在性能测试领域的运用,本文主要...

    高楼Zee
  • Python的Web Services客

    1、一些库的汇总信息:https://wiki.python.org/moin/WebServices

    py3study
  • 注册中心 Eureka 源码解析 —— 应用实例注册发现 (二)之续租

    本文主要分享 Eureka-Client 向 Eureka-Server 续租应用实例的过程。

    芋道源码
  • 跟着Nature microbiology学画图~箱线图放到频率分布直方图的右上角

    频率分布直方图之前的推文有过详细的介绍,点击下方蓝字直达,这里的代码就不再过多介绍

    用户7010445
  • SourceTree 从下载、安装到免登录的方法(Windows 版 )

    2.找到 C:Usersxx电脑名AppDataLocalAtlassianSourceTree文件下 增加名为accounts.json的文件

    yuezhongbao
  • 麦肯锡预测:2020年物联网市场将增长到37亿美元

    T客汇官网:tikehui.com 原文作者 | Louis Columbus 译者 | 李哲 ? 贝恩公司(Bain)预计,到2020年,提供硬件、软件和综...

    人称T客
  • 用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1

    我们来看看两个概念,EBNF和递归下降文法,以及如何用这两个方法来计算tryC中的表达式。

    用户7424068
  • 你的“跳一跳”榜上有名了吗?聊聊“跳一跳”开挂方法

    最近“跳一跳”在朋友圈风靡一时,吃饭的时候,人家跟你聊跳了多少步,你要没上200都不好意思跟人家打招呼。作为AI研发的机构,我们更关心怎么样才能自动让AI走的更...

    刀刀老高

扫码关注云+社区

领取腾讯云代金券