转载

编程开发(一):日志框架

日志实现框架

Log4j

Log4j 是目前最为流行的 Java日志框架 之一, 1999年 发布首个版本, 2012年 发布最后一个版本, 2015年 正式宣布终止,官方也已不建议使用,并逐步被 LogbackLog4j2 等日志框架所替代,可是无法掩饰光辉历程,以及优良的设计理念。尽管 Log4j 有着出色的历史战绩,但早已不是 Java 日志框架的最优选择,还在使用该日志框架的项目往往是历史遗留问题。

Log4j API 核心类:

  • 日志对象: org.apache.log4j.Logger
  • 日志级别: org.apache.log4j.Level
  • 日志管理器: org.apache.log4j.LogManager
  • 日志仓储: org.apache.log4j.spi.LoggerRepository
  • 日志附加器: org.apache.log4j.Appender
  • 日志过滤器: org.apache.log4j.spi.Filter
  • 日志格式布局: org.apache.log4j.Layout
  • 日志事件: org.apache.log4j.LoggingEvent
  • 日志配置器: org.apache.log4j.spi.Configurator
  • 日志诊断上下文: org.apache.log4j.NDCorg.apache.log4j.MDC

JUL

Java LoggingJava 标准的日志框架,也称为 Java Logging API ,即 JSR 47 。从 Java 1.4 版本开始, Java Logging 成为 Java SE 的功能模块,其实现类存放在 java.util.logging 包下。

使用 Java Logging 最大好处是它属于 JDK内置 ,不需要添加额外依赖,默认配置文件位于: jre/lib/logging.properties ,具体可以查看 LogManagerreadConfiguration 方法,启动的时候可以通过设置 VM 参数 java.util.logging.config.file 指定配置文件。

Java Logging API 核心类:

  • 日志对象: java.util.logging.Logger
  • 日志级别: java.util.logging.Level
  • 日志管理器: java.util.logging.LogManager
  • 日志处理器: java.util.logging.Handler
  • 日志过滤器: java.util.logging.Filter
  • 日志格式器: java.util.logging.Formatter
  • 日志记录: java.util.logging.LogRecord
  • 日志权限: java.util.logging.LoggingPermission
  • 日志JMX接口: java.util.logging.LoggingMXBean

Logback

LogbackLog4j 创始人设计的又一个开源日志框架,可以看成 Log4j 的替代者,在架构和特征上有着相当提升。 Logback 当前分成三个模块:

  • logback-core :其它两个模块的基础模块,提供一些关键的通用机制
  • logback-classic
    Log4j
    Log4j
    SLF4J API
    
  • logback-access
    logback-access
    Tomcat
    Jetty
    Servlet容器
    Http
    access日志
    

编程开发(一):日志框架

Logback 核心类:

  • 日志对象: ch.qos.logback.classic.Logger
  • 日志级别: ch.qos.logback.classic.Level
  • 日志管理器: ch.qos.logback.classic.LoggerContext
  • 日志附加器: ch.qos.logback.core.Appender
  • 日志过滤器: ch.qos.logback.core.filter.Filter
  • 日志格式布局: ch.qos.logback.core.Layout
  • 日志事件: ch.qos.logback.classic.spi.LoggingEvent
  • 日志配置器: ch.qos.logback.classic.spi.Configurator

核心架构

编程开发(一):日志框架

上图是 logback 日志框架的输出日志的核心流程:

  • Logger 作为日志框架的代言人,程序开发通过 Logger 即可完成日志输出工作;
  • Logger
    Filter
    Level
    LoggingEvent
    Appender
    
  • Appender
    Appender
    Filter
    

LoggerAppender 是日志框架比较核心组件, Logger 代表日志输入源,其配置样例见下:

编程开发(一):日志框架

Appender 代表日志输出源,其配置样例见下:

编程开发(一):日志框架

LoggerAppender 相互独立,都可以实现对日志过滤操作,同时可以实现多对多映射关系,在开发中可以对这些特性灵活应用。比如:生产中一个很常见的做法就是构建一个 Level=ErrorAppender ,然后让所有的 Logger 都指向该 Appender 就可以实现汇聚系统中所有 Error 级别的日志,可以快速监测系统运行是否出现异常状况。

Appender

编程开发(一):日志框架

<appender> 节点被配置时,必须配置两个属性 nameclassname 指定 Appender 的名称,而 class 指定 Appender 具体的实现类。

Appender核心类结构图:

编程开发(一):日志框架

UnsynchronizedAppenderBase :非线程安全的Appender基类,即 public void doAppend(E eventObject) 没有使用 synchronized 关键字,而 AppenderBase 类中的 doAppend() 方法都使用了 synchronized 关键字: public synchronized void doAppend(E eventObject)

Level

日志可以分配级别,包括: ALLTRACEDEBUGINFOWARNERROROFF ,其中 ALLOFF 日志级别是用于 AppenderLogger 过滤使用。

  • TRACE(追踪) :输出更细致的程序运行轨迹;
  • DEBUG(调试) :这个级别一般记录一些运行中的中间参数信息,只允许在开发环境开启,选择性在测试环境开启;
  • INFO(信息) :用来记录程序运行中的一些有用的信息,例如:程序运行开始、结束、耗时、重要参数等信息,需要注意有选择性的有意义的输出,到时候自己找问题看一堆日志却找不到关键日志就没有意义了;
  • WARN(警告) :一般用来记录一些用户输入参数错误;
  • ERROR(错误) :一般用来记录程序中发生的任何异常错误信息( Throwable ),或者是记录业务逻辑错误;

Logger

通过 LoggerFactory 获取 LoggerLogger getLogger(String name)LoggerFactory 采用工厂设计模式,内部维护一个 Map 缓存所有生成的 Logger 实例信息: Map<String, Logger> loggerCache = new ConcurrentHashMap()

继承规则

Logger 是有层次关系的,我们可一般性的理解为包名之间的父子继承关系。每个 Logger 通常以 class全限名称 为其名称。子 Logger 通常会从父 Logger 继承 Logger级别Appender 等信息。

编程开发(一):日志框架

统一日志API

日志框架无论 Log4j 还是 Logback ,虽然它们功能完备,但是各自 API 相互独立,并且各自为政。当应用系统在团队协作开发时,由于工程师人员可能有所偏好,因此,可能导致一套系统同时出现多套日志框架情况。

其次,最流行的日志框架基本上基于实现类编程,而非接口编程,因此,暴露一些无关紧要的细节给用户,这种耦合性是没有必要的。

诸如此类的原因,开源社区提供 统一日志API 框架,最为流行的是:

  • apache commons-logging
    JCL
    log4j
    java logging
    
  • slf4j
    log4j
    log4j2
    java logging
    logback
    

统一日志 API ,即日志门面接口层,直白点讲:提供了操作日志的接口,而具体实现交由 LogbackLog4j 等日志实现框架,这样就可以实现程序与具体日志框架间的解耦,对于底层日志框架的改变,并不影响到上层的业务代码,可以灵活切换日志框架。

日志体系

现在日志框架众多: slf4jjcljullog4jlog4j2logback 等,它们之间存在什么样的关系,我们在开发过程中又如何选取这些日志框架呢?

首先,看下Java日志体系:

编程开发(一):日志框架

通过上图可以概括日志体系大致分为三层:日志接口门面层、绑定/桥接层以及日志实现层。

jcl-over-slf4j.jar(jcl -> slf4j):将commons-logging日志桥接到slf4j
jul-to-slf4j.jar(jul -> slf4j):java.util.logging的日志桥接到slf4j

log4j-over-slf4j.jar(log4j -> slf4j):将log4j的日志,桥接到slf4j
slf4j-log4j12.jar(slf4j -> log4j):slf4j绑定到log4j,所以这个包不能和log4j-over-slf4j.jar不能同时使用,会出现死循环

slf4j-jcl.jar(slf4j -> jcl):slf4j绑定到commons-logging日志框架上
slf4j-jdk14.jar(slf4j -> jul):slf4j绑定到jdk日志框架上,不能喝jul-to-slf4j.jar同时使用,会出现死循环

slf4j-nop.jar:slf4j的空接口输出绑定,丢弃所有日志输出
slf4j-simple.jar:slf4j自带的简单日志输出接口
log4j-slf4j-impl.jar(slf4j -> log4j2):将slf4j绑定到log4j2日志框架上,不能和log4j-to-slf4j同时使用
log4j-to-slf4j.jar(log4j2 -> slf4j):将log4j2日志桥接到slf4j上,不能和log4j-slf4j-impl同时使用

最为熟悉和使用率较高的 log4j 其实就位于日志实现层,即其为一种日志实现框架。既然 log4j 已经足够系统使用进行日志输出了,为啥还多此一举弄个 日志接口门面层绑定/桥接层 ?看下图:

编程开发(一):日志框架

系统A 集成了 模块A模块B模块C 三个模块,但是这三个模块使用了不同的日志实现框架,现在 系统A 相当于同时存在了三个日志框架,那如何进行配置呢?每个框架都构建一个配置文件这种肯定是不行的,没法进行统一管理,日志较为混乱。

现在看下如何解决上述问题:

编程开发(一):日志框架

模块A模块B模块C 采用 slf4j 日志接口框架,而非具体日志实现类,具体使用哪种日志实现框架是由 系统A 配置决定的, 系统Aslf4j 绑定到 logback ,则统一采用 logback 日志框架, slf4j 绑定到 log4j 则统一采用 log4j 日志框架。 日志接口 --> 日志绑定 --> 日志实现 ,日志接口和日志实现进行了解耦,模块只关注接口不关注实现,具体采用哪种实现是由其所在的系统环境决定,这样就可以实现日志的统一配置和管理。

对于上述解决方案,如果 模块A模块B模块C 是新开发统一采用 slf4j 日志接口框架没问题,但是对于旧系统,比如 模块B模块C 都是很久之前开发的模块,分别采用了不同的日志实现框架,见下图:

编程开发(一):日志框架

如果 系统Aslf4j 绑定到 logback 日志框架上,但是 模块B模块C 由于没有采用 slf4j ,绑定对于它们来说是无效的,这时候就要使用 桥接

编程开发(一):日志框架

桥接的大致结构如上图,通过桥接把 log4jjdk log 等日志实现框架桥接到 slf4j 上,由于 slf4j 又被绑定到了 logback 上,则 模块B模块C 最终会被 logback 纳管,而不是 log4jjdk log ,同样可以实现日志统一配置管理。

以上就是项目开发中经常遇到的问题,以及绑定和桥接之间的区别。

spring 体系中日志框架

Spring框架

Spring Framework 4.X 及之前的版本,都是使用的 标准版JCL 日志框架,该依赖由 spring-core 间接引入。 Spring 框架的日志信息都是使用 JCL 标准接口来进行输出。下面说下项目中常碰到的三种情况:

  • log4j
    commons-logging
    log4j
    jcl
    log4j
    
  • log4j2
    commons-logging
    log4j2
    log4j2
    jcl
    log4j2
    log4j-jcl.jar
    
  • slf4j
    桥接模式
    jcl日志
    SLF4J
    jcl-over-slf4j.jar
    Spring框架
    

使用 spring 4.X 及之前版本的框架时一定要注意上面情况,否则很容易出现业务日志输出正常,但是 spring 框架本身日志没有输出的情况,导致一些错误无法察觉或者不利于排查。

spring5.0 带来了 commons-logging 桥接模块的封装,它被叫做 spring-jcl 而不是标准版 jcl ,无需添加额外依赖包,可自动检测绑定到 Log4j2SLF4J

SpringBoot框架

springboot-1.X - springboot-2.X :

编程开发(一):日志框架

SpringBoot 框架可以看出,默认采用 SLF4J+Logback 组合的日志框架,通过 桥接模式 将其它日志框架桥接到 SLF4J 上。

SLF4J

SLF4J(Simple Logging Facade For Java) 是一个为 Java 程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就像我们经常使用的 JDBC 一样,只是了一些标准规范接口。因此,单独的 SLF4J 是不能工作的,它必须搭配其他具体的日志实现方案。

SLF4JLogback 是同一个作者开发的,所以 Logback 天然与 SLF4J 适配,不需要引入额外适配库。

这里还有个比较有意思的事情, SLF4J 项目提供了很多适配库、桥接库,唯独没有提供对 Log4j2 的适配库和桥接库,不过 Apache Logging 项目组自己开发了: log4j-slf4j-impllog4j-to-slf4j

JCL

Jakarta commons-logging 简称 JCL ,是 apache 提供的一个通用日志门面接口,最后版本更新停留在 2014年 ,且默认只能提供对 Log4jJava Logging 进行适配。

JCL 已慢慢淡出人们的视线,一些历史遗留项目也开始慢慢由 JCL 转向 SLF4J ,如: Spring 5.0 开始没有再依赖原生的 JCL 框架, SpringBoot 默认采用 SLF4J+LogbackSLF4J 已经成为了 Java日志组件 的明星选手,可以完美替代 JCL ,使用 JCL 桥接库也能完美兼容一切使用 JCL 作为日志门面的类库,现在的新系统已经没有不使用 SLF4J 作为 统一日志API接口层 的理由了。

核心原理

SLF4JJCL 对比,二者最大区别在于它们的绑定机制的不同,这也决定了为什么 JCL 会被慢慢的淘汰掉的根本原因。

SLF4J绑定原理

1、 slf4j 定义好两个接口规范:

public interface LoggerFactoryBinder {
//获取一个ILoggerFactory实现类,采用工厂设计模式创建Logger
public ILoggerFactory getLoggerFactory();
public String getLoggerFactoryClassStr();
}
public interface ILoggerFactory {
public Logger getLogger(String name);
}

第一个接口 LoggerFactoryBinder 定义绑定类,如果日志框架需要和 slf4j 进行绑定,就要提供一个该接口实现类,并且名称是 StaticLoggerBinder ,这样,在 slf4j 模块中,使用 StaticLoggerBinder.getSingleton(); 就可以获取到这个绑定类,进而通过 StaticLoggerBinder 绑定类的 getLoggerFactory() 获取到 Logger 生产工厂 ILoggerFactory

注意:这里的绑定机制利用到了类加载原理,如果存在多个绑定类 StaticLoggerBinder ,根据类路径的前后顺序,只有有一个会被加载进来,这个加载进来的就实现了绑定。

2、 ILoggerFactory 也是 slf4j 模块提供的一个接口,因为各个日志框架中 LoggerFactory 不统一,所以 slf4j 提供一个接口,让各个日志框架把自己的 LoggerFactory 包装成 ILoggerFactory 接口,这样 slf4j 模块下就可以统一使用。这里利用到的是设计模式中的:适配模式。系统间对接比较常用的一种设计模式,系统间接口不统一,通过适配模式实现一致。

3、可以看下, slf4jlog4j 绑定使用 slf4j-log4j12.jar ,这个模块下 StaticLoggerBinder 实现见下:

public class StaticLoggerBinder implements LoggerFactoryBinder {

private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

public static final StaticLoggerBinder getSingleton() {
return SINGLETON;
}

public static String REQUESTED_API_VERSION = "1.6.99";

private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();

private final ILoggerFactory loggerFactory;

private StaticLoggerBinder() {
loggerFactory = new Log4jLoggerFactory();
try {
@SuppressWarnings("unused")
Level level = Level.TRACE;
} catch (NoSuchFieldError nsfe) {
Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
}
}

public ILoggerFactory getLoggerFactory() {
return loggerFactory;
}

public String getLoggerFactoryClassStr() {
return loggerFactoryClassStr;
}
}

4、 StaticLoggerBinder :静态绑定,这个静态是相对于 JCL 所使用的动态绑定来说的,为什么说是静态的呢?因为你如果要绑定,需要在环境中添加绑定相关的jar,这样slf4j就可以加载到绑定包中的 StaticLoggerBinder 类实现绑定。

接口和实现类之间采用一种松耦合的设计,有利于灵活的扩展,但是在使用时有需要一种技术把它们关联起来,这是软件设计中比较常用到的设计思想, JDK 1.6 对此专门提供了一种技术: SPISLF4J1.8版本 起,也开始使用 SPI 方式实现绑定,而不再采用通过寻找指定类 StaticLoggerBinder 的方式进行绑定。下面代码就是 slf4j-1.8 中使用 SPI 进行绑定核心代码:

private static List<SLF4JServiceProvider> findServiceProviders() {
ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
List<SLF4JServiceProvider> providerList = new ArrayList<SLF4JServiceProvider>();
for (SLF4JServiceProvider provider : serviceLoader) {
providerList.add(provider);
}
return providerList;
}

SLF4JServiceProvider 就是类似于上面的 LoggerFactoryBinder 接口,通过它可以获取到 ILoggerFactory ,这样其它日志框架和 slf4j 进行集成时只需要提供一个 SLF4JServiceProvider 接口的实现类即可,不再要求必须是像之前固定名称必须是: StaticLoggerBinder ,固定名称带来的一个问题是包路径也要一致,无形中存在侵入性,而使用 SPI 方式更加的灵活。比如我们常用到的 JDBC 也使用到 SPI ,感兴趣的可以多了解下,对系统设计还是比较实用的一种技术。

JCL绑定原理

JCL 采用动态绑定机制,缺点是容易引发混乱,在一个复杂甚至混乱的依赖环境下,确定当前正在生效的日志服务是很费力的,特别是在程序开发和设计人员并不理解 JCL 的机制时。

JCL 动态绑定的核心逻辑位于 LogFactoryImpl 类的 discoverLogImplementation 方法中如下代码块:

for(int i=0; i<classesToDiscover.length && result == null; ++i) {
/**
createLogFromClass()核心逻辑:通过Class.forName()加载适配器的类模板,
然后调用Constructor.newInstance()构建适配器类实例
*/

result = createLogFromClass(classesToDiscover[i], logCategory, true);
}

其中 classesToDiscover 数组的中定义了可以使用的适配器类,见下:

String[] classesToDiscover = {
"org.apache.commons.logging.impl.Log4JLogger",
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};

简单来说: JCL 模块中会有判断,当前项目中是否存在 Log4jAPI ,如果有就直接和 Log4j 进行绑定;如果没有,则继续向下查找,是否存在 JDK Log 相关 API ,如果有就绑定;如果 JDK Log 也没有,则提供一个 SimpleLog 默认实现,该实现什么也不做,输出的日志直接会被丢弃,什么也看不到。

总结

相较于 JCL动态绑定机制SLF4J 则简单得多,采用 静态绑定机制 ,可能你还没有很好理解这两者的本质区别,看下图:

编程开发(一):日志框架

JCL 框架自动检查当前环境中是否存在相关日志 API ,如果有就绑定,注意它内部有个固定的绑定顺序,这种所谓的动态绑定很容易出现问题,特别是系统较大可能会存在很多日志框架,就会出现混乱,不够灵活,这就导致了为啥 JCL 已经被慢慢淘汰掉。

slf4j 采用的静态绑定,不是直接和日志框架进行绑定,而是中间多了一个环节:绑定类,它就像一个开关一样,关键是可以进行控制,比如想和 log4j2 进行绑定,就添加 log4j-slf4j-impl.jar ,开关就会打开进行绑定。 slf4j 不管是采用 StaticLoggerBinder 还是后面采用的 SPI ,始终有个绑定类控制绑定关系。

总结

Java 日志组件选型的建议

  • API
    SLF4J
    slf4j-api
    logback-classic
    
  • 日志实现框架选型:如果最求高并发、高性能、日志量特别大的项目,可以采用 Log4j2 ,否则都采用 Logback
  • SpringBoot
    2.0
    logback+slf4j
    

再一个就是对 slf4jjcl 两种日志框架绑定机制的分析,学习了接口和实现类松耦合关系最后又是如何在运行时进行绑定,或许可以为我们以后的系统设计提供些思路,从而构建出更加灵活的、可扩展的应用。

长按识别关注, 持续输出原创   

编程开发(一):日志框架

原文  https://mp.weixin.qq.com/s/PsZ79kObMImJha3W0Uwc8w
正文到此结束
Loading...