Spring 切换日志系统

为何需要切换

由于历史原因,Spring最开始在core包中引入的是commons-logging(JCL标准实现)的日志系统,官方考虑到兼容问题,在后续的Spring版本中并未予以替换,而是继续沿用。如果考虑到性能、效率,应该自行进行替换,在项目中明确指定使用的日志框架,从而在编译时就指定日志框架。
commons-logging日志系统是基于运行发现算法(常见的方式就是每次使用org.apache.commons.logging.LogFactory.getLogger(xxx),就会启动一次发现流程),获取最适合的日志系统进行日志记录,其效率要低于使用SLF4J,在编译时明确指定日志系统的方式,目前常用的日志框架有logback、log4j、log4j2等。
动态绑定请参考:http://blog.csdn.net/yycdaizi/article/details/8276265

官方说明:
The mandatory logging dependency in Spring is the Jakarta Commons Logging API (JCL). We compile against JCL and we also make JCL Log objects visible for classes that extend the Spring Framework. It’s important to users that all versions of Spring use the same logging library: migration is easy because backwards compatibility is preserved even with applications that extend Spring. The way we do this is to make one of the modules in Spring depend explicitly on commons-logging (the canonical implementation of JCL), and then make all the other modules depend on that at compile time. If you are using Maven for example, and wondering where you picked up the dependency on commons-logging, then it is from Spring and specifically from the central module called spring-core.

Spring中强制使用的是Jakarta Commons Logging API (JCL)日志系统。我们基于JCL进行编译,构建JCL日志对象,这些同时也对扩展自Spring类可见的。对于使用者而言,确保不同版本的Spring使用相同的日志系统是非常重要的–代码迁移需要确保逆向兼容性。我们之所以这样做,是为了在Spring的一个包中明确的依赖于commons-logging(JCL权威实现),而其他包就基于这个包进行构建编译。如果你使用maven,你可以发现commons-logging以来自Spring-core包。

The nice thing about commons-logging is that you don’t need anything else to make your application work. It has a runtime discovery algorithm that looks for other logging frameworks in well known places on the classpath and uses one that it thinks is appropriate (or you can tell it which one if you need to). If nothing else is available you get pretty nice looking logs just from the JDK (java.util.logging or JUL for short). You should find that your Spring application works and logs happily to the console out of the box in most situations, and that’s important.

使用commons-logging的好处是,你不需要做其他额外事情就可以让程序正常工作。它有运行时的发现算法,能够在运行时从classpath自动发现其他日志框架,并自行挑选其中一个合适的,或者你自行指定一个。如果在运行时没有发现任何其他日志框架,则commons-loggin会直接使用JDK的日志系统(java.util.logging或JUL)。

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的运行时发现算法是合适的,但对于其他使用场景,却是问题重重。如果时间可以重来,让我们重新选择一个不同的日志系统,我们可能会选择SLF4J。

如何进行操作

以Log4J为例
1. 使用SLF4J-JCL桥接包替换commons-logging包。确保Spring框架使用的logging call能够转换为SLF4J的API
2. 引入SLF4J API包
3. 引入SLF4J-Log4J桥接包,以使得SLF4J使用Log4J进行日志记录
4. 引入Log4J API包

以Logback为例
1. 使用SLF4J-JCL桥接包替换commons-logging包。确保Spring框架使用的logging call能够转换为SLF4J的API
2. 引入SLF4J API包
3. 引入Logback包

官方说明
There are basically two ways to switch off commons-logging:

  • Exclude the dependency from the spring-core module (as it is the only module that explicitly depends on commons-logging)
  • Depend on a special commons-logging dependency that replaces the library with an empty jar

两步骤切换日志系统

  • 从spring-core包中排除掉commons-logging(Spring-core是唯一依赖该包的)
  • 依赖一个特殊的空的commons-logging包

SLF4J is a cleaner dependency and more efficient at runtime than commons-logging because it uses compile-time bindings instead of runtime discovery of the other logging frameworks it integrates. This also means that you have to be more explicit about what you want to happen at runtime, and declare it or configure it accordingly. SLF4J provides bindings to many common logging frameworks, so you can usually choose one that you already use, and bind to that for configuration and management.

SLF4J是编译时绑定其他日志框架,因而比起commons-logging的运行时发现方式效率要高。这也意味着,你不得不明确指定、明确声明和配置好运行时的日志系统。SLF4J支持当前多种通用日志框架,你可以绑定一种你当前使用的日志框架。

SLF4J provides bindings to many common logging frameworks, including JCL, and it also does the reverse: bridges between other logging frameworks and itself. So to use SLF4J with Spring you need to replace the commons-logging dependency with the SLF4J-JCL bridge. Once you have done that then logging calls from within Spring will be translated into logging calls to the SLF4J API, so if other libraries in your application use that API, then you have a single place to configure and manage logging.

SLF4J支持绑定包括JCL在内的多种通用日志框架:桥接不同日志框架和自身。如果想在Spring中使用SLF4J,你需要使用SLF4J-JCL替换commons-logging。一旦你替换完成,Spring的日志记录将转换为SLF4J API, 所以在你的应用中如果其他包也是用这些API, 那么你只需要在一个地方进行配置日志参数即可。

A common choice might be to bridge Spring to SLF4J, and then provide explicit binding from SLF4J to Log4J. You need to supply 4 dependencies (and exclude the existing commons-logging): the bridge, the SLF4J API, the binding to Log4J, and the Log4J implementation itself.

一个通常的选择是把Spring日志桥接到SLF4J,然后再明确将SLF4J绑定到Log4J,你需要提供4个依赖包(不包含commons-logging包):JCL桥接器(SLF4J-JCL)、SLF4J API、Log4J桥接器(SLF4J-Log4J)、Log4J包。

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方式,因为commons-logging有classloader问题,特别是当你在资源有限的平台时,例如OSGI。另一方面是性能问题,桥接是在编译阶段,而不是运行阶段。

A more common choice amongst SLF4J users, which uses fewer steps and generates fewer dependencies, is to bind directly to Logback. This removes the extra binding step because Logback implements SLF4J directly, so you only need to depend on two libraries not four ( jcl-over-slf4j and logback). If you do that you might also need to exclude the slf4j-api dependency from other external dependencies (not Spring), because you only want one version of that API on the classpath.

在SLF4J圈子,另一个常用的选择是绑定Logback,它使用了更少的步骤,并产生更少的依赖。使用Logback可以去除绑定步骤,因为它是直接实现自SLF4J,所以你只需要依赖2个jar包(jcl-over-slf4j和logback)。如果你这样做,你还是需要将其他包中(非Spring)的slf4j-api依赖去除,因为你只需要一个版本的SLF4J API
注:实际应该需要引入3个依赖(SLF4J-JCL、SLF4J API、Logback)

Gradle构建器配置

//将commons-logging包从当前依赖中剔除
configurations {
    compile.exclude group:'commons-logging'
    testCompile.exclude group:'commons-logging'
}

//引入日志桥接包和slf4j
def logger = [
            "org.slf4j:jcl-over-slf4j:$slf4j_version",
            "org.slf4j:slf4j-api:$slf4j_version",
            "org.slf4j:slf4j-log4j12:$slf4j_version",
            "log4j:log4j:$log4j_version",
    ]
compile logger
testCompile logger

Maven构建器配置

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>4.3.5.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.5.8</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.5.8</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.5.8</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.14</version>
    </dependency>
</dependencies>

注意:如果在依赖中剔除了common-loggin包,但又不引入其他日志系统,则Spring会在启动的时候报如下错误:
java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory

结果示例

bavatinolabdeMacBook-Pro:webflow bavatinolab$ ./gradlew -q dependencies --configuration compile

------------------------------------------------------------
Root project
------------------------------------------------------------

compile - Dependencies for source set 'main'.
+--- org.springframework:spring-context:4.3.4.RELEASE
|    +--- org.springframework:spring-aop:4.3.4.RELEASE
|    |    +--- org.springframework:spring-beans:4.3.4.RELEASE
|    |    |    \--- org.springframework:spring-core:4.3.4.RELEASE
|    |    \--- org.springframework:spring-core:4.3.4.RELEASE
|    +--- org.springframework:spring-beans:4.3.4.RELEASE (*)
|    +--- org.springframework:spring-core:4.3.4.RELEASE
|    \--- org.springframework:spring-expression:4.3.4.RELEASE
|         \--- org.springframework:spring-core:4.3.4.RELEASE
+--- org.springframework:spring-test:4.3.4.RELEASE
|    \--- org.springframework:spring-core:4.3.4.RELEASE
+--- org.springframework:spring-tx:4.3.4.RELEASE
|    +--- org.springframework:spring-beans:4.3.4.RELEASE (*)
|    \--- org.springframework:spring-core:4.3.4.RELEASE
+--- org.springframework:spring-web:4.3.4.RELEASE
|    +--- org.springframework:spring-aop:4.3.4.RELEASE (*)
|    +--- org.springframework:spring-beans:4.3.4.RELEASE (*)
|    +--- org.springframework:spring-context:4.3.4.RELEASE (*)
|    \--- org.springframework:spring-core:4.3.4.RELEASE
+--- org.springframework:spring-webmvc:4.3.4.RELEASE
|    +--- org.springframework:spring-aop:4.3.4.RELEASE (*)
|    +--- org.springframework:spring-beans:4.3.4.RELEASE (*)
|    +--- org.springframework:spring-context:4.3.4.RELEASE (*)
|    +--- org.springframework:spring-core:4.3.4.RELEASE
|    +--- org.springframework:spring-expression:4.3.4.RELEASE (*)
|    \--- org.springframework:spring-web:4.3.4.RELEASE (*)
+--- org.slf4j:jcl-over-slf4j:1.5.8
|    \--- org.slf4j:slf4j-api:1.5.8
+--- org.slf4j:slf4j-api:1.5.8
+--- org.slf4j:slf4j-log4j12:1.5.8
|    +--- org.slf4j:slf4j-api:1.5.8
|    \--- log4j:log4j:1.2.14
+--- log4j:log4j:1.2.14
+--- junit:junit:4.12
|    \--- org.hamcrest:hamcrest-core:1.3
+--- com.google.protobuf:protobuf-java:3.1.0
+--- com.google.protobuf:protobuf-java-util:3.1.0
|    +--- com.google.protobuf:protobuf-java:3.1.0
|    +--- com.google.guava:guava:18.0
|    \--- com.google.code.gson:gson:2.7
+--- org.mockito:mockito-all:2.0.2-beta
\--- org.testng:testng:6.9.6
     +--- com.google.inject:guice:4.0
     |    +--- javax.inject:javax.inject:1
     |    +--- aopalliance:aopalliance:1.0
     |    \--- com.google.guava:guava:16.0.1 -> 18.0
     +--- org.beanshell:bsh:2.0b4
     +--- org.apache.ant:ant:1.7.0
     |    \--- org.apache.ant:ant-launcher:1.7.0
     +--- com.beust:jcommander:1.48
     +--- org.yaml:snakeyaml:1.15
     \--- junit:junit:4.10 -> 4.12 (*)

(*) - dependencies omitted (listed previously)

这里写图片描述
可以看到,LogFactory已经被替换为SLF4J的实现。
这里写图片描述
Slf4j使用的是静态绑定

容器日志

某些web容器自身携带有JCL的日志系统实现,某些容器比如WAS,会使用父类优先的类加载机制,造成JCL使用的是容器的版本,而不是应用的中指定的commons-logging版本,因而会出现绑定失败等意料之外问题。
解决办法是修改类加载层级,例如was中调整为“parent last”,确保容器的JCL优先加载。

官方说明
Many people run their Spring applications in a container that itself provides an implementation of JCL. IBM Websphere Application Server (WAS) is the archetype. This often causes problems, and unfortunately there is no silver bullet solution; simply excluding commons-logging from your application is not enough in most situations.

很多人将他们的Spring应用运行在某些自身就已经提供了JCL实现的容器里。IBM Websphere应用服务器(WAS)就是典型。这通常会引起问题,而且非常不幸,这个没有银弹方案。简单的从应用中剔除掉commons-logging包不是最优方案。

To be clear about this: the problems reported are usually not with JCL perse, or even with commons-logging: rather they are to do with binding commons-logging to another framework (often Log4J). This can fail because commons-logging changed the way they do the runtime discovery in between the older versions (1.0) found in some containers and the modern versions that most people use now (1.1). Spring does not use any unusual parts of the JCL API, so nothing breaks there, but as soon as Spring or your application tries to do any logging you can find that the bindings to Log4J are not working.

需要明确的是:这个问题不是由于JCL或commons-logging引起,即便他们想把commons-logging绑定到其他框架(常常是Log4J)。引起失败的原因是,容器中的旧版本commons-logging(1.0)和当前应用中的新版本commons-logging(1.1)冲突。Spring并没有使用任何不确定的JCL API,所以问题不会发生在Spring。但是当代码进行日志记录的时候,就会发现绑定到Log4J失效。

In such cases with WAS the easiest thing to do is to invert the class loader hierarchy (IBM calls it “parent last”) so that the application controls the JCL dependency, not the container. That option isn’t always open, but there are plenty of other suggestions in the public domain for alternative approaches, and your mileage may vary depending on the exact version and feature set of the container.

如果在WAS中出现这种问题,一种简便做法是,颠倒WAS的类加载层次(IBM称之为“父亲后置”),使得应用控制JCL的依赖,而不是容器控制。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享