如何设计一个模仿 Log4j 的日志框架?—— 自研 Java Web 框架日志模块全解析(设计模式 + 功能演进)
从
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();
}
-
意图:定义算法骨架(过滤→阈值→最终输出),将可变步骤延迟到子类。
-
子类:
ConsoleAppender、FileAppender、RollingFileAppender、AsyncAppender只需实现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即终止。 -
扩展性:可轻松增加
RegexFilter、MDCFilter等。
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 解析逐步构建Appender、Filter、Layout对象,最后组装成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 |
按大小滚动 | maxFileSize, maxBackupIndex |
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(动态代理),实现日志、事务、权限控制。想看的评论区告诉我 👇
感谢你的阅读,一个赞就是对我最大的鼓励,也是我继续手写硬核教程的动力。
更多推荐
所有评论(0)