各位新年快乐,过了个新年,休(hua)息(shui)了三周,不过我又回来更新了,通过前面四篇想必小伙伴已经了解日志的使用以及最佳实践了,这个系列的文章也差很少要结束了,今天咱们来总结一下。html
这篇文章咱们讨论一下 SLF4j 的设计,以及 SLF4j 好在哪,以后进行一些答疑与前系列文章勘误,最最后咱们来了解一下如何正确的分文件输出日志。java
SLF4j 并无使用网上所谓的编译时绑定,其实是采用了约定俗成的方式,如何作的?很简单,就是直接加载org/slf4j/impl/StaticLoggerBinder.class
,找到一个直接使用,没找到或者找到多个报警告,,分析一下源码:android
咱们一块儿来看一个个 Logger 实例是如何建立的:git
org.slf4j.LoggerFactory#getLogger(java.lang.String)
,获取 logger 实例的真正入口ch.qos.logback.classic.LoggerContext#getLogger(java.lang.String)
,调用了 logback 的LoggerContext
(实现 LoggerFactory),具体如何调用到这里下面解析)childLogger = logger.createChildByName(childName);
建立了 loggger实例,继续跟进ch.qos.logback.classic.Logger#createChildByName
方法中能够看到childLogger = new Logger(childName, this, this.loggerContext);
,至此咱们目的也达到了,logger 是 new 出来的并非所谓的编译时绑定。咱们继续来跟踪如何调用到 logback 的 LoggerContext
(LogggerFactory),而且来验证一下是否真的是所谓的编译时绑定:github
仍是org.slf4j.LoggerFactory#getLogger(java.lang.String)
方法,此次咱们跟进到org.slf4j.LoggerFactory#getILoggerFactory
方法中发现调用了performInitialization
,跟进去发现调用了bind
,继续跟进发现调用了findPossibleStaticLoggerBinderPathSet
方法在当前ClassPath下查询了全部名为org/slf4j/impl/StaticLoggerBinder.class
类路径返回面试
真正代码以下,注释写的很明确:算法
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
复制代码
StaticLoggerBinder
这个类就是绑定的关键,点进去发现根本不是 SLF4j 的类,而是来自于 Logback,也就是说,SLF4j 使用了第三方(Logback、Log4j 等)提供的中介类,(Spring Boot 自动配置也部分使用了这种思想,之后的全栈系列文章将会有详细解析,欢迎关注),若是出出现NoClassDefFoundError
则提示一下使用者,而后再也不处理日志。spring
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.
复制代码
给出结论以前咱们先来明确一下 Java 的绑定(Binding)的概念,Java 自己只支持静态(static)绑定与运行时(runtime)绑定,直到与 JDK 1.6 版本一块儿发布的 JSR269 才能进行编译时绑定,员外理解的编译时绑定相似于 lomok 在编译过程当中修改字节码。SFL4j 的 logger 实例是 new 出来的,绑定 LogContext
的 StaticLoggerBinder
(中介类) 是写死的,编译时并无处理任何逻辑,也谈不上什么编译时绑定,员外翻遍了 SLF4j 文档也没有找到任何有关编译时绑定的材料,官方只提到了 “static binding”, 因此回到文章标题,网上流传的编译时绑定根本就是错的,SLF4j使用的是 Convention over Configuration(CoC)– 惯例优于配置原则,我无论你是什么日志框架,我只加载org.slf4j.impl.StaticLoggerBinder
。这完美契合了软件设计的 KISS(Keep It Simple, Stupid)原则,而 Commons-logging 魔法(magic)同样的动态加载虽然设计很高大上,在应用领域却直接被打脸,低效率、与 OSGi 共同使用所致使的 ClassLoader 问题更是火上浇油,因此员外与你们共勉,写代码切勿炫技。apache
以上是本文核心,略过的读者劳烦再读一次。架构
先了解一下为何说 SLF4j 更好,下面两段话来自于Spring 4.x 官方文档:
Not Using Commons Logging
Unfortunately, the runtime discovery algorithm in
commons-logging
, while convenient for the end-user, is problematic. If we could turn back the clock and start Spring now as a new project it would use a different logging dependency. The first choice would probably be the Simple Logging Facade for Java ( SLF4J), which is also used by a lot of other tools that people use with Spring inside their applications.不幸的是,
commons-logging
的运行时发现算法虽然对用户很方便,但却有问题。 若是咱们有后悔药可以将 Spring 做为一个新项目从新启动,首选多是 Simple Logging Facade for Java(SLF4J),Spring 所依赖的其余工具也能使用它。
That might seem like a lot of dependencies just to get some logging. Well it is, but it is optional, and it should behave better than the vanilla
commons-logging
with respect to classloader issues, notably if you are in a strict container like an OSGi platform. Allegedly there is also a performance benefit because the bindings are at compile-time not runtime.这看起来好像仅仅为了日志就须要不少依赖。但这些依赖都是可选的,在类加载器问题方面,它应该比普通的 Commons-logging 表现得更好,特别是若是您在 OSGi 平台这样的严格容器中。听说性能还有优点,由于绑定是在编译时而不是运行时。
这两段文字可谓是肺腑之言,公平公正,员外也没有处处去验证,所谓性能优点我认为做为static final
级别变量,性能优点也不会太大。员外认为 SLF4j 本质上更好的缘由在于其提供市面上全部日志框架的兼容解决方案。
第一篇文章「Java日志体系竟然这么复杂?——架构篇」其中 Spring Boot的使用依赖,我写到“Spring已经写好了一个log4j2-starter但缺乏桥接包”是不对的,员外出于好奇验证一下,之因此 Spring 没有依赖 jcl-over-slf4j 是由于 Spring Boot 2.x 版本之后依赖了其本身实现的 Spring-jcl 桥接,而 1.x 版本则带有 jcl-over-slf4j 依赖,因此抱歉,个人文章这里写错了,望各位周知。
第二篇文章「五年Java经验,面试仍是说不出日志该怎么写更好?——日志规范与最佳实践篇」其中 Log4j2配置文件那一段有误,缺了一个名为 STDOUT 的控制台Appender,代码以下:
<Console name="STDOUT">
<PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"/>
</Console>
复制代码
第四篇文章「这么香的日志动态级别与输出,你肯定不进来看看?——生产环境动态输入日志级别、文件」
这篇文章也是有一个配置文件粘贴错位置了,不过源码是正确的,请各位下载github源码,以源码为准。
第一个答疑是读者的一个小要求,问我能不能写一个YAML格式的 Log4j2 配置文件,固然能够了,下面是手写的,请测试一下再进入生产使用:
Configuration:
status: debug
name: YAMLConfig
properties:
property:
name: baseDir
value: logs
appenders:
RollingFile:
- name: RollingFile
fileName: ${baseDir}/log.log
filePattern: "${baseDir}/$${date:yyyy-MM}/log-%d{yyyy-MM-dd-HH}-%i.log.gz"
PatternLayout:
pattern: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %pid --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}"
Policies:
- TimeBasedTriggeringPolicy: true
SizeBasedTriggeringPolicy:
size: 250 MB
DefaultRollOverStrategy:
max: 100
Delete:
basePath: ${baseDir}
maxDepth: 2
IfFileName:
glob: "*/app-*.log.gz"
IfLastModified:
age: 30d
IfAny:
IfAccumulatedFileSize:
exceeds: 100 GB
IfAccumulatedFileCount:
exceeds: 10
Console:
name: STDOUT
PatternLayout:
Pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"
Filters:
ThresholdFilter:
level: debug
Loggers:
logger:
- name: org.apache.logging.log4j.test2
level: debug
additivity: false
AppenderRef:
ref: RollingFile
Root:
level: trace
AppenderRef:
ref: STDOUT
复制代码
另一个小伙伴在个人文章下提出了几个问题:
每一个日志文件大小,如何分割?日期分割?一天一个? 是一天一个目录仍是一天一个文件? 仍是一周一个目录? 区不区分error和info日志在不一样文件?打不打印其余级别日志? 能不能动态修改日志级别不停机?是否须要异步日志,大家的访问量到了默认同步日志扛不住的地步了么?怎么异步日志?
虽然这个小伙伴态度不是很好,可是问题仍是很好的:
每一个日志文件大小,如何分割?日期分割?一天一个? 是一天一个目录仍是一天一个文件? 仍是一周一个目录?
每一个文件大小我喜欢250M这个数字,也这么配的,日期分割这个就不该该我来说了,我说一个月一分割,通常应用都好几百个G了,我说一分钟一分割,好多应用还不到1M,因此按照本身线上的需求慢慢调整才行。
区不区分error和info日志在不一样文件?
员外坚定反对按照日志级别分文件,设想一下回溯现场的时候,info、warn、error 级别都是有用日志,若是分开了,是否是逐个去看?若是让我逐个去定位错误位置,我想我会骂娘的,至于如何正确的分文件输出日志,后面我会有补充,见下文。
打不打印其余级别日志?
打不打印其余级别日志根本就是个伪问题,不须要打印其余级别也就不须要那么多日志级别了,这个问题是否是能够理解为日志应该开到什么级别,我通常开 info 级别,我也见过线上只开 error 的,而后业务里的日志输出都是error的(反面教材)。
能不能动态修改日志级别不停机?
能的,参考我上一篇文章,并且这方面应该没有人作的比我文章里写的更好了。
是否须要异步日志,怎么异步日志?
我我的不倾向于异步日志,磁盘IO满了,开了异步也是缓冲区满,缓冲区满了要么阻塞,要么抛弃,至于开了异步所带来的性能优点并不大。怎么异步日志我文章里也有写,请参阅公众号。
大家的访问量到了默认同步日志扛不住的地步了么?
日志扛不住了要先考虑是否是过多,若是实在无法减小日志,就考虑将日志输出路径单独挂载磁盘、更换更好的磁盘等等。
读过员外的文章就知道,员外是同意分文件输出日志的,不过员外反对按照级别来输出文件。如何正确的按文件输出日志呢?之前文章没有写过,这里来补充一下。
很简单,配置多个appender,而后能够按照 loggger 来分文件,代码以下:
<Logger name="com.jiyuanwai.log.xxx" level="info" additivity="false">
<appender-ref ref="XXXFile"/>
</Logger>
复制代码
<Logger name="com.jiyuanwai.log.yyy" level="info" additivity="false">
<appender-ref ref="YYYFile"/>
</Logger>
复制代码
这个却是很简单,可是还有一个问题,单个类若是有多种日志想要输出到多个位置,该怎么办,解决方案有两种,一个类持有多个 logger 实例:
class A {
static final Logger log = LoggerFactory.getLogger("com.jiyuanwai.log.xxx");
static final Logger log = LoggerFactory.getLogger("com.jiyuanwai.log.yyy");
...
}
复制代码
这种办法实现简单,可是不优雅,咱们来尝试拿出另一套方案,就是 Maker 配合 Filter 来实现,固然根据之前的文章了解到咱们还可使用 Sift 配合 MDC 来实现,但员外不推荐,至于为何,做为公众号粉丝福利能够关注公众号回复 “Sift” 来获取答案,咱们来继续看 demo:
// Marker 也能够考虑 static final
Marker file1 = MarkerFactory.getMarker("file1");
Marker file2 = MarkerFactory.getMarker("file2");
log.info(file1, "A file 1 log.");
log.info(file2, "A file 2 log.");
复制代码
配置文件以下:
<appender name="FILE1" class="ch.qos.logback.core.FileAppender">
<file>${LOG_PATH}/testFile1.log</file>
<append>true</append>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
<marker>file1</marker>
</evaluator>
<!-- 不匹配 NEUTRAL不处理,ACCEPT接收,DENY抛弃 -->
<OnMismatch>DENY</OnMismatch>
<!-- 匹配处理方式 NEUTRAL不处理,ACCEPT接收,DENY抛弃 -->
<OnMatch>ACCEPT</OnMatch>
</filter>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE2" class="ch.qos.logback.core.FileAppender">
<file>${LOG_PATH}/testFile2.log</file>
<append>true</append>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
<!-- 此处能够配置多个 marker-->
<marker>file2</marker>
</evaluator>
<!-- 不匹配 NEUTRAL不处理,ACCEPT接收,DENY抛弃 -->
<OnMismatch>DENY</OnMismatch>
<!-- 匹配处理方式 NEUTRAL不处理,ACCEPT接收,DENY抛弃 -->
<OnMatch>ACCEPT</OnMatch>
</filter>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<logger name="com.jiyuanwai.logging.LoggingApplication" additivity="false">
<appender-ref ref="FILE1"/>
<appender-ref ref="FILE2"/>
</logger>
复制代码
以上只是抛砖引玉,日志分文件输出还能够写更多逻辑,小伙伴须要本身动手发掘。
至此日志系列就算是告一段落了,若是还有疑问小伙伴能够留言讨论,接下来一系列咱们进入Spring Boot + Vue 的全栈之路,敬请关注。
以上是我的观点,若是有问题或错误,欢迎留言讨论指正,码字不易,若是以为写的不错,求关注、求点赞、求转发。
扫码关注公众号,第一时间得到更新