从 System.out.println() 到可扩展的企业级日志系统,我们经历了什么?

一、引言:为什么需要自研日志框架?

在开发 CodeStats —— 一个完全自研的 Java Web 平台(含 IoC 容器、MVC、嵌入式 Tomcat、连接池、MyBatis 风格 Mapper)的过程中,日志是不可或缺的基础设施。然而,项目初期我们面临两个选择:

  • 引入 Log4j2 / Logback:功能强大但依赖重,且需要适配自研的类加载环境。

  • 自研一套轻量级日志框架:可控、无外部依赖、可深度集成到 Pipeline 和 IoC 生命周期中。

我们选择了后者。这不仅是一次技术练兵,更是对“一周 AI Java Web 框架”与 WWAIC 范式的实践:让代码结构清晰、模式可解释、扩展点暴露给 AI 辅助理解。

本文将全面剖析自研日志框架的设计模式功能全景使用示例以及心路历程,希望能为同样走上造轮子之路的开发者提供参考。


二、核心设计模式全景图

日志框架共包含 7 个核心包/类,我们从中提炼出 6 种经典设计模式

2.1 模板方法模式(Template Method)—— Appender 基类

所有输出目的地(控制台、文件、滚动文件、异步)都继承自抽象类 Appender

java

public abstract class Appender {
    protected String name;
    protected Layout layout;
    protected final List<Filter> filters = new ArrayList<>();
    protected Level threshold = Level.TRACE;

    public void doAppend(LogEvent event) {        // 模板方法
        if (!event.getLevel().isGreaterOrEqual(threshold)) return;
        for (Filter filter : filters) {
            Filter.Result r = filter.decide(event);
            if (r == Filter.Result.DENY) return;
            if (r == Filter.Result.ACCEPT) break;
        }
        append(event);                            // 钩子方法
    }

    protected abstract void append(LogEvent event); // 由子类实现
    public abstract void close();
}
  • 意图:定义算法骨架(过滤→阈值→最终输出),将可变步骤延迟到子类。

  • 子类ConsoleAppenderFileAppenderRollingFileAppenderAsyncAppender 只需实现 append() 和 close()

2.2 过滤器模式 + 责任链(Filter Chain)

每个 Appender 可持有多个 Filter,形成责任链。LevelRangeFilter 按级别范围过滤。

java

public abstract class Filter {
    public enum Result { ACCEPT, DENY, NEUTRAL }
    public abstract Result decide(LogEvent event);
}
  • 设计亮点:过滤器链在 doAppend 中按序执行,任一 DENY 即终止。

  • 扩展性:可轻松增加 RegexFilterMDCFilter 等。

2.3 策略模式(Strategy)—— Layout

Layout 负责将 LogEvent 格式化为字符串。PatternLayout 是具体策略,支持占位符解析(%d%level%msg%ex 等)。

java

public abstract class Layout {
    public abstract String format(LogEvent event);
}
  • 使用:每个 Appender 可设置不同的 Layout,实现控制台简洁、文件详细的需求。

2.4 建造者模式 + 工厂模式 —— LoggerFactory 与 Configuration

LoggerFactory 是门面,内部持有 Configuration 单例,负责从 XML 加载配置或降级到默认控制台配置。

java

public class LoggerFactory {
    private static Configuration config;
    private static final ConcurrentHashMap<String, Logger> loggers = ...;

    static {
        try {
            config = new Configuration();
            config.loadFromClasspath("log-config.xml");
        } catch (Exception e) {
            config = new Configuration();
            config.useDefaultConsoleConfig();   // 降级
        }
    }

    public static Logger getLogger(Class<?> clazz) {
        return getLogger(clazz.getName());
    }
}
  • 建造者本质Configuration 内部通过 XML 解析逐步构建 AppenderFilterLayout 对象,最后组装成 LoggerConfig

  • 工厂方法LoggerFactory.getLogger() 缓存并返回 Logger 实例。

2.5 观察者模式 —— AsyncAppender

异步 Appender 内部维护一个阻塞队列和工作线程,将日志事件的生产和消费解耦。

java

public class AsyncAppender extends Appender {
    private final BlockingQueue<LogEvent> queue;
    private final Appender delegate;
    private final Worker worker;

    @Override
    protected void append(LogEvent event) {
        queue.offer(event);   // 生产者
    }

    private class Worker extends Thread {
        public void run() {
            while (running) {
                LogEvent event = queue.take();   // 消费者
                delegate.doAppend(event);
            }
        }
    }
}
  • 事件驱动:日志写入不阻塞主线程,适合高吞吐场景。

2.6 组合模式 —— Configuration 与 LoggerConfig

一个 Configuration 包含多个 Appender 和多个 LoggerConfig(每个 LoggerConfig 又有自己的 Appender 列表),形成树状结构。root 是特殊的 LoggerConfig

java

public class Configuration {
    private final Map<String, Appender> appenders = new HashMap<>();
    private final Map<String, LoggerConfig> loggerConfigs = new HashMap<>();
    private LoggerConfig rootConfig;
}
  • 组合模式:个体(单个 Appender)和组合(LoggerConfig 包装多个 Appender)具有一致的行为接口(doAppend)。


三、功能全景:从开箱即用到生产就绪

3.1 日志级别

TRACE < DEBUG < INFO < WARN < ERROR < OFF,通过 Level.isGreaterOrEqual() 比较。

3.2 Appender 家族

名称 说明 关键参数
ConsoleAppender 输出到 System.out
FileAppender 输出到单个文件 fileName
RollingFileAppender 按大小滚动 maxFileSizemaxBackupIndex
AsyncAppender 异步代理其他 Appender queueSize

3.3 灵活布局 —— PatternLayout

支持的占位符:

符号 含义 示例
%d 日期时间 %d{yyyy-MM-dd HH:mm:ss.SSS}
%level 日志级别,可左对齐 %-5level
%logger Logger 名称,可截断 %logger{36}
%t 线程名
%msg / %m 日志消息
%n 换行
%ex 异常堆栈(含 cause 链)

3.4 过滤器 —— LevelRangeFilter

xml

<filter class="com.omni.framework.log.filter.LevelRangeFilter">
    <param name="minLevel" value="INFO"/>
    <param name="maxLevel" value="ERROR"/>
</filter>

3.5 配置加载 —— XML + 热降级

  • 从 classpath 加载 log-config.xml

  • 支持 <property> 定义变量(如 ${log.dir})。

  • 支持 <logger> 及 <root> 的 additivity 传播。

  • 若 XML 缺失或解析失败,自动降级为 ConsoleAppender + PatternLayout

3.6 异常处理完整堆栈

通过 ExceptionUtil.getFullStackTrace() 递归打印 cause 链,避免丢失根因。


四、使用示例:从零开始集成

4.1 快速入门 —— 无配置

java

import com.omni.framework.log.LoggerFactory;
import com.omni.framework.log.core.Logger;

public class Demo {
    private static final Logger log = LoggerFactory.getLogger(Demo.class);

    public static void main(String[] args) {
        log.info("应用启动");
        log.debug("调试信息, userId: {}", 123);
        try {
            throw new RuntimeException("测试异常");
        } catch (Exception e) {
            log.error("发生错误", e);
        }
    }
}

输出(默认控制台):

text

2025-05-28 10:00:00.123 [main] INFO  com.omni.Demo - 应用启动
2025-05-28 10:00:00.124 [main] DEBUG com.omni.Demo - 调试信息, userId: 123
2025-05-28 10:00:00.125 [main] ERROR com.omni.Demo - 发生错误
java.lang.RuntimeException: 测试异常
    at com.omni.Demo.main(Demo.java:11)

4.2 XML 配置 —— 生产环境推荐

xml

<!-- log-config.xml -->
<configuration>
    <property name="log.dir" value="/var/log/codestats"/>

    <appender name="CONSOLE" class="com.omni.framework.log.appender.ConsoleAppender">
        <layout class="com.omni.framework.log.layout.PatternLayout">
            <param name="pattern" value="%d [%t] %-5level %logger - %msg%n"/>
        </layout>
    </appender>

    <appender name="ROLLING" class="com.omni.framework.log.appender.RollingFileAppender">
        <param name="file" value="${log.dir}/app.log"/>
        <param name="maxFileSize" value="10MB"/>
        <param name="maxBackupIndex" value="5"/>
        <layout class="com.omni.framework.log.layout.PatternLayout">
            <param name="pattern" value="%d %level [%thread] %logger{36} - %msg%n"/>
        </layout>
        <filter class="com.omni.framework.log.filter.LevelRangeFilter">
            <param name="minLevel" value="INFO"/>
        </filter>
    </appender>

    <appender name="ASYNC" class="com.omni.framework.log.appender.AsyncAppender">
        <param name="queueSize" value="1024"/>
        <appender-ref ref="ROLLING"/>
    </appender>

    <logger name="com.omni.business" level="DEBUG" additivity="false">
        <appender-ref ref="ASYNC"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

4.3 与 Spring IoC 容器集成

在 SpringApplication 启动时,日志框架已静态初始化,可直接使用。同时支持通过 ConfigLoader 读取 application.properties 中的日志配置(如动态调整级别)。


五、设计心路历程:从 System.out 到可扩展架构

阶段一:原始社会 —— 到处都是 System.out.println()

项目早期,我们直接用 System.out 打印调试信息。后果是:

  • 无法区分级别,生产环境日志淹没在 DEBUG 中。

  • 无法重定向到文件,排查问题只能看控制台。

  • 无性能考虑,高频日志拖垮吞吐。

阶段二:第一个抽象 —— Logger 门面

借鉴 SLF4J 思想,定义 Logger 接口和 LoggerFactory。内部仍用 System.out,但为未来扩展留了空间。

阶段三:引入 Log4j2 架构 —— Appender + Layout + Filter

我们仔细研究了 Log4j2 的三大核心组件,决定重写一个“迷你版”:

  • Appender:负责输出位置(模板方法模式)。

  • Layout:负责格式化(策略模式)。

  • Filter:负责过滤(责任链模式)。

这一阶段完成了从“能用”到“可配置”的跨越。

阶段四:性能优化 —— 异步不阻塞

在一次压测中,同步文件写入导致 TPS 下降 40%。我们参考了 Logback 的 AsyncAppender,用 BlockingQueue + 单线程消费者实现异步代理。生产环境配置异步后,性能提升显著。

阶段五:工程化完善 —— XML 配置、滚动策略、异常堆栈

为了让运维人员能动态调整日志,我们增加了 XML 配置解析(使用 javax.xml.parsers 轻量级实现)。同时补充了 RollingFileAppender 和完整的异常堆栈打印(含 cause 链)。

阶段六:与自研框架深度融合

  • 上下文传递:在 DispatcherServlet 中,通过 MDC(未在本篇详述,但已预留)传递请求 ID。

  • 生命周期集成:在 Catalina 停止时,调用 LoggerFactory.shutdown() 优雅关闭异步线程。

  • 类加载隔离:每个 WebappClassLoader 可拥有独立的日志配置。


六、总结与展望

6.1 成果回顾

自研日志框架最终实现了:

  • 零外部依赖,完美融入自研 IoC 容器。

  • 高性能异步,生产环境压测 QPS 提升 3 倍。

  • 灵活配置,支持 XML、编程式、降级默认三种模式。

  • 可扩展,新增一个 Appender 只需实现两个方法。

6.2 对标 WWAIC 范式

参考:

https://blog.csdn.net/qq_41652036/article/details/161432743?spm=1001.2014.3001.5502

  • 每个模式都对应一个经典命名(如 Appender 的模板方法)。

  • 职责划分清晰,单一类不超过 300 行。

  • 关键决策点(如 doAppend 的过滤链)用注释说明。

6.3 未来规划

  • 支持 MDC(Mapped Diagnostic Context),实现请求链路追踪。

  • 支持 Lambda 表达式,延迟构造日志参数。

  • 提供 JMX 接口,动态修改日志级别。


造轮子不是为了重复发明,而是为了理解“为何是这样”。希望这篇日志框架的深度解析,能给你在自研基础组件时带来启发。如果你也对自研 Java Web 框架感兴趣,欢迎关注 CodeStats 项目,我们一起探讨“从零到一”的乐趣。

项目地址https://blog.csdn.net/qq_41652036/article/details/161399010?spm=1001.2014.3001.5502

### 如果这篇文章帮到了你

- **点个赞 👍** —— 让更多想手写 日志框架 却不敢开始的人看到它
- **收藏 ⭐** —— 下次你动手写自己的框架时,它就是你的 roadmap
- **评论 💬** —— 告诉我你在手写过程中遇到的最大坑,我会整理成 FAQ

**下篇预告**:给这个框架加上 AOP(动态代理),实现日志、事务、权限控制。想看的评论区告诉我 👇

感谢你的阅读,一个赞就是对我最大的鼓励,也是我继续手写硬核教程的动力。

更多推荐