JavaWeb 开发日志管理详解(内含Slf4j底层实现原理 + log4j + logback框架介绍)

日志管理系统的使用背景

我们在日常软件开发中,避免不了使用日志管理系统,它是为我们展示系统运行状况的重要手段,当我们的系统在上线运行后,每个模块代码的执行,或者产生的结果乃至突发的错误,我们都可以通过日志管理系统记录下来,在我们需要某些数据或者处理突发错误时能够快速定位问题代码的位置或者原因时,会非常的便利和高效。如果我们没有采取任何的代码执行记录,那一旦我们的系统出现问题,或者业务逻辑出现问题,我们在维护起来会非常的麻烦,因为我们无法定位问题所在的地方,也无法得知系统运行中数据的状态,那只能将所有代码一点一点的过滤,来确定错误所在位置,对于小的软件系统还勉强说得过去,稍微大一点的系统,这完全就是在大海捞针。所以,在我们日常的软件系统开发过程中,利用日志管理系统来即时记录代码运行结果或者异常处理时的位置,对于我们后期维护是非常必要的。

日志管理系统是什么?

其实“日志管理系统”是我们业内的专有名词,它的本质其实就是我们在业务代码中,穿插一些特殊的代码,这些特殊的代码用来记录一些我们想记录的内容。比如:当我们还是小白的时候,很多时候我们在排错的时候,喜欢使用System.out.println() 来打印一些变量的值,用于我们的检查,或者是打印一些我们制定的字符串内容,用来表示是否进入到了我们指定的方法里。其实,日志管理系统,做的就是这种事情,因为我们在实际开发和项目运行后,是不会用打印语句在控制台窗口上查看我们打印内容的,这样会产生非常多没用的代码,仅仅是用来方便我们开发人员排错或者监管系统运行,所以,我们不能用打印语句这种Low的办法,取而代之的,就是咱们说的日志管理系统。所谓管理系统,其实就是在书写核心业务逻辑代码的同时,再写一套关于打印出我们指定的内容的系统,但是,这种系统,其实开发一次就足够了,因为功能应该都是一样的,比如:什么时候打印,需要打印什么内容,把内容打印到哪里去?(是控制台上,还是可以单独的用IO输出成磁盘文件已达到持久化)那有了这些基本的内容,我们就可以自己去开发一套这样的系统了,然后在我们的每个项目中都可以引入使用。
那既然说到这里了,那么业内常说的log4j、logback、Apache common logging 其实都是日志框架,至于做了什么事,现在大家应该很清楚了吧,这种日志框架,其实就是前辈们自己写的一套作日志记录的系统代码,然后把它进行了封装,可以给现在的我们直接配置使用,就不用我们自己再去花费物力财力脑力开发了。那有同学可能会问,SUN公司官方jdk有没有直接提供一套这种日志框架呢,其实是有的,叫JDKLog,JDKLog 的优点是使用非常简单,直接在 JDK 中就可以使用。但 JDKLog 功能比较太过于简单,不支持占位符显示,拓展性比较差,所以现在用的人也很少。比如把JDKLog、log4j、logback同时对比的话,JDKLog就是小刀,Log4j是大炮,Logback是原子弹,这种形象的对比,相信不用多说大家就知道他们的分量了,那么具体的差别,请继续往下看。

Java Web中的各种日志管理系统?

1. JDK原生日志API
  • JDK中内置了原生的日志打印的API:java.util.logging.Logger,这个API不依赖于任何的第三方框架,是JDK内置的
  • 这个Logger的级别: SEVERE → WARNING → INFO → CONFIG → FINE → FINER → FINESET
  • 用法如下:
private Logger logger = Logger.getLogger("<class-name>");
logger.severe("<severe-msg>");
logger.warning("<warning-msg>");
logger.info("<info-msg>");
logger.config("<config-msg>");
logger.fine("<fine-msg>");
logger.finer("<finer-msg>");
logger.finest("<finest-msg>");
  • JDK日志配置
    默认情况下,日志的输出都是在控制台直接输出的,并没有输出到文件中,这是因为在JRE默认配置中,只配置了一个ConsoleHandler,这个配置文件位于/lib/logging.properties,注意一般情况下我在安装JDK的时候是不会安装公共JRE的,因为JDK中就已经安装过了,不过最新的JDK貌似已经把二者分开了,总之,根据自己的情况找到JRE的家目录,即可找到该文件;
    在logging.properties这个文件中有个handlers,其值只有一个handlers= java.util.logging.ConsoleHandler,如果想要同时输出到文件中, 只需要在后面追加java.util.logging.FileHandler,多个handler之间使用英文都好隔开即可;
    在增加了FileHanlder之后,那么FileHanlder的各种配置就生效了,就可以根据需要进行配置了,如:
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
  1. FileHandler.pattern:用来指定生成的日志文件的路径名称;/:本地路径名分隔符;%t:系统临时目录;%h:“user.home” 系统属性的值;%g:区分循环日志的生成号;%u:解决冲突的惟一号码;%%:转换为单个百分数符号"%" ;
  2. FileHandler.limit:用来限制文件的大小,以字节为单位,0表示没有限制;
  3. FileHandler.count:指定有多少输出文件参与循环(默认为1);
  4. FileHandler.formatter:指定要使用的 Formatter 类的名称(默认为java.util.logging.XMLFormatter)。 另外一个是:java.util.logging.SimpleFormatter。XMLFormatter是以xml样式输出,SimpleFormatter是以普通样式输出。
  5. FileHandler.append:指定是否应该将 FileHandler 追加到任何现有文件上(默认为false);
  6. SimpleFormatter.format:用来自定义SimpleFormatter的输出样式;如:%4s:s: %5s [%1$tc]%n,即输出样式为:: [<date/time>]
  • 日志打印级别的配置:
  1. 全局输出级别设置
[java.util.logging.ConsoleHandler/FileHandler].level=<level-name>
  1. 具体包名的输出级别设置
<package-name>.level=<level-name>
2. Apache common logging
  • Tomcat是Apache的一个开源项目,Apache旗下还有一个开源项目,名为Apache common logging,而Tomcat中的日志框架juli正是这个开源框架的一个易名项目,究其根源来说,Apache common logging是对JDK打印API的修改封装;

  • 为了不影响JDK中的Logger的正常使用,Tomcat中增加了自定义的Log配置,这个配置存在于Tomcat的脚本catalina.bat和catalina.sh中,如下:

LOGGING_CONFIG="-Djava.util.logging.config.file=$CATALINA_BASE/conf/logging.properties"
LOGGING_MANAGER="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager"
  1. 这两个参数配置给java虚拟机配置了两个东东, 一个是日志的配置文件还有一个日志管理器;
  • 在这个配置文件中,我们发现默认情况下,Tomcat支持文件和控制台两种输出方式;还有就是Tomcat的handler都带有一个数字前缀,自然就想到如果使用系统默认的类加载器的话,肯定会出错,Tomcat使用的是自定义的ClassLoader来解析这个数字前缀;而增加数字前缀的目的就是为了使得一个类模板生成多个实例,每个实例都管理一种类型的日志,如:
  1. 前缀1对应的日志:文件名以catalina.开头
  2. 前缀2对应的日志:文件名以localhost.开头
  3. 前缀3对应的日志:文件名以manager.开头
  4. 前缀4对应的日志:文件名以host-manager.开头
  • Tomcat与JDK日志的配置
  1. 有一点不同,JRE的logging.properties文件中使用handlers配置输出途径,而在Tomcat中使用.handlers配置,使用handlers配置上面4中不同的文件日志
  2. Tomcat的详细配置可查看:http://tomcat.apache.org/tomcat-8.0-doc/config [/valve.html]
3. Logback的API(Log4j)

提起这个日志框架,就不得不提起另外另个东东:slf4j和log4j,这三个框架什么关系呢?网上找到一张图:
在这里插入图片描述

  • slf4j的全称是:The Simple Logging Facade for(4) Java,即slf4j是一个门面,而log4j和logback则是这些接口的实现;log4j和logback是由同一个人完成的,前者是apache实现的,logback是为了替代log4j,logback可以说是slf4j的原生实现,目前除了老的项目,新项目的日志基本上都是使用logback;
  1. logback是直接实现了slf4j的接口,不消耗内存和计算开销的。slf4j的api在调用log4j时需要一个适配层;
  2. log4j和logback可以单独的使用,也可以绑定slf4j一起使用:单独使用的时候,分别调用框架自己的方法来输出日志信息;绑定slf4j一起使用,调用slf4j的api来输入日志信息,具体使用与底层日志框架无关,但是需要底层框架的配置文件;
  • 其实说到这里就能发现这个门面扮演什么角色,其实就是为了解决众口难调的问题,因为项目中我们用到很多的框架,如果每个框架都有自己的日志输出框架,那么项目中的日志维护便成了一个大问题,为了解决这个大问题,log4j和logback框架横空出世:我们项目中的所有框架的日志输出都交给slf4j,然后统一对消息进行处理;

slf4j 又是什么?

刚才看完了上面几种日志框架的介绍,想必同学们也对日志框架多少了解了一些,不光知道了框架有多种多样,同时配置起来也是各不相同,甚至功能也相差甚远。但是,可能有的同学在读到上面一段内容时,遇到了Slf4j这个名词,对它并不熟悉,其实它是干啥的呢?我们为什么要用它呢?
slf4j是门面模式的典型应用,因此在讲slf4j前,我们先简单回顾一下门面模式,
门面模式,其核心为外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。用一张图来表示门面模式的结构为:
门面模式介绍
门面模式的核心为Facade即门面对象,门面对象核心为几个点:
• 知道所有子角色的功能和责任
• 将客户端发来的请求委派到子系统中,没有实际业务逻辑
• 不参与子系统内业务逻辑的实现
大致上来看,对门面模式的回顾到这里就可以了,开始接下来对SLF4J的学习。

我们为什么要使用slf4j?
举个例子,比如:我们自己的系统中使用了logback这个日志系统。
同时,我们的系统又使用了第三方的jar包——A.jar,而A.jar中使用的日志系统为log4j。
甚至,我们的系统又使用了第三方的jar包——B.jar,而B.jar中使用的日志系统为slf4j-simple
那这样,我们的系统就不得不同时支持并维护logback、log4j、slf4j-simple三种日志框架,非常的不方便。
那当这种问题出现的时候,前辈们就想办法开始解决这种问题,解决这种问题无非就是,我们开发人员就不要去自己维护这几种不同风格的框架了,我们再写一套代码,专门来处理这种情况——多个日志框架同时存在需要维护。具体做什么呢?就是我们只需要告诉这套代码,我想用哪个牌子的日志管理系统,然后具体调用哪个框架怎么去记录、怎么打印等等这些具体的事情,就交给这套代码去做了,我们开发者只需要关注,用啥框架、需要记录啥内容即可。

走进Slf4j的世界

那解决这个问题的方式就是引入一个适配层,由适配层决定使用哪一种日志系统,而调用端只需要做的事情就是打印日志而不需要关心如何打印日志,slf4j或者commons-logging就是这种适配层,接下来我们就研究一下slf4j。

从上面的描述,我们必须清楚地知道一点:slf4j只是一个日志标准,并不是日志系统的具体实现。具体的实现还是那些log4j、logback等日志框架。

理解这句话非常重要,slf4j只做两件事情:
• 提供日志接口
• 提供获取具体日志对象的方法

slf4j-simple、logback都是slf4j的具体实现,log4j并不直接实现slf4j,但是有专门的一层桥接slf4j-log4j12来实现slf4j。

为了更理解slf4j,我们先看例子,再读源码,相信大家会对slf4j有更深刻的认识。

slf4j应用举例

上面讲了,slf4j的直接/间接实现有slf4j-simple、logback、slf4j-log4j12,我们先定义一个pom.xml,引入相关jar包:

<dependencies>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.11</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>1.7.25</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.2.3</version>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-simple</artifactId>
		<version>1.7.25</version>
	</dependency>
	<dependency>
		<groupId>log4j</groupId>
		<artifactId>log4j</artifactId>
		<version>1.2.17</version>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-log4j12</artifactId>
		<version>1.7.21</version>
	</dependency>
</dependencies>

写一段简单的Java代码:

  @Test
  public void testSlf4j() {
      Logger logger = LoggerFactory.getLogger(Object.class);
      logger.error("123");
  }

接着,我们将pom.xml中引入的依赖,除了junit和slf4j-api之外,全部删掉。如下:

<dependencies>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.11</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>1.7.25</version>
	</dependency>
</dependencies>

其实这么做,就是为了给大家说明一下刚才强调的内容。即不引入任何slf4j的实现类,运行Test方法,我们看一下控制台的输出为:
在这里插入图片描述
看到没有任何日志的输出,这验证了刚才说的:slf4j不提供日志的具体实现,只有slf4j是无法打印日志的。
接着打开logback-classic的注释,运行Test方法,我们看一下控制台的输出为:

22:02:14.552 [main] ERROR java.lang.Object - 123

可以看到,我们只要引入了一个slf4j的具体实现类,即可使用该日志框架输出日志。

最后做一个测验,我们把所有日志打开,引入logback-classic、slf4j-simple、log4j,运行Test方法,控制台输出为:
在这里插入图片描述
和上面的差别是,可以输出日志,但是会输出一些警告日志,提示我们同时引入了多个slf4j的实现,然后展示了,选择了其中的哪一个作为了我们默认使用的日志系统。

从例子我们可以得出一个重要的结论,即slf4j的作用:只要所有代码都使用门面对象slf4j,我们就不需要关心其具体实现,最终所有地方使用一种具体实现即可,更换、维护都非常方便。

slf4j实现原理

上面看了slf4j的示例,下面研究一下slf4j的实现,我们只关注重点代码。

slf4j的用法就是常年不变的一句"Logger logger = LoggerFactory.getLogger(Object.class);",可见这里就是通过LoggerFactory去拿slf4j提供的一个Logger接口的具体实现而已,LoggerFactory的getLogger的方法实现为:

 1 public static Logger getLogger(Class<?> clazz) {
 2     Logger logger = getLogger(clazz.getName());
 3     if (DETECT_LOGGER_NAME_MISMATCH) {
 4         Class<?> autoComputedCallingClass = Util.getCallingClass();
 5         if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
 6             Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
 7                             autoComputedCallingClass.getName()));
 8             Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
 9         }
10     }
11     return logger;
12 }

从第2行开始跟代码,一直跟到LoggerFactory的bind()方法:

1 private final static void bind() {
 2     try {
 3         Set<URL> staticLoggerBinderPathSet = null;
 4         // skip check under android, see also
 5         // http://jira.qos.ch/browse/SLF4J-328
 6         if (!isAndroid()) {
 7             staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
 8             reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
 9         }
10         // the next line does the binding
11         StaticLoggerBinder.getSingleton();
12         INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
13         reportActualBinding(staticLoggerBinderPathSet);
14         fixSubstituteLoggers();
15         replayEvents();
16         // release all resources in SUBST_FACTORY
17         SUBST_FACTORY.clear();
18     } catch (NoClassDefFoundError ncde) {
19         String msg = ncde.getMessage();
20         if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
21             INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
22             Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
23             Util.report("Defaulting to no-operation (NOP) logger implementation");
24             Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
25         } else {
26             failedBinding(ncde);
27             throw ncde;
28         }
29     } catch (java.lang.NoSuchMethodError nsme) {
30         String msg = nsme.getMessage();
31         if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
32             INITIALIZATION_STATE = FAILED_INITIALIZATION;
33             Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
34             Util.report("Your binding is version 1.5.5 or earlier.");
35             Util.report("Upgrade your binding to version 1.6.x.");
36         }
37         throw nsme;
38     } catch (Exception e) {
39         failedBinding(e);
40         throw new IllegalStateException("Unexpected initialization failure", e);
41     }
42 }

这个地方第7行是一个关键,看一下代码:

1 static Set<URL> findPossibleStaticLoggerBinderPathSet() {
 2     // use Set instead of list in order to deal with bug #138
 3     // LinkedHashSet appropriate here because it preserves insertion order
 4     // during iteration
 5     Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
 6     try {
 7         ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
 8         Enumeration<URL> paths;
 9         if (loggerFactoryClassLoader == null) {
10             paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
11         } else {
12             paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
13         }
14         while (paths.hasMoreElements()) {
15             URL path = paths.nextElement();
16             staticLoggerBinderPathSet.add(path);
17         }
18     } catch (IOException ioe) {
19         Util.report("Error getting resources from path", ioe);
20     }
21     return staticLoggerBinderPathSet;
22 }

这个地方重点其实就是第12行的代码,getLogger的时候会去classpath下找STATIC_LOGGER_BINDER_PATH,STATIC_LOGGER_BINDER_PATH值为"org/slf4j/impl/StaticLoggerBinder.class",即所有slf4j的实现,在提供的jar包路径下,一定是有"org/slf4j/impl/StaticLoggerBinder.class"存在的,我们可以看一下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们不能避免在系统中同时引入多个slf4j的实现,所以接收的地方是一个Set。大家应该注意到,上部分在演示同时引入logback、slf4j-simple、log4j的时候会有警告:
在这里插入图片描述
这就是因为有三个"org/slf4j/impl/StaticLoggerBinder.class"存在的原因,此时reportMultipleBindingAmbiguity方法控制台输出语句:

1 private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
2     if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
3         Util.report("Class path contains multiple SLF4J bindings.");
4         for (URL path : binderPathSet) {
5             Util.report("Found binding in [" + path + "]");
6         }
7         Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
8     }
9 }

那有同学可能会问,同时存在三个"org/slf4j/impl/StaticLoggerBinder.class"怎么办?首先确定的是这不会导致启动报错,其次在这种情况下编译期间,编译器会选择其中一个StaticLoggerBinder.class进行绑定,这个地方sfl4j也在reportActualBinding方法中报告了绑定的是哪个日志框架:

1 private static void reportActualBinding(Set<URL> binderPathSet) {
2     // binderPathSet can be null under Android
3     if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
4         Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
5     }
6 }

对照上面的截图,看最后一行,确实是"Actual binding is of type…"这句。

最后StaticLoggerBinder就比较简单了,不同的StaticLoggerBinder其getLoggerFactory实现不同,拿到ILoggerFactory之后调用一下getLogger即拿到了具体的Logger,可以使用Logger进行日志输出。

走进Logback的世界

在五年前,大部分公司使用的日志框架还是log4j,大约从16年中到现在,大部分公司的日志框架基本都换成了logback,总结一下,logback大约有以下的一些优点:

  • 内核重写、测试充分、初始化内存加载更小,这一切让logback性能和log4j相比有诸多倍的提升
  • logback非常自然地直接实现了slf4j,这个严格来说算不上优点,只是这样,再理解slf4j的前提下会很容易理解logback,也同时很容易用其他日志框架替换logback
  • logback有比较齐全的200多页的文档
  • logback当配置文件修改了,支持自动重新加载配置文件,扫描过程快且安全,它并不需要另外创建一个扫描线程
  • 支持自动去除旧的日志文件,可以控制已经产生日志文件的最大数量

总而言之,如果大家的项目里面需要选择一个日志框架,那么我个人非常建议使用logback,这也就是为什么在文章最开始,说它的原子弹。

- logback加载

我们简单分析一下logback加载过程,当我们使用logback-classic.jar时,应用启动,那么logback会按照如下顺序进行扫描:

  • 在系统配置文件System Properties中寻找是否有logback.configurationFile对应的value
  • 在classpath下寻找是否有logback.groovy(即logback支持groovy与xml两种配置方式)
  • 在classpath下寻找是否有logback-test.xml
  • 在classpath下寻找是否有logback.xml

以上任何一项找到了,就不进行后续扫描,按照对应的配置进行logback的初始化,具体代码实现可见ch.qos.logback.classic.util.ContextInitializer类的findURLOfDefaultConfigurationFile方法。

当所有以上四项都找不到的情况下,logback会调用ch.qos.logback.classic.BasicConfigurator的configure方法,构造一个ConsoleAppender用于向控制台输出日志,默认日志输出格式为"%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"。

- logback的configuration

logback的核心零件应当是Appender、Logger、Pattern,在这之前先简单了解一下logback的
< configuration>,< configuration>只有三个属性:

  • scan:当scan被设置为true时,当配置文件发生改变,将会被重新加载,默认为true
  • scanPeriod:检测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认为毫秒,当scan=true时这个值生效,默认时间间隔为1分钟
  • debug:当被设置为true时,将打印出logback内部日志信息,实时查看logback运行信息,默认为false
- logback的 logger 与 root

先从最基本的< logger>与< root>开始。
< logger>用来设置某一个包或者具体某一个类的日志打印级别、以及指定< appender>。< logger>可以包含零个或者多个< appender-ref>元素,标识这个appender将会添加到这个logger。< logger>仅有一个name属性、一个可选的level属性和一个可选的additivity属性:

  • name:用来指定受此logger约束的某一个包或者具体的某一个类
  • level:用来设置打印级别,五个常用打印级别从低至高依次为TRACE、DEBUG、INFO、WARN、ERROR,如果未设置此级别,那么当前logger会继承上级的级别
  • additivity:是否向上级logger传递打印信息,默认为true
    < root>也是< logger>元素,但是它是根logger,只有一个level属性,因为它的name就是ROOT,源码在LoggerContext中:
 1 public LoggerContext() {
 2     super();
 3     this.loggerCache = new ConcurrentHashMap<String, Logger>();
 4 
 5     this.loggerContextRemoteView = new LoggerContextVO(this);
 6     this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
 7     this.root.setLevel(Level.DEBUG);
 8     loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
 9     initEvaluatorMap();
10     size = 1;
11     this.frameworkPackages = new ArrayList<String>();
12 }

Logger的构造函数为:

Logger(String name, Logger parent, LoggerContext loggerContext) {
    this.name = name;
    this.parent = parent;
    this.loggerContext = loggerContext;
}

看到第一个参数就是Root的name,而这个Logger.ROOT_LOGGER_NAME的定义为final public String ROOT_LOGGER_NAME = “ROOT”,由此可以看出< root>节点的name就是"ROOT"。
接着写一段代码来测试一下:

 1 public class Slf4jTest {
 2 
 3     @Test
 4     public void testSlf4j() {
 5         Logger logger = LoggerFactory.getLogger(Object.class);
 6         logger.trace("=====trace=====");  
 7         logger.debug("=====debug=====");  
 8         logger.info("=====info=====");  
 9         logger.warn("=====warn=====");  
10         logger.error("=====error=====");  
11     }
12     
13 }

logback.xml的配置为:

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 4         <layout class="ch.qos.logback.classic.PatternLayout">
 5             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 6         </layout>
 7     </appender>
 8     
 9     <root level="info">
10         <appender-ref ref="STDOUT" />
11     </root>
12     
13 </configuration>

root将打印级别设置为"info"级别,< appender>暂时不管,控制台的输出为:

2020-01-27 02:20:04.779 [main] INFO  java.lang.Object - =====info=====
2020-01-27 02:20:04.782 [main] WARN  java.lang.Object - =====warn=====
2020-01-27 02:20:04.782 [main] ERROR java.lang.Object - =====error=====

logback.xml的意思是,当Test方法运行时,root节点将日志级别大于等于info的交给已经配置好的名为"STDOUT"的< appender>进行处理,"STDOUT"将信息打印到控制台上。

接着理解一下< logger>节点的作用,logback.xml修改一下,加入一个只有name属性的< logger>:

1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3 
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <layout class="ch.qos.logback.classic.PatternLayout">
 6             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 7         </layout>
 8     </appender>
 9      
10     <logger name="java" />
11       
12      <root level="debug">
13          <appender-ref ref="STDOUT" />
14      </root>
15       
16 </configuration>

注意这个name表示的是LoggerFactory.getLogger(XXX.class),XXX的包路径,包路径越少越是父级,我们测试代码里面是Object.class,即name="java"是name="java.lang"的父级,root是所有的父级。看一下输出为:

2020-01-27 02:20:04.963 [main] DEBUG java.lang.Object - =====debug=====
2020-01-27 02:20:04.965 [main] INFO  java.lang.Object - =====info=====
2020-01-27 02:20:04.966 [main] WARN  java.lang.Object - =====warn=====
2020-01-27 02:20:04.966 [main] ERROR java.lang.Object - =====error=====

出现这样的结果是因为:

  • < logger>中没有配置level,即继承父级的level,< logger>的父级为,那么level=debug
  • 没有配置additivity,那么additivity=true,表示此< logger>的打印信息向父级< root>传递
  • 没有配置< appender-ref>,表示此< logger>不会打印出任何信息
    由此可知,< logger>的打印信息向< root>传递,< root>使用"STDOUT"这个< appender>打印出所有大于等于debug级别的日志。举一反三,我们将< logger>的additivity配置为false,那么控制台应该不会打印出任何日志,因为< logger>的打印信息不会向父级< root>传递且< logger>没有配置任何< appender>,大家可以自己试验一下。

接着,我们再配置一个< logger>:

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3  
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <layout class="ch.qos.logback.classic.PatternLayout">
 6             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 7         </layout>
 8     </appender>
 9      
10     <logger name="java" additivity="false" />
11     <logger name="java.lang" level="warn">
12         <appender-ref ref="STDOUT" />
13     </logger>
14     
15     <root level="debug">
16         <appender-ref ref="STDOUT" />
17     </root>
18      
19 </configuration>

如果读懂了上面的例子,那么这个例子应当很好理解:

  • LoggerFactory.getLogger(Object.class),首先找到name="java.lang"这个,将日志级别大于等于warn的使用"STDOUT"这个打印出来
  • name="java.lang"这个没有配置additivity,那么additivity=true,打印信息向上传递,传递给父级name="java"这个
  • name="java"这个的additivity=false且不关联任何,那么name="java"这个不会打印任何信息

由此分析,得出最终的打印结果为:

2020-01-27 02:22:33.147 [main] WARN  java.lang.Object - =====warn=====
2020-01-27 02:22:33.150 [main] ERROR java.lang.Object - =====error=====

举一反三,上面的name="java"这个< appender>可以把additivity设置为true试试看是什么结果,如果对前面的分析理解的朋友应该很容易想到,有两部分日志输出,一部分是日志级别大于等于warn的、一部分是日志级别大于等于debug的。

- logback的 appender

接着看一下< appender >,< appender >是< configuration >的子节点,是负责写日志的组件。< appender >有两个必要属性name和class:

  • name指定< appender >的名称
  • class指定< appender >的全限定名
    < appender >有好几种,上面我们演示过的是ConsoleAppender,ConsoleAppender的作用是将日志输出到控制台,配置示例为:
1 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
2     <encoder>
3         <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
4     </encoder>
5 </appender>

其中,encoder表示对参数进行格式化。我们和上一部分的例子对比一下,发现这里是有所区别的,上面使用了< layout >定义< pattern >,这里使用了< encoder >定义< pattern >,简单说一下:

  • < encoder >是0.9.19版本之后引进的,以前的版本使用< layout >,logback极力推荐的是使用< encoder >而不是< layout >
  • 最常用的FileAppender和它的子类的期望是使用< encoder >而不再使用< layout >
    关于< encoder >中的格式下一部分再说。接着我们看一下FileAppender,FileAppender的作用是将日志写到文件中,配置示例为:
1 <appender name="FILE" class="ch.qos.logback.core.FileAppender">  
2     <file>D:/123.log</file>  
3     <append>true</append>  
4     <encoder>  
5         <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>  
6     </encoder>  
7 </appender>

它的几个节点为:

  • 表示写入的文件名,可以使相对目录也可以是绝对目录,如果上级目录不存在则自动创建
  • 如果为true表示日志被追加到文件结尾,如果是false表示清空文件
  • 表示输出格式,后面说
  • 如果为true表示日志会被安全地写入文件,即使其他的FileAppender也在向此文件做写入操作,效率低,默认为false

接着来看一下RollingFileAppender,RollingFileAppender的作用是滚动记录文件,先将日志记录到指定文件,当符合某个条件时再将日志记录到其他文件,RollingFileAppender配置比较灵活,因此使用得更多,示例为:

1 <appender name="ROLLING-FILE-1" class="ch.qos.logback.core.rolling.RollingFileAppender">   
2     <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">   
3         <fileNamePattern>rolling-file-%d{yyyy-MM-dd}.log</fileNamePattern>   
4         <maxHistory>30</maxHistory>    
5     </rollingPolicy>   
6     <encoder>   
7         <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>   
8     </encoder>   
9 </appender>

这种是仅仅指定了< rollingPolicy >的写法,< rollingPolicy >的作用是当发生滚动时,定义RollingFileAppender的行为,其中上面的TimeBasedRollingPolicy是最常用的滚动策略,它根据时间指定滚动策略,既负责滚动也负责触发滚动,有以下节点:

  • < fileNamePattern >,必要节点,包含文件名及"%d"转换符,"%d"可以包含一个Java.text.SimpleDateFormat指定的时间格式,如%d{yyyy-MM},如果直接使用%d那么格式为yyyy-MM-dd。RollingFileAppender的file子节点可有可无,通过设置file可以为活动文件和归档文件指定不同的位置
  • < maxHistory >,可选节点,控制保留的归档文件的最大数量,如果超出数量就删除旧文件,假设设置每个月滚动且< maxHistory >是6,则只保存最近6个月的文件

想其他还有SizeBasedTriggeringPolicy,用于按照文件大小进行滚动,可以自己查阅一下资料。

- 异步写日志

日志通常来说都以文件形式记录到磁盘,例如使用,这样的话一次写日志就会发生一次磁盘IO,这对于性能是一种损耗,因此更多的,对于每次请求必打的日志(例如请求日志,记录请求API、参数、请求时间),我们会采取异步写日志的方式而不让此次写日志发生磁盘IO,阻塞线程从而造成不必要的性能损耗。(不要小看这个点,可以网上查一下服务端性能优化的文章,只是因为将日志改为异步写,整个QPS就有了大幅的提高)。
接着我们看下如何使用logback进行异步写日志配置:

1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3 
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <encoder>
 6             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 7         </encoder>
 8     </appender>
 9     
10     <appender name="ROLLING-FILE-1" class="ch.qos.logback.core.rolling.RollingFileAppender">   
11         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">   
12               <fileNamePattern>D:/rolling-file-%d{yyyy-MM-dd}.log</fileNamePattern>   
13               <maxHistory>30</maxHistory>    
14         </rollingPolicy>   
15         <encoder>
16               <pattern>%-4relative [%thread] %-5level %lo{35} - %msg%n</pattern>   
17         </encoder>   
18     </appender>
19     
20     <!-- 异步输出 -->  
21     <appender name ="ASYNC" class= "ch.qos.logback.classic.AsyncAppender">  
22         <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->  
23         <discardingThreshold>0</discardingThreshold>  
24         <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->  
25         <queueSize>256</queueSize>  
26         <!-- 添加附加的appender,最多只能添加一个 -->  
27         <appender-ref ref ="ROLLING-FILE-1"/>  
28     </appender>
29     
30     <logger name="java" additivity="false" />
31     <logger name="java.lang" level="DEBUG">
32         <appender-ref ref="ASYNC" />
33     </logger>
34     
35     <root level="INFO">
36         <appender-ref ref="STDOUT" />
37     </root>
38     
39 </configuration>

即,我们引入了一个AsyncAppender,先说一下AsyncAppender的原理,再说一下几个参数:

当我们配置了AsyncAppender,系统启动时会初始化一条名为"AsyncAppender-Worker-ASYNC"的线程

当Logging Event进入AsyncAppender后,AsyncAppender会调用appender方法,
appender方法中再将event填入Buffer(使用的Buffer为BlockingQueue,具体实现为ArrayBlockingQueye)前,
会先判断当前Buffer的容量以及丢弃日志特性是否开启,当消费能力不如生产能力时,
AsyncAppender会将超出Buffer容量的Logging Event的级别进行丢弃,
作为消费速度一旦跟不上生产速度导致Buffer溢出处理的一种方式。

上面的线程的作用,就是从Buffer中取出Event,交给对应的appender进行后面的日志推送

从上面的描述我们可以看出,AsyncAppender并不处理日志,只是将日志缓冲到一个BlockingQueue里面去,
并在内部创建一个工作线程从队列头部获取日志,之后将获取的日志循环记录到附加的其他appender上去,
从而达到不阻塞主线程的效果。因此AsyncAppender仅仅充当的是事件转发器,
必须引用另外一个appender来做事。

从上述原理,我们就能比较清晰地理解几个参数的作用了:

  • discardingThreshold,假如等于20则表示,表示当还剩20%容量时,将丢弃TRACE、DEBUG、INFO级别的Event,只保留WARN与ERROR级别的Event,为了保留所有的events,可以将这个值设置为0,默认值为queueSize/5
  • queueSize比较好理解,BlockingQueue的最大容量,默认为256
  • includeCallerData表示是否提取调用者数据,这个值被设置为true的代价是相当昂贵的,为了提升性能,默认当event被加入BlockingQueue时,event关联的调用者数据不会被提取,只有线程名这些比较简单的数据
  • appender-ref表示AsyncAppender使用哪个具体的进行日志输出
- - < encoder >

< encoder >节点负责两件事情:

  • 把日志信息转换为字节数组
  • 把字节数组写到输出流

目前PatternLayoutEncoder是唯一有用的且默认的encoder,有一个节点,就像上面演示的,用来设置日志的输入格式,使用“%+转换符"的方式,如果要输出"%“则必须使用”%“对”%"进行转义。

- - < filter >

最后来看一下< filter >,< filter >是< appender >的一个子节点,表示在当前给到的日志级别下再进行一次过滤,最基本的Filter有ch.qos.logback.classic.filter.LevelFilter和ch.qos.logback.classic.filter.ThresholdFilter,首先看一下LevelFilter:

1 <configuration scan="false" scanPeriod="60000" debug="false">
 2 
 3     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 4         <encoder>
 5             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 6         </encoder>
 7         <filter class="ch.qos.logback.classic.filter.LevelFilter">
 8             <level>WARN</level>
 9             <onMatch>ACCEPT</onMatch>
10             <onMismatch>DENY</onMismatch>
11         </filter>
12     </appender>
13     
14     <logger name="java" additivity="false" />
15     <logger name="java.lang" level="DEBUG">
16         <appender-ref ref="STDOUT" />
17     </logger>
18     
19     <root level="INFO">
20         <appender-ref ref="STDOUT" />
21     </root>
22     
23 </configuration>

看一下输出:

2020-01-27 02:30:08.843 [main] WARN  java.lang.Object - =====warn=====

看到尽管< logge r>配置的是DEBUG,但是输出的只有warn,因为在中对匹配到WARN级别时做了ACCEPT(接受),对未匹配到WARN级别时做了DENY(拒绝),当然只能打印出WARN级别的日志。
再看一下ThresholdFilter,配置为:

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3 
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <encoder>
 6             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 7         </encoder>
 8         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
 9             <level>INFO</level>
10         </filter>
11     </appender>
12     
13     <logger name="java" additivity="false" />
14     <logger name="java.lang" level="DEBUG">
15         <appender-ref ref="STDOUT" />
16     </logger>
17     
18     <root level="INFO">
19         <appender-ref ref="STDOUT" />
20     </root>
21     
22 </configuration>

看一下输出为:

2020-01-27 02:30:45.353 [main] INFO  java.lang.Object - =====info=====
2020-01-27 02:30:45.358 [main] WARN  java.lang.Object - =====warn=====
2020-01-27 02:30:45.359 [main] ERROR java.lang.Object - =====error=====

因为ThresholdFilter的策略是,会将日志级别小于的全部进行过滤,因此虽然指定了DEBUG级别,但是只有INFO及以上级别的才能被打印出来。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章