package com.example.demo;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.pattern.Converter;
import ch.qos.logback.core.pattern.ConverterUtil;
import ch.qos.logback.core.pattern.parser.Node;
import ch.qos.logback.core.pattern.parser.Parser;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.ILoggerFactory;
import org.slf4j.impl.StaticLoggerBinder;
/**
* @author zhen_hong
* @date 2022/8/9 11:21
*/
public class LogbackDemo1Test {
public static void main(String[] args) throws Exception {
// 绑定日志实现,这里使用的是logback实现并解析配置,如果存在logback.xml,logback.groovy and so on 或者系统属性中有指定对应的路径
ILoggerFactory loggerContext = StaticLoggerBinder.getSingleton().getLoggerFactory();
/**
* String fqcn, Logger logger, Level level, String message,
* Throwable throwable, Object[] argArray
*/
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger)loggerContext.getLogger(LogbackTest.class.getName());
// 清除掉绑定时设置的appenders,因为在绑定日志实现的时候,会自动配置
// 即使没有找到配置文件也会默认给ROOT logger 添加 ConsoleAppender
logger.detachAndStopAllAppenders();
Context context = (Context)loggerContext;
// 咱们自己创建一个 ConsoleAppender
ConsoleAppender consoleAppender = new ConsoleAppender();
consoleAppender.setContext(context);
// 日志输出编码器,比如设置字符集等操作,通过layout解析出来的日志字符串做进一步的处理
PatternLayoutEncoder patternLayoutEncoder = new PatternLayoutEncoder() {
@Override
public void setLayout(Layout<ILoggingEvent> layout) {
this.layout = layout;
}
};
patternLayoutEncoder.setContext(context);
// pattern 解析器,将 pattern 构建成用户能够读的日志
PatternLayout patternLayout = new PatternLayout();
patternLayout.setContext(context);
// 设置格式化 pattern
patternLayout.setPattern("%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n");
patternLayout.start();
patternLayoutEncoder.setLayout(patternLayout);
patternLayoutEncoder.start();
consoleAppender.setEncoder(patternLayoutEncoder);
consoleAppender.setName("console");
consoleAppender.start();
logger.addAppender(consoleAppender);
logger.info("这个是一个测试方法");
}
}
从上面的一个简单的例子我们看出,要想实现日志打印需要拥有这么几个组件
获取 Logger 对象的方法很简单,就是通过 loggerContext.getLogger 获取即可,如果说你传入的loggerName在loggerContext不存在
那么将会自动创建一个,并且每一级的父logger都是上一级,最顶级是ROOT,代码如下:
public final Logger ch.qos.logback.classic.LoggerContext.getLogger(java.lang.String)(final String name) {
if (name == null) {
throw new IllegalArgumentException("name argument cannot be null");
}
// if we are asking for the root logger, then let us return it without
// wasting time
if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
return root;
}
int i = 0;
Logger logger = root;
// check if the desired logger exists, if it does, return it
// without further ado.
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);
}
// move i left of the last point
i = h + 1;
synchronized (logger) {
childLogger = logger.getChildByName(childName);
if (childLogger == null) {
childLogger = logger.createChildByName(childName);
loggerCache.put(childName, childLogger);
incSize();
}
}
logger = childLogger;
if (h == -1) {
return childLogger;
}
}
}
如果我们传入的loggerName是 com.demo.Test, 那么它的父子结构会是这样的
ROOT
|
com
|
com.demo
|
com.demo.test
看了以上例子之后,你会发现如果我们要定义其他的Appender或者增加logger,我们只需要创建一个logger,然后给这个logger设置
自己的Appender就可以轻松驾驭实现自己的需求,但是如果直接在代码里写死的话就失去了灵活性,那么我们可不可以通过配置的方式
来定义Appender呢?当然可以,代码如下:
<configuration debug="false" scan="true" scanPeriod="1 seconds">
<contextName>logbackcontextName>
<if condition='property("env").contains("dev")'>
<then>
<property scope="context" name="log.level" value="DEBUG" />
then>
<else>
<property scope="context" name="log.level" value="INFO" />
else>
if>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
pattern>
encoder>
appender>
<root level="${log.level}">
<appender-ref ref="console" />
root>
configuration>
package com.example.demo;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import java.net.URL;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.slf4j.impl.StaticLoggerBinder;
import org.springframework.core.io.ClassPathResource;
/**
* @author zhen_hong
* @date 2022/8/9 11:21
*/
public class LogbackDemo2Test {
public static void main(String[] args) throws Exception {
// 绑定日志实现,这里使用的是logback实现并解析配置,如果存在logback.xml,logback.groovy and so on 或者系统属性中有指定对应的路径
// ILoggerFactory loggerContext = StaticLoggerBinder.getSingleton().getLoggerFactory();
ILoggerFactory loggerContext = LoggerFactory.getILoggerFactory();
// 获取根logger
ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger)loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
// 清除默认的基本 appender 配置
rootLogger.detachAndStopAllAppenders();
// 解析配置文件
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext((LoggerContext)loggerContext);
URL url = Thread.currentThread().getContextClassLoader().getResource("logback-custom.xml");
configurator.doConfigure(url);
org.slf4j.Logger logger = loggerContext.getLogger(LogbackParseTest.class.getName());
logger.info("这个是一个测试方法");
}
}
看到上面的代码,使用了 JoranConfigurator 对 xml 配置文件进行了解析,将解析出来的 Appender,Encoder,Layout,Logger 设置
到了LoggerContext 中,那么它具体是怎么解析的呢?这个问题留到下一章分析,下面我先来看看另外一个例子:
package com.example.demo;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.pattern.Converter;
import ch.qos.logback.core.pattern.ConverterUtil;
import ch.qos.logback.core.pattern.parser.Node;
import ch.qos.logback.core.pattern.parser.Parser;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.ILoggerFactory;
import org.slf4j.impl.StaticLoggerBinder;
/**
* @author zhen_hong
* @date 2022/8/9 11:21
*/
public class LogbackDemo3Test {
public static void main(String[] args) throws Exception {
// 绑定日志实现,这里使用的是logback实现并解析配置,如果存在logback.xml,logback.groovy and so on 或者系统属性中有指定对应的路径
ILoggerFactory loggerContext = StaticLoggerBinder.getSingleton().getLoggerFactory();
// 使用logback解析器解析pattern
Parser<ILoggingEvent> p = new Parser<>("%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n");
p.setContext((LoggerContext)loggerContext);
Node t = p.parse();
// 转换器,根据pattern中的%d,%logger创建
Map<String, String> converterMap = new HashMap<>();
converterMap.putAll(PatternLayout.DEFAULT_CONVERTER_MAP);
// 编译转换器,用于处理形如 %d{xxx} 的占位符
Converter c = p.compile(t, converterMap);
StringBuilder buf = new StringBuilder();
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger)loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
// 构建一个日志事件
ILoggingEvent event = new LoggingEvent(LogbackTest.class.getName(), logger, Level.INFO
, "这个是一个测试方法", null, null);
((LoggingEvent) event).setTimeStamp(System.currentTimeMillis());
ConverterUtil.startConverters(c);
while (c != null) {
c.write(buf, event);
c = c.getNext();
}
System.out.println(buf.toString());
}
}
上面这段代码相当于将 Appender,Encoder,Layout 的基本实现逻辑给暴露出来了,Layout将日志格式pattern进行解析,最后编译
成Converter,比如 %d{HH:mm:ss.SSS},对应的Converter就是DateConverter,百分号后边的这个d就是用来指定Converter的指令,
PatternLayout.DEFAULT_CONVERTER_MAP 这个是logback默认实现的一些指令与Converter的对应关系,比如 d -》ch.qos.logback.classic.pattern.DateConverter
这一章主要是从一个小例子开始去了解我要打印一个日志需要哪些个组件,当了解了需要什么组件之后,那么我们就可以知道哪些组件是
可以定制的,从而引出通过配置化的方式去定制组件。额外的,在 LogbackDemo3Test.java 这个例子我们直接深入Appender基本上实现
打印出了日志,让我们对后边分析Appender源码有一个大致的印象,从而更快速的去理解它的运作方式。