二、快速开始

一、从一个小例子开始


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("这个是一个测试方法");
    }

}

从上面的一个简单的例子我们看出,要想实现日志打印需要拥有这么几个组件

  • LoggerContext:即日志上下文,可以用来保存一些日志配置过程中的额外属性和获取Logger对象
  • Appender:输出日志,输出到哪,可以自己实现
  • Encoder:在输出日志之前对日志进行编码
  • Layout:在输出日志之前对日志进行一些格式化操作
  • Logger:直接面向用户打印日志的门面接口

获取 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呢?当然可以,代码如下:

  • logback-custom.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">

  <contextName>logback</contextName>

  <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>

  <!--<logger name="com.example.demo.LogbackParseTest" level="${log.level}" additivity="false">
    <appender-ref ref="console" />
  </logger>-->

</configuration>

  • 一个简单的java程序例子
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源码有一个大致的印象,从而更快速的去理解它的运作方式。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值