当前位置: 首页 > news >正文

【Linux】手搓日志(附源码)

1.认识日志

计算机中的⽇志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要⼯具。

日志一般有下面几个必备要素

  • 时间戳
  • ⽇志内容
  • ⽇志等级: DEBUG(测试)  INFO(正常)  WARNING(有错但不影响)  ERROR(有错且会导致结果出错)  FATAL(有错且不能再往后运行)
这⼏个指标是可选的:⽂件名、⾏号、⽇志内容、进程编号...
我们实现的日志样例如下:
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world

我们实现的日志具备如下功能:

  1. 形成完整的日志信息
  2. 能刷新到目标文件(显示器、指定文件),实现这个功能会用到策略模式

2.日志的实现

2.1 刷新模式

首先要设计刷新策略,是往显示器打印?还是往指定文件里打印?这里要用策略模式,就要用到多态,多态相关知识在:【C++】多态详细讲解 。

#pragma once
#include <iostream>
#include <string>
#include "Mutex.hpp"namespace MyLog
{using namespace MyMutex;// 刷新策略:策略模式class RefreshStrategy // 基类{public:virtual void Refresh(const std::string &message) = 0;virtual ~RefreshStrategy() = default;};class LogToConsole : public RefreshStrategy // 往控制台(显示器)刷新{public:LogToConsole() {}virtual void Refresh(const std::string &message){LockGuard lg(&_mutex); // 打印要加锁std::cout << message << std::endl;}~LogToConsole() {}private:Mutex _mutex;};
}

上面是实现往显示器打印的策略,还要实现一个往指定文件里打印的策略。

#pragma once
#include <iostream>
#include <string>
#include "Mutex.hpp"namespace MyLog
{using namespace MyMutex;// 刷新策略:策略模式class RefreshStrategy // 基类{public:virtual void Refresh(const std::string &message) = 0;virtual ~RefreshStrategy() = default;};class LogToConsole : public RefreshStrategy // 往控制台(显示器)刷新{public:LogToConsole() {}virtual void Refresh(const std::string &message){LockGuard lg(&_mutex); // 打印要加锁std::cout << message << std::endl;}~LogToConsole() {}private:Mutex _mutex;};const std::string default_path = "./log";     // 默认路径const std::string default_file = "mylog.log"; // 默认文件名class LogToFile : public RefreshStrategy // 往指定文件刷新{public:LogToFile(const std::string &path = default_path, const std::string &file = default_file): _path(path),_file(file){}~LogToFile() {}private:std::string _path;std::string _file;};
}

往指定文件里打印就要考虑这个路径是否存在,不存在就要新建,这里用C++17里的文件操作,用到的头文件是#include <filesystem> 

public:LogToFile(const std::string &path = default_path, const std::string &file = default_file): _path(path),_file(file){if (std::filesystem::exists(_path)) // 判断路径是否存在return;std::filesystem::create_directories(_path); // 创建路径}

存在直接返回,不存在就创建,创建路径也有可能失败,所以用try catch捕捉异常,并且往文件里写也要加锁。

class LogToFile : public RefreshStrategy // 往指定文件刷新
{
public:LogToFile(const std::string &path = default_path, const std::string &file = default_file): _path(path),_file(file){LockGuard lg(&_mutex);              // 加锁if (std::filesystem::exists(_path)) // 判断路径是否存在return;try{std::filesystem::create_directories(_path); // 创建路径}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}virtual void Refresh(const std::string &message) override{}~LogToFile() {}private:std::string _path;std::string _file;Mutex _mutex;
};

刷新的部分是往文件里刷新,这里用C++的文件操作,需包含头文件#include <fstream>

class LogToFile : public RefreshStrategy // 往指定文件刷新
{
public:LogToFile(const std::string &path = default_path, const std::string &file = default_file): _path(path),_file(file){LockGuard lg(&_mutex);              // 加锁if (std::filesystem::exists(_path)) // 判断路径是否存在return;try{std::filesystem::create_directories(_path); // 创建路径}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}virtual void Refresh(const std::string &message) override{LockGuard lg(&_mutex); // 加锁std::string file_name = _path + (_path.back() == '/' ? "" : "/") + _file; // 把路径和文件名拼起来std::ofstream out_file(file_name, std::ios::app);  // 追加写入 的方式打开文件if (!out_file.is_open())  // 打开失败直接返回return;out_file << message << "\r\n"; // 以流形式把message和换行符写到out_file里out_file.close();}~LogToFile() {}private:std::string _path;std::string _file;Mutex _mutex;
};

现在我们就有了两种策略,一种叫LogToConsole往显示器刷新,一种叫LogToFile往指定文件里刷新。

在Main.cc里测试一下。

#include "Log.hpp"
#include <memory>using namespace MyLog;int main()
{// 直接定义基类指针,构建显示器策略std::unique_ptr<RefreshStrategy> strategy = std::make_unique<LogToConsole>();// std::unique_ptr<RefreshStrategy> strategy = std::make_unique<LogToFile>(); // 这是构建文件策略strategy->Refresh("hello mylog!");return 0;
}

make_unique作用是构建对象,返回智能指针。

程序运行之后就会往显示器刷新日志信息。

如果是往指定文件里刷新,程序运行后会新增一个log文件夹,里面有mylog.log文件。

int main()
{// 直接定义基类指针,构建显示器策略//std::unique_ptr<RefreshStrategy> strategy = std::make_unique<LogToConsole>();std::unique_ptr<RefreshStrategy> strategy = std::make_unique<LogToFile>(); // 这是构建文件策略strategy->Refresh("hello mylog!");return 0;
}

打开文件就会发现,日志信息刷新到了mylog.log文件里。

如果我们多执行几次,日志信息就会追加式的写入到文件里了。

2.2 形成完整日志信息

我们要实现的日志样式如下。

[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world

首先我们把日志等级定义出来,这里就用enum。

enum class LogLevel
{DEBUG,INFO,WARNING,ERROR,FATAL
};

我们还需要一个类,真正的表示日志的日志类,这个类最主要的作用就是选择刷新策略,类成员用智能指针。

class MyLog
{
public:MyLog(){RefreshLogToConsole(); // 默认往显示器刷新}void RefreshLogToConsole() // 选择刷新到显示器{_refresh_strategy = std::make_unique<LogToConsole>();}void RefreshLogToFile() // 选择刷新到文件{_refresh_strategy = std::make_unique<LogToFile>();}~MyLog() {}private:std::unique_ptr<RefreshStrategy> _refresh_strategy;
};

我们需要设计一个内部类,表示一条日志信息。

class MyLog
{
public:MyLog(){RefreshLogToConsole(); // 默认往显示器刷新}void RefreshLogToConsole() // 选择刷新到显示器{_refresh_strategy = std::make_unique<LogToConsole>();}void RefreshLogToFile() // 选择刷新到文件{_refresh_strategy = std::make_unique<LogToFile>();}class LogMessage //内部类{public:LogMessage(LogLevel log_level, const std::string &filename, int line): _log_level(log_level),_curr_time(), /*获取时间稍后实现*/_pid(getpid()),_filename(filename),_line(line){_log_info = }~LogMessage() {}private:LogLevel _log_level;    // 日志等级std::string _curr_time; // 时间pid_t _pid;             // 进程IDstd::string _filename;  // 文件名int _line;              // 行号std::string _log_info;  // 合并之后的消息};~MyLog() {}private:std::unique_ptr<RefreshStrategy> _refresh_strategy;
};

日志等级、文件名和行号是需要我们传进来的,别的不用,在拼成一整条完整信息的时候要用到stringstream,头文件#include <sstream>

    class LogMessage // 内部类{public:LogMessage(LogLevel log_level, const std::string &filename, int line): _log_level(log_level),_curr_time(), /*获取时间稍后实现*/_pid(getpid()),_filename(filename),_line(line){std::stringstream ss; // 定义一个stringstream流// 类型自动转化为string对象然后,把所有内容格式化输入到这个流里,ss << "[" << _curr_time << "]"<< "[" << _log_level << "]" /*这里有问题*/<< "[" << _pid << "]"<< "[" << _filename << "]"<< "[" << _line << "]"<< " - ";}~LogMessage() {}private:LogLevel _log_level;    // 日志等级std::string _curr_time; // 时间pid_t _pid;             // 进程IDstd::string _filename;  // 文件名int _line;              // 行号std::string _log_info;  // 合并之后的消息};

这里需要把enum里的内容转成字符串,我们要多写一个接口。

std::string LevelToStr(LogLevel lev)
{switch (lev){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "UNKNOWN";}
}

现在就没有错误了,然后我们还要把ss里的内容给_log_info。

    public:LogMessage(LogLevel log_level, const std::string &filename, int line): _log_level(log_level),_curr_time(), /*获取时间稍后实现*/_pid(getpid()),_filename(filename),_line(line){std::stringstream ss; // 定义一个stringstream流// 类型自动转化为string对象然后,把所有内容格式化输入到这个流里,ss << "[" << _curr_time << "]"<< "[" << LevelToStr(_log_level) << "]" // 转化<< "[" << _pid << "]"<< "[" << _filename << "]"<< "[" << _line << "]"<< " - ";_log_info = ss.str();}

日志的消息后半部分要支持多次<<的输入,也就是消息内容要支持多参数,比如LogMessage<< "1" << "2" << "3" ,我们要对<<进行重载;而且还要支持不同类型的<<,所以实现的时候需要加模板,传的是什么参数类型可以自动推导。

template<typename T> //类型自动推导
/*返回值类型*/ operator<<(const T& info)
{std::stringstream ss; //还是用stringstream流ss << info;_log_info += ss.str(); //消息前后部分拼起来
}

此时就是我们把Log<< "1"拼完成了,还要继续拼 << "2" ,所以这个运算符重载的返回值类型要是当前类的引用,返回值为当前类对象。

    class LogMessage // 内部类{public:LogMessage(LogLevel log_level, const std::string &filename, int line): _log_level(log_level),_curr_time("xxxx"), /*获取时间稍后实现*/_pid(getpid()),_filename(filename),_line(line){std::stringstream ss; // 定义一个stringstream流// 类型自动转化为string对象然后,把所有内容格式化输入到这个流里,ss << "[" << _curr_time << "]"<< "[" << LevelToStr(_log_level) << "]" // 转化<< "[" << _pid << "]"<< "[" << _filename << "]"<< "[" << _line << "]"<< " - ";_log_info = ss.str();}template<typename T> //类型自动推导LogMessage &operator<<(const T& info){std::stringstream ss; //还是用stringstream流ss << info;_log_info += ss.str(); //消息前后部分拼起来return *this;}~LogMessage() {}private://...};

我们刷新日志信息的时候,不用手动刷新,在LogMessage析构的时候自动刷新就行。刷新策略可以直接获得,因为LogMessage是Log的内部类。

class MyLog
{
public:MyLog(){RefreshLogToConsole(); // 默认往显示器刷新}void RefreshLogToConsole() // 选择刷新到显示器{_refresh_strategy = std::make_unique<LogToConsole>();}void RefreshLogToFile() // 选择刷新到文件{_refresh_strategy = std::make_unique<LogToFile>();}class LogMessage // 内部类{public:LogMessage(LogLevel log_level, const std::string &filename, int line, Log &log): _log_level(log_level),_curr_time("xxxx"), /*获取时间稍后实现*/_pid(getpid()),_filename(filename),_line(line),_log(log){std::stringstream ss; // 定义一个stringstream流// 类型自动转化为string对象然后,把所有内容格式化输入到这个流里,ss << "[" << _curr_time << "]"<< "[" << LevelToStr(_log_level) << "]" // 转化<< "[" << _pid << "]"<< "[" << _filename << "]"<< "[" << _line << "]"<< " - ";_log_info = ss.str();}template <typename T> // 类型自动推导LogMessage &operator<<(const T &info){std::stringstream ss; // 还是用stringstream流ss << info;_log_info += ss.str(); // 消息前后部分拼起来return *this;}~LogMessage() // 析构时自动刷新{if (_log._refresh_strategy){_log._refresh_strategy->Refresh(_log_info); // 刷新日志信息}}private:LogLevel _log_level;    // 日志等级std::string _curr_time; // 时间pid_t _pid;             // 进程IDstd::string _filename;  // 文件名int _line;              // 行号std::string _log_info;  // 合并之后的消息Log &_log;              // 外部类的引用};~MyLog() {}private:std::unique_ptr<RefreshStrategy> _refresh_strategy;
};

Log类里面我们需要重载(),在外部调用的时候直接()调用,就像仿函数,参数需要传日志等级,文件名和行号,返回值类型就是logMessage

    LogMessage operator()(LogLevel level, std::string _filename, int _line){return LogMessage(level, _filename, _line, *this);}

这里返回的是临时对象,我们用(),然后会LogMessage() << "1" << "2" << "3",会调用operator<<,虽然是临时对象,只要<<运算符没结束,这个临时对象就会一直存在,<<结束后,LogMessage临时对象就会自动释放,就会自动调LogMessage的析构函数做刷新。

//Main.cc文件
#include "MyLog.hpp"
using namespace MyLog;int main()
{MyLog logger;logger(LogLevel::DEBUG, "Main.cc", 6) << "hello log ";logger(LogLevel::DEBUG, "Main.cc", 7) << "hello log ";logger(LogLevel::DEBUG, "Main.cc", 8) << "hello log ";logger(LogLevel::DEBUG, "Main.cc", 9) << "hello log ";return 0;
}

还可以追加式的打印。

int main()
{MyLog logger;logger(LogLevel::DEBUG, "Main.cc", 6) << "hello log " << 1  << 'c'<< 3.14;logger(LogLevel::DEBUG, "Main.cc", 7) << "hello log " << 2  << 'd'<< 3.14;logger(LogLevel::DEBUG, "Main.cc", 8) << "hello log " << 3  << 'r'<< 3.14;logger(LogLevel::DEBUG, "Main.cc", 9) << "hello log " << 4  << 'g'<< 3.14;return 0;
}

这是往显示器打印,我们也可以选择往文件里打印。

int main()
{MyLog logger;logger.RefreshLogToFile(); //往文件里打印logger(LogLevel::DEBUG, "Main.cc", 6) << "hello log ";logger(LogLevel::DEBUG, "Main.cc", 7) << "hello log ";logger(LogLevel::DEBUG, "Main.cc", 8) << "hello log ";logger(LogLevel::DEBUG, "Main.cc", 9) << "hello log ";return 0;
}

2.3 简化用户操作

定义一个宏,我们只需要传日志的等级就行,还会用到C/C++里的预处理符,叫__FILE____LINE__

//MyLog.hpp文件
MyLog mylog; //定义全局日志对象
#define LOG(level)  mylog(level, __FILE__, __LINE__) 

还可以用一个宏选择哪种刷新模式。

MyLog mylog; // 定义全局日志对象
#define LOG(level) mylog(level, __FILE__, __LINE__)
#define Refresh_Log_To_Console() mylog.RefreshLogToConsole() // 显示器
#define Refresh_Log_To_File() mylog.RefreshLogToFile()       // 文件

以后我们在用这个日志的时候,就可以直接用宏定义。

#include "MyLog.hpp"
using namespace MyLog;int main()
{Refresh_Log_To_Console();LOG(LogLevel::DEBUG) << "hello log..." << 123;LOG(LogLevel::DEBUG) << "hello log..." << 456;LOG(LogLevel::DEBUG) << "hello log..." << 789;return 0;
}

此时文件名和行号就不用手动输入了,直接能获取到。

如果要往文件里刷新,就用文件的那个宏。

#include "MyLog.hpp"
using namespace MyLog;int main()
{Refresh_Log_To_Console();LOG(LogLevel::DEBUG) << "hello log..." << 123;LOG(LogLevel::DEBUG) << "hello log..." << 456;Refresh_Log_To_File();LOG(LogLevel::DEBUG) << "hello log..." << 789;LOG(LogLevel::DEBUG) << "hello log..." << 910;return 0;
}

2.4 获取时间

获取时间有很多做法,这里选择用localtime_r函数获取,这个函数是一个可重入的安全的函数,多线程访问时是没有问题的。

#include <time.h>struct tm *localtime_r(const time_t *timep, struct tm *result);

这个函数可以把一个time_t类型转化成一个struct tm的结构,成功就返回这个结构体,失败返回空。

  • time_t其实就是一个时间戳,用来获取时间的
  • struct tm的结构如下,有年月日、时分秒等
std::string GetTime()
{time_t curr_time = time(nullptr);struct tm stm;localtime_r(&curr_time, &stm);std::stringstream ss;ss << stm.tm_year << "-"<< stm.tm_mon << "-"<< stm.tm_mday << " "<< stm.tm_hour << ":"<< stm.tm_min << ":"<< stm.tm_sec;return ss.str();
}

#include "MyLog.hpp"
using namespace MyLog;int main()
{Refresh_Log_To_Console();LOG(LogLevel::DEBUG) << "hello log..." << 123;LOG(LogLevel::DEBUG) << "hello log..." << 456;LOG(LogLevel::DEBUG) << "hello log..." << 789;return 0;
}

现在的日期是2025年10月10日,运行发现年和月并不对,因为需要加上1900,月是从0开始的,所以月要加1.

std::string GetTime()
{time_t curr_time = time(nullptr);struct tm stm;localtime_r(&curr_time, &stm);std::stringstream ss;ss << stm.tm_year + 1900 << "-"<< stm.tm_mon + 1 << "-"<< stm.tm_mday << " "<< stm.tm_hour << ":"<< stm.tm_min << ":"<< stm.tm_sec;return ss.str();
}

这个日期的显示还有一种写法,如下。

std::string GetTime()
{time_t curr_time = time(nullptr);struct tm stm;localtime_r(&curr_time, &stm);char timebuffer[124];snprintf(timebuffer, 124, "%4d-%02d-%02d %02d:%02d:%02d",stm.tm_year + 1900,stm.tm_mon + 1,stm.tm_mday,stm.tm_hour,stm.tm_min,stm.tm_sec);return timebuffer;
}

3.源码

//MyLog.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"namespace MyLog
{using namespace MyMutex;const std::string default_path = "./log";     // 默认路径const std::string default_file = "mylog.log"; // 默认文件名// 刷新策略:策略模式class RefreshStrategy // 基类{public:virtual void Refresh(const std::string &message) = 0;virtual ~RefreshStrategy() = default;};class LogToConsole : public RefreshStrategy // 往控制台(显示器)刷新{public:LogToConsole() {}virtual void Refresh(const std::string &message) override{LockGuard lg(&_mutex); // 打印要加锁std::cout << message << "\r\n";}~LogToConsole() {}private:Mutex _mutex;};class LogToFile : public RefreshStrategy // 往指定文件刷新{public:LogToFile(const std::string &path = default_path, const std::string &file = default_file): _path(path),_file(file){LockGuard lg(&_mutex);              // 加锁if (std::filesystem::exists(_path)) // 判断路径是否存在return;try{std::filesystem::create_directories(_path); // 创建路径}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}virtual void Refresh(const std::string &message) override{LockGuard lg(&_mutex);                                                    // 加锁std::string file_name = _path + (_path.back() == '/' ? "" : "/") + _file; // 把路径和文件名拼起来std::ofstream out_file(file_name, std::ios::app);                         // 追加写入 的方式打开文件if (!out_file.is_open())                                                  // 打开失败直接返回return;out_file << message << "\r\n"; // 以流形式把message和换行符写到out_file里out_file.close();}~LogToFile() {}private:std::string _path;std::string _file;Mutex _mutex;};enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};std::string LevelToStr(LogLevel lev){switch (lev){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "UNKNOWN";}}// std::string GetTime()// {//     time_t curr_time = time(nullptr);//     struct tm stm;//     localtime_r(&curr_time, &stm);//     std::stringstream ss;//     ss << stm.tm_year + 1900 << "-"//        << stm.tm_mon + 1 << "-"//        << stm.tm_mday << " "//        << stm.tm_hour << ":"//        << stm.tm_min << ":"//        << stm.tm_sec;//     return ss.str();// }std::string GetTime(){time_t curr_time = time(nullptr);struct tm stm;localtime_r(&curr_time, &stm);char timebuffer[124];snprintf(timebuffer, 124, "%4d-%02d-%02d %02d:%02d:%02d",stm.tm_year + 1900,stm.tm_mon + 1,stm.tm_mday,stm.tm_hour,stm.tm_min,stm.tm_sec);return timebuffer;}class MyLog{public:MyLog(){RefreshLogToConsole(); // 默认往显示器刷新}void RefreshLogToConsole() // 选择刷新到显示器{_refresh_strategy = std::make_unique<LogToConsole>();}void RefreshLogToFile() // 选择刷新到文件{_refresh_strategy = std::make_unique<LogToFile>();}class LogMessage // 内部类{public:LogMessage(LogLevel log_level, std::string filename, int line, MyLog &log): _log_level(log_level),_curr_time(GetTime()),_pid(getpid()),_filename(filename),_line(line),_log(log){std::stringstream ss; // 定义一个stringstream流// 类型自动转化为string对象然后,把所有内容格式化输入到这个流里,ss << "[" << _curr_time << "]"<< "[" << LevelToStr(_log_level) << "]" // 转化<< "[" << _pid << "]"<< "[" << _filename << "]"<< "[" << _line << "]"<< " - ";_log_info = ss.str();}template <typename T> // 类型自动推导LogMessage &operator<<(const T &info){std::stringstream ss; // 还是用stringstream流ss << info;_log_info += ss.str(); // 消息前后部分拼起来return *this;}~LogMessage(){if (_log._refresh_strategy){_log._refresh_strategy->Refresh(_log_info); // 刷新日志信息}}private:LogLevel _log_level;    // 日志等级std::string _curr_time; // 时间pid_t _pid;             // 进程IDstd::string _filename;  // 文件名int _line;              // 行号std::string _log_info;  // 合并之后的消息MyLog &_log;            // 外部类的引用};LogMessage operator()(LogLevel level, std::string filename, int line){return LogMessage(level, filename, line, *this);}~MyLog() {}private:std::unique_ptr<RefreshStrategy> _refresh_strategy;};MyLog mylog; // 定义全局日志对象
#define LOG(level) mylog(level, __FILE__, __LINE__)
#define Refresh_Log_To_Console() mylog.RefreshLogToConsole() // 显示器
#define Refresh_Log_To_File() mylog.RefreshLogToFile()       // 文件}

本篇分享就到这里,我们下篇见~

http://www.dtcms.com/a/469183.html

相关文章:

  • Excel 下拉选项设置 级联式
  • pycharm自动化测试初始化
  • nacos3.0.4升级到3.1.0
  • linux入门5.5(高可用)
  • JAVA·数组的定义与使用
  • Transformer 面试题及详细答案120道(81-90)-- 性能与评估
  • 可以做软件的网站有哪些功能中国新闻社待遇
  • 【鉴权架构】SpringBoot + Sa-Token + MyBatis + MySQL + Redis 实现用户鉴权、角色管理、权限管理
  • 三星S25Ultra/S24安卓16系统Oneui8成功获取完美root权限+LSP框架
  • ffmpeg 播放视频 暂停
  • 老题新解|大整数的因子
  • Eureka的自我保护机制
  • 探索颜色科学:从物理现象到数字再现
  • AirSim_SimJoyStick
  • 第五部分:VTK高级功能模块(第149章 Remote模块 - 远程模块类)
  • 道可云人工智能每日资讯|《政务领域人工智能大模型部署应用指引》发布
  • 自己做网站哪家好win10 wordpress安装教程视频
  • wordpress整体搬迁宁波seo深度优化平台有哪些
  • 4K Wallpaper mac v2.7.dmg 安装教程(Mac电脑详细安装步骤4K壁纸Mac下载安装)
  • Mac 软件出现「应用程序“xxx”不能打开」的解决办法
  • 东航集团客户网站是哪家公司建设4k高清视频素材网站
  • Compose 在Row、Column上使用focusRestorer修饰符失效原因
  • Sora 2:当AI视频“以假乱真”,内容创作进入新纪元,体验AI创作能力
  • 推荐一个浏览器代理插件(Tajang Proxy),支持Chrome和Edge
  • conda|如何通过命令行在mac上下载conda
  • VS Code 二次开发:跨平台图标定制全攻略
  • 关于微信小程序开发几点总结
  • 杭州建站价格邢台wap网站建设费用
  • kafka4使用记录
  • 2100AI智能生活