C++硬核开发指南:从零搭建环境到现代特性实战
2026/7/16 1:59:55 网站建设 项目流程

1. 为什么C++依然是硬核开发者的首选

如果你在技术社区里泡久了,会发现一个有趣的现象:每当有新的编程语言发布,总有人会问“C++是不是要过时了?” 但十几年过去了,从游戏引擎、高频交易系统到操作系统内核、数据库,C++的身影依然无处不在。我见过太多项目,前期为了快速迭代用了其他语言,到了性能瓶颈期,最终还是得用C++来重写核心模块。这就是C++的底气——它提供了一种独特的平衡,让你在享受接近硬件底层的控制力和极致性能的同时,又能使用现代的高级抽象来管理复杂度。

很多人对C++的印象还停留在“复杂”和“难学”上,这其实是个误区。C++的复杂性,很大程度上源于它的“工具箱”属性。它不像某些语言只给你一把瑞士军刀,而是给了你一个完整的车间,里面有从螺丝刀到数控机床的所有工具。问题不在于工具多,而在于你是否清楚什么时候该用哪一把。从C++98到C++23,这门语言一直在进化,但它的核心哲学没变:信任程序员,不隐藏成本,提供零开销抽象。这意味着,你写的每一行代码,其背后的机器指令和内存操作都是清晰可预测的。对于需要榨干每一分硬件性能的场景,比如游戏渲染、实时音视频处理、金融交易引擎,这种确定性是无价的。

学习C++,从来不是简单地学语法。它更像是在学习一套完整的“计算机系统观”。你得理解数据在内存中如何布局,理解CPU的缓存行,理解编译器的优化策略。这个过程很痛苦,但一旦打通,你对计算机的理解会上升一个维度,再看其他语言,很多设计选择你一眼就能看透本质。这也是为什么很多资深架构师和技术负责人都有深厚的C++背景——这门语言逼着你去思考底层。

2. 从零到一:搭建你的C++学习环境与心智模型

2.1 编译器与构建工具选型:别在第一步就踩坑

新手最容易在环境配置上浪费大量时间。我的建议是:在初期,选择最主流、问题最少的工具链,把精力集中在语言本身。

对于编译器,GCC (GNU Compiler Collection)Clang是两大首选。在Linux或macOS上,直接用包管理器安装即可(如sudo apt install g++brew install llvm)。在Windows上,如果你不想折腾完整的Linux子系统,那么MSVC (Microsoft Visual C++)是最顺滑的选择,它集成在Visual Studio中。很多人纠结哪个更好,其实对于学习标准C++而言,它们对语言特性的支持都已非常完善。关键在于,请务必使用支持C++17或更高标准的版本。你可以通过g++ --versionclang++ --version查看版本,并在编译时加上-std=c++17-std=c++20标志。

关于集成开发环境(IDE),争论更多。我的看法是:

  • Visual Studio (Windows)Visual Studio Code (全平台)+C/C++扩展:这是目前最友好、生态最全的组合。VS提供了开箱即用的强大调试器和智能提示,VSCode则轻量灵活,通过配置也能获得接近IDE的体验。
  • CLion (JetBrains):如果你习惯IntelliJ系列的操作,并且预算允许,CLion在代码分析、重构和CMake集成方面做得非常出色。
  • 初期可以不用IDE:我强烈建议初学者在第一个月,尝试用文本编辑器(如VSCode)写代码,然后在终端里用命令行手动编译。这能帮你清晰地理解编译、链接的整个过程,而不是被IDE的“一键运行”蒙在鼓里。你会真正明白#include、头文件、源文件、目标文件、库这些概念是如何串联起来的。

构建工具方面,小项目可以用简单的Makefile,但一旦项目结构稍微复杂,CMake几乎是现代C++项目的标配。它虽然有自己的学习曲线,但学会了就能一劳永逸地解决跨平台构建问题。不要怕,从最简单的CMakeLists.txt开始:

cmake_minimum_required(VERSION 3.10) project(MyFirstCPPProject) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(my_app main.cpp utils.cpp)

2.2 建立正确的心智模型:理解“对象生命周期”和“资源管理”

在写第一行“Hello World”之前,我希望你先在脑子里建立两个核心概念,这能帮你避开未来80%的诡异Bug。

第一个概念是“对象生命周期”。在C++中,每个对象都有明确的生与死:

  • 自动存储期(栈对象):在作用域内(比如函数体内)定义的普通变量。进入作用域时诞生(构造函数被调用),离开作用域时自动死亡(析构函数被调用)。它的内存分配和释放是自动的、快速的。
  • 动态存储期(堆对象):通过new运算符创建的对象。它的生命周期完全由程序员控制,必须显式地用delete来结束它的生命。忘记delete会导致内存泄漏;delete太早或多次delete会导致悬空指针或程序崩溃。
  • 静态存储期:在全局或使用static关键字定义的变量。它们在程序启动时诞生,程序结束时死亡。

理解这个,你就明白了为什么不能返回一个局部变量的地址或引用——因为函数结束,那个局部变量就“死”了,你拿到的地址指向的是一块无效内存。

第二个概念是“资源管理”。内存只是资源的一种,文件句柄、网络套接字、锁、图形API对象等都是资源。C++的核心哲学是RAII (Resource Acquisition Is Initialization),翻译过来是“资源获取即初始化”。这个拗口名字背后的思想极其简单而强大:将资源的生命周期与一个对象的生命周期绑定。在对象的构造函数中获取资源(分配内存、打开文件),在析构函数中释放资源(释放内存、关闭文件)。这样,只要这个对象正常离开作用域,资源就会被自动、正确地释放,无需手动干预。

这就是为什么现代C++要极力推荐使用智能指针(std::unique_ptr,std::shared_ptr)和容器(std::vector,std::string),而不是裸的new/delete和C风格数组。它们就是RAII的完美实践,帮你自动管理资源,极大地减少了内存泄漏和资源泄漏的风险。从一开始就养成使用它们的习惯,是写出健壮C++代码的关键。

3. 核心知识体系深度拆解:不止于语法

很多C++教程停留在语法层面,但真正让你写出工业级代码的,是语法背后的编程范式和思想。我把C++的核心知识分为四个层次,你需要层层递进地掌握。

3.1 第一层:基础语法与面向过程编程

这一层是地基,必须打牢。变量、类型、循环、分支、函数,这些和其他语言类似。但C++有几个特别需要吃透的点:

  • 引用(Reference):理解引用是“别名”的本质。它必须在定义时初始化,且一旦绑定就不能再指向其他对象。引用传递在函数参数中非常高效,避免了拷贝开销,是实现“函数内修改实参”和“避免大对象拷贝”的利器。void modify(int& x) { x = 10; }
  • 常量正确性(Const Correctness):这是C++代码质量的试金石。能用const修饰的,一定要加上。它向编译器和未来的阅读者(包括你自己)做出了承诺:这个对象/参数/返回值不会被修改。编译器会帮你守住这个承诺,提前发现许多潜在的错误。例如,const std::vector<int>&表示一个不可修改的vector引用,既高效又安全。
  • 函数重载与默认参数:这是C++提供灵活接口的方式。但要注意,仅返回值类型不同不能构成重载。默认参数则要小心,它只在函数声明处指定一次,通常放在头文件中。

3.2 第二层:面向对象编程(OOP)与内存模型

这是C++的经典部分,也是容易产生误解的部分。

  • 类与对象:掌握构造/析构函数、拷贝构造/赋值运算符(“三/五法则”)、成员初始化列表。这里的关键是理解“深拷贝”与“浅拷贝”。如果你的类管理了动态内存(有一个指针成员),编译器生成的默认拷贝构造函数只会进行浅拷贝(复制指针值),这会导致两个对象指向同一块内存,析构时会被释放两次(双重释放,灾难!)。你必须自己实现拷贝构造函数和拷贝赋值运算符来进行深拷贝(分配新内存并复制内容),或者使用智能指针让它们自动管理。
  • 继承与多态:理解“是一个(is-a)”关系。掌握公有继承(public inheritance)的意义。多态的核心是虚函数(virtual function)基类指针/引用。当通过基类指针调用一个虚函数时,实际调用的是指针所指对象的实际类型的函数版本。这背后的机制是虚函数表(vtable),每个多态类都有一个,里面存放了虚函数的地址。对象内部则有一个隐藏的指针(vptr)指向这个表。理解这个机制,你就能明白多态带来的运行时开销(一次间接寻址)和为什么构造函数/析构函数中调用虚函数的行为可能不符合预期。
  • 内存分区:这是理解C++程序运行的基础。务必搞清楚:
    • 栈(Stack):存放局部变量、函数参数等。由编译器自动管理,速度快,但空间有限。
    • 堆(Heap):供new/malloc动态申请的内存区域。空间大,但需要手动管理,分配和释放速度较慢。
    • 全局/静态存储区:存放全局变量和静态变量。
    • 常量存储区:存放字符串常量等。
    • 代码区:存放程序的二进制代码。

3.3 第三层:泛型编程与标准模板库(STL)

这是C++生产力飞跃的关键。STL不是简单的“库”,它是一种基于泛型编程的架构。

  • 模板(Template):理解函数模板和类模板。模板的本质是“代码生成器”。编译器根据你使用的具体类型,在编译期实例化出一份对应的代码。这带来了类型安全和高性能(无运行时开销)。
  • STL六大组件
    1. 容器(Containers)vector(动态数组,首选序列容器)、list(双向链表)、deque(双端队列)、map/set(基于红黑树的有序关联容器)、unordered_map/unordered_set(基于哈希表的无序关联容器,C++11加入,通常更快)。选择容器的第一原则是根据你的访问模式:需要随机访问用vector,需要频繁在头部插入删除用dequelist,需要快速查找键值用unordered_map
    2. 算法(Algorithms)sort,find,copy,transform等超过100个泛型算法。它们通过迭代器操作容器,实现了算法与数据结构的分离。牢记“迭代器失效”规则:在遍历容器(使用迭代器)时,如果进行了可能引起内存重新分配的操作(如vectorpush_back),之前的迭代器可能失效,继续使用会导致未定义行为。
    3. 迭代器(Iterators):连接容器和算法的桥梁。理解五种迭代器类别:输入、输出、前向、双向、随机访问。vector提供随机访问迭代器,list提供双向迭代器。
    4. 仿函数(Functors):行为类似函数的对象。在C++11后,很多场景被lambda表达式取代,但理解其原理仍有必要。
    5. 适配器(Adapters)stack,queue,priority_queue,它们基于底层容器(如deque)提供特定的接口。
    6. 分配器(Allocators):负责内存分配,高级主题,初学者通常使用默认的std::allocator即可。

3.4 第四层:现代C++(C++11/14/17/20)核心特性

这是让C++编程体验发生质变的部分。如果你还在用C++98的风格写代码,那真的错过了太多。

  • 自动类型推导(auto):让编译器根据初始化表达式推导变量类型。auto i = 42; // int。它能简化代码,尤其是在迭代器和模板类型很长的时候。但不要滥用,在类型清晰有助于代码可读性时,应显式写出类型。
  • 范围for循环(Range-based for loop)for (const auto& element : container)。比手动操作迭代器简洁安全得多。
  • 智能指针(Smart Pointers)彻底改变C++资源管理方式
    • std::unique_ptr<T>:独占所有权。一个对象只能被一个unique_ptr拥有。它不能被复制,只能被移动(std::move)。当unique_ptr离开作用域,它指向的对象会被自动删除。这是替代裸指针的首选。
    • std::shared_ptr<T>:共享所有权。通过引用计数管理资源。当最后一个shared_ptr被销毁时,资源才会被释放。用于需要共享所有权的场景,但要小心循环引用导致的内存泄漏,这时需要std::weak_ptr<T>来打破循环。
    • 经验法则:默认使用unique_ptr;需要共享时再考虑shared_ptr;几乎永远不要使用裸指针new/delete
  • Lambda表达式:在需要函数对象的地方就地定义匿名函数。[capture-list](parameters) -> return-type { body }。捕获列表[=](按值捕获)、[&](按引用捕获)、[this]等需要仔细理解,错误捕获会导致悬空引用。
  • 右值引用与移动语义(Move Semantics):这是C++11最革命性的特性之一,旨在解决不必要的深拷贝带来的性能问题。
    • 左值(lvalue):有名字、有地址的持久对象。
    • 右值(rvalue):临时对象,字面量(如42),表达式求值产生的临时结果。
    • T&&是右值引用,它主要用来“接管”即将消亡的临时对象的资源。std::move的作用仅仅是将一个左值强制转换为右值引用,它本身不移动任何东西。真正的移动操作发生在类的移动构造函数移动赋值运算符中,它们通常直接“窃取”源对象(右值)的内部资源(如指针),然后将源对象置于有效但可析构的状态。
    • 移动语义使得像vector扩容时,转移旧元素到新内存变得非常高效。
  • 并发编程支持(<thread>,<mutex>,<atomic>,<future>:标准库终于原生支持多线程。理解std::thread,std::mutex(互斥锁),std::lock_guard/std::unique_lock(RAII锁管理),std::async(异步任务)的基本用法。但并发编程水很深,数据竞争、死锁、内存序(memory order)都是高级话题。

4. 实战进阶:从“会写”到“写好”

掌握了核心语法和特性,只是万里长征第一步。如何组织代码、如何调试、如何优化,才是区分普通程序员和资深开发者的关键。

4.1 工程化实践:头文件、源文件与编译单元

一个典型的C++项目结构如下:

my_project/ ├── include/ # 对外公开的头文件 (.h/.hpp) │ └── mylib.h ├── src/ # 源文件 (.cpp) │ ├── mylib.cpp │ └── main.cpp ├── lib/ # 第三方库 ├── build/ # 构建输出目录 (由CMake生成) └── CMakeLists.txt # CMake构建脚本

头文件(.h/.hpp)是接口声明,应包含:

  • 函数声明(无函数体)
  • 类/结构体定义
  • 模板定义(因为模板编译机制特殊,定义通常也放在头文件)
  • 内联函数定义
  • 常量定义(constexpr
  • 使用#pragma once或传统的#ifndef ... #define ... #endif防止重复包含。

源文件(.cpp)是实现,包含:

  • 函数定义(函数体)
  • 全局/静态变量定义
  • 包含对应的头文件

编译与链接:每个.cpp文件是一个独立的编译单元。编译器将其编译成目标文件(.o.obj)。链接器再将所有目标文件以及所需的库文件链接在一起,生成最终的可执行文件。理解这个过程,才能解决“未定义的引用(undefined reference)”这类链接错误。

4.2 调试与性能分析实战

  • 调试器(GDB/LLDB):不要只会用printf调试。掌握在IDE中或命令行下设置断点、单步执行、查看变量、查看调用栈。这是定位复杂逻辑错误的终极武器。
  • Sanitizers:现代编译器(GCC/Clang)提供的运行时检测工具,比Valgrind更高效。
    • -fsanitize=address:检测内存错误(越界、释放后使用等)。
    • -fsanitize=leak:检测内存泄漏。
    • -fsanitize=undefined:检测未定义行为(如除零、有符号溢出)。
    • 在开发测试阶段加上这些标志,能帮你提前发现大量隐蔽的Bug。
  • 性能剖析(Profiling):当程序慢的时候,靠猜是没用的。使用perf(Linux) 或Instruments(macOS) 等工具进行性能剖析,找到真正的热点函数(Hotspot)。优化前先测量,优化后再测量验证。

4.3 深入理解:拷贝、移动与完美转发

这是现代C++的深水区,也是写出高效代码的关键。

  • 拷贝 vs 移动:拷贝是“复制一份”,移动是“转移所有权”。对于管理资源的类(如含有指针的类),实现移动语义可以极大提升性能。编译器会在某些情况下自动使用移动(如函数返回局部对象),但你需要正确实现移动构造函数和移动赋值运算符。
  • 完美转发(Perfect Forwarding):目标是让一个函数模板将其参数原封不动地(保持其值类别:左值/右值,以及const/volatile属性)转发给另一个函数。这通过“万能引用”(Universal Reference)T&&std::forward<T>实现。它是实现泛型包装函数(如make_unique)的基础。
    template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); }
    这里Args&&...是万能引用包,std::forward<Args>(args)...保证了每个参数都被完美转发给T的构造函数。

4.4 设计模式与最佳实践

  • RAII:如前所述,这是C++的基石。所有资源管理都应遵循此模式。
  • PIMPL(Pointer to IMPLementation):将类的私有实现细节放到一个前向声明的内部类中,在主类中仅用一个指针持有它。这可以减少编译依赖,实现接口与实现的分离,提高编译速度。
  • Rule of Three/Five/Zero
    • 三法则:如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,那么它很可能需要全部三个。
    • 五法则:在C++11后,增加了移动构造函数和移动赋值运算符。
    • 零法则:最高境界是让类不直接管理任何资源(依赖智能指针和标准库容器),这样编译器生成的默认特殊成员函数就是正确的,你不需要自己写。
  • 类型安全优于宏:尽量使用constexprenum class(强类型枚举)、inline函数等代替#define宏,宏缺乏类型检查,容易引入难以调试的错误。
  • 优先使用标准库:不要自己造轮子。std::vector比你手写的动态数组更安全高效;std::string比你用char*更省心;std::algorithm里的算法经过千锤百炼。

5. 避坑指南与高频问题排查

这里记录了我自己和身边同行们踩过的实实在在的坑,希望能帮你节省大量调试时间。

5.1 内存相关“惨案”

  • 悬空指针(Dangling Pointer):指针指向的内存已被释放。
    • 场景:函数返回了局部变量的地址;一个shared_ptr被释放后,另一个裸指针还指着原来的内存。
    • 排查:使用AddressSanitizer (-fsanitize=address) 可以快速定位。代码审查时,警惕任何返回局部变量地址或引用的函数。
  • 内存泄漏(Memory Leak):分配的内存未被释放。
    • 场景new了但没有delete;循环引用导致shared_ptr无法释放。
    • 排查:使用LeakSanitizer (-fsanitize=leak) 或Valgrind的memcheck。对于循环引用,检查是否该用weak_ptr代替shared_ptr
  • 越界访问(Out-of-Bounds Access):访问了数组或容器有效范围之外的内存。
    • 场景vector的下标访问未检查;使用指针算术错误。
    • 排查:AddressSanitizer对此类错误极其敏感。养成使用at()方法(会进行边界检查)调试的习惯,或者使用迭代器。
  • 双重释放(Double Free):同一块内存被释放了两次。
    • 场景:浅拷贝后,两个对象的析构函数都对同一个指针执行了delete;在多线程环境中不加锁地操作同一裸指针。
    • 排查:AddressSanitizer同样能捕获。坚持使用智能指针可以根本性避免此问题。

5.2 多线程“雷区”

  • 数据竞争(Data Race):多个线程同时读写同一内存位置,且至少有一个是写操作,且没有同步。
    • 现象:程序结果不确定,有时对有时错,极难复现。
    • 解决:使用互斥锁(# 1. 概述

本文,我们来分享 MyBatis 的日志模块,对应logging包。如下图所示:logging

在 《精尽 MyBatis 源码解析 —— 项目结构一览》 中,简单介绍了这个模块如下:

无论在开发测试环境中,还是在线上生产环境中,日志在整个系统中的地位都是非常重要的。良好的日志功能可以帮助开发人员和测试人员快速定位 Bug 代码,也可以帮助运维人员快速定位性能瓶颈等问题。目前的 Java 世界中存在很多优秀的日志框架,例如 Log4j、 Log4j2、Apache Commons Log、java.util.logging、slf4j 等。

MyBatis 作为一个设计优良的框架,除了提供详细的日志输出信息,还要能够集成多种日志框架,其日志模块的一个主要功能就是集成第三方日志框架

本文涉及的类如下图所示:类图

  • 正如上文所说,MyBatis 的logging包,是集成第三方日志框架。所以,logging包下的代码,整体是使用适配器模式,对第三方日志框架做统一的适配。

下面,让我们开始遨游logging包。

2. LogFactory

org.apache.ibatis.logging.LogFactory,Log 工厂类。

2.1 构造方法

// LogFactory.java /** * Marker to be used by logging implementations that support markers */ public static final String MARKER = "MYBATIS"; /** * 使用的 Log 的构造方法 */ private static Constructor<? extends Log> logConstructor; static { // <1> 逐个尝试,判断使用哪个 Log 的实现类,即初始化 logConstructor 属性 tryImplementation(LogFactory::useSlf4jLogging); tryImplementation(LogFactory::useCommonsLogging); tryImplementation(LogFactory::useLog4J2Logging); tryImplementation(LogFactory::useLog4JLogging); tryImplementation(LogFactory::useJdkLogging); tryImplementation(LogFactory::useNoLogging); }
  • <1>处,在类加载时,通过tryImplementation方法,逐个尝试,判断使用哪个 Log 的实现类,即初始化logConstructor属性。代码如下:

    // LogFactory.java private static void tryImplementation(Runnable runnable) { // 若 logConstructor 为空,则执行回调方法 if (logConstructor == null) { try { runnable.run(); } catch (Throwable t) { // ignore } } }
    • 通过这样的方式,就能保证使用首个被找到的 Log 的实现类。
  • useSlf4jLogginguseCommonsLogginguseLog4J2LogginguseLog4JLogginguseJdkLogginguseNoLogging方法,代码如下:

    // LogFactory.java public static synchronized void useSlf4jLogging() { setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class); } public static synchronized void useCommonsLogging() { setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class); } public static synchronized void useLog4JLogging() { setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class); } public static synchronized void useLog4J2Logging() { setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class); } public static synchronized void useJdkLogging() { setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class); } public static synchronized void useStdOutLogging() { setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class); } public static synchronized void useNoLogging() { setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class); }
    • 在每个方法中,会调用#setImplementation(Class<? extends Log> implClass)方法,初始化logConstructor属性。代码如下:

      // LogFactory.java private static void setImplementation(Class<? extends Log> implClass) { try { // 获得参数为 String 的构造方法 Constructor<? extends Log> candidate = implClass.getConstructor(String.class); // 创建 Log 对象 Log log = candidate.newInstance(LogFactory.class.getName()); // 打印日志,提示使用的 Log 实现类 if (log.isDebugEnabled()) { log.debug("Logging initialized using '" + implClass + "' adapter."); } // 设置 logConstructor 属性 logConstructor = candidate; } catch (Throwable t) { throw new LogException("Error setting Log implementation. Cause: " + t, t); } }
      • 通过这样的方式,就能保证使用首个被找到的 Log 的实现类。

2.2 getLog

#getLog(...)方法,获得 Log 对象。代码如下:

// LogFactory.java public static Log getLog(Class<?> aClass) { return getLog(aClass.getName()); } public static Log getLog(String logger) { try { return logConstructor.newInstance(logger); } catch (Throwable t) { throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t); } }
  • 通过logConstructor属性,创建 Log 对象。

3. Log

org.apache.ibatis.logging.Log,MyBatis Log 接口。代码如下:

// Log.java public interface Log { boolean isDebugEnabled(); boolean isTraceEnabled(); void error(String s, Throwable e); void error(String s); void debug(String s); void trace(String s); void warn(String s); }
  • 定义了日志的接口方法。

3.1 适配器

Log 接口的实现类比较多,我们以org.apache.ibatis.logging.slf4j.Slf4jImpl类,作为一个例子,来看看实现类的代码。代码如下:

// Slf4jImpl.java public class Slf4jImpl implements Log { private Log log; public Slf4jImpl(String clazz) { // 使用 SLF4J 的 LoggerFactory 获得 org.slf4j.Logger 对象 Logger logger = LoggerFactory.getLogger(clazz); // 如果使用 SLF4J 桥接包,例如 slf4j-log4j12 ,则 logger 类型为 org.slf4j.impl.Log4jLoggerAdapter // 如果是 Logback,则类型为 ch.qos.logback.classic.Logger if (logger instanceof LocationAwareLogger) { try { // check for slf4j >= 1.6 method signature logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class); log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger); return; } catch (SecurityException e) { // fail-back to Slf4jLoggerImpl } catch (NoSuchMethodException e) { // fail-back to Slf4jLoggerImpl } } // Logger is not LocationAwareLogger or slf4j version < 1.6 log = new Slf4jLoggerImpl(logger); } @Override public boolean isDebugEnabled() { return log.isDebugEnabled(); } @Override public boolean isTraceEnabled() { return log.isTraceEnabled(); } @Override public void error(String s, Throwable e) { log.error(s, e); } @Override public void error(String s) { log.error(s); } @Override public void debug(String s) { log.debug(s); } @Override public void trace(String s) { log.trace(s); } @Override public void warn(String s) { log.warn(s); } }
  • 在构造方法中,我们可以看到,使用 SLF4J 的org.slf4j.LoggerFactory获得org.slf4j.Logger对象。之后,根据不同的情况,创建Slf4jLocationAwareLoggerImplSlf4jLoggerImpl对象。为什么呢?因为 SLF4J 有桥接包,例如slf4j-log4j12,此时logger类型为org.slf4j.impl.Log4jLoggerAdapter。如果是 Logback,则类型为ch.qos.logback.classic.Logger。所以,通过这样的方式,进行适配。

  • 那么,Slf4jLocationAwareLoggerImplSlf4jLoggerImpl的代码实现,就非常简单了,实现 Log 接口,对应的方法,调用org.slf4j.Logger对应的方法。以Slf4jLoggerImpl举例子,代码如下:

    // Slf4jLoggerImpl.java class Slf4jLoggerImpl implements Log { private org.slf4j.Logger log; public Slf4jLoggerImpl(org.slf4j.Logger logger) { log = logger; } @Override public boolean isDebugEnabled() { return log.isDebugEnabled(); } @Override public boolean isTraceEnabled() { return log.isTraceEnabled(); } @Override public void error(String s, Throwable e) { log.error(s, e); } @Override public void error(String s) { log.error(s); } @Override public void debug(String s) { log.debug(s); } @Override public void trace(String s) { log.trace(s); } @Override public void warn(String s) { log.warn(s); } }
    • 是不是很简单。
  • 其它 Log 接口的实现类,也是类似的思路。感兴趣的胖友,可以自己看看。

3.2 StdOutImpl

org.apache.ibatis.logging.stdout.StdOutImpl,实现 Log 接口,StdOut 实现类。代码如下:

// StdOutImpl.java public class StdOutImpl implements Log { public StdOutImpl(String clazz) { // Do Nothing } @Override public boolean isDebugEnabled() { return true; } @Override public boolean isTraceEnabled() { return true; } @Override public void error(String s, Throwable e) { System.err.println(s); e.printStackTrace(System.err); } @Override public void error(String s) { System.err.println(s); } @Override public void debug(String s) { System.out.println(s); } @Override public void trace(String s) { System.out.println(s); } @Override public void warn(String s) { System.out.println(s); } }
  • 直接使用System.outSystem.err输出日志。

3.3 NoLoggingImpl

org.apache.ibatis.logging.nologging.NoLoggingImpl,实现 Log 接口,空实现类。代码如下:

// NoLoggingImpl.java public class NoLoggingImpl implements Log { public NoLoggingImpl(String clazz) { // Do Nothing } @Override public boolean isDebugEnabled() { return false; } @Override public boolean isTraceEnabled() { return false; } @Override public void error(String s, Throwable e) { // Do Nothing } @Override public void error(String s) { // Do Nothing } @Override public void debug(String s) { // Do Nothing } @Override public void trace(String s) { // Do Nothing } @Override public void warn(String s) { // Do Nothing } }
  • 每个方法,空实现。

4. BaseJdbcLogger

logging包中,jdbc包下,有将 JDBC 操作,打印出相应的日志。核心类就是org.apache.ibatis.logging.jdbc.BaseJdbcLogger。但是,实际上,这个类我们平时不会直接使用到,而是通过org.apache.ibatis.logging.jdbc.ConnectionLoggerorg.apache.ibatis.logging.jdbc.StatementLoggerorg.apache.ibatis.logging.jdbc.PreparedStatementLoggerorg.apache.ibatis.logging.jdbc.ResultSetLogger等等。关于这块,我们放在后面的文章,详细解析。

5. 总结

总的来说,logging包,是对第三方日志框架的集成,代码比较简单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询