进阶之路:Java 日志框架全画传(上)
ccwgpt 2024-10-01 08:12 27 浏览 0 评论
导读:随着互联网和大数据的蓬勃发展,分布式日志系统以及日志分析系统得到了广泛地应用。目前,几乎在所有应用程序中,都会用到各种各样的日志框架来记录程序的运行信息。鉴于此,工程师十分有必要熟悉主流的日志记录框架。
作者 | 书澜
前言
日志的有无虽然不影响应用程序的运行结果,但是没有日志的应用程序是不完整的,甚至可以说是有缺陷的。优雅的日志系统可以记录操作轨迹,监控系统运行状况以及回溯系统故障。在工作中,部分工程师对主流的日志框架仍然是一知半解,日常应用还停留在复制粘贴的层面,因此写作本文,希望对读者有所帮助。
本系列文章分为上、中、下三篇,将全面系统地介绍 Java 日志框架,主要内容有:
- 日志的意义与价值
- Java 日志框架进化史
- 日志门面与日志系统
- 日志框架的使用选择
- 日志使用中需要遵循的规范及注意事项
- 日志使用示例及常见报错
本篇为上篇,将详细解读主流 Java 日志框架。
日志的意义与价值
- 在开发调试阶段: 日志系统有助于更快的定位问题。
- 在应用运维阶段: 日志系统有助于记录大部分的异常信息,通过收集日志信息可以对系统的运行状态进行实时监控预警。
- 在数据分析阶段: 日志中通常包含大量的用户数据,包括点击行为、兴趣偏好等,基于这些数据可以对用户进行“画像”,进而助力战略决策。随着大数据技术日渐成熟,海量日志分析已经在互联网公司得到广泛应用。
Java 日志框架进化史
在开发过程中,工程师不得不面对一个很现实的问题:Java “混乱”的日志框架体系。为什么说“混乱”呢?原因在于早期 Java 日志框架没有制定统一的标准,使得很多应用程序会同时使用多种日志框架。Java 日志框架的发展历程大致可分为以下几个阶段:
- Log4j
Apache Log4j 是一种基于 Java 的日志记录工具,它是 Apache 软件基金会的一个项目。在 jdk1.3 之前,还没有现成的日志框架,Java 工程师只能使用原始的 System.out.println(), System.err.println() 或者 e.printStackTrace()。通过把 debug 日志写到 StdOut 流,错误日志写到 ErrOut 流,以此记录应用程序的运行状态。这种原始的日志记录方式缺陷明显,不仅无法实现定制化,而且日志的输出粒度不够细。鉴于此,1999 年,大牛 Ceki Gülcü 创建了 Log4j 项目,并几乎成为了 Java 日志框架的实际标准。
- JUL
Log4j 作为 Apache 基金会的一员,Apache 希望将 Log4j 引入 jdk,不过被 sun 公司拒绝了。随后,sun 模仿 Log4j,在 jdk1.4 中引入了 JUL(java.util.logging)。
- Commons Logging
为了解耦日志接口与实现,2002 年 Apache 推出了 JCL(Jakarta Commons Logging),也就是 Commons Logging。Commons Logging 定义了一套日志接口,具体实现则由 Log4j 或 JUL 来完成。Commons Logging 基于动态绑定来实现日志的记录,在使用时只需要用它定义的接口编码即可,程序运行时会使用 ClassLoader 寻找和载入底层的日志库,因此可以自由选择由 log4j 或 JUL 来实现日志功能。
- Slf4j&Logback
大牛 Ceki Gülcü 与 Apache 基金会关于 Commons-Logging 制定的标准存在分歧,后来,Ceki Gülcü 离开 Apache 并先后创建了 Slf4j 和 Logback 两个项目。Slf4j 是一个日志门面,只提供接口,可以支持 Logback、JUL、log4j 等日志实现,Logback 提供具体的实现,它相较于 log4j 有更快的执行速度和更完善的功能。
- Log4j 2
为了维护在 Java 日志江湖的地位,防止 JCL、Log4j 被 Slf4j、Logback 组合取代 ,2014 年 Apache 推出了 Log4j 2。Log4j 2 与 log4j 不兼容,经过大量深度优化,其性能显著提升。
日志门面与日志系统
在上文中已经提及,目前常用的日志框架有 Log4j,Log4j 2,Commons Logging,Slf4j,Logback,JUL。这些日志框架可以分为两种类型:门面日志和日志系统。
- 日志门面:只提供日志相关的接口定义,即相应的 API,而不提供具体的接口实现。日志门面在使用时,可以动态或者静态地指定具体的日志框架实现,解除了接口和实现的耦合,使用户可以灵活地选择日志的具体实现框架。
- 日志系统:与日志门面相对,它提供了具体的日志接口实现,应用程序通过它执行日志打印的功能。
如上图所示,Commons-Logging 和 Slf4j 属于日志门面框架,Log4j、Logback、和 JUL 则属于具体的日志系统框架。阅读至此,想必读者一定疑惑——为何如此设计?为何不简单一点?为何分成了门面和实现?
在回答上述问题之前,我们先一起简单回顾一下门面模式(软件设计模式的一种,也称外观模式、正面模式)。门面模式的核心为:外部客户端与一个子系统的通信,必须通过一个统一的外观对象进行,使得子系统更易于使用,其本质就是为子系统中的一组接口提供一个统一的高层接口,如下图所示:
门面模式的核心是门面对象 Facade,它有如下几个特点:
- 知道所有子模块的责任和功能
- 将客户端发来的请求委派到子系统中,本身没有具体业务逻辑
- 不参与子系统内业务逻辑的实现
了解过门面模式的基本信息,再回到最初的问题——为什么日志框架要使用门面模式呢?其实答案很简单,在工程开发中常遇到这样的场景:
- 我们自己的系统中使用了 Logback 这个日志系统
- 我们的系统使用了 A.jar,A.jar 中使用的日志系统为 Log4j
- 我们的系统又使用了 B.jar,而 B.jar 中使用的日志系统为 JUL
在上述场景中,我们的系统需要同时支持并维护 Logback、Log4j、JUL 三种日志框架,其繁琐程度不言而喻。为了解决这个问题,可以引入一个适配层,由适配层决定具体使用哪一种日志系统,应用程序中的调用者只管打印日志,而不必关心日志是如何被打印出来的,如此,问题迎刃而解。显然,Slf4j 和 Commons-Logging 就是这种适配层,而 JUL、Log4j 和 Logback 等就是打印日志的具体实现。换言之,日志门面(适配层)只需要提供日志的接口,日志系统的具体实现则交由其它日志框架,这样就避免了需要维护复杂日志系统的问题。
避免环形依赖
Slf4j 的作者 Ceki Gülcü 当年因为觉得 Commons-Logging 的 API 设计的不好,性能也不够高,因而设计了 Slf4j。而他为了 Slf4j 能够兼容各种类型的日志系统实现,还设计了相当多的 adapter 和 bridge 来连接,如下图所示:
这些 adapter 和 bridge 在此就不做详细介绍,读者需要时可自行查阅上图找到对应的 jar 包。这里只想引出一个由此产生的问题,那就是日志框架的循环依赖问题。具体而言,如果在应用中使用 Slf4j 作为日志门面,就需要引入 slf4j-api-xx.jar,如果同时又引入了 slf4j-log4j12-xx.jar,log4j-xx.jar,log4j-over-slf4j-xx.jar 这几个包,在这种情况下,调用 slf4j-api 就会出现死循环(如下图所示)。
鉴于此,在引入日志框架依赖的时候要尽力避免,比如以下组合就不能同时出现:
- jcl-over-slf4j 和 slf4j-jcl
- log4j-over-slf4j 和 slf4j-log4j12
- jul-to-slf4j 和 slf4j-jdk14
日志框架的使用选择
- 日志框架之间的关系
在介绍日志框架的使用之前,简要回顾一下前面四节的内容。Commons Logging 和 Slf4j 是日志门面。Log4j 和 Logback 则是具体的日志实现方案。可以简单的理解为接口与接口的实现,调用者只需要关注接口而无需关注具体的实现,从而做到解耦。在整个日志框架中主要包括日志门面、日志适配器、日志库三个部分,它们之间的关系如下图所示:
- 日志框架的使用选择
比较常用的组合使用方式是 Slf4j 与 Logback 组合使用,Commons Logging 与 Log4j 组合使用,Logback 必须配合 Slf4j 使用。由于 Logback 和 Slf4j 是同一个作者,其兼容性不言而喻。这里顺便介绍一个小故事:Apache 曾试图说服 Log4j 以及其它的日志来按照 Commons-Logging 的标准编写,但是由于 Commons-Logging 的类加载机制在实际应用中存在问题(它使用 ClassLoader 寻找和载入底层的日志库),实现起来也不友好,因此 Log4j 的作者便开发了 Slf4j,与 Commons-Logging 两分天下。
关于如何选择日志框架,如果是新的项目 (没有历史包袱,无需切换日志框架),建议使用 Slf4j 与 Logback 组合,这样有如下的几个优点:
1. Slf4j 实现机制决定 Slf4j 限制较少,使用范围更广。相较于 Commons-Logging,Slf4j 在编译期间便静态绑定本地的 Log 库,其通用性要好得多。
2. Logback 拥有更好的性能。Logback 声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高,这个操作在 Logback 中只需 3 纳秒,而在 Log4j 则需要 30 纳秒。
3. Slf4j 支持参数化,使用占位符号,代码更为简洁,如下例子。
// 在使用 Commons-Logging 时,通常的做法是
if(log.isDebugEnabled()){
log.debug("User name:" + user.getName() + " buy goods id :" + good.getId());
}
// 在 Slf4j 阵营,你只需这么做:
log.debug("User name:{} ,buy goods id :{}", user.getName(),good.getId());
4. Logback 的所有文档是免费提供的,Log4j 只提供部分免费文档而需要用户去购买付费文档。
5. MDC (Mapped Diagnostic Contexts) 用 Filter,将当前用户名等业务信息放入MDC 中,在日志 format 定义中即可使用该变量。具体而言,在诊断问题时,通常需要打出日志。如果使用 Log4j,则只能降低日志级别,但是这样会打出大量的日志,影响应用性能;如果使用 Logback,保持原定日志级别而过滤某种特殊情况,如 Alice 这个用户登录,日志将打在 DEBUG 级别而其它用户可以继续打在 WARN 级别。实现这个功能只需加 4 行 XML 配置。
6. 自动压缩日志。RollingFileAppender 在产生新文件的时候,会自动压缩已经打出来的日志文件。压缩过程是异步的,因此在压缩过程中应用几乎不会受影响。
举例说明:如果直接使用 Slf4j 和 Logback 组合,可通过如下配置进行集成:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
对于已有工程,需要根据所使用的日志库来确定门面适配器从而使用 Slf4j。Slf4j 的设计思想比较简洁,使用了 Facade 设计模式,Slf4j 本身只提供了一个 slf4j-api-version.jar 包,这个 jar 中主要是日志的抽象接口,jar 包中本身并没有对抽象出来的接口做实现。对于不同的日志实现方案(例如 Logback,Log4j 等),封装出不同的桥接组件(例如 logback-classic-version.jar,slf4j-log4j12-version.jar),这样使用过程中可以灵活地选取自己项目里的日志实现。
举例说明,如果已有工程中使用了 Log4j 日志库,可通过如下配置进行集成:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-log4j12.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
下面是 Slf4j 与其它日志组件调用关系图:
具体的接入方式参见下图:
如果老代码中直接使用非 Slf4j 日志库提供的接口打印日志,需要引入日志库适配器来桥接遗留的 api。在实际环境中我们经常会遇到不同的组件使用的日志框架不同的情况,例如 Spring Framework 使用的是日志组件是 Commons Logging,XSocket 依赖的则是 Java Util Logging。
如果在同一项目中使用不同的组件时,如何解决不同组件依赖的日志组件不一致的情况呢?这就需要统一日志方案,统一使用 Slf4j,把他们的日志输出重定向到 Slf4j,然后 Slf4j 又会根据绑定器把日志交给具体的日志实现工具。Slf4j 带有几个桥接模块,可以重定向 Log4j,JCL 和 java.util.logging 中的 Api 到 Slf4j。
举例说明:如果老代码中直接使用了 Log4j 日志库接口打印日志,需引入如下配置:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>${log4j-over-slf4j.version}</version>
</dependency>
桥接方式参加下图:
- 排除项目中依赖的第三方包的日志依赖
在实际使用过程中,项目会根据需要引入一些第三方组件,例如常用的 Spring,而 Spring 本身的日志实现使用了 Commons Logging,如果想使用 Slf4j+Logback 组合,这时候需要在项目中将 Commons Logging 排除掉,通常会用到以下 3 种方案,各有利弊,可以根据项目的实际情况选择最适合自己项目的解决方案。
- 方案一:采用 maven 的 exclusion 方案
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
<version>${springframework.version}</version>
</dependency>
这种方案优点是 exclusion 是 maven 原生提供的,不足之处是如果有多个组件都依赖了 commons-logging,则需要在很多处增加 exclusion,比较繁琐。
- 方案二:在 maven 声明 commons-logging 的 scope 为 provided
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
<scope>provided</scope>
</dependency>
这种方案虽然简洁,但也有缺点,在调试代码时有可能导致 IDE 将 commons-logging 放置在 classpath 下,从而导致程序运行时出现异常。
- 方案三:在 maven 私服中增加类似于 99.0-does-not-exist 这种虚拟的版本号
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>99.0-does-not-exist</version>
</dependency>
这种方案好处在于声明方式比较简单,用 IDE 调试代码时也不会出现问题,不足之处是 99.0-does-not-exist 这种版本是 maven 中央仓库中可能不存在,需要发布到自己的 maven 私服中。
明天将推出本系列中篇,延续日志话题,结合具体案例介绍日志使用中需要遵循的规范及注意事项,欢迎大家继续关注。
相关推荐
- Java七大热门技术框架源码解析(25章) 完结
-
获课》aixuetang.xyz/5699/Hibernate与MyBatis源码级PK:ORM框架的两种哲学在Java持久层框架领域,Hibernate与MyBatis代表了两种截然不同的设计哲学。...
- 【25章】Java七大热门技术框架源码解析
-
获课》aixuetang.xyz/5699/Java高级面试:七大框架源码精讲与实战解析在当今Java技术生态中,对主流框架源码的深入理解已成为高级开发者面试的核心竞争力。掌握Spring、MyBat...
- 饿了么董事长吴泽明兼任CEO,韩鎏分管即时物流中心
-
饿了么调整组织架构。2月11日,饿了么董事长吴泽明(花名:范禹)通过公司全员信宣布饿了么最新组织调整:即日起,吴泽明将兼任饿了么CEO,韩鎏(花名:昊宸)专注分管即时物流中心,继续向吴泽明汇报。吴泽明...
- 饿了么100%迁至阿里云,快速扩容可支持1亿人同时点单
-
来源:环球网6月17日,记者获悉,饿了么已完成100%上云,所有业务系统、数据库设施等均已迁移至阿里云。高峰期,饿了么可在阿里云上快速扩容,可以支持1亿人同时在线点单,这意味着饿了么的服务能力再次全面...
- 饿了么组织架构调整:董事长吴泽明兼任CEO 韩鎏专注即时物流中心管理
-
近日,饿了么董事长吴泽明(花名:范禹)通过公司全员信宣布饿了么最新组织调整:即日起,吴泽明将兼任饿了么CEO,韩鎏(花名:昊宸)专注分管即时物流中心,继续向吴泽明汇报。吴泽明在内部信中表示,考虑即时物...
- 饿了么组织架构调整:董事长吴泽明兼任CEO
-
Tech星球2月11日消息,据新浪科技报道,今日饿了么董事长吴泽明(花名:范禹)通过公司全员信宣布饿了么最新组织调整:即日起,吴泽明将兼任饿了么CEO,韩鎏(花名:昊宸)专注分管即时物流中心,继续向吴...
- 饿了么又调整了组织架构,董事长吴泽明兼任CEO
-
2月11日,饿了么董事长,花名为范禹的吴泽明,通过公司全员信宣布最新组织调整:从即日起,吴泽明将兼任饿了么CEO。公司原CEO,花名为昊宸的韩鎏今后专注分管即时物流中心,继续向吴泽明汇报。在内部信中,...
- SpringBoot项目快速开发框架JeecgBoot——Web处理!
-
Web处理JeecgBoot框架主要用于Web开发领域。下面介绍JeecgBoot在Web开发中的常用功能,如控制器、登录、系统菜单、权限模块的角色管理和用户管理。首先启动后台项目,将其导入IDE...
- 腾讯即将开源Kuikly:基于Kotlin的纯原生跨端解决方案
-
IT之家3月4日消息,腾讯日前在端服务网站发布预告,即将开源Kuikly跨端开发框架。预告海报介绍称,Kuikly是基于KotlinKMM技术、客户端开发友好的全新跨端解决方案,可...
- Python构建MCP服务器完整教程:5步打造专属AI工具调用系统
-
模型控制协议(ModelControlProtocol,MCP)是一种专为实现AI代理与工具解耦而设计的通信协议,为AI驱动应用程序的开发提供了高度的灵活性和模块化架构。通过MCP服务器,AI代...
- Python3使用diagrams生成架构图(python模块制作)
-
目录技术背景diagrams的安装基础逻辑关系图组件簇的定义总结概要参考链接技术背景对于一个架构师或者任何一个软件工程师而言,绘制架构图都是一个比较值得学习的技能。这就像我们学习的时候整理的一些Xmi...
- Python 失宠!Hugging Face 用 Rust 新写了一个 ML框架,现已低调开源
-
大数据文摘受权转载自AI前线整理|褚杏娟近期,HuggingFace低调开源了一个重磅ML框架:Candle。Candle一改机器学习惯用Python的做法,而是Rust编写,重...
- Python Web 框架(Python Web 框架)
-
Tornado、Flask、Django三个PythonWeb框架的主要区别和适用场景:特点/框架TornadoFlaskDjango类型异步非阻塞Web服务器和框架轻量级微框架全功能...
- 构建并发布你的自定义 Python 包(python如何创建自定义模块)
-
Python让你可以重用代码,并将代码分享给他人以节省时间和精力。所以,当你编写了一些方便的脚本,希望你的同事或其他人也能使用时,接下来该怎么做呢?这篇文章就来解决打包和分发的问题。我们将专注于将你...
- Python 应用开发框架 BeeWare 简明实用教程
-
1.BeeWare简介BeeWare是一个Python框架,用于开发跨平台原生应用。它支持Android、iOS、Windows、macOS和Linux,并提供原生用户体验。2.安装B...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- shiro框架 (61)
- 定时任务框架 (56)
- java日志框架 (61)
- JAVA集合框架 (47)
- mfc框架 (52)
- abb框架断路器 (48)
- grpc框架 (55)
- ppt框架 (48)
- 内联框架 (52)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)