缘由

最近在玩 jmeter,看到结果的报告中有一列数据是 timestamp,可读性太差了,
就想着能不能自己加一列比较直观的时间数据到报告中,于是开始了源码折腾之旅

简单看下需求(这里是 Http 请求,即 HttpSample,其他的应该差不多)

这是修改前

timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
1614223478272,5396,访问百度,200,OK,线程组 1-1,text,true,,2497,118,2,2,https://www.baidu.com/,5392,0,3678
1614223478608,5358,访问百度,200,OK,线程组 1-2,text,true,,2497,118,1,1,https://www.baidu.com/,5358,0,5032

这是修改后

timeStamp,readableTime,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
1614223478272,2021-2-25-11-24-38-72,5396,访问百度,200,OK,线程组 1-1,text,true,,2497,118,2,2,https://www.baidu.com/,5392,0,3678
1614223478608,2021-2-25-11-24-38-608,5358,访问百度,200,OK,线程组 1-2,text,true,,2497,118,1,1,https://www.baidu.com/,5358,0,5032

增加可读时间列: 年 - 月-日 - 小时 - 分钟 - 秒-毫秒

前提

  1. 先拉取源码(当前源码为 5.5 的版本):https://github.com/apache/jmeter
  2. 用 idea 打开,可能会自动下载 gradle,再拉取各种依赖

开始折腾之启动

  1. 首先要能本地调试跑起来,找到程序的入口类,在 src 目录中 launcher 模块:

    org.apache.jmeter.NewDriver
    
  2. 修改该类中的启动目录的位置

    //JMETER_INSTALLATION_DIRECTORY=tmpDir;
    JMETER_INSTALLATION_DIRECTORY = "jmeter(本地jmeter源码目录的绝对路径)";
    
  3. 尝试启动 NewDriver

    # 带GUI
    org.apache.jmeter.NewDriver
    # 不带GUI
    org.apache.jmeter.NewDriver -n -t /Users/chencarl/Downloads/workspace/java/github/jmeter/jmx/HTTP请求-baidu.jmx -l baiduResult.jtl
    

首先尝试非 GUI 模式

  1. 从入口类往下追;
  2. 从结果中有 sentBytes 去查找代码;
  3. 从 jmeter.log 中查看相关日志信息;
    找到了下面的代码:

    public class HTTPHC4Impl extends HTTPHCAbstractImpl {
    
    @Override
    protected HTTPSampleResult sample(URL url, String method,
            boolean areFollowingRedirect, int frameDepth) {
    
        if (log.isDebugEnabled()) {
            log.debug("Start : sample {} method {} followingRedirect {} depth {}",
                    url, method, areFollowingRedirect, frameDepth);
        }
        JMeterVariables jMeterVariables = JMeterContextService.getContext().getVariables();
    
        HTTPSampleResult res = createSampleResult(url, method);
    
        CloseableHttpClient httpClient = null;
        HttpRequestBase httpRequest = null;
        HttpContext localContext = new BasicHttpContext();
        HttpClientContext clientContext = HttpClientContext.adapt(localContext);
        clientContext.setAttribute(CONTEXT_ATTRIBUTE_AUTH_MANAGER, getAuthManager());
        HttpClientKey key = createHttpClientKey(url);
        MutableTriple<CloseableHttpClient, AuthState, PoolingHttpClientConnectionManager> triple;
        try {
            triple = setupClient(key, jMeterVariables, clientContext);
            httpClient = triple.getLeft();
            URI uri = url.toURI();
            httpRequest = createHttpRequest(uri, method, areFollowingRedirect);
            setupRequest(url, httpRequest, res); // can throw IOException
        } catch (Exception e) {
            res.sampleStart();
            res.sampleEnd();
            errorResult(e, res);
            return res;
        }
    
        setupClientContextBeforeSample(jMeterVariables, localContext);
    
        res.sampleStart();
    
        final CacheManager cacheManager = getCacheManager();
        if (cacheManager != null && HTTPConstants.GET.equalsIgnoreCase(method) && cacheManager.inCache(url, httpRequest.getAllHeaders())) {
            return updateSampleResultForResourceInCache(res);
        }
        CloseableHttpResponse httpResponse = null;
        try {
            currentRequest = httpRequest;
            handleMethod(method, res, httpRequest, localContext);
            // store the SampleResult in LocalContext to compute connect time
            localContext.setAttribute(CONTEXT_ATTRIBUTE_SAMPLER_RESULT, res);
            // perform the sample
            httpResponse =
                    executeRequest(httpClient, httpRequest, localContext, url);
            saveProxyAuth(triple, localContext);
            if (log.isDebugEnabled()) {
                log.debug("Headers in request before:{}", Arrays.asList(httpRequest.getAllHeaders()));
            }
            // Needs to be done after execute to pick up all the headers
            final HttpRequest request = (HttpRequest) localContext.getAttribute(HttpCoreContext.HTTP_REQUEST);
            if (log.isDebugEnabled()) {
                log.debug("Headers in request after:{}, in localContext#request:{}",
                        Arrays.asList(httpRequest.getAllHeaders()),
                        Arrays.asList(request.getAllHeaders()));
            }
            extractClientContextAfterSample(jMeterVariables, localContext);
            // We've finished with the request, so we can add the LocalAddress to it for display
            if (localAddress != null) {
                request.addHeader(HEADER_LOCAL_ADDRESS, localAddress.toString());
            }
            res.setRequestHeaders(getAllHeadersExceptCookie(request));
    
            Header contentType = httpResponse.getLastHeader(HTTPConstants.HEADER_CONTENT_TYPE);
            if (contentType != null){
                String ct = contentType.getValue();
                res.setContentType(ct);
                res.setEncodingAndType(ct);
            }
            HttpEntity entity = httpResponse.getEntity();
            if (entity != null) {
                res.setResponseData(readResponse(res, entity.getContent(), entity.getContentLength()));
            }
    
            res.sampleEnd(); // Done with the sampling proper.
            currentRequest = null;
    

嗯,看重点有 2 行:

// 在执行之前,设置时间戳,timeStamp,由于我们是增加可读时间,所以就在sampleStart方法里面增加数据了
res.sampleStart();

// 在执行之后,设置其他数据
res.sampleEnd();

增加 setReadableTime 方法调用

public void sampleStart() {
        if (startTime == 0) {
            long currentTime = currentTimeInMillis();
            setStartTime(currentTime);
            setReadableTime(currentTime);
        } else {
            log.error("sampleStart called twice", new Throwable(INVALID_CALL_SEQUENCE_MSG));
        }
    }
public void setReadableTime(long currentTime) {
    LocalDateTime localDateTime = Instant.ofEpochMilli(currentTime).atZone(ZoneId.systemDefault()).toLocalDateTime();
    StringBuilder sb = new StringBuilder();
    sb.append(localDateTime.getYear()).
            append("-").append(localDateTime.getMonthValue())
            .append("-").append(localDateTime.getDayOfMonth())
            .append("-").append(localDateTime.getHour())
            .append("-").append(localDateTime.getMinute())
            .append("-").append(localDateTime.getSecond())
            .append("-").append(localDateTime.getNano()/1000000);
    this.readableTime = sb.toString();
}

GUI 模式


第一张图是说在执行 jmeter 界面中可以设置结果的位置,
第二张图是说在结果中要显示哪些列,其中 Save ReadableTime 是新增加的选项,
而这个字符串是写在 messages.properties 文件中的,
因为开始我以为是用 this.getClass().getClassLoader().getResourceAsStream 方法读取的,
找了半天也没有找到,实际它是在下面的代码中进行读取的,读取的结果存放在 ResourceBundle 中

public class JMeterUtils implements UnitTestManager {
    public static void setLocale(Locale loc) {
        log.info("Setting Locale to {}", loc);
        /*
         * See bug 29920. getBundle() defaults to the property file for the
         * default Locale before it defaults to the base property file, so we
         * need to change the default Locale to ensure the base property file is
         * found.
         */
        Locale def = null;
        boolean isDefault = false; // Are we the default language?
        if (loc.getLanguage().equals(ENGLISH_LANGUAGE)) {
            isDefault = true;
            def = Locale.getDefault();
            // Don't change locale from en_GB to en
            if (!def.getLanguage().equals(ENGLISH_LANGUAGE)) {
                Locale.setDefault(Locale.ENGLISH);
            } else {
                def = null; // no need to reset Locale
            }
        }
        if ("ignoreResources".equals(loc.toString())){ // $NON-NLS-1$
            log.warn("Resource bundles will be ignored");
            ignoreResources = true;
            // Keep existing settings
        } else {
            ignoreResources = false;
            ResourceBundle resBund = ResourceBundle.getBundle("org.apache.jmeter.resources.messages", loc); // $NON-NLS-1$
            resources = resBund;
            locale = loc;
            final Locale resBundLocale = resBund.getLocale();
            if (!isDefault && !resBundLocale.equals(loc)) {
                // Check if we at least found the correct language:
                if (resBundLocale.getLanguage().equals(loc.getLanguage())) {
                    log.info("Could not find resources for '{}', using '{}'", loc, resBundLocale);
                } else {
                    log.error("Could not find resources for '{}'", loc);
                }
            }
        }

保存结果的动作在 CSVSaveService 中,分两部分,


public static String printableFieldNamesToString(
        SampleSaveConfiguration saveConfig) {
    StringBuilder text = new StringBuilder();
    String delim = saveConfig.getDelimiter();

    appendFields(saveConfig.saveTimestamp(), text, delim, TIME_STAMP);
    appendFields(saveConfig.saveReadableTime(), text, delim, READABLE_TIME);
public static String resultToDelimitedString(SampleEvent event,
            SampleResult sample,
            SampleSaveConfiguration saveConfig,
            final String delimiter) {
        StringQuoter text = new StringQuoter(delimiter.charAt(0));
        if (saveConfig.saveTimestamp()) {
            if (saveConfig.printMilliseconds()) {
                text.append(sample.getTimeStamp());
            } else if (saveConfig.threadSafeLenientFormatter() != null) {
                String stamp = saveConfig.threadSafeLenientFormatter().format(
                        new Date(sample.getTimeStamp()));
                text.append(stamp);
            }
        }

        if (saveConfig.saveReadableTime()) {
            text.append(sample.getReadableTime());
        }


↙↙↙阅读原文可查看相关链接,并与作者交流