Qt+QtWebApp开发笔记(二):http服务器日志系统介绍、添加日志系统至Demo测试

news2024/12/29 10:52:15

若该文为原创文章,转载请注明原文出处
本文章博客地址:https://hpzwl.blog.csdn.net/article/details/130762721

红胖子网络科技博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结合等等)持续更新中…

Qt开发专栏:三方库开发技术

上一篇:《Qt+QtWebApp开发笔记(一):QtWebApp介绍、下载和搭建基础封装http轻量级服务器Demo》
下一篇:敬请期待…


前言

  上一篇使用QtWebApp的基于Qt的轻量级http服务器实现了一个静态网页返回的Demo,网页服务器很重要的就是日志,因为在服务器类上并没有直接返回,所以,本篇先把日志加上。


Demo

  在这里插入图片描述

下载地址

  链接:https://pan.baidu.com/s/1BPVRLS07qk-WPi-txERKbg?pwd=1234


日志系统

生产环境需要查看旧的日志消息,例如两天前的日志消息。

  可以简单地将输出重定向到一个文件(MyFirstWebApp>logfile.txt),但这有两个问题:

  • 在许多系统上,输出重定向有些慢。
  • 日志文件将变得无限大,如果不短时间停止web服务器,就无法防止这种情况发生。

  因此,最好让web服务器自己将所有消息写入文件。这就是记录器模块的作用。
  要将日志模块的源代码包括到项目中,请在项目文件中添加一行:

include(../QtWebApp/QtWebApp/logging/logging.pri)

  这个而模块也是QtWebApp的logging模块,如下:
  在这里插入图片描述
  然后在程序的*.ini文件中添加另一个部分:

[logging]
minLevel=WARNING
bufferSize=100
fileName=../logs/webapp1.log
maxSize=1000000
maxBackups=2
timestampFormat=dd.MM.yyyy hh:mm:ss.zzz
msgFormat={timestamp} {typeNr} {type} {thread} {msg}

  日志级别有:DEBUG(别名ALL)、INFO、WARN或WARNING、CRITICAL(别名ERROR)、FATAL。信息级别由Qt 5.5引入。
  上面的示例配置启用线程本地缓冲区,**这些缓冲区将不太重要的消息保留在内存中,直到出现警告或严重错误。**然后,错误消息与收集到的低级消息一起写入日志文件。只要一切正常,使用缓冲区可以大大减少日志文件的大小。像这样的系统操作员。
  但是,缓冲区的内存和性能成本都很高。收益通常大于成本。要禁用缓冲区,请将bufferSize设置为0。在这种情况下,只有配置了minLevel及以上级别的消息才会写入日志文件。
  如果没有指定文件名,则记录器会写入控制台。日志文件的路径可以是绝对路径,也可以是相对于配置文件的文件夹的路径。maxSize参数限制日志文件的大小(以字节为单位)。当超过此限制时,记录器将启动一个新文件。设置maxBackups指定磁盘上应保留多少旧日志文件。
  时间戳格式设置的作用。QDateTime::toString()的文档以获得对字符的解释,还有更多可用的内容。msgFormat设置指定每条消息的格式。以下字段可用:

  • {timestamp}:创建日期和时间
  • {typeNr}:数字格式的消息类型或级别(0=DEBUG, 4=INFO, 1=WARNING, 2=CRITICAL, 3=FATAL)
  • {type}:字符串格式的消息类型或级别(DEBUG, INFO, WARNING, CRITICAL, FATAL)
  • {thread}:线程的ID号
  • {msg}:消息文本
  • {xxx}:可以自己定义的任何记录器变量QT 5.0及更新版本在调试模式下有一些附加变量:
{file}:Filename of source code where the message was generated
{function}:Function where the message was generated
{line}:Line number where the message was generated

  Qt开发人员将这三个字段添加到他们的框架中。也可以使用\n在消息格式中插入换行符和插入  制表符。上述所有变量也可以在日志消息中使用,例如:

qCritical("An error occured in {file}: out of disk space");

  需要一个指向FileLogger实例的全局指针,以便整个程序都可以访问它。首先添加到global.h:

#include "httpsessionstore.h"
#include "staticfilecontroller.h"
#include "templatecache.h"
#include "filelogger.h"

using namespace stefanfrings;

/** Storage for session cookies */
extern HttpSessionStore* sessionStore;

/** Controller for static files */
extern StaticFileController* staticFileController;

/** Cache for template files */
extern TemplateCache* templateCache;

/** Redirects log messages to a file */
extern FileLogger* logger;

#endif // GLOBAL_H

  global.cpp:

#include "global.h"

HttpSessionStore* sessionStore;
StaticFileController* staticFileController;
TemplateCache* templateCache;
FileLogger* logger;

  在main.cpp中,配置FileLogger的实例:

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    QString configFileName=searchConfigFile();

    // Configure logging
    QSettings* logSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    logSettings->beginGroup("logging");
    logger=new FileLogger(logSettings,10000,&app);
    logger->installMsgHandler();

    // Log the library version
    qDebug("QtWebApp has version %s",getQtWebAppLibVersion());

    // Session store
    QSettings* sessionSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    sessionSettings->beginGroup("sessions");
    sessionStore=new HttpSessionStore(sessionSettings,&app);

    // Static file controller
    QSettings* fileSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    fileSettings->beginGroup("files");
    staticFileController=new StaticFileController(fileSettings,&app);

    // Configure template cache
    QSettings* templateSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    templateSettings->beginGroup("templates");
    templateCache=new TemplateCache(templateSettings,&app);

    // HTTP server
    QSettings* listenerSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    listenerSettings->beginGroup("listener");
    new HttpListener(listenerSettings,new RequestMapper(&app),&app);

    return app.exec();
}

  数字10000是以毫秒为单位的刷新间隔,记录器使用它来重新加载配置文件。因此,可以在程序运行时编辑任何记录器设置,并且更改在几秒钟后生效,而无需重新启动服务器。如果不希望自动重新加载,请使用值0。
  给了一个示例代码,用于查询和记录库的版本号。一些人要求添加该功能。
不要忘记创建一个空文件夹MyFirstWebApp/logs。记录器本身不会创建文件夹。
  现在可以启动应用程序并查看会发生什么。因为程序没有错误,所以日志文件保持为空。但  可以看到控制台窗口中的输出已降至最低:
  在这里插入图片描述

  让在logincontroller.cpp中插入一条qCritical()消息,然后可以看到日志缓冲区工作:
  然后打开URLhttp://localhost:8080/login?username=test&password=wrong.
  再次查看日志文件,它就在那里:
  在这里插入图片描述

  现在通过将min Level降低到DEBUG来进行另一个测试。保存ini文件,等待10秒,然后打开URLhttp://localhost:8080/hello.再次检查日志文件。可以看到,尽管没有发生错误,但现在所有的调试消息都已写入。因此,在不重新启动程序的情况下更改日志级别可以很好地工作。
  在这里插入图片描述

  其实这个很容易看出来,是直接对qt的几个日志等级进行了(PS:这个日志库还不错,installMsgHandler可以截断qDebug等相关的错误信息,可以直接无缝使用到每一个qt项目中,有这个兴趣可以试一试)。
  在这里插入图片描述


记录器变量

  写到记录器支持用户定义的变量。这些变量是线程本地的,在清除它们之前一直保留在内存中。对于web应用程序,在每条消息中记录当前用户的名称可能很有用。向requestmapper.cpp添加代码以设置记录器变量:

void RequestMapper::service(HttpRequest& request, HttpResponse& response) {
    QByteArray path=request.getPath();
    qDebug("RequestMapper: path=%s",path.data());
    HttpSession session=sessionStore->getSession(request,response,false);
    QString username=session.get("username").toString();
    logger->set("currentUser",username);
    ...
}

  通过这种方式,请求映射器在将请求传递给控制器类之前,为所有传入的HTTP请求查询调用用户的名称。
  现在可以修改ini文件以使用该变量:

msgFormat={timestamp} {typeNr} {type} {thread} User:{currentUser} {msg}

  运行程序并打开URLhttp://localhost:8080/login?username=test&password=hello两次。然后再次检查日志文件:
  在这里插入图片描述

  在用户登录之前,可以看到变量{currentUser}为空。然后,所有以下请求都会以该用户的名称记录。
  注意:在RequestMapper类中放置了许多静态资源(logger、sessionStore、staticFileController、templateCache)。在实际应用程序中,建议创建一个单独的类,例如名称为“Globals”的类,这样每个人都知道在哪里可以找到这样的资源。或者按照在Demo1项目中的例子,将它们放在任何类之外的cpp源文件中。

日志缓冲区和线程池

  由于线程被重新用于后续的HTTP请求,记录器可能会输出更多的细节。例如,假设第一个成功的HTTP请求会产生一些隐藏的调试消息,然后由同一线程处理的第二个请求会产生错误。然后,日志文件将包含错误消息以及所有缓冲的调试消息。但其中一些来自以前的HTTP请求,并不需要它。
  要清除两个HTTP请求之间的缓冲区,请添加到requestmapper.cpp:

void RequestMapper::service(HttpRequest& request, HttpResponse& response) {
    ...
    else {
        response.setStatus(404,"Not found");
        response.write("The URL is wrong, no such document.");
    }

    qDebug("RequestMapper: finished request");
    logger->clear(true,true);
}

  因此,每当HTTP请求的处理完成时,都要清理记录器的内存。当同一个线程处理下一个请求时,它将以空缓冲区开始。(碰到错误则会输出到文件,所以一个http请求完成了,就是其前面的日志都是无错误,所以可以清空了)。

双文件记录器

  该项目还包含一个DualFileLogger类,可用于并行生成两个日志文件。这可能对以下设置组合有用:

  • 主记录日志文件
minLevel=INFO
bufferSize=0
  • 第二日志文件
minLevel=ERROR (or WARNING)
bufferSize=100

  这样,主日志文件就不包含调试消息。但是,当发生错误时,辅助日志文件会包含该错误以及多达100条相关的调试消息。如果错误消息本身无法识别问题原因,则此文件特别有用。

总结

  这个日志logging模块起到的最大作用,是因为在QtWebApp三方源码中的qDebug,qWarn,QFatal等相关系统直接输出到控制台的,使用该日志则截断才可以获取httpservice模块以及其他模块中的打印调试信息,而这些信息是在函数返回值中没有体现的。
  为了能查看到三方模块日志,则必须要使用logging模块,或者自己写一个模块去截断,或者直接修改三方源码中的调试信息的代码。
  使用httpservice肯定是最好使用logging模块了。


Demo增量:添加logging日志模块

步骤一:准备代码模板

  准备之前的demo模板:
  在这里插入图片描述

步骤二:拷贝logging模块

  将QtWebApp中的logging,符合模块化设计准则,如下图:
  在这里插入图片描述

  拷贝到的Demo
  在这里插入图片描述

  添加模块进入工程:

# logging模块,QtWebApp自带的三方模块
include ($$PWD/modules/logging/logging.pri)

  在这里插入图片描述
  第三方的模块。

步骤三:添加配置logging的配置文件

  先把上一篇的Demo配置文件加了listener之后就读不出的问题解决了,其实区别关键在下面:
  在这里插入图片描述

  beginGroup就是进入了这一组,这一组拿到key就可以不带前缀。
  在这里插入图片描述

  然后开始添加日志配置,也在httpServerManager,因为配置文件beginGroup之后就是操作单独一组了,这里从第三方源码中也可以看出来:
  在这里插入图片描述

  本次加入logging,也要进行配置文件分组的区分,原来的_pSettings改成_pHttpListenerSettings,然后新增_pLoggingListenerSettings用于配置logging模块的配置实例:

步骤四:新增logging日志代码

  在这里插入图片描述

  在这里插入图片描述

  在这里插入图片描述

步骤五:运行结果

   在这里插入图片描述
  至此,日志加入成功

步骤六:日志配置调整

  在这里插入图片描述

  修改下日志时间:
  在这里插入图片描述

  记录日志则是:
  在这里插入图片描述


Demo源码

HttpServerManager.h

#ifndef HTTPSERVERMANAGER_H
#define HTTPSERVERMANAGER_H

#include <QObject>
#include <QMutex>

#include "httplistener.h"
#include "filelogger.h"

#include "HelloWorldRequestHandler.h"

class HttpServerManager : public QObject
{
    Q_OBJECT
private:
    explicit HttpServerManager(QObject *parent = 0);

public:
    static HttpServerManager *getInstance();

public slots:
    void slot_start();                              // 开启线程
    void slot_stop();                               // 停止线程

private:
    static HttpServerManager *_pInstance;
    static QMutex _mutex;

private:
    bool _running;                                  // 运行状态

private:
    HttpListener *_pHttpListener;                   // http服务监听器
    QSettings *_pHttpListenerSettings;              // http服务器配置文件

    FileLogger *_pFileLogger;                       // 日志记录
    QSettings *_pFileLoggerSettings;                // 日志配置文件

private:
    QString _ip;                // 服务器监听ip(若为空,则表示监听所有ip)
    quint16 _port;              // 服务器监听端口
    int _minThreads;            // 空闲最小线程数
    int _maxThreads;            // 负载最大线程数
    int _cleanupInterval;       // 空线程清空间隔(单位:毫秒)
    int _readTimeout;           // 保持连接空载超时时间(单位:毫秒)
    int _maxRequestSize;        // 最大请求数
    int _maxMultiPartSize;      // 上载文件最大数(单位:字节)
};


#endif // HTTPSERVERMANAGER_H

HttpServerManager.cpp

#include "HttpServerManager.h"
#include <QApplication>

#include <QDir>

#include <QDebug>
#include <QDateTime>
//#define LOG qDebug()<<__FILE__<<__LINE__
//#define LOG qDebug()<<__FILE__<<__LINE__<<__FUNCTION__
//#define LOG qDebug()<<__FILE__<<__LINE__<<QThread()::currentThread()
//#define LOG qDebug()<<__FILE__<<__LINE__<<QDateTime::currentDateTime().toString("yyyy-MM-dd")
#define LOG qDebug()<<__FILE__<<__LINE__<<QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz")

HttpServerManager *HttpServerManager::_pInstance = 0;
QMutex HttpServerManager::_mutex;

HttpServerManager::HttpServerManager(QObject *parent)
    : QObject(parent),
      _pHttpListener(0),
      _pHttpListenerSettings(0),
      _pFileLogger(0),
      _pFileLoggerSettings(0),
      _running(false),
      _port(8088),
      _minThreads(2),
      _maxThreads(10),
      _cleanupInterval(60000),
      _readTimeout(60000),
      _maxRequestSize(100),
      _maxMultiPartSize(1024*1024*1024)
{

}

HttpServerManager *HttpServerManager::getInstance()
{
    if(!_pInstance)
    {
        QMutexLocker lock(&_mutex);
        if(!_pInstance)
        {
            _pInstance = new HttpServerManager();
        }
    }
    return _pInstance;
}

void HttpServerManager::slot_start()
{
    if(_running)
    {
        LOG << "It's running!!!";
        return;
    }
    _running = true;
    LOG << "Succeed to run";

    QString httpServerPath = QString("%1/etc/httpServer.ini").arg(qApp->applicationDirPath());
    LOG << httpServerPath << "exit:" << QFile::exists(httpServerPath);

    // 启动日志几里路
    {
        if(!_pFileLoggerSettings)
        {
            _pFileLoggerSettings = new QSettings(httpServerPath, QSettings::IniFormat);
        }
        _pFileLoggerSettings->beginGroup("logging");

        // 日志不会主动创建文件夹,这里需要补全
        {
            QFileInfo fileInfo(httpServerPath);
            QString dirPath = fileInfo.dir().absolutePath();
            dirPath = QString("%1/%2")
                    .arg(dirPath)
                    .arg(_pFileLoggerSettings->value("fileName").toString());
            dirPath = dirPath.mid(0, dirPath.lastIndexOf("/"));
            QDir dir;
            dir.mkpath(dirPath);
        }
        _pFileLogger = new FileLogger(_pFileLoggerSettings);
        _pFileLogger->installMsgHandler();
    }

    // 启动http的监听
    {
        if(!_pHttpListenerSettings)
        {
            _pHttpListenerSettings = new QSettings(httpServerPath, QSettings::IniFormat);
        }
        _pHttpListenerSettings->beginGroup("listener");
        _pHttpListener = new HttpListener(_pHttpListenerSettings, new HelloWorldRequestHandler);
    }
    LOG;
}

void HttpServerManager::slot_stop()
{
    if(!_running)
    {
        LOG <<"It's not running!!!";
        return;
    }
    _running = false;
    LOG << "Succeed to stop";
}

工程模板v1.1.0

  在这里插入图片描述


入坑

入坑一:日志一直不出来

问题

  日志一直不出来

原因

  在这里插入图片描述

  日志log文件的路径是基于ini配置文件的相对路径

解决

  在这里插入图片描述


上一篇:《Qt+QtWebApp开发笔记(一):QtWebApp介绍、下载和搭建基础封装http轻量级服务器Demo》
下一篇:敬请期待…


若该文为原创文章,转载请注明原文出处
本文章博客地址:https://hpzwl.blog.csdn.net/article/details/130762721

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/544674.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Clickhouse 入门到精通-Clickhouse工作原理

Clickhouse 为什么做查询分析那么快&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f; 因为clickhouse使用了下列方案&#xff1a; clickhouse 数据分区clickhouse 列式存储clickhouse 一级索引&#xff08;主键索引&#…

企业数字转型加速器!居然是他!该不会还有人没用上吧?

随着数字化时代的到来和技术的发展&#xff0c;企业数字化转型已经成为全球企业发展的重要趋势。然而&#xff0c;数字化转型的过程却并非一帆风顺&#xff0c;常常因为 IT 复杂度高、开发周期长等问题而遇到许多挑战&#xff0c;这时候低代码开发平台就能够发挥重要作用。 低代…

我们为什么还要学习Altium Designer?

Altium Designe&#xff08;简称“AD”&#xff09;是电子设计领域中备受推崇的软件工具之一&#xff0c;拥有强大的功能和灵活的设计环境&#xff0c;也是要用最广泛的EDA工具之一&#xff0c;为电子工程师提供了无限可能&#xff0c;但很多工程师学完AD基本操作就转投其他EDA…

支付宝小程序打包成APP

发行——原生App-云打包——填写安卓包的信息&#xff08;安卓证书可在香蕉云编下载&#xff09;——打包——下载APK 第一步&#xff1a;点击菜单栏发行 第二步&#xff1a;选择远程APP-云打包 第三步&#xff1a;在香蕉云编&#xff08;https://www.yunedit.com/&#xff0…

K8S之yaml文件,声明式管理方法

目录 第一章.声明式管理方法 1.1.声明式管理方法 1.2.kubectl create 和 kubectl apply区别 1.3.查看资源配置清单 1.4.解释资源配置清单 1.5.修改资源配置清单并应用 第二章.yaml文件格式 2.1.yaml文件简述 2.2.YAML 语法格式 2.3.查看 api 资源版本标签 2.4.写一个…

MVC中Controller向View传值的几种方式

MVC中Controller向View传值的几种方式 文章目录 MVC中Controller向View传值的几种方式一、ViewModel使用ViewModel 二、ViewData在控制器和视图间使用ViewData传递数据在 ViewDataTest 视图中使用ViewData的数据在视图和部分视图间使用ViewData 三、ViewBag四、TempData五、Ses…

搭建短链服务

目录 一、背景 1.1短链接的优势 1.1.1优点一 1.1.2优点二 1.1.3优点三 1.1.4优点四 1.1.4优点五 二、原理 2.1利用http重定向 3.1实现方案 3.1.1发号器实现 3.1.2存储实现 3.1.3映射实现 3.2架构图 一、背景 短链在互联网中盛行&#xff0c;搭建自己短链平台&…

硬件工程师-MOS管

MOSFET 场效应管 N管 P管 对标三极管 N管 P管 三极管具有功率放大的作用 MOSFET也具有功率作用&#xff0c; 控制级的电流很小 控制信号的内阻大 输出级的电流很大 输出信号的内阻很小 三极管的缺点&#xff1a;流控…

Blender 建模键盘(PS修图、UV贴图、Cycles渲染引擎)

目录 1. 键盘模型1.1 键盘底座1.2 底座细节1.3 logo位置1.4 键盘按键1.5 按键添加1.6 合并按键 2. 贴图、渲染2.1 到PS添加按键文字2.2 保存png图片2.3 图像纹理2.4 UV编辑2.5 添加平面2.6 添加环境纹理2.7 灯光、摄像机2.8 渲染属性2.9 渲染出图 1. 键盘模型 原图 1.1 键盘底…

三、IOC容器(2)

四、IOC操作Bean管理&#xff08;xml注入集合属性&#xff09; 4.在集合里面设置对象类型值 ①Course类 ②Stu类 ③配置xml文件 ④测试 5.把集合注入部分提取出来 在Spring配置文件中引入名称空间 util 2.使用util标签完成list集合注入 ①提取list集合类型的属性注入 <…

如何调整碳化硅 MOSFET 驱动来减少功率损耗

如何调整碳化硅 MOSFET 驱动来减少功率损耗 1.如何减少传导损耗&#xff1f;2.如何减少开关损耗&#xff1f;2.1 关断损耗 (Eoff) 取决于 Rg 和 Vgs-off2.2 开通损耗 (Eon) vs. Rg2.3 开通损耗 Eon 和反向恢复损耗 Err 的米勒效应2.4 对驱动电流的要求 作者&#xff1a;Xiou 参…

销售/回收DSOS254A是德keysight MSOS254A混合信号示波器

Agilent DSOS254A、Keysight MSOS254A、 混合信号示波器&#xff0c;2.5 GHz&#xff0c;20 GSa/s&#xff0c;4 通道&#xff0c;16 数字通道。 ​Infiniium S 系列示波器 信号保真度方面树立新标杆 500 MHz 至 8 GHz 出色的信号完整性使您可以看到真实显示的信号&#xff1…

远程桌面连接不上解决方法

远程桌面连接是一种方便快捷的技术&#xff0c;可以让用户在不同的设备之间共享桌面和访问远程计算机。然而&#xff0c;有时候我们可能会遇到远程桌面连接无法正常连接的问题。在本篇文章中&#xff0c;我们将详细介绍远程桌面连接无法连接的常见原因&#xff0c;并提供相对应…

0601-指针的基础

内存 物理存储器和存储地址空间 物理存储器&#xff1a;实际存在的具体存储器芯片。比如&#xff1a;内存条、RAM芯片、ROM芯片。 存储地址空间&#xff1a;对存储器编码的范围。 编码&#xff1a;对每个物理存储单元&#xff08;一个字节&#xff09;分配一个号码寻址&…

520网络情人节:用双语告白你的女神男神!

网络情人节&#xff08;Network Valentines Day&#xff09;——520、521被喻为“我愿意、我爱你” 的意思&#xff0c;又被称为“结婚吉日”、“表白日”、“撒娇日”、“求爱节”。每年5月20日和5月21日的“网络情人节”也成为了情侣们扎堆登记结婚、隆重举办婚宴 的吉日。 5…

系统分析师经典易错题,解题思路一

数据库通常采用三级模式结构,其中,视图对应外模式,基本表对应模式,存储文件对应内模式。数据的独立性是由DBMS的二级映像功能来保证的。数据的独立性包括数据的物理独立性和数据逻辑独立性。数据的物理独立性是指当数据库的内模式发生改变时,数据的逻辑结构不变。为了保证…

HTML获取SpringBoot从model传的值

controller层如下&#xff1a; html获取格式&#xff1a;[[${传入的值}]] 效果图&#xff1a;

Mybatis操作数据库执行流程的先后顺序是怎样的?

MyBatis是一个支持普通SQL查询、存储及高级映射的持久层框架&#xff0c;它几乎消除了JDBC的冗余代码。使Java开发人员可以使用面向对象的编程思想来操作数据库。对于MyBatis的工作原理和操作流程的理解&#xff0c;我们先来看下面的工作流程图。 MaBatis的工作流程 在上图中…

VMware和Ubuntu20.04的安装

VMware安装&#xff1a; 1、下载好VM后右击管理员运行&#xff1a; PS&#xff1a;推荐大家去官网下载&#xff0c;如果需要许可密钥的话&#xff0c;可以去搜一下&#xff0c;或者私信一下UP&#xff0c;链接放下面了。 VM官网 因为我已经安装好了&#xff0c;所以就不在贴…

Appium自动化环境搭建保姆级教程

APP自动化测试运行环境比较复杂&#xff0c;稍微不注意安装就会失败。我见过不少朋友&#xff0c;装了1个星期&#xff0c;Appium 的运行环境还没有搭好的。 搭建环境本身不是一个有难度的工作&#xff0c;但是 Appium 安装过程中确实存在不少隐藏的比较深的坑&#xff0c;如果…