logback日志打印出方法_我再问你一遍,你确定你会用logback?(二)

源码分析

logback 非常庞大、复杂,如果要将 logback 所有模块分析完,估计要花相当长的时间,所以,本文还是和以前一样,仅针对核心代码进行分析,当分析的方法存在多个实现时,也只会挑选其中一个进行讲解。文中没有涉及到的部分,感兴趣的可以自行研究。

接下来通过解决以下几个问题来逐步分析 logback 的源码:

  1. slf4j 是如何实现门面模式的?
  2. logback 如何加载配置?
  3. 获取我们所需的 logger?
  4. 如何将日志打印到控制台?

slf4j是如何实现门面模式的

slf4j 使用的是门面模式,不管使用什么日志实现,项目代码都只会用到 slf4j-api 中的接口,而不会使用到具体的日志实现的代码。slf4j 到底是如何实现门面模式的?接下来进行源码分析:

在我们的应用中,一般会通过以下方式获取 Logger 对象,我们就从这个方法开始分析吧:

Logger logger = LoggerFactory.getLogger(LogbackTest.class);

进入到 LoggerFactory.getLogger(Class> clazz)方法,如下。在调用这个方法时,我们一般会以当前类的 Class 对象作为入参。当然,logback 也允许你使用其他类的 Class 对象作为入参,但是,这样做可能不利于对 logger 的管理。通过设置系统属性-Dslf4j.detectLoggerNameMismatch=true,当实际开发中出现该类问题,会在控制台打印提醒信息。

    public static Logger getLogger(Class> clazz) {        // 获取Logger对象,后面继续展开        Logger logger = getLogger(clazz.getName());        // 如果系统属性-Dslf4j.detectLoggerNameMismatch=true,则会检查传入的logger name是不是CallingClass的全限定类名,如果不匹配,会在控制台打印提醒        if (DETECT_LOGGER_NAME_MISMATCH) {            Class> autoComputedCallingClass = Util.getCallingClass();            if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {                Util.report(String.format("Detected logger name mismatch. Given name: "%s"; computed name: "%s".", logger.getName(),                                autoComputedCallingClass.getName()));                Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");            }        }        return logger;    }

进入到LoggerFactory.getLogger(String name)方法,如下。在这个方法中,不同的日志实现会返回不同的ILoggerFactory实现类:

    public static Logger getLogger(String name) {        // 获取工厂对象,后面继续展开        ILoggerFactory iLoggerFactory = getILoggerFactory();        // 利用工厂对象获取Logger对象        return iLoggerFactory.getLogger(name);    }

进入到getILoggerFactory()方法,如下。INITIALIZATION_STATE代表了初始化状态,该方法会根据初始化状态的不同而返回不同的结果。

    static final SubstituteLoggerFactory SUBST_FACTORY = new SubstituteLoggerFactory();    static final NOPLoggerFactory NOP_FALLBACK_FACTORY = new NOPLoggerFactory();     public static ILoggerFactory getILoggerFactory() {        // 如果未初始化        if (INITIALIZATION_STATE == UNINITIALIZED) {            synchronized (LoggerFactory.class) {                if (INITIALIZATION_STATE == UNINITIALIZED) {                    // 修改状态为正在初始化                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;                    // 执行初始化                    performInitialization();                }            }        }        switch (INITIALIZATION_STATE) {        // 如果StaticLoggerBinder类存在,则通过StaticLoggerBinder获取ILoggerFactory的实现类        case SUCCESSFUL_INITIALIZATION:            return StaticLoggerBinder.getSingleton().getLoggerFactory();        // 如果StaticLoggerBinder类不存在,则返回NOPLoggerFactory对象        // 通过NOPLoggerFactory获取到的NOPLogger没什么用,它的方法几乎都是空实现        case NOP_FALLBACK_INITIALIZATION:            return NOP_FALLBACK_FACTORY;        // 如果初始化失败,则抛出异常        case FAILED_INITIALIZATION:            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);        // 如果正在初始化,则SubstituteLoggerFactory对象,这个对象不作扩展        case ONGOING_INITIALIZATION:            return SUBST_FACTORY;        }        throw new IllegalStateException("Unreachable code");    }

以上方法需要重点关注 StaticLoggerBinder这个类,它并不在 slf4j-api 中,而是在 logback-classic 中,如下图所示。其实分析到这里应该可以理解:slf4j 通过 StaticLoggerBinder 类与具体日志实现进行关联,从而实现门面模式。

06d30c02b6094b06f40a8f0ad951cde4.png

logback_StaticLoggerBinder_01

接下来再简单看下LoggerFactory.performInitialization(),如下。这里会执行初始化,所谓的初始化就是查找 StaticLoggerBinder 这个类是不是存在,如果存在会将该类绑定到当前应用,同时,根据不同情况修改INITIALIZATION_STATE。代码比较多,我概括下执行的步骤:

  1. 如果 StaticLoggerBinder 存在且唯一,修改初始化状态为 SUCCESSFUL_INITIALIZATION;
  2. 如果 StaticLoggerBinder 存在但为多个,由 JVM 决定绑定哪个 StaticLoggerBinder,修改初始化状态为 SUCCESSFUL_INITIALIZATION,同时,会在控制台打印存在哪几个 StaticLoggerBinder,并提醒用户最终选择了哪一个 ;
  3. 如果 StaticLoggerBinder 不存在,打印提醒,并修改初始化状态为 NOP_FALLBACK_INITIALIZATION;
  4. 如果 StaticLoggerBinder 存在但 getSingleton() 方法不存在,打印提醒,并修改初始化状态为 FAILED_INITIALIZATION;
    private final static void performInitialization() {        // 查找StaticLoggerBinder这个类是不是存在,如果存在会将该类绑定到当前应用        bind();        // 如果检测存在        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {            // 判断StaticLoggerBinder与当前使用的slf4j是否适配            versionSanityCheck();        }    }    private final static void bind() {        try {            // 使用类加载器在classpath下查找StaticLoggerBinder类。如果存在多个StaticLoggerBinder类,这时会在控制台提醒并列出所有路径(例如同时引入了logback和slf4j-log4j12 的包,就会出现两个StaticLoggerBinder类)            Set staticLoggerBinderPathSet = null;            if (!isAndroid()) {                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);            }                        // 这一步只是简单调用方法,但是非常重要。            // 可以检测StaticLoggerBinder类和它的getSingleton方法是否存在,如果不存在,分别会抛出 NoClassDefFoundError错误和NoSuchMethodError错误            // 注意,当存在多个StaticLoggerBinder时,应用不会停止,由JVM随机选择一个。            StaticLoggerBinder.getSingleton();                        // 修改状态为初始化成功            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;            // 如果存在多个StaticLoggerBinder,会在控制台提醒用户实际选择的是哪一个            reportActualBinding(staticLoggerBinderPathSet);                        // 对SubstituteLoggerFactory的操作,不作扩展            fixSubstituteLoggers();            replayEvents();            SUBST_FACTORY.clear();                    } catch (NoClassDefFoundError ncde) {            // 当StaticLoggerBinder不存在时,会将状态修改为NOP_FALLBACK_INITIALIZATION,并抛出信息            String msg = ncde.getMessage();            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;                Util.report("Failed to load class "org.slf4j.impl.StaticLoggerBinder".");                Util.report("Defaulting to no-operation (NOP) logger implementation");                Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");            } else {                failedBinding(ncde);                throw ncde;            }        } catch (java.lang.NoSuchMethodError nsme) {            // 当StaticLoggerBinder.getSingleton()方法不存在时,会将状态修改为初始化失败,并抛出信息            String msg = nsme.getMessage();            if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {                INITIALIZATION_STATE = FAILED_INITIALIZATION;                Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");                Util.report("Your binding is version 1.5.5 or earlier.");                Util.report("Upgrade your binding to version 1.6.x.");            }            throw nsme;        } catch (Exception e) {            failedBinding(e);            throw new IllegalStateException("Unexpected initialization failure", e);        }    }

这里再补充一个问题,slf4j-api 中不包含 StaticLoggerBinder 类,为什么能编译通过呢?其实我们项目中用到的 slf4j-api 是已经编译好的 class 文件,所以不需要再次编译。但是,编译前 slf4j-api 中是包含 StaticLoggerBinder.java 的,且编译后也存在 StaticLoggerBinder.class ,只是这个文件被手动删除了。

logback如何加载配置

前面说过,logback 支持采用 xml、grovy 和 SPI 的方式配置文件,本文只分析 xml 文件配置的方式。

logback 依赖于 Joran(一个成熟的,灵活的并且强大的配置框架 ),本质上是采用 SAX 方式解析 XML。因为 SAX 不是本文的重点内容,所以这里不会去讲解相关的原理。

logback 加载配置的代码还是比较繁琐,且代码量较大,这里就不一个个方法地分析了,而是采用类图的方式来讲解。下面是 logback 加载配置的大致图解:

2743af38dfc9cad3c851a3b80293b055.png

logback_joran

这里再补充下图中几个类的作用:

类名描述SaxEventRecorderSaxEvent 记录器。继承了 DefaultHandler,所以在解析 xml 时会触发对应的方法,
这些方法将触发的参数封装到 saxEven 中并放入 saxEventList 中SaxEventSAX 事件体。用于封装 xml 事件的参数。Action执行的配置动作。ElementSelector节点模式匹配器。RuleStore用于存放模式匹配器-动作的键值对。

结合上图,我简单概括下整个执行过程:

  1. 使用 SAX 方式解析 XML,解析过程中根据当前的元素类型,调用 DefaultHandler 实现类的方法,构造 SaxEvent 并将其放入集合 saxEventList 中;
  2. 当 XML 解析完成,会调用 EventPlayer 的方法,遍历集合 saxEventList 的 SaxEvent 对象,当该对象能够匹配到对应的规则,则会执行相应的 Action。

简单看下LoggerContext

现在回到 StaticLoggerBinder.getLoggerFactory()方法,如下。这个方法返回的 ILoggerFactory 其实就是 LoggerContext。

    private LoggerContext defaultLoggerContext = new LoggerContext();    public ILoggerFactory getLoggerFactory() {        // 如果初始化未完成,直接返回defaultLoggerContext        if (!initialized) {            return defaultLoggerContext;        }                if (contextSelectorBinder.getContextSelector() == null) {            throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);        }        // 如果是DefaultContextSelector,返回的还是defaultLoggerContext        // 如果是ContextJNDISelector,则可能为不同线程提供不同的LoggerContext 对象        // 主要取决于是否设置系统属性-Dlogback.ContextSelector=JNDI        return contextSelectorBinder.getContextSelector().getLoggerContext();    }

下面简单看下 LoggerContext 的 UML 图。它不仅作为获取 logger 的工厂,还绑定了一些全局的 Object、property 和 LifeCycle。

bae95899d752a0a20aea5772cd5d7fa4.png

logback_LoggerContext_UML

获取logger对象

这里先看下 Logger 的 UML 图,如下。在 Logger 对象中,持有了父级 logger、子级 logger 和 appender 的引用。

3fd93c3e5f381cf82f3548d39d4f4266.png

logback_Logger_UML

进入LoggerContext.getLogger(String)方法,如下。这个方法逻辑简单,但是设计非常巧妙,可以好好琢磨下。我概括下主要的步骤:

  1. 如果获取的是 root logger,直接返回;
  2. 如果获取的是 loggerCache 中缓存的 logger,直接返回;
  3. 循环获取 logger name 中包含的所有 logger,如果不存在就创建并放入缓存;
  4. 返回 logger name 对应的 logger。
    public final Logger getLogger(final String name) {        if (name == null) {            throw new IllegalArgumentException("name argument cannot be null");        }        // 如果获取的是root logger,直接返回        if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {            return root;        }        int i = 0;        Logger logger = root;        // 在loggerCache中缓存着已经创建的logger,如果存在,直接返回        Logger childLogger = (Logger) loggerCache.get(name);        if (childLogger != null) {            return childLogger;        }        // 如果还找不到,就需要创建        // 注意,要获取以cn.zzs.logback.LogbackTest为名的logger,名为cn、cn.zzs、cn.zzs.logback的logger不存在的话也会被创建        String childName;        while (true) {            // 从起始位置i开始,获取“.”的位置            int h = LoggerNameUtil.getSeparatorIndexOf(name, i);            // 截取logger的名字            if (h == -1) {                childName = name;            } else {                childName = name.substring(0, h);            }            // 修改起始位置,以获取下一个“.”的位置            i = h + 1;            synchronized (logger) {                // 判断当前logger是否存在以childName命名的子级                childLogger = logger.getChildByName(childName);                if (childLogger == null) {                    // 通过当前logger来创建以childName命名的子级                    childLogger = logger.createChildByName(childName);                    // 放入缓存                    loggerCache.put(childName, childLogger);                    // logger总数量+1                    incSize();                }            }            // 当前logger修改为子级logger            logger = childLogger;            // 如果当前logger是最后一个,则跳出循环            if (h == -1) {                return childLogger;            }        }    }

进入Logger.createChildByName(String)方法,如下。

    Logger createChildByName(final String childName) {        // 判断要创建的logger在名字上是不是与当前logger为父子,如果不是会抛出异常        int i_index = LoggerNameUtil.getSeparatorIndexOf(childName, this.name.length() + 1);        if (i_index != -1) {            throw new IllegalArgumentException("For logger [" + this.name + "] child name [" + childName                            + " passed as parameter, may not include '.' after index" + (this.name.length() + 1));        }        // 创建子logger集合        if (childrenList == null) {            childrenList = new CopyOnWriteArrayList();        }        Logger childLogger;        // 创建新的logger        childLogger = new Logger(childName, this, this.loggerContext);        // 将logger放入集合中        childrenList.add(childLogger);        // 设置有效日志等级        childLogger.effectiveLevelInt = this.effectiveLevelInt;        return childLogger;    }

logback 在类的设计上非常值得学习, 使得许多代码逻辑也非常简单易懂。

打印日志到控制台

这里以Logger.debug(String)为例,如下。这里需要注意 TurboFilter 和 Filter 的区别,前者是全局的,每次发起日志记录请求都会被调用,且在日志事件创建前调用,而后者是附加的,作用范围较小。因为实际项目中 TurboFilter 使用较少,这里不做扩展,感兴趣可参考这里。

    public static final String FQCN = ch.qos.logback.classic.Logger.class.getName();    public void debug(String msg) {        filterAndLog_0_Or3Plus(FQCN, null, Level.DEBUG, msg, null, null);    }    private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,                    final Throwable t) {        // 使用TurboFilter过滤当前日志,判断是否通过        final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);        //  返回NEUTRAL表示没有TurboFilter,即无需过滤        if (decision == FilterReply.NEUTRAL) {            // 如果需要打印日志的等级小于有效日志等级,则直接返回            if (effectiveLevelInt > level.levelInt) {                return;            }        } else if (decision == FilterReply.DENY) {            // 如果不通过,则不打印日志,直接返回            return;        }        // 创建LoggingEvent        buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);    }

进入Logger.buildLoggingEventAndAppend(String, Marker, Level, String, Object[], Throwable),如下。 logback 中,日志记录请求会被构造成日志事件 LoggingEvent,传递给对应的 appender 处理。

    private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,                    final Throwable t) {        // 构造日志事件LoggingEvent        LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);        // 设置标记        le.setMarker(marker);        // 通知LoggingEvent给当前logger持有的和继承的appender        callAppenders(le);    }

进入到Logger.callAppenders(ILoggingEvent),如下。

    public void callAppenders(ILoggingEvent event) {        int writes = 0;        // 通知LoggingEvent给当前logger的持有的和继承的appender处理日志事件        for (Logger l = this; l != null; l = l.parent) {            writes += l.appendLoopOnAppenders(event);            // 如果设置了logger的additivity=false,则不会继续查找父级的appender            // 如果没有设置,则会一直查找到root logger            if (!l.additive) {                break;            }        }        // 当前logger未设置appender,在控制台打印提醒        if (writes == 0) {            loggerContext.noAppenderDefinedWarning(this);        }    }    private int appendLoopOnAppenders(ILoggingEvent event) {        if (aai != null) {            // 调用AppenderAttachableImpl的方法处理日志事件            return aai.appendLoopOnAppenders(event);        } else {            // 如果当前logger没有appender,会返回0            return 0;        }    }

在继续分析前,先看下 Appender 的 UML 图(注意,Appender 还有很多实现类,这里只列出了常用的几种)。Appender 持有 Filter 和 Encoder 到引用,可以分别对日志进行过滤和格式转换。

本文仅涉及到 ConsoleAppender 的源码分析。

bae81209319921835cfe7f257c531c88.png

logback_Appender_UML

继续进入到AppenderAttachableImpl.appendLoopOnAppenders(E),如下。这里会遍历当前 logger 持有的 appender,并调用它们的 doAppend 方法。

    public int appendLoopOnAppenders(E e) {        int size = 0;        // 获得当前logger的所有appender        final Appender[] appenderArray = appenderList.asTypedArray();        final int len = appenderArray.length;        for (int i = 0; i < len; i++) {            // 调用appender的方法            appenderArray[i].doAppend(e);            size++;        }        // 这个size为appender的数量        return size;    }

为了简化分析,本文仅分析打印日志到控制台的过程,所以进入到UnsynchronizedAppenderBase.doAppend(E)方法,如下。

    public void doAppend(E eventObject) {        // 避免doAppend方法被重复调用??        // TODO 这一步不是很理解,同一个线程还能同时调用两次这个方法?        if (Boolean.TRUE.equals(guard.get())) {            return;        }        try {            guard.set(Boolean.TRUE);            // 过滤当前日志事件是否允许打印            if (getFilterChainDecision(eventObject) == FilterReply.DENY) {                return;            }            // 调用实现类的方法            this.append(eventObject);        } catch (Exception e) {            if (exceptionCount++ < ALLOWED_REPEATS) {                addError("Appender [" + name + "] failed to append.", e);            }        } finally {            guard.set(Boolean.FALSE);        }    }

进入到OutputStreamAppender.append(E),如下。

    protected void append(E eventObject) {        // 如果appender未启动,则直接返回,不处理日志事件        if (!isStarted()) {            return;        }        subAppend(eventObject);    }    protected void subAppend(E event) {        // 这里又判断一次??        if (!isStarted()) {            return;        }        try {            // 这一步不是很懂 TODO            if (event instanceof DeferredProcessingAware) {                ((DeferredProcessingAware) event).prepareForDeferredProcessing();            }                        // 调用encoder的方法将日志事件转化为字节数组            byte[] byteArray = this.encoder.encode(event);            // 打印日志            writeBytes(byteArray);        } catch (IOException ioe) {            this.started = false;            addStatus(new ErrorStatus("IO failure in appender", this, ioe));        }    }

看下LayoutWrappingEncoder.encode(E),如下。

    public byte[] encode(E event) {        // 根据配置格式处理日志事件        String txt = layout.doLayout(event);        // 将字符转化为字节数组并返回        return convertToBytes(txt);    }

后面会调用PatternLayout.doLayout(ILoggingEvent)将日志的消息进行处理,这部分内容我就不继续扩展了,感兴趣可以自行研究。


关注我,私信回复【资料】即可领取视频中java相关资料以及一份227页最新的bat大厂面试宝典

最后

欢迎大家一起交流,喜欢文章记得关注我点个赞哟,感谢支持!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值