一、logback简介

logback是log4j创始人写的,性能比log4j要好,目前主要分为3个模块

  1. logback-core:核心代码模块
  2. logback-classic:log4j的一个改良版本,同时实现了slf4j的接口,这样你如果之后要切换其他日志组件也是一件很容易的事
  3. logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能

二、logback.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<!--debug 要不要打印 logback内部日志信息,true则表示要打印。建议开启-->
<!--scan 配置发送改变时,要不要重新加载-->
<configuration debug="true" scan="true" scanPeriod="1 seconds">
<contextName>logback</contextName>
<!--定义参数,后面可以通过${app.name}使用-->
<property name="app.name" value="logback_test"/>
<!--ConsoleAppender 用于在屏幕上输出日志-->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!--定义了一个过滤器,在LEVEL之下的日志输出不会被打印出来-->
<!--这里定义了DEBUG,也就是控制台不会输出比ERROR级别小的日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<!-- encoder 默认配置为PatternLayoutEncoder -->
<!--定义控制台输出格式-->
<encoder>
<pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern>
</encoder>
</appender> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--定义日志输出的路径-->
<!--这里的scheduler.manager.server.home 没有在上面的配置中设定,所以会使用java启动时配置的值-->
<!--比如通过 java -Dscheduler.manager.server.home=/path/to XXXX 配置该属性-->
<file>${scheduler.manager.server.home}/logs/${app.name}.log</file>
<!--定义日志滚动的策略TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,
既负责滚动也负责出发滚动。有以下子节点:<fileNamePattern>:必要节点,包含文件名及“%d”转换符,
“%d”可以包含一个java.text.SimpleDateFormat指定的时间格式,如:%d{yyyy-MM}。如果直接使用 %d-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--定义文件滚动时的文件名的格式-->
<fileNamePattern>${scheduler.manager.server.home}/logs/${app.name}.%d{yyyy-MM-dd.HH}.log.gz
</fileNamePattern>
<!--60天的时间周期,日志量最大20GB-->
<maxHistory>60</maxHistory>
<!-- 该属性在 1.1.6版本后 才开始支持-->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<!--每个日志文件最大100MB-->
<maxFileSize>100MB</maxFileSize>
</triggeringPolicy>
<!--定义输出格式-->
<encoder>
<pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern>
</encoder>
</appender> <!--root是默认的logger 这里设定输出级别是debug-->
<root level="info">
<!--定义了两个appender,日志会通过往这两个appender里面写-->
<appender-ref ref="stdout"/>
<appender-ref ref="file"/>
</root>
<!--对于类路径以 com.example.logback 开头的Logger,输出级别设置为warn,并且只输出到控制台-->
<!--这个logger没有指定appender,它会继承root节点中定义的那些appender-->
<logger name="com.example.logback" level="warn"/> <!--通过 LoggerFactory.getLogger("mytest") 可以获取到这个logger-->
<!--由于这个logger自动继承了root的appender,root中已经有stdout的appender了,自己这边又引入了stdout的appender-->
<!--如果没有设置 additivity="false" ,就会导致一条日志在控制台输出两次的情况-->
<!--additivity表示要不要使用rootLogger配置的appender进行输出-->
<logger name="mytest" level="info" additivity="false">
<appender-ref ref="stdout"/>
</logger> <!--由于设置了 additivity="false" ,所以输出时不会使用rootLogger的appender-->
<!--但是这个logger本身又没有配置appender,所以使用这个logger输出日志的话就不会输出到任何地方-->
<logger name="mytest2" level="info" additivity="false"/>
</configuration>

三、实现原理

1、获取LoggerFactory

slf4j委托具体实现框架的StaticLoggerBinder来返回一个ILoggerFactory,从而对接到具体实现框架上,我们看下这个类(省略了部分代码)

public class StaticLoggerBinder implements LoggerFactoryBinder {

  private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();static {
SINGLETON.init();
}public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}/**
* Package access for testing purposes.
*/
void init() {
try {
try {
new ContextInitializer(defaultLoggerContext).autoConfig();
} catch (JoranException je) {
Util.report("Failed to auto configure default logger context", je);
}
// logback-292
if(!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
}
contextSelectorBinder.init(defaultLoggerContext, KEY);
initialized = true;
} catch (Throwable t) {
// we should never get here
Util.report("Failed to instantiate [" + LoggerContext.class.getName()
+ "]", t);
}
}
public ILoggerFactory getLoggerFactory() {
if (!initialized) {
return defaultLoggerContext;
} if (contextSelectorBinder.getContextSelector() == null) {
throw new IllegalStateException(
"contextSelector cannot be null. See also " + NULL_CS_URL);
}
return contextSelectorBinder.getContextSelector().getLoggerContext();
}
}

可以看到

  • 1、通过getSingleton()获取该类的单例
  • 2、static块来保证初始化调用init()方法
  • 3、在init方法中,委托ContextInitializer类对LoggerContext进行初始化。这里如果找到了任一配置文件,就会根据配置文件去初始化LoggerContext,如果没找到,会使用默认配置。
  • 4、然后初始化ContextSelectorStaticBinder,在这个类内部new一个DefaultContextSelector,并把第一步中配置完毕的LoggerContext传给DefaultContextSelector
  • 5、调用getLoggerFactory()方法,直接返回3中配置的LoggerContext,或者委托DefaultContextSelector类返回LoggerContext

这里可以看出所有的配置均保存在LoggerContext这个类中,只要获取到了该类,就能得到log的所有配置,我们的logger就保存在该类的Map<String, Logger> loggerCache中,key为logger的name.

2、获取logger

public final Logger getLogger(final String name) {

        if (name == null) {
throw new IllegalArgumentException("name argument cannot be null");
} // 如果请求的是ROOT Logger,那么就直接返回root
if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
return root;
} int i = 0;
Logger logger = root; // 请求的Logger是否已经创建过了,如果已经创建过,就直接从loggerCache中返回
Logger childLogger = (Logger) loggerCache.get(name);
// if we have the child, then let us return it without wasting time
if (childLogger != null) {
return childLogger;
} // if the desired logger does not exist, them create all the loggers
// in between as well (if they don't already exist)
String childName;
while (true) {
int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
if (h == -1) {
childName = name;
} else {
childName = name.substring(0, h);
}
i = h + 1;
synchronized (logger) {
childLogger = logger.getChildByName(childName);
if (childLogger == null) {
//创建Logger实例
childLogger = logger.createChildByName(childName);
loggerCache.put(childName, childLogger);
incSize();
}
}
logger = childLogger;
if (h == -1) {
return childLogger;
}
}
}

3、logger.info()记录日志

slf4j定义了Logger接口记录日志的方法是info()、warn()、debug()等,这些方法只是入口,logback是这样实现这些方法的

该方法首先要请求TurboFilter来判断是否允许记录这次日志信息。TurboFilter是快速筛选的组件,筛选发生在LoggingEvent创建之前,这种设计也是为了提高性能

如果经过过滤,确定要记录这条日志信息,则进入buildLoggingEventAndAppend方法

private void buildLoggingEventAndAppend(final String localFQCN,
final Marker marker, final Level level, final String msg,
final Object[] params, final Throwable t) {
LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
le.setMarker(marker);
callAppenders(le);
}

在这个方法里,首先创建了LoggingEvent对象,然后调用callAppenders()方法,要求该Logger关联的所有Appenders来记录日志

LoggingEvent对象是承载了日志信息的类,最后输出的日志信息,就来源于这个事件对象

/**
* Invoke all the appenders of this logger.
*
* @param event
* The event to log
*/
public void callAppenders(ILoggingEvent event) {
int writes = 0;
for (Logger l = this; l != null; l = l.parent) {
writes += l.appendLoopOnAppenders(event);
if (!l.additive) {
break;
}
}
// No appenders in hierarchy
if (writes == 0) {
loggerContext.noAppenderDefinedWarning(this);
}
}

经过前面的Filter过滤、日志级别匹配、创建LoggerEvent对象,终于进入了记录日志的方法。该方法会调用此Logger关联的所有Appender,而且还会调用所有父Logger关联的Appender,直到遇到父Logger的additive属性设置为false为止,这也是为什么如果子Logger和父Logger都关联了同样的Appender,则日志信息会重复记录的原因

private int appendLoopOnAppenders(ILoggingEvent event) {
if (aai != null) {
return aai.appendLoopOnAppenders(event);
} else {
return 0;
}
}

实际上调用的AppenderAttachableImpl的appendLoopOnAppenders()方法

到这里,为了记录一条日志信息,长长的调用链终于告一段落了,通过调用Appender的doAppend(LoggingEvent e)方法,委托Appender来最终记录日志

UnsynchronizedAppenderBase里面的doAppend()方法,它主要是记录了Status状态,然后检查Appender上的Filter是否满足过滤条件,最后再调用实现子类的appender()方法。很眼熟是吗,这里用到了一个设计模式——模板方法

public void doAppend(E eventObject) {
// WARNING: The guard check MUST be the first statement in the
// doAppend() method. // prevent re-entry.
if (Boolean.TRUE.equals(guard.get())) {
return;
} try {
guard.set(Boolean.TRUE); if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus(
"Attempted to append to non started appender [" + name + "].",
this));
}
return;
} if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
} // ok, we now invoke derived class' implementation of append
this.append(eventObject); } catch (Exception e) {
if (exceptionCount++ < ALLOWED_REPEATS) {
addError("Appender [" + name + "] failed to append.", e);
}
} finally {
guard.set(Boolean.FALSE);
}
} abstract protected void append(E eventObject);

上面的代码非常简单,就不用说了,我们就直接看看实现类的append()方法是怎么实现的,这里我们选择OutputStreamAppender实现类

首先检查一下这个Appender是否已经启动,如果没启动就直接返回,如果已经启动,则又进入一个subAppend()方法

RollingFileAppender覆盖了subAppend()方法,实现了翻滚策略

@Override
protected void subAppend(E event) {
// The roll-over check must precede actual writing. This is the
// only correct behavior for time driven triggers. // We need to synchronize on triggeringPolicy so that only one rollover
// occurs at a time
synchronized (triggeringPolicy) {
if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
rollover();
}
} super.subAppend(event);
}

进入super.subAppend(event);

protected void subAppend(E event) {
if (!isStarted()) {
return;
}
try {
// this step avoids LBCLASSIC-139
if (event instanceof DeferredProcessingAware) {
((DeferredProcessingAware) event).prepareForDeferredProcessing();
}
// the synchronization prevents the OutputStream from being closed while we
// are writing. It also prevents multiple threads from entering the same
// converter. Converters assume that they are in a synchronized block.
// lock.lock(); byte[] byteArray = this.encoder.encode(event);
writeBytes(byteArray); } catch (IOException ioe) {
// as soon as an exception occurs, move to non-started state
// and add a single ErrorStatus to the SM.
this.started = false;
addStatus(new ErrorStatus("IO failure in appender", this, ioe));
}
}

通过this.encoder.encode(event)方法格式化需要记录的日志,然后通过writeBytes(byteArray)写入日志。

四、通过代码动态生成logger对象

public class LoggerHolder {

    public static Logger getLogger(String name) {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
//如果未创建该logger
if (loggerContext.exists(name) == null) {
return buildLogger(name);
}
//如果已经创建,则返回
return loggerContext.getLogger(name);
} private static Logger buildLogger(String name) { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger logger = loggerContext.getLogger(name);
//配置rollingFileAppender
RollingFileAppender rollingFileAppender = new RollingFileAppender();
rollingFileAppender.setName(name);
//配置rollingPolicy
TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy();
rollingPolicy.setFileNamePattern("/data/pjf/" + name + "/" + name + ".%d{yyyyMMdd}.log");
rollingFileAppender.setRollingPolicy(rollingPolicy);
//配置encoder
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setCharset(UTF_8);
encoder.setPattern("%msg%n");
rollingFileAppender.setEncoder(encoder);
//配置logger
logger.addAppender(rollingFileAppender);
logger.setAdditive(false);
logger.setLevel(Level.INFO);
return logger;
}
}

具体参考:写的很详细,

读logback源码系列文章(一)——对接slf4j
读logback源码系列文章(二)——提供ILoggerFactory
读logback源码系列文章(三)——创建Logger
读logback源码系列文章(四)——记录日志
读logback源码系列文章(五)——Appender
读logback源码系列文章(六)——ContextInitializer
读logback源码系列文章(七)——配置的实际工作类Action
读logback源码系列文章(八)——记录日志的实际工作类Encoder

logger(二)logback简介及其实现原理的更多相关文章

  1. logger(三)log4j2简介及其实现原理

    一.log4j2简介 log4j2是log4j 1.x和logback的改进版,据说采用了一些新技术(无锁异步.等等),使得日志的吞吐量.性能比log4j 1.x提高10倍,并解决了一些死锁的bug, ...

  2. logger(一)slf4j简介及其实现原理

    一.slf4j简介 slf4j(Simple logging facade for Java)是对所有日志框架制定的一种规范.标准.接口,并不是一个框架的具体的实现,因为接口并不能独立使用,需要和具体 ...

  3. 【转】logback 常用配置详解(序)logback 简介

    原创文章,转载请指明出处:http://aub.iteye.com/blog/1101222, 尊重他人即尊重自己 详细整理了logback常用配置, 不是官网手册的翻译版,而是使用总结,旨在更快更透 ...

  4. logback常用配置详解及logback简介

    logback 简介(一) Ceki Gülcü在Java日志领域世界知名.他创造了Log4J ,这个最早的Java日志框架即便在JRE内置日志功能的竞争下仍然非常流行.随后他又着手实现SLF4J 这 ...

  5. Logback简介及配置文件logback.xml详解

    logback简介及配置文件说明 @author:wangyq @date:2021年3月31日 logback简介 ​ Logback是由log4j创始人设计的另一个开源日志组件,官方网站: htt ...

  6. logback 常用配置详解(序)logback 简介

    转自:http://aub.iteye.com/blog/1101260 logback 简介 Ceki Gülcü在Java日志领域世界知名.他创造了Log4J ,这个最早的Java日志框架即便在J ...

  7. Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理

    Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理 2017年01月04日 08:52:12 阅读数:18366 基于Lucene检索引擎我们开发了自己的全文检索系统,承担起后台PB ...

  8. [转] logback 常用配置详解(序)logback 简介

    转载文章:原文出处:http://aub.iteye.com/blog/1101222 logback 简介 Ceki Gülcü在Java日志领域世界知名.他创造了Log4J ,这个最早的Java日 ...

  9. 【转】Android LCD(二):LCD常用接口原理篇

    关键词:android LCD TFT TTL(RGB)  LVDS  EDP MIPI  TTL-LVDS  TTL-EDP 平台信息:内核:linux2.6/linux3.0系统:android/ ...

随机推荐

  1. 接口测试Post和Get区别(面试题)

    1. HTTP接口传递数据最常用的方式: Get方式是从服务器上获取数据:在做数据查询时,建议用Get方式:如:商品信息接口.搜索接口.博客访客接口等. Post方式是向服务器传送数据 :在做数据添加 ...

  2. Task异常捕获的几种方式

    在调用Task的Wait()方法或Result属性处会抛出Task中的异常. 但是如果没有返回结果,或者不想调用Wait()方法,该怎么获取异常呢? 可以使用ContinueWith()方法 var ...

  3. HTML基础五-starrysky页面动起来

    Starrysky前端框架 链接:https://pan.baidu.com/s/1P8mPrHZjyRtzw1NWnAx-9w 提取码:cjl5 接口文档:https://www.showdoc.c ...

  4. [LeetCode] 255. Verify Preorder Sequence in Binary Search Tree 验证二叉搜索树的先序序列

    Given an array of numbers, verify whether it is the correct preorder traversal sequence of a binary ...

  5. C语言实现五子棋

    可以称得上史上最简单的五子棋版本了. 可以使用curses库来改进页面和下棋方式. 并且对于输入的坐标没有进行鉴别,如果输入的坐标超过棋盘大小,就会段错误退出. 我改进了一点,但是还是没有完全避免这个 ...

  6. Elasticsearch由浅入深(六)批量操作:mget批量查询、bulk批量增删改、路由原理、增删改内部原理、document查询内部原理、bulk api的奇特json格式

    mget批量查询 批量查询的好处就是一条一条的查询,比如说要查询100条数据,那么就要发送100次网络请求,这个开销还是很大的如果进行批量查询的话,查询100条数据,就只要发送1次网络请求,网络请求的 ...

  7. W5500封装

    W5500是韩国一款集成全硬件 TCP/IP 协议栈的嵌入式以太网控制器,W5500同时也是一颗工业级以太网控制芯片,最近发现我们国内也有和W5500 芯片一样芯片 介绍给大家 如下图:

  8. PyQt5笔记之菜单栏

    目录 菜单栏 创建单层菜单 创建多层菜单 右键打开菜单 官方菜单实例 菜单常用方法 菜单栏 创建单层菜单 import sys from PyQt5.QtWidgets import QApplica ...

  9. Rancher 部署 loonflow 工单系统

    上篇文章介绍用实例主机部署:https://www.cnblogs.com/weavepub/p/11672305.html,本文采用Rancher上部署. 文章所有的文件都托管在Github:htt ...

  10. vue学习面向对象,在项目中怎么用呢?

    面向对象感觉很牛逼,可是在项目中怎么用呢? 我至今见到的用法,写了一个用户对象. 效果:只要执行了new User(userInfo)就会在cookie,localStorage存放数据. 所以最简单 ...