公司有一个基于 spring-cloud-netflix-zuul
框架开发的网关项目,最近发现了一个问题:异常日志只输出到了 Console
控制台,没有输出到日志文件中。
日志输出的位置是在 SendErrorFilter
类中,如下:
1 | log.warn("Error during filtering", exception.getThrowable()); |
这个类使用了 JCL
(apache commons logging
) 日志框架进行日志输出操作,而我们项目中统一配置的日志框架是 slf4j + logback
,猜测是这个原因导致的?对这几种日志框架的了解不并多,带着上面的问题,深入了解一下。
常用的日志框架
首先了解一下 slf4j
和 logback
的区别:
slf4j
。SLF4J
是一个 日志门面(Facade
),它提供了一套统一的日志接口,不直接处理日志的记录。通过使用SLF4J
,可以实现与具体的日志框架解耦(如Logback
、Log4j
、JUL
),使得代码只依赖于SLF4J
的API
。这意味着可以在不改变应用程序代码的情况下,切换日志框架。Logback
。Logback
是一个 具体的日志实现框架,是SLF4J
的原生实现之一
那么为什么项目中都是 SLF4J
和 Logback
配合使用,而不是单独使用其中一个?首先,slf4j
只是一个 日志门面,不包含日志的具体实现,因此无法单独使用。Logback
是 日志实现框架,可以单独使用。但是实际开发中,还是推荐两者配合使用,因为它们配合使用包含如下几个优点:
- 抽象与实现分离:
SLF4J
提供抽象接口,Logback
提供具体实现。通过这种分离,可以根据需求轻松切换不同的日志实现,而不需要修改代码中的日志调用部分。 - 灵活性:在使用
SLF4J
时,可以在开发和生产环境中使用不同的日志实现。例如,开发时使用Logback
,生产环境可以切换到一个更合适的日志框架。 - 兼容性:许多第三方库使用
SLF4J
作为日志接口,通过配合使用Logback
,可以确保所有日志都能统一管理和输出,而不必担心日志输出的多样性或冗余性。
除了这两种日志框架外,还有其他几种常用的日志框架,下面就按照 日志门面框架 和 日志具体实现框架 将他们区分开来:
- 常用志门面框架
SLF4J
:JCL
(Apache Commons Logging
):Apache
软件基金会开发的框架 - 常用日志具体实现框架
Logback
:Log4j
的继任者Log4j
:最早的Java
日志框架之一Log4j2
:Log4j
的新版,解决了Log4j
的一些性能和设计问题JUL
(java.util.logging
):java
标准库中的日志框架,Oracle
开发
JCL
打印的日志为何没有写入到日志中
看完上面日志框架的概念后,我们回到最初的问题:为何 SendErrorFilter
类输出的日志没有写入到日志文件中?
在 SendErrorFilter
类中,使用了 JCL
进行日志输出:
1 | log.warn("Error during filtering", exception.getThrowable()); |
这行日志在 Console
控制台打印出来了,但是并没有写入到日志文件中。
在网关项目中,我添加了一个自定义异常过滤类 ErrorFilter
,其中使用了 slf4j
进行日志输出,这里的日志能够正常输出到日志文件中:
1 | log.error("服务 [{}] 不可用", RequestContext.getCurrentContext().get("serviceId").toString()); |
Logback
的日志输出是在 RollingFileAppender$subAppend()
中进行的,在这里打了一个断点,发现 SendErrorFilter
中的日志打印并没有触发断点。
搜寻了相关问题的处理办法,网上给出的解释大多是因为 使用了两套日志门面,项目默认使用的日志框架是 slf4j + logback
,而引入的 spring-cloud-netflix-zuul
使用的 JCL
框架。在项目中增加 jcl-over-slf4j
依赖即可解决问题:
1 | <dependency> |
这里先说下 jcl-over-slf4j
依赖的作用。SendErrorFilter
中的 log
是 org.apache.commons.logging.Log
实例(JCL
),引入 jcl-over-slf4j
后,log
实例在初始化时,最终返回的会是一个 slf4j
兼容的 Logger
实例,之后日志就会按照配置的 slf4j+logback
进行输出。
但是加上这个依赖后,问题仍然没有解决,因此只能尝试一步步对代码进行定位。
移除 jcl-over-slf4j
依赖,对 SendErrorFilter
打印日志的逻辑进行了 Debug
,如下:
定位到 spring-jcl
包下的 LogAdapter
类,找到 Slf4jLocationAwareLog
子类,如下:
这里的 Logger
类是 logback-classic
包下的,也就是说 SendErrorFilter
类中通过 [JCL
日志门面] 输出的日志,最终是在 [Logback
日志实现框架] 中完成的输出。上面 jcl-over-slf4j
要做的工作正是这个,而 LogAdapter
类已经帮忙完成了这个工作。我们重点看下 LogAdapter
类:
1 | final class LogAdapter { |
LogAdapter
可以自动检测当前的类路径中引入的日志框架,并选择适当的日志实现进行加载和使用。LogAdapter
中定义了这么几种情况:
- 有
log4j
的依赖,并且桥接到了slf4j
,使用slf4j
;没有桥接到slf4j
,使用log4j
- 没有
log4j
的依赖,检测是否包含slf4j
拓展接口(LocationAwareLogger
),之后检测是否包含slf4j
标准接口。 - 不满足以上情况,使用
JUL
。
我们回过头来再来看下 SendErrorFilter
中创建日实例部分的代码:
1 | private static final Log log = LogFactory.getLog(SendErrorFilter.class); |
看下 LogFactory$getLog()
方法:
1 | public static Log getLog(Class<?> clazz) { |
可以看到,日志框架实例最终是在 LogAdapter
中完成创建的,并且项目中包含 slf4j
依赖,因此最终加载的日志系统是 slf4j
,并且对应的 log
实例是通过 slf4j
的拓展接口 LocationAwareLogger
完成初始化的。
了解了日志加载的具体的原理后,可以定位到上面的日志没有打印到文件中与项目 使用了两套日志门面无关。即使项目中包含多套日志门面,通过 spring-jcl
包下的 LogAdapter
类,也可以完成日志系统的适配工作。
因此最终问题产生的原因,应该还是是与 logback
配置有关,对日志配置进行 Review
,发现了如下配置:
1 | <!--代码块1--> |
解释一下上面的几行配置:
- 代码块1:定义一个
Logger
,它可以捕获com.xxx
包及其子包下的所有类的info
级别及更高级别的日志消息,并发送到一个名为syslog
的Appender
中处理。 - 代码块2:定义一个
Logger
,它可以捕获com.xxx.mapper
包及其子包下所有类的error
级别及更高级别的日志消息。 - 代码块3:定义了根
Logger
,根Logger
是一个特殊的Logger
,它会捕获所有未被其他Logger
定义的info
级别及以上的日志消息,并发送到名为STDOUT
的Appender
中处理。(*注:每个日志记录请求首先会传递到根Logger
*)
名为 syslog
的 Appender
配置如下,作用是 将日志消息输出到指定的日志文件中:
1 | <springProperty name="LOG_HOME" scope="context" source="logbackPath" defaultValue="c:/log" /> |
名为 STDOUT
的 Appender
配置如下,作用是 将日志消息输出控制台:
1 | <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
而 SendErrorFilter
类位于 spring-cloud-netflix-zuul
依赖下 org.springframework.cloud.netflix.zuul
包下,而上面的配置限定了只会将 com.xxx
包下的日志输出到日志文件中,因此 SendErrorFilter
类中输出的日志并不会输出到到日志文件中,只会通过根 Logger
的配置输出到控制台。
而如果想让 SendErrorFilter
输出的错误信息输出到日志中,则添加下面的配置即可:
1 | <logger name="org.springframework.cloud" level="info"> |
这个配置会将 org.springframework.cloud
包下的所有日志都输出到日志中,也可以再缩小一下范围,限制只有 SendErrorFilter
类的日志会输出到日志文件中:
1 | <logger name="org.springframework.cloud.netflix.zuul.filters.post" level="info"> |
总结
本篇文章花了很大的篇幅来定位上面的问题,主要还是对 logback
配置不太熟悉导致的,导致定位问题的方向错了。不过正是因为这个,了解到了日志框架包含 日志门面 和 日志实现框架,并且了解了 Spring
中日志适配的原理。