
1. 为什么C依然是硬核开发者的首选如果你在技术社区里泡久了会发现一个有趣的现象每当有新的编程语言发布总有人会问“C是不是要过时了” 但十几年过去了从游戏引擎、高频交易系统到操作系统内核、数据库C的身影依然无处不在。我见过太多项目前期为了快速迭代用了其他语言到了性能瓶颈期最终还是得用C来重写核心模块。这就是C的底气——它提供了一种独特的平衡让你在享受接近硬件底层的控制力和极致性能的同时又能使用现代的高级抽象来管理复杂度。很多人对C的印象还停留在“复杂”和“难学”上这其实是个误区。C的复杂性很大程度上源于它的“工具箱”属性。它不像某些语言只给你一把瑞士军刀而是给了你一个完整的车间里面有从螺丝刀到数控机床的所有工具。问题不在于工具多而在于你是否清楚什么时候该用哪一把。从C98到C23这门语言一直在进化但它的核心哲学没变信任程序员不隐藏成本提供零开销抽象。这意味着你写的每一行代码其背后的机器指令和内存操作都是清晰可预测的。对于需要榨干每一分硬件性能的场景比如游戏渲染、实时音视频处理、金融交易引擎这种确定性是无价的。学习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而言它们对语言特性的支持都已非常完善。关键在于请务必使用支持C17或更高标准的版本。你可以通过g --version或clang --version查看版本并在编译时加上-stdc17或-stdc20标志。关于集成开发环境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::vectorint表示一个不可修改的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六大组件容器Containersvector动态数组首选序列容器、list双向链表、deque双端队列、map/set基于红黑树的有序关联容器、unordered_map/unordered_set基于哈希表的无序关联容器C11加入通常更快。选择容器的第一原则是根据你的访问模式需要随机访问用vector需要频繁在头部插入删除用deque或list需要快速查找键值用unordered_map。算法Algorithmssort,find,copy,transform等超过100个泛型算法。它们通过迭代器操作容器实现了算法与数据结构的分离。牢记“迭代器失效”规则在遍历容器使用迭代器时如果进行了可能引起内存重新分配的操作如vector的push_back之前的迭代器可能失效继续使用会导致未定义行为。迭代器Iterators连接容器和算法的桥梁。理解五种迭代器类别输入、输出、前向、双向、随机访问。vector提供随机访问迭代器list提供双向迭代器。仿函数Functors行为类似函数的对象。在C11后很多场景被lambda表达式取代但理解其原理仍有必要。适配器Adaptersstack,queue,priority_queue它们基于底层容器如deque提供特定的接口。分配器Allocators负责内存分配高级主题初学者通常使用默认的std::allocator即可。3.4 第四层现代CC11/14/17/20核心特性这是让C编程体验发生质变的部分。如果你还在用C98的风格写代码那真的错过了太多。自动类型推导auto让编译器根据初始化表达式推导变量类型。auto i 42; // int。它能简化代码尤其是在迭代器和模板类型很长的时候。但不要滥用在类型清晰有助于代码可读性时应显式写出类型。范围for循环Range-based for loopfor (const auto element : container)。比手动操作迭代器简洁安全得多。智能指针Smart Pointers彻底改变C资源管理方式。std::unique_ptrT独占所有权。一个对象只能被一个unique_ptr拥有。它不能被复制只能被移动std::move。当unique_ptr离开作用域它指向的对象会被自动删除。这是替代裸指针的首选。std::shared_ptrT共享所有权。通过引用计数管理资源。当最后一个shared_ptr被销毁时资源才会被释放。用于需要共享所有权的场景但要小心循环引用导致的内存泄漏这时需要std::weak_ptrT来打破循环。经验法则默认使用unique_ptr需要共享时再考虑shared_ptr几乎永远不要使用裸指针new/delete。Lambda表达式在需要函数对象的地方就地定义匿名函数。[capture-list](parameters) - return-type { body }。捕获列表[]按值捕获、[]按引用捕获、[this]等需要仔细理解错误捕获会导致悬空引用。右值引用与移动语义Move Semantics这是C11最革命性的特性之一旨在解决不必要的深拷贝带来的性能问题。左值lvalue有名字、有地址的持久对象。右值rvalue临时对象字面量如42表达式求值产生的临时结果。T是右值引用它主要用来“接管”即将消亡的临时对象的资源。std::move的作用仅仅是将一个左值强制转换为右值引用它本身不移动任何东西。真正的移动操作发生在类的移动构造函数和移动赋值运算符中它们通常直接“窃取”源对象右值的内部资源如指针然后将源对象置于有效但可析构的状态。移动语义使得像vector扩容时转移旧元素到新内存变得非常高效。并发编程支持thread,mutex,atomic,future标准库终于原生支持多线程。理解std::thread,std::mutex互斥锁,std::lock_guard/std::unique_lockRAII锁管理,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更高效。-fsanitizeaddress检测内存错误越界、释放后使用等。-fsanitizeleak检测内存泄漏。-fsanitizeundefined检测未定义行为如除零、有符号溢出。在开发测试阶段加上这些标志能帮你提前发现大量隐蔽的Bug。性能剖析Profiling当程序慢的时候靠猜是没用的。使用perf(Linux) 或Instruments(macOS) 等工具进行性能剖析找到真正的热点函数Hotspot。优化前先测量优化后再测量验证。4.3 深入理解拷贝、移动与完美转发这是现代C的深水区也是写出高效代码的关键。拷贝 vs 移动拷贝是“复制一份”移动是“转移所有权”。对于管理资源的类如含有指针的类实现移动语义可以极大提升性能。编译器会在某些情况下自动使用移动如函数返回局部对象但你需要正确实现移动构造函数和移动赋值运算符。完美转发Perfect Forwarding目标是让一个函数模板将其参数原封不动地保持其值类别左值/右值以及const/volatile属性转发给另一个函数。这通过“万能引用”Universal ReferenceT和std::forwardT实现。它是实现泛型包装函数如make_unique的基础。templatetypename T, typename... Args std::unique_ptrT make_unique(Args... args) { return std::unique_ptrT(new T(std::forwardArgs(args)...)); }这里Args...是万能引用包std::forwardArgs(args)...保证了每个参数都被完美转发给T的构造函数。4.4 设计模式与最佳实践RAII如前所述这是C的基石。所有资源管理都应遵循此模式。PIMPLPointer to IMPLementation将类的私有实现细节放到一个前向声明的内部类中在主类中仅用一个指针持有它。这可以减少编译依赖实现接口与实现的分离提高编译速度。Rule of Three/Five/Zero三法则如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个那么它很可能需要全部三个。五法则在C11后增加了移动构造函数和移动赋值运算符。零法则最高境界是让类不直接管理任何资源依赖智能指针和标准库容器这样编译器生成的默认特殊成员函数就是正确的你不需要自己写。类型安全优于宏尽量使用constexpr、enum class强类型枚举、inline函数等代替#define宏宏缺乏类型检查容易引入难以调试的错误。优先使用标准库不要自己造轮子。std::vector比你手写的动态数组更安全高效std::string比你用char*更省心std::algorithm里的算法经过千锤百炼。5. 避坑指南与高频问题排查这里记录了我自己和身边同行们踩过的实实在在的坑希望能帮你节省大量调试时间。5.1 内存相关“惨案”悬空指针Dangling Pointer指针指向的内存已被释放。场景函数返回了局部变量的地址一个shared_ptr被释放后另一个裸指针还指着原来的内存。排查使用AddressSanitizer (-fsanitizeaddress) 可以快速定位。代码审查时警惕任何返回局部变量地址或引用的函数。内存泄漏Memory Leak分配的内存未被释放。场景new了但没有delete循环引用导致shared_ptr无法释放。排查使用LeakSanitizer (-fsanitizeleak) 或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. LogFactoryorg.apache.ibatis.logging.LogFactoryLog 工厂类。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 的实现类。useSlf4jLogging、useCommonsLogging、useLog4J2Logging、useLog4JLogging、useJdkLogging、useNoLogging方法代码如下// 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. Logorg.apache.ibatis.logging.LogMyBatis 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对象。之后根据不同的情况创建Slf4jLocationAwareLoggerImpl或Slf4jLoggerImpl对象。为什么呢因为 SLF4J 有桥接包例如slf4j-log4j12此时logger类型为org.slf4j.impl.Log4jLoggerAdapter。如果是 Logback则类型为ch.qos.logback.classic.Logger。所以通过这样的方式进行适配。那么Slf4jLocationAwareLoggerImpl和Slf4jLoggerImpl的代码实现就非常简单了实现 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 StdOutImplorg.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.out和System.err输出日志。3.3 NoLoggingImplorg.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.ConnectionLogger、org.apache.ibatis.logging.jdbc.StatementLogger、org.apache.ibatis.logging.jdbc.PreparedStatementLogger、org.apache.ibatis.logging.jdbc.ResultSetLogger等等。关于这块我们放在后面的文章详细解析。5. 总结总的来说logging包是对第三方日志框架的集成代码比较简单。