Spring周边:日志——中

  • 2019 年 11 月 26 日
  • 笔记

有日志么?

日志有价值么?

日志框架了解么?

“日志”这个词最早见于航海领域,是记录航行主要情况的载体文件,内容包括操作指令、气象、潮流、航向、航速、旅客、货物等,是处理海事纠纷或者海难的原始依据之一。尔后延伸到航空领域,黑匣子就是一个重要的航空日志载体,调查空难原因时第一反应是找到黑匣子,并通过解析其中的日志信息来还原空难的事实真相 码出高效:Java开发手册

门面设计模式是面面向对象设计模式中的一种,日志框架采用的就是这种模式,类似JDBC的设计理念。它只提供一套接口规范,自身不负责日志功能的实现,目的是让使用者不需要关注底层具体是哪个日志库来负责日志打印及具体的使用细节。目前用的最广泛的日志门面有两种:SLF4J 和 Apache Commons Logging(JCL)。

本期内容提要

Apache Commons Logging(JCL)

JUL、Log4j1、Log4j2、Logback

的集成原理

1. 前置知识——SPI

SPI ,全称为 Service Provider Interface,是一种服务发现机制。是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

1.1. 规则

  1. 当服务提供者提供了接口的一种具体实现后,在 jar 包 META-INF/services 目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  2. 接口实现类所在的 jar 包放在主程序的 classpath 中;
  3. 主程序通过 java.util.ServiceLoder 动态装载实现模块,它通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  4. SPI 的实现类必须携带一个不带参数的构造方法;

1.2. 示例

接口:

package webj2ee.spi;    public interface SPIService {      void execute();  }

实现类:

package webj2ee.spi;    public interface SPIService {      void execute();  }
package webj2ee.spi;    public class SPIImpl2 implements SPIService {      @Override      public void execute() {          System.out.println("SPIService Impl 2...");      }  }

SPI配置:webj2ee.spi.SPIService

webj2ee.spi.SPIImpl1  webj2ee.spi.SPIImpl2

测试类:

import webj2ee.spi.SPIService;    import java.util.ServiceLoader;    public class SPIServiceTest {      public static void main(String[] args) {          ServiceLoader<SPIService> services = ServiceLoader.load(SPIService.class);          for (SPIService s : services) {              s.execute();          }      }  }

1.3. 原理分析

a. ServiceLoader 核心成员变量:

b. ServiceLoader 构造器,内部构建懒加载迭代器;

c. LazyIterator 主体结构;

d. LazyIterator 的 hasNextService() 原理

e. LazyIterator 的 nextService() 原理

2. JCL 深度分析

Apache Commons Logging(JCL) 是 Apache 下的开源项目,是 Apache 提供的日志的门面接口。提供简单的日志实现以及日志解耦功能。

2.1. POM 依赖

<dependency>      <groupId>commons-logging</groupId>      <artifactId>commons-logging</artifactId>      <version>1.2</version>  </dependency>

2.2. 核心接口

JCL 日志门面对外暴露两个接口:

  • org.apache.commons.logging.Log:日志对象
  • org.apache.commons.logging.LogFactory:日志对象工厂
package webj2ee;    import org.apache.commons.logging.Log;  import org.apache.commons.logging.LogFactory;    public class JCL {      private static Log logger = LogFactory.getLog(JCL.class);        public static void main(String[] args) {          logger.info("From jcl's info msg!");          logger.warn("From jcl's warn msg!");          logger.error("From jcl's error msg!");      }  }

JCL 日志对象接口,定义了日志操作的 5 个级别:

TRACE< DEBUG < INFO < WARN < ERROR

2.3. 源码总体构成

2.4. 源码分析——Log

  • org.apache.commons.logging.Log 是日志对象接口;
  • 对日志划分 6 个等级:TRACE < DEBUG < INFO < WARN < ERROR < FATAL;
  • 常用实现类:NoOpLog、SimpleLog、Jdk14Logger、Log4JLogger;

2.4.1. NoOpLog

NoOpLog 实现了 Log 接口,所有实现都是空的,没什么卵用,了解即可。

2.4.2. SimpleLog

JCL 内置的简单日志记录器,实现了 Log 接口,日志的输出目标是 System.err;

  • SimpleLog 本身提供了几个简单的控制参数;
  • SimpleLog 构造器,主要负责解析日志记录器的级别;
  • SimpleLog 的日志格式控制;
  • SimpleLog 默认输出到 System.err;

commons-logging.properties:控制 JCL 使用 SimpleLog 作为日志记录器;

org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog

simplelog.properties:SimpleLog 的参数配置;

## default level  org.apache.commons.logging.simplelog.defaultlog=INFO    ## webj2ee.JCL's log level  org.apache.commons.logging.simplelog.log.webj2ee.JCL=WARN    ## enable showdatetime  org.apache.commons.logging.simplelog.showdatetime=true  org.apache.commons.logging.simplelog.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS zzz    ## enable showlogname;  org.apache.commons.logging.simplelog.showShortLogname=false  org.apache.commons.logging.simplelog.showlogname=true

测试程序:

package webj2ee;    import org.apache.commons.logging.Log;  import org.apache.commons.logging.LogFactory;    public class JCL {      private static Log logger = LogFactory.getLog(JCL.class);        public static void main(String[] args) {          logger.info("From jcl's info msg!");          logger.warn("From jcl's warn msg!");          logger.error("From jcl's error msg!");      }  }

输出结果:

2.4.3. Jdk14Logger

JCL 内置的 JUL(Java Util Logging)日志记录适配器,将日志记录的工作委托给 JUL 处理 ;

  • Jdk14Logger 与 JUL 、JCL 的关系;
  • Jdk14Logger 构造器,通过 getLogger() 方法获取 JUL 的日志记录器;
  • Jdk14Logger 最终使用 JUL 的 logp 方法记录日志;
  • JUL 与 JCL 的日志级别映射关系;

2.4.4. Log4JLogger

JCL 内置的 Log4j1 日志记录适配器,将日志记录的工作委托给 Log4j1 处理 ;

  • Log4JLogger 与 Log4j、JCL 的关系;
  • Log4JLogger 构造器,通过 getLogger() 方法获取 Log4j 的日志记录器;

2.5. 源码分析——LogFactory

2.5.1. LogFactory 基本关系回顾

2.5.2. LogFactory 最常用方法

上述获取Log的过程大致分成2个阶段

  • 获取 LogFactory;
  • 通过 LogFactory 的 getInstance 接口获取 Log;

2.5.3. 获取 LogFactory 的过程解析

1. LogFactory 的关键常量

2. LogFactory实现类查找过程:

  1. 尝试通过系统属性【org.apache.commons.logging.LogFactory】值获取;
  2. 尝试通过 SPI 模式【META-INF/services/org.apache.commons.logging.LogFactory】获取;
  3. 尝试通过JCL配置文件【commons-logging.properties】中配置的【org.apache.commons.logging.LogFactory】属性值获取;
  4. 如果还找不到,就使用 JCL 默认的 LogFactory 实现类 【org.apache.commons.logging.impl.LogFactoryImpl】;

其实大多数情况下,使用的都是 JCL 默认提供的 LogFactory 实现类 LogFactoryImpl。

2.5.4. LogFactoryImpl 获取 Log 的过程解析

1. LogFactoryImpl 的关键常量

2. LogFactoryImpl.getInstance 的关键路径

  1. 从 commons-logging.properties 配置文件中寻找属性为 org.apache.commons.logging.Log 对应的 Log 实现类;
  2. 从系统属性中寻找属性为 org.apache.commons.logging.Log 对应的 Log 实现类;
  3. 如果还没招到,则按照 classesToDiscover 中定义的顺序寻找;

3. JCL 与 JUL 集成

从前面分析可知, JCL 通过自动发现,即可完成与 JUL 的集成;

POM 配置:只需要引入JCL;

<dependency>      <groupId>commons-logging</groupId>      <artifactId>commons-logging</artifactId>      <version>1.2</version>  </dependency>

测试程序:

package webj2ee;    import org.apache.commons.logging.Log;  import org.apache.commons.logging.LogFactory;    public class JCL {      private static Log logger = LogFactory.getLog(JCL.class);        public static void main(String[] args) {          logger.info("From jcl's info msg!");          logger.warn("From jcl's warn msg!");          logger.error("From jcl's error msg!");      }  }

JVM 启动参数:使用自定义的 JUL 配置;

-Djava.util.logging.config.file=D:/jul.properties

D:/jul.properties:

handlers= java.util.logging.ConsoleHandler  .level= INFO    java.util.logging.ConsoleHandler.level = INFO  java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter    java.util.logging.SimpleFormatter.format=[JUL] %4$s: %5$s [%1$tc]%n

运行效果:

4. JCL 与 Log4j1 集成

从前面分析可知, JCL 通过自动发现,即可完成与 Log4j1 的集成,且自动发现的优先级最高;

POM配置:引入 JCL 和 Log4j1.x;

<dependency>      <groupId>commons-logging</groupId>      <artifactId>commons-logging</artifactId>      <version>1.2</version>  </dependency>    <dependency>      <groupId>log4j</groupId>      <artifactId>log4j</artifactId>      <version>1.2.17</version>  </dependency>

测试程序:

package webj2ee;    import org.apache.commons.logging.Log;  import org.apache.commons.logging.LogFactory;    public class JCL {      private static Log logger = LogFactory.getLog(JCL.class);        public static void main(String[] args) {          logger.info("From jcl's info msg!");          logger.warn("From jcl's warn msg!");          logger.error("From jcl's error msg!");      }  }

log4j.properties:使用自定义的 Log4j 配置;

log4j.rootLogger=INFO, Console    #Console  log4j.appender.Console=org.apache.log4j.ConsoleAppender  log4j.appender.Console.layout=org.apache.log4j.PatternLayout  log4j.appender.Console.layout.ConversionPattern=[LOG4J1] %d [%t] %-5p [%c] - %m%n

5. JCL 与 Log4j2 集成

从前面分析可知, JCL 自身没有 Log4j2 的适配器,所以引入 Log4j2 提供的 JCL 适配器。

POM配置:引入JCL、Log4j2、log4j-jcl

<dependency>      <groupId>commons-logging</groupId>      <artifactId>commons-logging</artifactId>      <version>1.2</version>  </dependency>    <dependency>      <groupId>org.apache.logging.log4j</groupId>      <artifactId>log4j-jcl</artifactId>      <version>2.12.1</version>  </dependency>    <dependency>      <groupId>org.apache.logging.log4j</groupId>      <artifactId>log4j-api</artifactId>      <version>2.12.1</version>  </dependency>  <dependency>      <groupId>org.apache.logging.log4j</groupId>      <artifactId>log4j-core</artifactId>      <version>2.12.1</version>  </dependency>

测试程序:

package webj2ee;    import org.apache.commons.logging.Log;  import org.apache.commons.logging.LogFactory;    public class JCL {      private static Log logger = LogFactory.getLog(JCL.class);        public static void main(String[] args) {          logger.info("From jcl's info msg!");          logger.warn("From jcl's warn msg!");          logger.error("From jcl's error msg!");      }  }

log4j2.xml:使用自定义的 Log4j2 配置文件;

<?xml version="1.0" encoding="UTF-8"?>  <Configuration status="WARN">      <Appenders>          <Console name="Console" target="SYSTEM_OUT">              <PatternLayout pattern="[LOG4J2] %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>          </Console>      </Appenders>        <Loggers>          <Root level="debug">              <AppenderRef ref="Console"/>          </Root>      </Loggers>  </Configuration>

运行结果:

集成原理分析

log4j-jcl 干了什么?

  1. log4j-jcl 通过 SPI 形式,控制 JCL 使用 log4j-jcl 中的 LogFactory 的实现类 org.apache.logging.log4j.jcl.LogFactoryImpl
  2. org.apache.logging.log4j.jcl.LogFactoryImpl 则再通过 适配器 模式,最终返回的是 org.apache.logging.log4j.jcl.Log4jLog 日志记录器,它会将日志记录工作委托给 Log4j2 实现。

6. JCL 与 Logback 集成

  • JCL 自身没有到 Logback 的适配器;
  • Logback 默认实现的是 SLF4J 接口,通过 SLF4J-API 访问;
  • 所以 JCL 需要将日志记录请求中转给 SLF4J,然后 SLF4J 再中转给 Logback;

POM配置:引入JCL、SLF4J-API、Logback、jcl-over-slf4j

<dependency>      <groupId>commons-logging</groupId>      <artifactId>commons-logging</artifactId>      <version>1.2</version>  </dependency>    <dependency>      <groupId>org.slf4j</groupId>      <artifactId>jcl-over-slf4j</artifactId>      <version>1.7.29</version>  </dependency>    <dependency>      <groupId>org.slf4j</groupId>      <artifactId>slf4j-api</artifactId>      <version>1.7.29</version>  </dependency>  <dependency>      <groupId>ch.qos.logback</groupId>      <artifactId>logback-classic</artifactId>      <version>1.2.3</version>  </dependency>  <dependency>      <groupId>ch.qos.logback</groupId>      <artifactId>logback-core</artifactId>      <version>1.2.3</version>  </dependency>

测试程序:

package webj2ee;    import org.apache.commons.logging.Log;  import org.apache.commons.logging.LogFactory;    public class JCL {      private static Log logger = LogFactory.getLog(JCL.class);        public static void main(String[] args) {          logger.info("From jcl's info msg!");          logger.warn("From jcl's warn msg!");          logger.error("From jcl's error msg!");      }  }

logback.xml:使用自定义的 Logback 配置;

<?xml version="1.0" encoding="UTF-8"?>  <configuration>      <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">          <encoder>              <pattern>[LOGBACK] %d{HH:mm:ss.SSS} [[%thread]] %-5level %logger{36} - %msg%n</pattern>          </encoder>      </appender>      <root level="DEBUG">          <appender-ref ref="STDOUT" />      </root>  </configuration>

运行结果:

集成原理分析

jcl-over-slf4j 干了什么?

显然,还是老路子,通过 SPI 机制使用自定义的 LogFactory 实现类,然后实例化中转到 SLF4J 的 Log 实现类;

参考:

《码出高效:Java 开发手册》 《Java 开发手册 1.5.0》