美文网首页springboot
Java日志实现知多少?

Java日志实现知多少?

作者: BeautifulHao | 来源:发表于2019-05-12 12:29 被阅读62次
    骨灰级程序员

    1.Java日志历史

    Java 拥有功能和性能都非常强大的日志库,但不幸的是,这样的日志库有不止一个——相信每个Java程序员都曾经迷失在JUL(Java Util Log), JCL(Commons Logging), Log4j, SLF4J, Logback,Log4j2 等等的迷宫中。让我们回顾下讲讲这段“腥风血雨”的历史。

    • Java Util Log

      来自官方JDK,带着标准和权威的光环,小名:JUL。从JDK1.4 才开始加入(2002年),当时各种第三方日志组件及其盛行,且JUL性能和使用的确又没有其他组件方便。虽然JDK1.5对其进行了改进,但还是不影响很多项目不选用该组件的命运。

    • Log4j 1.x
      在JUL推出的前一年,Gülcü 发布了Log4j 1.x,虽然进不了JDK,但是Log4j 1.x进入了Apache 基金会顶级项目。Log4j 在设计上非常优秀,对后续的 Java Log 框架有长久而深远的影响,也产生了Log4c, Log4s, Log4perl 等到其他语言的移植。但是,后浪推前浪,前浪的性能终究还是不断被后期的日志框架赶超,比如后期的LogbackLog4j2

    • JCL
      那么问题来了,JDK官方自带JUL,第三方有Log4J,不同项目或者开源Jar,采用了不同的日志实现库,那么是不是意味着要整合使用,得写多个配置文件呢。正式这个问题,带来了JCL的出现。JCL,大名:Commons Logging,同样也是Apache下的项目,但是JCL 是一个Log Facade(门面Api),只提供 Log API,不提供实现。在程序中日志创建和记录都是用JCL中的接口,在真正运行时,然后有适配器Adapter 来使用 Log4j 或者 JUL 作为Log 实现。(当前ClassPath中有什么实现,如果有Log4j 就是用 Log4j, 如果啥都没有就是用 JDK 的 JUL)。是不是感觉从面向对象的高度,JCL有种很先进的感觉。这就是面向接口编程的体现。

      这样,在你的项目中,如果用Log4j, 就添加 Log4j 的jar包进去,然后写一个 Log4j 的配置文件;如果喜欢用JUL,就只需要写个 JUL 的配置文件。如果有其他的新的日志库出现,也只需要它提供一个Adapter,运行的时候把这个日志库的 jar 包加进去。


      JCL

    合久必分,分久必合。历史就是这样,日志组件在接下去的历史演进中,又出现了跌宕起伏。一种平衡替换另一种平衡。

    • SLF4J/Logback
      Gülcü (对头,又是ta)认为 JCL 的 API 设计得不好,容易让使用者写出性能有问题的代码,Gülcü ,不安于现状,不基于JCL添加实现类,而是创立了SLF4J 和 Logback项目,目的就是为了提高日志组件的性能。SLF4J的全称:Simple Logging Facade for Java,看其意思就是门面API,而Logback作为其实现类。当然 Logback 则是作为 Log4j 的继承者来开发的,提供了性能更好的实现,异步 logger,Filter等更多的特性。现在事情变复杂了。我们有了两个流行的 Log Facade,以及三个流行的 Log Implementation。


      jcl+slf4j

    当你感觉现在差不多了吧的时候,三国时期的故事,其实又开始上演了。

    • Log4j2
      维护 Log4j 的人似乎坐立不安,他们不想坐视用户一点点被 SLF4J /Logback 蚕食,继而搞出了 Log4j2。
      Log4j2 和 Log4j1.x 并不兼容,设计上很大程度上模仿了 SLF4J/Logback,性能上也获得了很大的提升。
      Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。


      jcl+slf4j+log4f2

    =========================分割线========================

    Gülcü 是个追求完美的人,各种纷纷扰扰的历史看在了ta眼里,他决定让这些Log之间都能够方便的互相替换,所以做了各种 Adapter 和 Bridge 来连接:


    adpater adpater-jar

    到这里,日志演进总算有所停歇。

    2.Spring Boot 日志使用

    2.1. 依赖分析

    历史回顾不是我们的目的,结合现在流行的开源框架Spring Boot,我们再来谈谈具体项目该如何结合实际,使用日志。
    构建Spring Boot Web项目,版本:2.1.4。分析下Pom.xml的依赖:


    logback maven

    可以发现,Spring Boot采用了SLF4J+Logback的组合来完成日志的记录。并且作者把Log4j和JUL的日志组件适配到了slf4j。的确,Spring Boot为Java coder做了太多的工作。

    2.2. 日志初始化过程

    以上面构建的Spring Boot项目为例,添加简单日志记录代码:

    @SpringBootApplication
    public class SpringBootLoggerDemoApplication {
    
        private static Logger logger = LoggerFactory.getLogger(SpringBootLoggerDemoApplication.class);
    
        public static void main(String[] args) {
            SpringApplication.run(SpringBootLoggerDemoApplication.class, args);
            logger.debug("hello logger");
        }
    
    }
    

    LoggerFactory.java

        /**
         * Return a logger named according to the name parameter using the
         * statically bound {@link ILoggerFactory} instance.
         * 
         * @param name
         *            The name of the logger.
         * @return logger
         */
        public static Logger getLogger(String name) {
            ILoggerFactory iLoggerFactory = getILoggerFactory();
            return iLoggerFactory.getLogger(name);
        }
    

    通过getILoggerFactory获取日志工厂:

        /**
         * Return the {@link ILoggerFactory} instance in use.
         * <p/>
         * <p/>
         * ILoggerFactory instance is bound with this class at compile time.
         * 
         * @return the ILoggerFactory instance in use
         */
        public static ILoggerFactory getILoggerFactory() {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                synchronized (LoggerFactory.class) {
                    if (INITIALIZATION_STATE == UNINITIALIZED) {
                        INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                        performInitialization();
                    }
                }
            }
            ...省略...
        }
    

    这里代码定位到performInitialization:

        private final static void performInitialization() {
            bind();
            ...省略..
        }
    

    bind()具体日志实现:

     private final static void bind() {
            try {
                Set<URL> staticLoggerBinderPathSet = null;
                if (!isAndroid()) {
                    staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                    reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
                }
                ...省略...
            } 
        }
    

    关键代码就在findPossibleStaticLoggerBinderPathSet:

     private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
    
        static Set<URL> findPossibleStaticLoggerBinderPathSet() {
            // use Set instead of list in order to deal with bug #138
            // LinkedHashSet appropriate here because it preserves insertion order
            // during iteration
            Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
            try {
                ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
                Enumeration<URL> paths;
                if (loggerFactoryClassLoader == null) {
                    paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
                } else {
                    paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
                }
                while (paths.hasMoreElements()) {
                    URL path = paths.nextElement();
                    staticLoggerBinderPathSet.add(path);
                }
            } catch (IOException ioe) {
                Util.report("Error getting resources from path", ioe);
            }
            return staticLoggerBinderPathSet;
        }
    

    代码最终到最后,其实是通过ClassLoader在ClassPath里面加载指定实现类org/slf4j/impl/StaticLoggerBinder.class来实现日志组件的加载,核心就在:

     //org/slf4j/impl/StaticLoggerBinder.class           
    
    if (loggerFactoryClassLoader == null) {    
        paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
     } else {
        paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
    }
    

    类路径下看看backLog代码:

    lackLog code

    这里有个题外话,之前看过很多介绍Java SPI文章,老是把日志的加载机制归类为SPI,其实通过上面的介绍,现在可以结论,其实不是。
    https://www.jianshu.com/p/46b42f7f593c

    高级开发必须理解的Java中SPI机制

    3.日志切换方法及原理

    行文到此,主题介绍似乎差不多了,但是好像还有个问题,要是我要在Spring Boot换其他日志组件怎么办呐。其实Spring Boot已经为我们考虑过这个问题了,为我们提供了一个自动配置的starter:spring-boot-starter-log4j2

    Starter for using Log4j2 for logging. An alternative to spring-boot-starter-logging

    修改方式也简单:

        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-logging</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-log4j2</artifactId>
            </dependency>
    
    log4j2 maven

    依赖切换成了最后的log4j-api+log4j-core。

    log4j

    参考:

    https://zhuanlan.zhihu.com/p/24272450

    https://www.slf4j.org

    相关文章

      网友评论

        本文标题:Java日志实现知多少?

        本文链接:https://www.haomeiwen.com/subject/cchcaqtx.html