C++ 日志系统实战第五步:日志器的设计

news2025/6/3 13:08:51

全是通俗易懂的讲解,如果你本节之前的知识都掌握清楚,那就速速来看我的项目笔记吧~   

本文项目代码编写收尾!

日志器类 (Logger) 设计(建造者模式)

        日志器主要用于和前端交互。当我们需要使用日志系统打印 log 时,只需创建 Logger 对象,调用该对象的 debug、info、warn、error、fatal 等方法,即可输出想打印的日志。它支持解析可变参数列表和输出格式,能做到像使用 printf 函数一样打印日志。

当前日志系统支持同步日志与异步日志两种模式。两种日志器的差异仅在于日志的落地方式:

  • 同步日志器:直接输出日志消息。
  • 异步日志器:将日志消息放入缓冲区,由异步线程进行输出。

因此,设计日志器类时,先设计一个 Logger 基类,在此基础上,继承出 SyncLogger 同步日志器和 AsyncLogger 异步日志器。

        由于日志器模块整合了多个模块,创建一个日志器时,需要设置日志器名称、日志输出等级、日志器类型、日志输出格式以及落地方向(落地方向可能有多个) ,整个日志器的创建过程较为复杂。为保持良好的代码风格,编写出优雅的代码,日志器的创建采用建造者模式。

 

logger.hpp 

#pragma once
/*
日志器实现
1.抽象基类
2.派生出日志器具体实现类
*/
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "message.hpp"
#include "looper.hpp"
#include <atomic>
#include <mutex>
#include <cstdarg>

namespace mylog
{
    class Logger
    {
    public:
        Logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector<mylog::LogSink::ptr> sinks) : logger_name_(logger_name),
                                                                                                                                        limit_level_(limit_level),
                                                                                                                                        sinks_(sinks.begin(), sinks.end()),
                                                                                                                                        format_builder_(format_builder)
        {
        }
        using ptr = std::shared_ptr<Logger>;
        // 完成构造日志消息构造过程并格式化,然后调用log函数输出日志
        void debug(std::string file_, size_t line_, const std::string &fat, ...)
        {
            // 1.判断是否达到日志等级
            if (limit_level_ > Level::Debug)
            {
                return;
            }
            // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息
            va_list args;
            va_start(args, fat);
            char *res;
            int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中
            if (len < 0)
            {
                return;
            }
            va_end(args);
            serialize(Level::Debug, file_, line_, res); // 序列化日志消息
            free(res);                                  // 释放res
        }
        void info(std::string file_, size_t line_, const std::string &fat, ...)
        {
            // 1.判断是否达到日志等级
            if (limit_level_ > Level::Info)
            {
                return;
            }
            // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息
            va_list args;
            va_start(args, fat);
            char *res;
            int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中
            if (len < 0)
            {
                return;
            }
            va_end(args);
            serialize(Level::Info, file_, line_, res); // 序列化日志消息
            free(res);                                 // 释放res
        }
        void warning(std::string file_, size_t line_, const std::string &fat, ...)
        {
            // 1.判断是否达到日志等级
            if (limit_level_ > Level::Warning)
            {
                return;
            }
            // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息
            va_list args;
            va_start(args, fat);
            char *res;
            int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中
            if (len < 0)
            {
                return;
            }
            va_end(args);
            serialize(Level::Warning, file_, line_, res); // 序列化日志消息
            free(res);                                    // 释放res
        }
        void error(std::string file_, size_t line_, const std::string &fat, ...)
        {
            // 1.判断是否达到日志等级
            if (limit_level_ > Level::Error)
            {
                return;
            }
            // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息
            va_list args;
            va_start(args, fat);
            char *res;
            int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中
            if (len < 0)
            {
                return;
            }
            va_end(args);
            serialize(Level::Error, file_, line_, res); // 序列化日志消息
            free(res);                                  // 释放res
        }
        void fatal(std::string file_, size_t line_, const std::string &fat, ...)
        {
            // 1.判断是否达到日志等级
            if (limit_level_ > Level::Fatal)
            {
                return;
            }
            // 2.对fmt字符串和不定参数进行字符串组织,得到最终的日志消息
            va_list args;
            va_start(args, fat);
            char *res;
            int len = vasprintf(&res, fat.c_str(), args); // 获取格式化后的字符串在res中
            if (len < 0)
            {
                return;
            }
            va_end(args);
            serialize(Level::Fatal, file_, line_, res); // 序列化日志消息
            free(res);                                  // 释放res
        }

    protected:
        void serialize(Level level, std::string file, size_t line, const char *str) // 序列化日志消息
        {
            // 3.构造日志消息
            LogMessage message(level, file.c_str(), line, str, logger_name_.c_str());
            // 4.格式化日志消息
            std::stringstream ss;
            format_builder_->format(ss, message);
            // 5.输出日志消息
            log(ss.str().c_str(), ss.str().size());
        }
        // 抽象接口完成实际的落地输出——不同日志器实现类完成具体的落地输出
        virtual void log(const char *msg, size_t line) = 0;

    protected:
        std::mutex mutex_;                       // 日志器互斥锁
        std::string logger_name_;                // 日志器名称
        std::atomic<Level> limit_level_;         // 日志等级限制
        std::vector<mylog::LogSink::ptr> sinks_; // 日志输出器
        FormatBuilder::ptr format_builder_;      // 日志格式构造器
    };

    // 日志器具体实现类——同步日志器
    class sync_logger : public Logger
    {
        // 同步日志器,将日志直接通过落地模块句柄进行日志落地
    public:
        sync_logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector<mylog::LogSink::ptr> sinks) : Logger(logger_name, limit_level, format_builder, sinks)
        {
        }
        void log(const char *data, size_t len) override
        {
            // 加锁
            std::unique_lock<std::mutex> lock(mutex_);
            if (sinks_.empty())
            {
                return;
            }
            for (auto &sink : sinks_)
            {
                sink->log(data, len); // 调用sink的log函数输出日志
            }
        }
    };

    class Async_logger : public Logger
    {
    public:
        Async_logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector<mylog::LogSink::ptr> sinks, AsyncType async_type) : Logger(logger_name, limit_level, format_builder, sinks)
        {
            looper_ = std::make_shared<Async_looper>(std::bind(&Async_logger::reallog, this, std::placeholders::_1));
        }
        void log(const char *data, size_t len) override // 将数据写入缓冲区
        {
            looper_->push(data, len);
        }

        void reallog(Buffer &buf)
        {
            if (sinks_.empty())
            {
                return;
            }
            for (auto &sink : sinks_)
            {
                sink->log(buf.begin(), buf.readable_size()); // 调用sink的log函数输出日志
            }
        }

    private:
        Async_looper::ptr looper_;
    };

    /*使用建造者模式来建造日志器,而不是让用户直接构造日志器,这样可以避免用户构造错误的日志器,简化用户的使用复杂度*/
    enum class LoggerType
    {
        Loggertype_Sync,  // 同步日志器
        Loggertype_Async, // 异步日志器
    };
    // 1.抽象一个建筑者类
    //   1.设置日志器类型
    //   2.将不同类型日志器的创建放到同一个日志器建造者中完成
    class Logger_builder
    {
    public:
        Logger_builder() : type_(LoggerType::Loggertype_Sync), limit_level_(Level::Debug),
                           async_type_(AsyncType::ASYNC_SAFE)
        {
        }
        void buildLoggerType(LoggerType type)
        {
            type_ = type;
        }
        void buildLoggerName(std::string logger_name)
        {
            logger_name_ = logger_name;
        }
        void buildLimitLevel(Level limit_level)
        {
            limit_level_ = limit_level;
        }
        void buildFormatBuilder(const std::string &pattern)
        {
            format_builder_ = std::make_shared<FormatBuilder>(pattern);
        }
        void buildAsyncType(AsyncType async_type)
        {
            async_type_ = async_type;
        }
        template <typename LogSinkType, typename... Args>
        void buildSinks(Args &&...args)
        {
            LogSink::ptr psink = std::make_shared<LogSinkType>(std::forward<Args>(args)...);
            sinks_.push_back(psink);
        }
        virtual Logger::ptr build() = 0;

    protected:
        AsyncType async_type_;
        LoggerType type_;
        std::string logger_name_;
        Level limit_level_;
        FormatBuilder::ptr format_builder_;
        std::vector<mylog::LogSink::ptr> sinks_;
    };

    // 2.派生出具体建筑者类——局部日志器的建造者 & 全局的日志器建造者
    class Local_logger_builder : public Logger_builder
    {
    public:
        Logger::ptr build() override
        {
            assert(logger_name_.empty() == false);
            if (format_builder_.get() == nullptr)
            {
                format_builder_ = std::make_shared<FormatBuilder>();
            }
            if (sinks_.empty())
            {
                sinks_.push_back(std::make_shared<StdoutSink>());
            }
            if (type_ == LoggerType::Loggertype_Async)
            {
                return std::make_shared<Async_logger>(logger_name_,limit_level_, format_builder_, sinks_, async_type_);
            }
            return std::make_shared<sync_logger>(logger_name_, limit_level_, format_builder_, sinks_);
        }
    };
}

 只有在交换的时候,生产者和消费者才会产生一次锁冲突

 

 buffer.hpp

#pragma once

/*实现异步日志缓冲区*/
#include "util.hpp"
#include <vector>
#include <cassert>

namespace mylog
{
#define BUFFER_SIZE (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024) // 内存阈值,没有超过阈值,内存翻倍扩容,达到阈值后,内存线性增长
#define INCREASE_BUFFER_SIZE (1 * 1024 * 1024)  // 增加缓冲区大小
    class Buffer
    {
    private:
        std::vector<char> buffer_; // 缓冲区
        size_t reader_index;       // 当前可读的位置
        size_t writer_index;       // 当前可写的位置

    private:
        void move_writer(size_t len) // 移动写指针
        {
            assert(len + writer_index <= writeable_size());
            writer_index += len;
        }

        void ensure_Enough_capacity(size_t len) // 确保缓冲区有足够的空间容纳len字节
        {
            if (len <= writeable_size()) // 如果写入数据超过可写空间,不需要扩容
            {
                return;
            }
            else // 如果写入数据超过可写空间,需要扩容
            {
                size_t new_size = 0;
                while (new_size < len)
                {
                    if (buffer_.size() < THRESHOLD_BUFFER_SIZE)
                    {
                        new_size = buffer_.size() * 2; // 小于阈值,翻倍增长
                    }
                    else
                    {
                        new_size = buffer_.size() + INCREASE_BUFFER_SIZE; // 大于阈值,线性增长
                    }
                    buffer_.resize(new_size); // 扩容
                }
            }
        }

    public:
        Buffer() : buffer_(BUFFER_SIZE), reader_index(0), writer_index(0) {}

        void push(const char *data, size_t len) // 写入数据
        {
            // 当前可写空间不足
            //  if(len>writeable_size()) //如果写入数据超过可写空间
            //  {
            //      return;//1.直接返回
            //  }
            // 2.动态空间,用于极限性能测试——扩容
            ensure_Enough_capacity(len);
            // 写入数据
            std::copy(data, data + len, &buffer_[writer_index]);
            // 将写入位置后移len个字节
            move_writer(len);
        }

        const char *begin() // 返回缓冲区起始位置
        {
            return &buffer_[reader_index];
        }

        size_t readable_size() // 返回可读字节数
        {
            return writer_index - reader_index;
        }

        size_t writeable_size() // 返回可写字节数
        {
            // 对于扩容思路来说,不存在可写空间大小,业务总是可写
            return buffer_.size() - writer_index;
        }

        void move_reader(size_t len) // 移动读指针
        {
            assert(len <= readable_size());
            reader_index += len;
        }

        void reset() // 重置缓冲区
        {
            reader_index = 0;
            writer_index = 0;
        }

        void swap(Buffer &other) // 交换缓冲区
        {
            buffer_.swap(other.buffer_);
            std::swap(reader_index, other.reader_index);
            std::swap(writer_index, other.writer_index);
        }

        bool empty() // 判断缓冲区是否为空
        {
            return reader_index == writer_index;
        }
    };
}

双缓冲区异步任务处理器(AsyncLooper)设计

  • 设计思想:异步处理线程 + 数据池
    • 使用者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执行操作。
  • 任务池的设计思想:双缓冲区阻塞数据池
  • 优势
    • 避免了空间的频繁申请释放。
    • 尽可能的减少了生产者与消费者之间锁冲突的概率,提高了任务处理效率。

        在任务池的设计中,有很多备选方案,比如循环队列等等。但是不管是哪一种都会涉及到锁冲突的情况,因为在生产者与消费者模型中,任何两个角色之间都具有互斥关系,因此每一次的任务添加与取出都有可能涉及锁的冲突。

        而双缓冲区不同,双缓冲区是处理器将一个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进行处理。虽然同时多线程写入也会冲突,但是冲突并不会像每次只处理一条的时候频繁(减少了生产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。

0

 looper.hpp

/*异步器*/
#pragma once

#include "buffer.hpp"
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <memory>
#include <atomic>

namespace mylog
{
    using Functor = std::function<void(Buffer &)>;
    enum class AsyncType
    {
        ASYNC_SAFE,  // 安全状态,表示缓冲区满了则阻塞,防止内存资源耗尽
        ASYNC_UNSAFE // 非安全状态
    };
    class Async_looper
    {

    public:
        using ptr = std::shared_ptr<Async_looper>;
        Async_looper(const Functor &callback, AsyncType type = AsyncType::ASYNC_SAFE) : stop_(false), thread_(std::thread(&Async_looper::threadEntry, this)), callback_(callback), type_(type) {}

        void stop()
        {
            stop_ = true;
            con_cv_.notify_all(); // 通知消费者线程退出
            thread_.join();
        }

        ~Async_looper()
        {
            stop();
        }

        void push(const char *data, size_t len)
        {
            // 1.无线扩容——非安全; 2.固定大小——生产缓冲区满后,生产者线程阻塞;
            std::unique_lock<std::mutex> lock(mutex_);
            // 条件变量控制,若缓冲区剩余大小大于数据长度,可以添加数据,否则等待
            if (type_ == AsyncType::ASYNC_SAFE)
                pro_cv_.wait(lock, [&]()
                             { return pro_buffer_.writeable_size() >= len; });
            // 添加数据到缓冲区
            pro_buffer_.push(data, len);
            // 通知消费者线程有数据可消费
            if (type_ == AsyncType::ASYNC_SAFE)
                con_cv_.notify_all();
        }

    private:
        // 线程入口函数——对消费缓冲区的数据进行处理,处理完毕后,初始化缓冲区
        void threadEntry()
        {
            while (true)
            {
                // 1.判断生产缓冲区有没有数据,有就交换; .无数据,等待;
                {
                    std::unique_lock<std::mutex> lock(mutex_);
                    con_cv_.wait(lock, [&]()
                                 { return stop_ || !pro_buffer_.empty(); });
                    
                    // 如果停止且生产者缓冲区为空则退出
                    if (stop_ && pro_buffer_.empty())
                        break;
                    // 1.交换生产者缓冲区和消费者缓冲区
                    con_buffer_.swap(pro_buffer_);
                    // 2.通知生产者线程可以继续添加数据
                    if (type_ == AsyncType::ASYNC_SAFE)
                        pro_cv_.notify_all();
                }
                // 3.被唤醒后,对消费缓冲区进行数据处理
                callback_(con_buffer_);
                // 4.初始化消费者缓冲区
                con_buffer_.reset();
            }
        }

    private:
        Functor callback_; // 具体对缓冲区数据进行操作的回调函数
    private:
        AsyncType type_;
        std::atomic<bool> stop_;
        Buffer pro_buffer_;              // 生产者缓冲区
        Buffer con_buffer_;              // 消费者缓冲区
        std::mutex mutex_;               // 互斥锁
        std::condition_variable pro_cv_; // 生产者条件变量
        std::condition_variable con_cv_; // 消费者条件变量
        std::thread thread_;             // 异步线程
    };
}
class Async_logger : public Logger
    {
        public:
        Async_logger(std::string logger_name, Level limit_level, FormatBuilder::ptr format_builder, std::vector<mylog::LogSink::ptr> sinks,AsyncType async_type) : Logger(logger_name, limit_level, format_builder, sinks)
        {
            looper_ = std::make_shared<Async_looper>(std::bind(&Async_logger::reallog, this, std::placeholders::_1));
        }
        void log(const char *data, size_t len) override//将数据写入缓冲区
        {
            looper_->push(data, len);
        }

        void reallog(Buffer &buf)
        {
            if(sinks_.empty())
            {
                return;
            }
            for(auto &sink : sinks_)
            {
                sink->log(buf.begin(), buf.writeable_size()); // 调用sink的log函数输出日志
            }
        }
        private:
        Async_looper::ptr looper_;
    };

单例日志器管理类设计(单例模式)

        日志的输出,我们希望能够在任意位置都可以进行,但是当我们创建了一个日志器之后,就会受到日志器所在作用域的访问属性限制。

        因此,为了突破访问区域的限制,我们创建一个日志器管理类,且这个类是一个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的日志器来进行日志输出了。

        基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便于能够在任何位置通过日志器名称能够获取到指定的日志器进行日志输出。

 在logger.hpp里面

class LoggerManager
    {
    private:
        std::mutex mutex_;
        Logger::ptr root_logger_;                              // 默认日志器
        std::unordered_map<std::string, Logger::ptr> loggers_; // 日志器名称-日志器映射表
    private:
        LoggerManager()
        {
            std::unique_ptr<mylog::Local_logger_builder> builder(new mylog::Local_logger_builder());
            builder->buildLoggerName("root");
            root_logger_ = builder->build();
            addLogger(root_logger_);
        }

    public:
        static LoggerManager &getInstance()
        {
            // 在C++11之后,针对静态变量,编译器在编译的层面实现了线程安全
            // 当静态局部变量在没有构造完成之前,其他线程进入就会阻塞
            static LoggerManager eton;
            return eton;
        }
        void addLogger(Logger::ptr logger)
        {
            if (hasLogger(logger->get_logger_name()))
                return;
            std::unique_lock<std::mutex> lock(mutex_);
            loggers_.insert(std::make_pair(logger->get_logger_name(), logger));
        }
        bool hasLogger(const std::string &name)
        {
            std::unique_lock<std::mutex> lock(mutex_);
            auto it = loggers_.find(name);
            if (it != loggers_.end())
            {
                return true;
            }
            return false;
        }
        Logger::ptr getLogger(const std::string &name)
        {
            std::unique_lock<std::mutex> lock(mutex_);
            auto it = loggers_.find(name);
            if (it != loggers_.end())
            {
                return it->second;
            }
            return nullptr;
        }
        Logger::ptr rootLogger()
        {
            return root_logger_;
        }
    };

日志宏 & 全局接口设计(代理模式)

  • 提供全局的日志器获取接口。
  • 使用代理模式,通过全局函数或宏函数来代理 Logger 类的 log、debug、info、warn、error、fatal 等接口,以便于控制源码文件名称和行号的输出控制,简化用户操作。
  • 当仅需标准输出日志的时候,可以通过主日志器来打印日志。且操作时只需要通过宏函数直接进行输出即可。

mylog.h

#pragma once

#include"logger.hpp"

namespace mylog
{
    Logger::ptr get_logger(const std::string &name)
    {
        return mylog::LoggerManager::getInstance().getLogger(name);
    }
    Logger::ptr rootLogger()
    {
        return mylog::LoggerManager::getInstance().rootLogger();
    }

    #define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
    #define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
    #define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
    #define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
    #define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)

    #define DEBUG(fmt, ...) mylog::rootLogger()->debug(fmt, ##__VA_ARGS__)
    #define INFO(fmt, ...) mylog::rootLogger()->info(fmt, ##__VA_ARGS__)
    #define WARN(fmt, ...) mylog::rootLogger()->warn(fmt, ##__VA_ARGS__)
    #define ERROR(fmt, ...) mylog::rootLogger()->error(fmt, ##__VA_ARGS__)
    #define FATAL(fmt, ...) mylog::rootLogger()->fatal(fmt, ##__VA_ARGS__)
}

后续还有测试代码,期待共同实现~

  如果你对日志系统感到兴趣,欢迎关注我👉【A charmer】

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

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

相关文章

性能测试-jmeter实战1

课程&#xff1a;B站大学 记录软件测试-性能测试学习历程、掌握前端性能测试、后端性能测试、服务端性能测试的你才是一个专业的软件测试工程师 性能测试-jmeter实战1 为什么需要性能测试呢&#xff1f;性能测试的作用&#xff1f;性能测试体系性能测试基础性能测试工具性能监控…

杏仁海棠花饼的学习日记第十四天CSS

一&#xff0c;前言 第二天&#xff0c;今天看CSS。 二&#xff0c;CSS简介及导入方式 CSS简介 CSS&#xff08;层叠样式表&#xff0c;Cascading Style Sheets&#xff09;是一种用于描述 HTML 或 XML&#xff08;包括 SVG、XHTML 等&#xff09;文档呈现效果的样式语言。…

ESP8266远程控制:实现网络通信与设备控制

概述&#xff1a; 最近一直在弄esp8266的网络通信&#xff0c;但是一直都还没搞懂到底esp8266可不可以通过连接一个网络过后&#xff0c;在很远的地方使用网络将其关掉 在网上找了两个教程都有程序&#xff0c;都跑通了 第一个 第二个找不到了&#xff0c;但是程序有 CSDN上放文…

【机器学习基础】机器学习入门核心算法:隐马尔可夫模型 (HMM)

机器学习入门核心算法&#xff1a;隐马尔可夫模型 &#xff08;HMM&#xff09; 一、算法逻辑与核心思想二、算法原理与数学推导核心问题与算法推导 三、模型评估四、应用案例1. 语音识别 (Speech Recognition)2. 自然语言处理 (Natural Language Processing - NLP)3. 手写体识…

Leetcode 2819. 购买巧克力后的最小相对损失

1.题目基本信息 1.1.题目描述 现给定一个整数数组 prices&#xff0c;表示巧克力的价格&#xff1b;以及一个二维整数数组 queries&#xff0c;其中 queries[i] [ki, mi]。 Alice 和 Bob 去买巧克力&#xff0c;Alice 提出了一种付款方式&#xff0c;而 Bob 同意了。 对于…

AI炼丹日志-25 - OpenAI 开源的编码助手 Codex 上手指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇&#xff1a; MyBatis 更新完毕目前开始更新 Spring&#xff0c;一起深入浅出&#xff01; 大数据篇 300&#xff1a; Hadoop&…

C# 类和继承(使用基类的引用)

使用基类的引用 派生类的实例由基类的实例和派生类新增的成员组成。派生类的引用指向整个类对象&#xff0c;包括 基类部分。 如果有一个派生类对象的引用&#xff0c;就可以获取该对象基类部分的引用&#xff08;使用类型转换运算符把 该引用转换为基类类型&#xff09;。类…

进程间通信(消息队列)

目录 一 原理 二 API 1. ftok 2. msgget 3. msgctl 4. msgsnd 5. msgrcv 三 demo代码 四 基于责任链模式和消息队列对数据处理 1. 什么是责任链模式 2. 下面基于责任链模式来对消息队列获取的消息进行处理 前置 其实system v 版本的进程间通信&#xff0c;设计的接…

Nginx--手写脚本压缩和切分日志(也适用于docker)

原文网址&#xff1a;Nginx--手写脚本压缩和切分日志&#xff08;也适用于docker&#xff09;_IT利刃出鞘的博客-CSDN博客 简介 本文介绍nginx如何手写脚本压缩和切分日志。 1.创建切分日志的脚本 创建脚本文件&#xff1a;/work/tmp/nginx-log_sh&#xff08;后边要用run-…

OpenCv高阶(十八)——dlib人脸检测与识别

文章目录 一、dlib库是什么&#xff1f;二、opencv库与dlib库的优缺点对比1、opencv优缺点2、dlib库优缺点 三、dlib库的安装1、在线安装2、本地安装 四、dlib库的人脸检测器1. 基于 HOG 的检测器2. 基于 CNN 的检测器 五、dlib人脸检测的简单使用1、导入必要库2、初始化人脸检…

中山大学无人机具身导航新突破!FlightGPT:迈向通用性和可解释性的无人机视觉语言导航

作者&#xff1a;Hengxing Cai 1 , 2 ^{1,2} 1,2, Jinhan Dong 2 , 3 ^{2,3} 2,3, Jingjun Tan 1 ^{1} 1, Jingcheng Deng 4 ^{4} 4, Sihang Li 2 ^{2} 2, Zhifeng Gao 2 ^{2} 2, Haidong Wang 1 ^{1} 1, Zicheng Su 5 ^{5} 5, Agachai Sumalee 6 ^{6} 6, Renxin Zhong 1 ^{1} …

WIN11+CUDA11.8+VS2019配置BundleFusion

参考&#xff1a; BundleFusion:VS2019 2017 ,CUDA11.5,win11&#xff0c;Realsense D435i离线数据包跑通&#xff0c;环境搭建 - 知乎 Win10VS2017CUDA10.1环境下配置BundleFusion - 知乎 BundleFusionWIN11VS2019 CUDA11.7环境配置-CSDN博客 我的环境&#xff1a;Win 11…

WPF prism

Prism Prism.Dryloc 包 安装 Nuget 包 - Prism.DryIoc 1. 修改 App.xaml 修改 App.xaml 文件&#xff0c;添加 prism 命名空间, 继承由 Application → PrismApplication&#xff0c;删除默认启动 url, StartupUri“MainWindow.xaml” <dryioc:PrismApplicationx:Class…

[Redis] Redis:高性能内存数据库与分布式架构设计

标题&#xff1a;[Redis] 浅谈分布式系统 水墨不写bug 文章目录 一、什么是Redis&#xff1f;一、核心定位二、核心优势三、典型应用场景四、Redis vs 传统数据库 二、架构选择与设计1、单机架构&#xff08;应用程序 数据库服务器&#xff09;2、应用程序和数据库服务器分离3…

React 第四十九节 Router中useNavigation的具体使用详解及注意事项

前言 useNavigation 是 React Router 中一个强大的钩子&#xff0c;用于获取当前页面导航的状态信息。 它可以帮助开发者根据导航状态优化用户体验&#xff0c;如显示加载指示器、防止重复提交等。 一、useNavigation核心用途 检测导航状态&#xff1a;判断当前是否正在进行…

【JavaEE】Spring事务

目录 一、事务简介二、Spring事务的实现2.1 事务的操作2.2 分类2.2.1 Spring编程式事务2.2.2 Spring 声明式事务 Transactional2.2.2.1 Transactional 详解2.2.2.1.1 rollbackFor2.2.2.1.2 Isolation2.2.2.1.3 propagation 一、事务简介 事务&#xff1a;事务是⼀组操作的集合…

Android15 userdebug版本不能remount

背景描述&#xff1a; 最近调试Android Vendor Hal的时候发现一个奇怪的现象: android userdebug版本刷到设备中&#xff0c;执行adb root没提示错误&#xff0c;但是没有获取到root权限。 Android设备运行的系统版本有三种情况&#xff1a;user版本、userdebug版本和eng版本…

R包安装报错解决案例系列|R包使用及ARM架构解决data.table安装错误问题

有不少同学是Mac系统的&#xff0c;分析过程中会发现部分R包总是安装不成功&#xff0c;这是因为部分R包基于windowsx86架构编译的&#xff0c;最常见的就是含 C/C/Fortran 的包&#xff0c;对于初学者都是建议linux和win去做&#xff0c;Windows 通常直接安装预编译好的二进制…

Linux上安装MongoDB

目录 一、在Linux系统安装MongoDB服务器 1、下载MongoDB 2、上传MongoDB并解压 3、创建必要目录 4、配置环境变量 5、创建配置文件 6、启动命令 7、验证安装 二、在Linux系统安装MongoDB客户端Shell 1、下载MongoDB Shell 2、上传MongoDB Shell并解压 3、配置环境变…

Redis最佳实践——安全与稳定性保障之访问控制详解

Redis 在电商应用的安全与稳定性保障之访问控制全面详解 一、安全访问控制体系架构 1. 多层级防护体系 #mermaid-svg-jpkDj2nKxCq9AXIW {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-jpkDj2nKxCq9AXIW .error-ico…