1 2 3 4

SLF4J 快速入门 / 绑定原理

官网: http://www.slf4j.org/
GitHub: https://github.com/qos-ch/slf4j

一、简介

SLF4J(Simple Logging Façade for Java)日志框架,是各种日志框架的简单门面(simple facade)或抽象接口,允许用户部署时选择具体的日志实现。

相较于 JCL 有什么优点:

  • 其在设计上简单得多,因此也足够健壮。
  • 静态绑定非常简单 但足够有效,解决了困扰 JCL 的类加载器(class loader)问题
  • 参数化日志的增强,解决了重要的日志性能问题
  • 在 org.slf4j.Logger 接口中,Marker 对象的引入为更进阶的日志系统预留了空间;同时也允许切换回传统的日志系统

二、需求及包引入

要求及说明:

  • JDK版本要求:1.5+
  • 向后兼容性:
    slf4j-api 自身目前是向后兼容所有版本的,意味着可以从 1.0 升至任意更新版本。
    但根据slf4j-api版本不同,具体到绑定层上,则可能需要特定版本的绑定。例如 slf4j-api-1.5.6 需使用 slf4j-simple-1.5.6 而 slf4j-simple-1.4.2 将无法工作。
  • 包依赖整体逻辑,参考:Java 日志框架概述(slf4j / log4j / JUL / Common-logging(JCL) / logback)
  • 所有绑定层/桥接层库,均随 SLF4J 一起发布,可在此处寻找: https://github.com/qos-ch/slf4j

简单示例:引入 SLF4j API 及其实现(以logback为例)

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

三、SLF4J API 简单使用示例

  1. 声明及获取 LOGGER
    private static final Logger LOGGER = LoggerFactory.getLogger(DistrictController.class);
  2. 打印日志
    LOGGER.debug("Attempted to do something in Obj {}", district);

四、更多相关知识

1. 日志等级

分为5个等级,可见 org.slf4j.event

  • ERROR
  • WARN
  • INFO
  • DEBUG
  • TRACE

2. SLF4J 日志接口设计

  1. 日志打印接口
    slf4j 以"日志等级"作为方法名,并至少接受一个 String 类型的消息描述,如:
    debug(String msg)
    debug(String format, Object arg)
    debug(String msg, Throwable t)
    …
    
    至于为什么不直接接受 Object ,参考: http://www.slf4j.org/faq.html#string_or_object
  2. 模板/参数化消息
    SLFJ4J 为消息提供了参数化(如下图),以解决 debug("hello" + "world") 这种使用方式无论是否启用该等级日志都会进行字符串连接"的问题,避免日志开销(据官方讲30倍的开销)
    image
    消息中参数占位符(formatting anchor):{}
    例:logger.debug("Temperature set to {}. Old temperature was {}.", t, oldT);
    更多性能 / 转义 / 匹配规则信息,可参考: http://www.slf4j.org/faq.html#logging_performance

五、绑定实现原理

  1. 在第一次调用 LoggerFactory#getLogger 时,会尝试调用 LoggerFactory#bind 进行日志工厂的初始化。
  2. 根据版本不同,实现绑定的方式也不一致。但并非网上所谓的“编译时绑定”这么高深,关于这种说法后面会解释。
    绑定这种术语其实本身就是slf4j官方自创的
    • 在 slf4j 1.8 版本之前:LoggerFactory#bind 基于 COC(Convention over Configuration)的思想,约定大于配置,单纯调用 org.slf4j.impl.StaticLoggerBinder#getSingleton 来初始化。
      但实际上 slf4j-api 根本不包含此类,而是由各实现/绑定包(如 slf4j-log4j)来提供
      源码如下:

      import org.slf4j.impl.StaticLoggerBinder
      
      private final static void bind() {
      	try {
      		StaticLoggerBinder.getSingleton();
      		INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
      	} catch (NoClassDefFoundError ncde) {
      		INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
      		Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
      		Util.report("Defaulting to no-operation (NOP) logger implementation");
      		Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
      	}
      }
      

      这也是为什么官方特别说明,放且仅放一个绑定,不要在类路径上放置多个绑定,因为会冲突。(you simply drop one and only one binding of your choice onto the appropriate class path location. Do not place more than one binding on your class path. Here is a graphical illustration of the general idea)

    • 在 slf4j 1.8 之后:采用 Java SPI (Service Provider Interface) 机制

      private final static void bind() {
      		List<SLF4JServiceProvider> providersList = findServiceProviders();
      		if (providersList != null && !providersList.isEmpty()) {
      		   PROVIDER = providersList.get(0);
      		   PROVIDER.initialize();
      		   INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
      		}
      }
      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;
      }
      

      以 slf4j-log4j12 为例,基于 SPI 实现了 SLF4JServiceProvider
      image

六、常见问题

  1. 网上为什么说“编译时绑定”
    答:虽然有些迷惑的说法,但也并不是毫无道理。原因如下:
    1. 官方的说法自身就很具误导性,感觉是有意的:
      "In fact, each SLF4J binding is hardwired at compile time to use one and only one specific logging framework. For example, the slf4j-log4j12-1.7.28.jar binding is bound at compile time to use log4j”
    2. 虽有些不太准确,但 1.8 以前时的“静态绑定”一定程度上也能解释为编译时绑定。
  2. 是否应该将类的 Logger 成员声明为静态?
    http://www.slf4j.org/faq.html#declared_static

七、相关踩坑

  1. 引用调用SLF4J API 运行报错
    SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
    SLF4J: Defaulting to no-operation (NOP) logger implementation
    SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
    
    答:SLF4J只是接口层,未找到实现则默认的空实现(nop)【1.6 开始】,并打印警告
    可引入 SLF4J 实现/绑定层,如:
    <dependency>
    	<groupId>ch.qos.logback</groupId>
    	<artifactId>logback-classic</artifactId>
    	<version>1.2.3</version>
    	<scop>test</scop>
    </dependency>
    
  2. 在多个参数情况下,若想打印 Throwable 堆栈信息,需注意Throwable必须放在最后一个
    例:logger.error("错误消息:{}",e.getMessage(),e);

参考:
SLF4J FAQ
Introduction to SLF4J
Slf4j打印异常的堆栈信息

posted @ 2021-08-12 13:12  SimpleIto  阅读(2413)  评论(0编辑  收藏  举报