• 使用 W3C Performance 对象通过 R 和 JavaScript 将浏览器内的性能数据可视化[转]


    当考虑 Web 性能指标时,需要关注的目标数字应该是从您自己的用户那里获得的实际用户指标。最常见的方法是利用 Splunk 之类的工具来分析您的机器数据,该工具支持您分析和可视化您的访问权限和错误日志。利用这些工具,您可以收集某些方面的性能数据,比如读取资产的文件 I/O 时间,以及 API 请求的访问时间。但是,您仍然需要推断客户端性能数据,将信号调用方在某些高级的检查点上,或者只利用类似 WebPagetest 的工具运行综合测试。现在,W3C 已将 API 标准化,用户可以通过使用 Performance 对象(该对象对于所有现代浏览器中的 Windows 对象而言是一个本机对象)捕获并报告浏览器内的性能数据。

    捕获并报告浏览器内性能数据的 API

    在 2010 年年末,万维网联盟 (W3C) 建立了一个新的工作组,即 Web 性能工作组,该工作组提供了用来测量用户代理特性和 API 的应用程序性能的各个方面的方法。该小组还开发了一个支持将浏览器暴露给 JavaScript 的 API,这是一个关键的 Web 性能指标。

    在这个 API 中,该工作组创建了大量的新对象和事件,可量化性能指标和优化性能。总的说来,这些对象和界面包括:

    • Performance 对象:暴露多个对象,比如 PerformanceNavigationPerformanceTiming 和MemoryInfo,并能记录高精度时间(high resolution time),从而获得亚毫秒级计时。
    • Page Visibility API:使您能够确定某个给定页面是可见的还是隐藏的,从而能够优化动画的内存使用,或优化用于轮询操作的网络资源。

    使用这些对象和界面捕获浏览器内的性能指标并将它们可视化。

    Performance 对象

    如果在 JavaScript 控制台中键入 window.performance,则会返回一个类型为 Performance 的对象,以及该对象所暴露的一些对象和方法。目前,标准的对象集包含:

    • window.performance.timing 用于类型 PerformanceTiming
    • window.performance.navigation 用于类型 PerformanceNavigation
    • window.performance.memory 用于类型 MemoryInfo(仅适用于 Chrome 浏览器)

    图 1 显示了 Performance 对象的屏幕截图,可展开该对象来显示 PerformanceTiming 对象及其属性。

    图 1. Performance 对象

    已扩展的 Performance 对象

    Performance 对象被显示在控制台中,随它一起显示的还有展开的 PerformanceTiming 对象。

    PerformanceTiming 对象

    PerformanceTiming 对象是以公共属性的形式暴露的,在浏览器中执行检索和呈现内容的步骤中,它是一个关键指标。表 1 显示了与PerformanceTiming 对象中的每一个属性相对应的描述。

    表 1. PerformanceTiming 对象属性
    对象属性描述
    navigationStart 在导航开始的时候、在浏览器开始卸载前一页(如果有这样的页面)的时候,或者在开始提取内容的时候,捕获所需的数据。它将包含 unloadEventStart 数据或 fetchStart 数据。要想跟踪端到端的时间,可从使用这个值开始。
    unloadEventStartunloadEventEnd 在浏览器开始卸载前一页或已完成前一页的卸载的时候,捕获所需的数据(如果相同域中有前一页需要卸载的话)。
    domainLookupStartdomainLookupEnd 在浏览器开始和完成针对所请求内容的 DNS 查找时,捕获所需的数据。
    redirectStart/redirectEnd 在浏览器开始和完成任何 HTTP 重定向时捕获所需的数据。
    connectStart/connectEnd 在浏览器开始和完成建立当前页面的远程服务器 TCP 连接时捕获所需的数据。
    fetchStart 在浏览器首次开始检查用于所请求资源的缓存时捕获所需的数据。
    requestStart 在浏览器通过发送 HTTP 请求来获得所请求的资源时捕获所需的数据。
    responseStart/responseEnd 在浏览器首次进行注册并完成注册收到服务器响应时捕获所需的数据。
    domLoading/domComplete 在文档开始和完成加载时捕获所需的数据。
    domContentLoadedEventEnd/domContentLoadedEventStart 在文档的 DOMContentLoaded 开始和完成加载时捕获所需的数据,这相当于浏览器已完成所有内容的加载并运行页面中包含的所有脚本。
    domInteractive 在页面的 Document.readyState 属性变为 interactive 时捕获所需的数据,这会导致触发 readystatechange 事件。
    loadEventStart/loadEventEnd 在加载事件触发前和加载事件触发后立刻捕获所需的数据。

    要想将上述步骤及其相应内容的顺序更好地可视化,请参见图 2。

    图 2. 可视化 PerformanceTiming 属性的顺序

    可视化 PerformanceTiming 属性的顺序

     

    Performance 导航

    图 3 显示了包含已展开的 PerformanceNavigation 对象的 Performance 对象。

    图 3. PerformanceNavigation 对象

    PerformanceNavigation 对象

    请注意,导航对象有两个只读属性:redirectCount 和 type。顾名思义,redirectCount 属性是 HTTP 重定向的数量,浏览器根据它们来获取当前页面。

    HTTP 重定向是 Web 性能的一个重要因素,因为它们会导致每一次重定向都需要执行一次完整的 HTTP 往返过程。原始请求是从 Web 服务器返回的,作为包含新位置路径的 301 或 302。然后,浏览器必须初始化一个新的 TCP 连接,并发送一个新请求来获得新位置。这一附加步骤为原始资源请求增加了额外的延迟。

    redirectCount 属性如清单 1 所示。

    清单 1. 访问 redirectCount 属性
    >>> performance.navigation.redirectCount
    0

    导航对象的另一个属性是 typenavigation.type 属性是用下列常量表示的 4 个值中的一个:

    • TYPE_NAVIGATE 的值为 0,表示可通过单击一个链接、提交表单或直接在地址栏中输入 URL 导航到当前页面。
    • TYPE_RELOAD 的值为 1,表示通过重载操作到达当前页面。
    • TYPE_BACK_FORWARD 的值为 2,表示通过使用浏览器历史记录、使用 back 或 forward 按钮、以编程方式,或者通过浏览器的历史对象来导航到页面。
    • TYPE_RESERVED 的值为 255,它是其他任何导航类型的全方位指示。

    信息汇总

    要想使用这些对象来捕获和可视化客户端性能指标,可先创建一个 JavaScript 库,收集 PerformanceTiming 数据,并将这些数据发送到某个端点进行收集和分析。查看这个 JavaScript 库,该库恰好用于完成这项工作。

    perfLogger.js 脚本使用了 Performance 对象。创建一个名为 perfLogger 的命名空间,并声明一些局部变量来保存根据 PerformanceTiming 属性推测的值。

    您可以通过使用这些示例和下面这些模式来计算时间:

    • 要计算认知时间(perceived time) — 从 timing.navigationStart 中减去当前时间。
    • 要计算到达页面过程中所经历的所有重定向时间 — 从 timing.redirectStart 中减去 timing.redirectEnd
    • 要想获得执行 DNS 查找所用的时间 — 从 timing.domainLookupStart 中减去 timing.domainLookupEnd,要想获得呈现页面所用的时间,请从 xs 中减去当前时间。

    声明并初始化局部变量后,使用 public getter 函数从命名空间暴露它们,如清单 2 所示。

    清单 2. 暴露 public getter 函数的局部变量

    var perfLogger = function(){
        var serverLogURL = "/lib/savePerfData.php",
        loggerPool = [],
        _pTime = Date.now() - performance.timing
    .navigationStart || 0,
        _redirTime = performance.timing.redirectEnd 
    - performance.timing.redirectStart || 0,
            _cacheTime = performance.timing.domainLookupStart 
    - performance.timing.fetchStart || 0,
            _dnsTime = performance.timing.domainLookupEnd 
    - performance.timing.domainLookupStart || 0,
            _tcpTime = performance.timing.connectEnd 
    - performance.timing.connectStart || 0,
            _roundtripTime = performance.timing.responseEnd 
    - performance.timing.connectStart || 0,
            _renderTime = Date.now() - performance.timing
    .domLoading || 0;
    
    //expose derived performance data
        perceivedTime: function(){
            return _pTime;
        }, 
        redirectTime: function(){
            _redirTime;
        }, 
        cacheTime: function(){
            return _cacheTime;
        }, 
        dnsLookupTime: function(){
            return _dnsTime;
        },
        tcpConnectionTime: function(){
            return _tcpTime;
        },
        roundTripTime: function(){
            return _roundtripTime;
        },
        pageRenderTime: function(){
            return _renderTime;
        },
        
    
    }

    您可以从命名空间访问属性,如清单 3 所示。

    清单 3. 从命名空间访问属性

    perfLogger.pageRenderTime
    perfLogger. roundTripTime
    perfLogger. tcpConnectionTime
    perfLogger. dnsLookupTime
    perfLogger. cacheTime
    perfLogger. redirectTime
    perfLogger. perceivedTime

    在命名空间中,函数 logToServer 将指标重新写回您在变量 serverLogURL 中定义的端点,如清单 4 所示。

    清单 4. logToServer 函数
    function logToServer(id){
                var params = "data=" + JSON.stringify(jsonConcat
    (loggerPool[id],TestResults.prototype));
                console.log(params)
                var xhr = new XMLHttpRequest();
                xhr.open("POST", serverLogURL, true);
                xhr.setRequestHeader("Content-type", 
    "application/x-www-form-urlencoded");
                xhr.setRequestHeader("Content-length", params.length);
                xhr.setRequestHeader("Connection", "close");
                xhr.onreadystatechange = function()
                  {
                if (xhr.readyState==4 && xhr.status==200)
                {
                   console.log('log written');
                }
             };
           xhr.send(params);    
        }

    perfLogger.js 库还提供了一些基准测试功能,您可以在其中测试 JavaScript 的专用数据块,甚至可以运行一组花费 N 时间量的测试来执行真正的基准测试。

    perfLogger.js 库的完整源代码如清单 5 所示。

    清单 5. perfLogger.js 库的完整源代码
    var perfLogger = function(){
        var serverLogURL = "/lib/savePerfData.php",
            loggerPool = [],
            _pTime = Date.now() - performance.timing.navigationStart 
    || 0,
            _redirTime = performance.timing.redirectEnd 
    - performance.timing.redirectStart || 0,
            _cacheTime = performance.timing.domainLookupStart 
    - performance.timing.fetchStart || 0,
            _dnsTime = performance.timing.domainLookupEnd 
    - performance.timing.domainLookupStart || 0,
            _tcpTime = performance.timing.connectEnd 
    - performance.timing.connectStart || 0,
            _roundtripTime = performance.timing.responseEnd 
    - performance.timing.connectStart || 0,
            _renderTime = Date.now() - performance.timing.domLoading 
    || 0;
            
            function TestResults(){};
            TestResults.prototype.perceivedTime = _pTime;
            TestResults.prototype.redirectTime = _redirTime;
            TestResults.prototype.cacheTime = _cacheTime;
            TestResults.prototype.dnsLookupTime = _dnsTime;
            TestResults.prototype.tcpConnectionTime = _tcpTime;
            TestResults.prototype.roundTripTime = _roundtripTime;
            TestResults.prototype.pageRenderTime = _renderTime;
            
            function jsonConcat(object1, object2) {
             for (var key in object2) {
              object1[key] = object2[key];
             }
             return object1;
            }
                                
            function calculateResults(id){
                loggerPool[id].runtime = loggerPool[id].stopTime 
    - loggerPool[id].startTime;
            }
            
            function setResultsMetaData(id){
                loggerPool[id].url = window.location.href;
                loggerPool[id].useragent = navigator.userAgent;
            }
            
            function drawToDebugScreen(id){
                var debug = document.getElementById("debug")
                var output = formatDebugInfo(id)
                if(!debug){
                    var divTag = document.createElement("div");
                    divTag.id = "debug";
                    divTag.innerHTML = output
                    document.body.appendChild(divTag);           
                }else{
                    debug.innerHTML += output
                }
            }
    
            function logToServer(id){
                var params = "data=" + JSON.stringify(jsonConcat(
        loggerPool[id],TestResults.prototype));
                console.log(params)
                var xhr = new XMLHttpRequest();
                xhr.open("POST", serverLogURL, true);
                xhr.setRequestHeader("Content-type", 
    "application/x-www-form-urlencoded");
                xhr.setRequestHeader("Content-length", params.length);
                xhr.setRequestHeader("Connection", "close");
                xhr.onreadystatechange = function()
                  {
                  if (xhr.readyState==4 && xhr.status==200)
                    {
                        //console.log(xhr.responseText);
                    }
                  };
                xhr.send(params);    
            }
            
            function formatDebugInfo(id){
                var debuginfo = "<p><strong>" 
    + loggerPool[id].description + "</strong><br/>";    
                if(loggerPool[id].avgRunTime){
                    debuginfo += "average run time: " + loggerPool[id]
    .avgRunTime + "ms<br/>";
                }else{
                    debuginfo += "run time: " + loggerPool[id].runtime 
    + "ms<br/>";
                }
                debuginfo += "path: " + loggerPool[id].url 
    + "<br/>";
                debuginfo += "useragent: " +  loggerPool[id].useragent 
    + "<br/>";
                
                debuginfo += "Perceived Time: " + 
    loggerPool[id].perceivedTime + "<br/>";
                debuginfo += "Redirect Time: " + 
    loggerPool[id].redirectTime + "<br/>";
                debuginfo += "Cache Time: " + 
    loggerPool[id].cacheTime + "<br/>";
                debuginfo += "DNS Lookup Time: " + 
    loggerPool[id].dnsLookupTime + "<br/>";
                debuginfo += "tcp Connection Time: " + 
    loggerPool[id].tcpConnectionTime + "<br/>";
                debuginfo += "roundTripTime: "+ 
    loggerPool[id].roundTripTime + "<br/>";
                debuginfo += "pageRenderTime: " + 
    loggerPool[id].pageRenderTime + "<br/>";
                debuginfo += "</p>";
                return debuginfo
            }
            
        return {        
        startTimeLogging: function(id, descr,drawToPage
    ,logToServer){
            loggerPool[id] = new TestResults();
            loggerPool[id].id = id;
            loggerPool[id].startTime =  performance.now();
            loggerPool[id].description = descr;
            loggerPool[id].drawtopage = drawToPage;
            loggerPool[id].logtoserver = logToServer
        },
        
        stopTimeLogging: function(id){
            loggerPool[id].stopTime =  performance.now();
            calculateResults(id);
            setResultsMetaData(id);    
            if(loggerPool[id].drawtopage){
                drawToDebugScreen(id);
            }
            if(loggerPool[id].logtoserver){
                logToServer(id);
            }
        },
        
        logBenchmark: function(id, timestoIterate, func, debug, log){
            var timeSum = 0;
            for(var x = 0; x < timestoIterate; x++){
                perfLogger.startTimeLogging(id, "benchmarking "+ func, 
        false, false);
                func();
                perfLogger.stopTimeLogging(id)
                timeSum += loggerPool[id].runtime
            }
            loggerPool[id].avgRunTime = timeSum/timestoIterate
            if(debug){
                    drawToDebugScreen(id)
            }
            if(log){
                    logToServer(id)
            }
        },
        
        //expose derived performance data
        perceivedTime: function(){
            return _pTime;
        }, 
        redirectTime: function(){
            _redirTime;
        }, 
        cacheTime: function(){
            return _cacheTime;
        }, 
        dnsLookupTime: function(){
            return _dnsTime;
        },
        tcpConnectionTime: function(){
            return _tcpTime;
        },
        roundTripTime: function(){
            return _roundtripTime;
        },
        pageRenderTime: function(){
            return _renderTime;
        },
        
        showPerformanceMetrics: function(){
            this.startTimeLogging("no_id", "draw perf data to page"
    ,true,true);
            this.stopTimeLogging("no_id");
            
        }
        
    }
    }();
    
    performance.now = (function() {
      return performance.now       ||
             performance.mozNow    ||
             performance.msNow     ||
             performance.oNow      ||
             performance.webkitNow ||
             function() { return new Date().getTime(); };
    })();
    View Code

    实现和可视化

    要想使用 perfLogger.js 脚本可视化浏览器内的性能,可以将该脚本嵌入页面中,在页面的 onload 事件上,您可以将性能数据推送回端点,将它们保存为一个平面文件。GitHub 中的 perfLogger 项目附带了一个 PHP 脚本,名为 savePerfData.php,该脚本恰好提供了此功能。该文件的源代码如清单 6 所示。

    清单 6. savePerfData.php 的源代码
    <?php
    require("util/fileio.php");
    
    $logfile = "log/runtimeperf_results.txt";
    $benchmarkResults = formatResults($_POST["data"]);
    
    saveLog($benchmarkResults, $logfile);
    
    function formatResults($r){
        print_r($r);
        $r = stripcslashes($r);
        $r = json_decode($r);
        if(json_last_error() > 0){
            die("invalid json");
        }
        return($r);
    }
    
    function formatNewLog($file){
        $headerline = "IP, TestID, StartTime, StopTime, RunTime, 
    URL, UserAgent, PerceivedLoadTime, PageRenderTime, RoundTripTime, 
    TCPConnectionTime, DNSLookupTime, CacheTime, RedirectTime";
        appendToFile($headerline, $file);
    }
    
    
    function saveLog($obj, $file){
        if(!file_exists($file)){
            formatNewLog($file);
        }
        $obj->useragent = cleanCommas($obj->useragent);
        $newLine = $_SERVER["REMOTE_ADDR"] . "," . $obj->id .","
    . $obj->startTime . "," . $obj->stopTime . "," . $obj->runtime . ","
    . $obj->url . "," . $obj->useragent . $obj->perceivedTime . ","
    . $obj->pageRenderTime . "," . $obj->roundTripTime . ","
    . $obj->tcpConnectionTime . "," . $obj->dnsLookupTime . "," 
    . $obj->cacheTime . "," . $obj->redirectTime;
        appendToFile($newLine, $file);
    }
    
    function cleanCommas($data){
        return implode("", explode(",", $data));
    }
    
    ?>
    View Code

    这个 PHP 实际上将 perfLogger.js 发送的 POST 数据保存为一个平面文件,格式如清单 7 所示。

    清单 7. perfLogger.js 发送的 POST 数据
    IP, TestID, StartTime, StopTime, RunTime, URL, UserAgent, 
    PerceivedLoadTime, PageRenderTime, RoundTripTime, TCPConnectionTime, 
    DNSLookupTime, CacheTime, RedirectTime
    75.149.106.130,page_render,1341243219599,1341243220218,619
    ,http://www.tom-barker.com/blog/?p=x,Mozilla/5.0 (Macintosh; 
    Intel Mac OS X 10.5; rv:13.0) Gecko/20100101 
    Firefox/13.0.1790,261,-2,36,0,-4,0

    此时此刻,您可以看到有一些很好的数据点值得关注,例如:

    • 用户代理所用的平均加载时间
    • 在平均加载时间方面,HTTP 事务流程的哪一部分所用的时间最多
    • 总体的加载时间分布

    在 GitHub 存储库中,还有一个 R 脚本,名为 runtimePerformance.R,该脚本将会摄取您生成的这个日志文件,并实现数据可视化(参见清单 8)。

    清单 8. 名为 runtimePerformance.R 的 R 脚本
    dataDirectory <- "/Applications/MAMP/htdocs/lab/log/"
    chartDirectory <- "/Applications/MAMP/htdocs/lab/charts/"
    testname = "page_render"
    
    perflogs <- read.table(paste(dataDirectory, "runtimeperf
    _results.csv", sep=""), header=TRUE, sep=",")
    perfchart <- paste(chartDirectory, "runtime_",testname, ".
    pdf", sep="")
    
    loadTimeDistrchart <- paste(chartDirectory, 
    "loadtime_distribution.pdf", sep="")
    requestBreakdown <- paste(chartDirectory, 
    "avgtime_inrequest.pdf", sep="")
    loadtime_bybrowser <- paste(chartDirectory, 
    "loadtime_bybrowser.pdf", sep="")
    
    pagerender <- perflogs[perflogs$TestID == "page_render",]
    df <- data.frame(pagerender$UserAgent, pagerender$RunTime)
    df <- by(df$pagerender.RunTime, df$pagerender.UserAgent, mean)
    df <- df[order(df)]
    
    pdf(perfchart, width=10, height=10)
    opar <- par(no.readonly=TRUE)
        par(las=1, mar=c(10,10,10,10))        
        barplot(df, horiz=TRUE)
    par(opar)    
    dev.off()
    
    
    getDFByBrowser<-function(data, browsername){
        return(data[grep(browsername, data$UserAgent),])
    }
    
    
    printLoadTimebyBrowser <- function(){
        chrome <- getDFByBrowser(perflogs, "Chrome")
        firefox <- getDFByBrowser(perflogs, "Firefox")
         ie <- getDFByBrowser(perflogs, "MSIE")
    
        meanTimes <- data.frame(mean(chrome$PerceivedLoadTime),
     mean(firefox$PerceivedLoadTime), mean(ie$PerceivedLoadTime))
        colnames(meanTimes) <- c("Chrome", "Firefox",
    "Internet Explorer")
        pdf(loadtime_bybrowser, width=10, height=10)
            barplot(as.matrix(meanTimes), main="Average Perceived Load 
    Time
    By Browser", ylim=c(0, 600), ylab="milliseconds")
        dev.off()
    }
    
    
    pdf(loadTimeDistrchart, width=10, height=10)
        hist(perflogs$PerceivedLoadTime, main="Distribution of 
    Perceived 
    Load Time", xlab="Perceived Load Time in Milliseconds", 
    col=c("#CCCCCC"))
    dev.off()
    
    avgTimeBreakdownInRequest <- function(){
    
    #expand exponential notation
    options(scipen=100, digits=3)
    
    #set any negative values to 0
    perflogs$PageRenderTime[perflogs$PageRenderTime < 0] <- 0
    perflogs$RoundTripTime[perflogs$RoundTripTime < 0] <- 0
    perflogs$TCPConnectionTime[perflogs$TCPConnectionTime < 0] <- 0
    perflogs$DNSLookupTime[perflogs$DNSLookupTime < 0] <- 0
    
    #capture avg times
    avgTimes <- data.frame(mean(perflogs$PageRenderTime), 
    mean(perflogs$RoundTripTime), mean(perflogs$TCPConnectionTime),
    mean(perflogs$DNSLookupTime))
    colnames(avgTimes) <- c("PageRenderTime", "RoundTripTime", 
    "TCPConnectionTime", "DNSLookupTime")
    pdf(requestBreakdown, width=10, height=10)
    opar <- par(no.readonly=TRUE)
        par(las=1, mar=c(10,10,10,10))        
        barplot(as.matrix(avgTimes), horiz=TRUE, main="Average Time 
    Spent
    During HTTP Request", xlab="Milliseconds")
    par(opar)    
    dev.off()
        
    }
    
    printLoadTimebyBrowser()
    avgTimeBreakdownInRequest()

    这个 R 脚本附带了一些内置的功能,例如 printLoadTimebyBrowser 和 avgTimeBreakdownInRequest。图 4 是 printLoadTimebyBrowser 输出的屏幕截图。

    图 4. printLoadTimebyBrowser 的输出

    输出

    图 5 是 avgTimeBreakdownInRequest 的屏幕截图。

    图 5. avgTimeBreakdownInRequest 代码的输出code

    输出

    在将性能数据加载到 R 会话中之后,所有已摄取的数据指标都存储在一个名为 perflogs 的数据帧内,这样您就可以访问单独的列,如清单 9 所示。

    清单 9. 已摄取的指标存储在名为 perflogs 的数据帧内
    perflogs$PerceivedLoadTime
    perflogs$ PageRenderTime
    perflogs$RoundTripTime
    perflogs$TCPConnectionTime
    perflogs$DNSLookupTime
    perflogs$UserAgent

    该代码支持您开始执行一些探索性的数据分析,比如创建柱状图来查看用户群的感知加载时间的分布,如清单 10 所示。

    清单 10. 用户群的感知加载时间
    hist(perflogs$PerceivedLoadTime, main="Distribution of 
    Perceived Load Time", xlab="Perceived Load Time in Milliseconds", 
    col=c("#CCCCCC"))
    dev.off()

    图 6 显示了用户群的感知加载时间分布柱状图。

    图 6. 用户群的感知加载时间分布柱状图

    用户群的感知加载时间分布柱状图

    结束语

    本文帮助您更好地了解了 Performance 对象中一些功能,并模拟了如何使用可从该对象收集的浏览器内指标。通过这里介绍的模型,您可以从实际用户群中捕获真正的用户指标,这是您可以收集并跟踪的最有价值的性能指标类型。

    如果愿意的话,您还可以使用该模型中的 perfLogger.js 和所有实用程序文件。您可以随时发表您自己的意见以及对该项目的更改。



    源文链接

  • 相关阅读:
    从.Net到Java学习第十篇——Spring Boot文件上传和下载
    Access denied for user 'root'@'localhost' (using password:YES) Mysql5.7
    从.Net到Java学习第八篇——SpringBoot实现session共享和国际化
    从.Net到Java学习第九篇——SpringBoot下Thymeleaf
    从.Net到Java学习第七篇——SpringBoot Redis 缓存穿透
    从.Net到Java学习第六篇——SpringBoot+mongodb&Thymeleaf&模型验证
    从.Net到Java学习第五篇——Spring Boot &&Profile &&Swagger2
    从.Net到Java学习第四篇——spring boot+redis
    从.Net到Java学习第三篇——spring boot+mybatis+mysql
    从.Net到Java学习第一篇——开篇
  • 原文地址:https://www.cnblogs.com/jiuyi/p/6508280.html
Copyright © 2020-2023  润新知