Java日志源码详解,SpringBoot日志 slf4j、logback、log4j


一、前提


在Java中说起日志,定听过这样几个名词:slf4j、logback、log4j,在正式开始之前,先了解几个简单的概念

  1. slf4j、logback、log4j 的作者都是一个人
  2. slf4j 的全名是 Simple Logging Facade for Java,它只是一个门面,可以简单理解是一个接口,具体实现由logback和log4j去实现
  3. logback、log4j 都是出自一个人,而logback是后面出来的,那不言而喻,一个人做了两个东西,肯定是对一个东西不是很满意,实际上logback也比log4j更有优势
  4. SpringBoot项目默认的日志就是用 logback,从侧面体现了它确有优势

二、原生Java使用日志


1、证明 slf4j 是一个门面

在一个基本的Java项目(只引入JDK)要想打印日志,只需要引入 slf4j 和 它的实现类基本包就可以了。
在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.xdx</groupId>
    <artifactId>logLook</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        
        <!-- 门面 slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        
        <!-- 使用 logback-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

        <!-- 使用 log4j-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.13.3</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.13.3</version>
            <scope>compile</scope>
        </dependency>
        
    </dependencies>
</project>

1-1、空实现-证明

去掉pom文件中的 log4j和logback的依赖,运行代码异常如下:找不到实现类
Failed to load class “org.slf4j.impl.StaticLoggerBinder” 注意这个包名后面会用到

在这里插入图片描述


1-2、独自logback、独自 log4j、一起使用

单独使用logback——只需要注释log4j的依赖。正常运行


单独使用 log4j——只需要注释logback的依赖。正常运行

在这里插入图片描述


一起使用——会提示找到多个实现类,最终按照pom文件的引用依赖顺序选择一个(可以试试把log4j依赖放前面)

在这里插入图片描述


2、Logger工厂创建 (ILoggerFactory)重要


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

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}
  1. 创建Logger之前,先创建了一个工厂,再基于这个工厂来创建具体的Logger。(ILoggerFactory)
  2. 工厂就是来创建对象的,刚刚我们看到logback和log4j不同的实现,其本质就是不同的工厂创建的不同的对象。

2-1、工厂创建的开始
  1. INITIALIZATION_STATE 是当前工厂的状态,根据不同的状态执行不同的代码,默认是 UNINITIALIZED 未初始化。
  2. StaticLoggerBinder.getSingleton().getLoggerFactory(); 初始化好就会创建工厂类,这行就是返回创建好的工厂。
public static ILoggerFactory getILoggerFactory() {
    // 默认未初始化状态
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                // 正在初始化
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                // 初始化代码
                performInitialization();
            }
        }
    }
    switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:  // 初始化成功所执行的代码
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    case NOP_FALLBACK_INITIALIZATION:
        return NOP_FALLBACK_FACTORY;
    case FAILED_INITIALIZATION:
        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
    case ONGOING_INITIALIZATION:
        // support re-entrant behavior.
        // See also http://jira.qos.ch/browse/SLF4J-97
        return SUBST_FACTORY;
    }
    throw new IllegalStateException("Unreachable code");
}


2-2、绑定真正的工厂类

private final static void performInitialization() {
    bind();
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
        versionSanityCheck();
    }
}


private final static void bind() {
    try {
        Set<URL> staticLoggerBinderPathSet = null;
        // 判断是否是安卓,Java不是安卓,所以会执行
        // 这个if中的代码没有实际的作用【作用就是如果你有多个工厂实现,就打印日志提醒你】
        if (!isAndroid()) {
            // 找到当前可以被绑定的工厂
            staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            // 如果找到多个就打印日志,上面看到的多个绑定日志就是这里打印的
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
        }
        // 初始化LoggerFactory,这里就是按照顺序读取一个工厂bean
        StaticLoggerBinder.getSingleton();
        
        // 设置初始化状态为成功
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
        reportActualBinding(staticLoggerBinderPathSet);
    } catch (NoClassDefFoundError ncde) {
        // 异常处理
    } catch (java.lang.NoSuchMethodError nsme) {
         // 异常处理
    } catch (Exception e) {
         // 异常处理
    } finally {
        postBindCleanUp();
    }
}

2-3、slf4j 如何做到不同实现的随机切换

其实很简单,它里面写死了真正工厂类的权限定名:org.slf4j.impl.StaticLoggerBinder,也就是谁要使用slf4j这个门面,谁就要写一个这个类,用这个类去生成工厂。

在这里插入图片描述


##### 2-3-1、找到多个实现类,怎么做提示?

上面验证,当引入log4j和logback的时候,会提示有多个实现类,它是怎么做的呢?
就是用了一个写死的完整的类权限定名,如果加载到多个,就返回多个,就打印日志。

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


private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
    if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
        Util.report("Class path contains multiple SLF4J bindings.");
        for (URL path : binderPathSet) {
            Util.report("Found binding in [" + path + "]");
        }
        Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
    }
}

private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
    return binderPathSet.size() > 1;
}

2-3-2、如果引入多个实现类,最终用哪个呢?

答案是谁先引入就用谁,在绑定的时候,会有下面的代码,这代码就是去初始化工厂类。

现在已经很明了它是怎么选择实现类了,而log4j和logback都是同一个作者,作者说logback比log4j好,那我们后续对日志的解读当然都是基于logback来了。

StaticLoggerBinder.getSingleton();

public static StaticLoggerBinder getSingleton() {
    return SINGLETON;
}

private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

private StaticLoggerBinder() {
    defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
}

private LoggerContext defaultLoggerContext = new LoggerContext();

2-4、工厂类的初始化

  1. 创建了工厂类class LoggerContext implements ILoggerFactory
  2. 创建了根Logger,并为其设置了默认的配置(这个根Logger是干嘛的后面再说)
    • 默认的level:DEBUG
    • 默认的layout:%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
    • 默认的appende:ConsoleAppender
StaticLoggerBinder.getSingleton();

public static StaticLoggerBinder getSingleton() {
    return SINGLETON;
}

private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

static {
    SINGLETON.init();
}

StaticLoggerBinder 里面有个静态代码块,里面执行了 init 方法。

// 使用logback的工厂是 LoggerContext
private LoggerContext defaultLoggerContext = new LoggerContext();

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 (Exception t) { // see LOGBACK-1159
        Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
    }
}

创建根Logger,设置默认的等级为 DEBUG

public LoggerContext() {
    super();
    this.loggerCache = new ConcurrentHashMap<String, Logger>();
    this.loggerContextRemoteView = new LoggerContextVO(this);
    this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
    this.root.setLevel(Level.DEBUG);
    loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
    initEvaluatorMap();
    size = 1;
    this.frameworkPackages = new ArrayList<String>();
}

设置根Logger的 Layout和Appender

public void autoConfig() throws JoranException {
    StatusListenerConfigHelper.installIfAsked(loggerContext);
    URL url = findURLOfDefaultConfigurationFile(true);
    if (url != null) {
        configureByResource(url);
    } else {
        Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
        if (c != null) {
            try {
                c.setContext(loggerContext);
                c.configure(loggerContext);
            } catch (Exception e) {
                throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
                                .getCanonicalName() : "null"), e);
            }
        } else {
            BasicConfigurator basicConfigurator = new BasicConfigurator();
            basicConfigurator.setContext(loggerContext);
            basicConfigurator.configure(loggerContext);
        }
    }
}


public void configure(LoggerContext lc) {
    addInfo("Setting up default configuration.");
    
    ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender<ILoggingEvent>();
    ca.setContext(lc);
    ca.setName("console");
    LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<ILoggingEvent>();
    encoder.setContext(lc);
    

    // same as 
    // PatternLayout layout = new PatternLayout();
    // layout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
    TTLLLayout layout = new TTLLLayout();
    layout.setContext(lc);
    layout.start();
    encoder.setLayout(layout);
    
    ca.setEncoder(encoder);
    ca.start();
    
    Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
    rootLogger.addAppender(ca);
}

// 这里可以先看看,只有 ROOT的Logger才有这个 Appender,数据是存在 aai 中
public synchronized void addAppender(Appender<ILoggingEvent> newAppender) {
    if (aai == null) {
        aai = new AppenderAttachableImpl<ILoggingEvent>();
    }
    aai.addAppender(newAppender);
}

3、Logger 创建


上面已经得出来最终的工厂类是 LoggerContext,现在只需要看它里面的 getLogger方法是如何创建Logger的即可。

Logger的创建并不像我们想象的那样每个类创建一个,它是有子父级概念的,和我们的包路径一样的结构,比如类权限定名为 cn.xdx.JavaLog,则会创建4个Logger,分别是 ROOT、cn、cn.xdx、cn.xdx.JavaLog,并且它是一个树结构,ROOT节点的字节点包含了下面的三个。

@Override
public final Logger getLogger(final String name) {

    if (name == null) {
        throw new IllegalArgumentException("name argument cannot be null");
    }
    // 判断是不是获取 根 Logger
    if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
        return root;
    }

    int i = 0;
    Logger logger = root;

    // 判断当前Logger 是否已经创建过了,如果存在就返回
    Logger childLogger = (Logger) loggerCache.get(name);
    // if we have the child, then let us return it without wasting time
    if (childLogger != null) {
        return childLogger;
    }

    String childName;
    while (true) {
        // 用【.】去切割权限定名,h就是返回有几层
        int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
        // 获取当前层级
        if (h == -1) {
            childName = name;
        } else {
            childName = name.substring(0, h);
        }
        i = h + 1;
        // 从根节点开始便利,直到找到了当前的Logger
        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;
        }
    }
}

新Logger的创建

Logger createChildByName(final String childName) {
    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));
    }

    if (childrenList == null) {
        childrenList = new CopyOnWriteArrayList<Logger>();
    }
    Logger childLogger;
    childLogger = new Logger(childName, this, this.loggerContext);
    childrenList.add(childLogger);
    childLogger.effectiveLevelInt = this.effectiveLevelInt;
    return childLogger;
}

Logger(String name, Logger parent, LoggerContext loggerContext) {
    this.name = name;
    this.parent = parent;
    this.loggerContext = loggerContext;
}

4、Logger 输出到控制台的过程

public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(JavaLog.class);
    logger.error("xxxxx");
}

上面已经得到了Logger的实现类 ch.qos.logback.classic.Logger,现在就来看看这个 logger.error 是如何输出到控制的。


4-1、方法的重载

每种级别的方法参数都有很多,所以都会做参数不同的方法重载。

public void error(String msg) {
    filterAndLog_0_Or3Plus(FQCN, null, Level.ERROR, 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) {
    // 循环去运行过滤器
    final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);
    // 如果状态是 中性,就判断当前是否要判断过滤级别
    if (decision == FilterReply.NEUTRAL) {
        if (effectiveLevelInt > level.levelInt) {
            return;
        }
    } else if (decision == FilterReply.DENY) {
        return;
    }
    buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
}

4-2、过滤掉不想输出的日志

有两种情况,即便是调用了日志打印方法也不会输出日志

  1. 被过滤器过滤了
  2. 日志的级别过低(比如我们设置的是error级别,但是你打印 info级别的就行)
final FilterReply getTurboFilterChainDecision_0_3OrMore(final Marker marker, final Logger logger, final Level level, final String format,
                final Object[] params, final Throwable t) {
    // 如果没有过滤器就返回 中性状态
    if (turboFilterList.size() == 0) {
        return FilterReply.NEUTRAL;
    }
    // 循环去运行所有过滤器
    return turboFilterList.getTurboFilterChainDecision(marker, logger, level, format, params, t);
}

自定义过滤器

public class MyLogBackFilter extends TurboFilter {
    @Override
    public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {

        System.out.println("kkkkkk");
        return FilterReply.ACCEPT;
    }
}


public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(JavaLog.class);
    LoggerContext loggerContext =  (LoggerContext)LoggerFactory.getILoggerFactory();
    loggerContext.addTurboFilter(new MyLogBackFilter());

    logger.error("xxxxx");
}

4-3、构建日志事件

其实就是组装这次日志打印的信息

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);
}

public LoggingEvent(String fqcn, Logger logger, Level level, String message, Throwable throwable, Object[] argArray) {
    this.fqnOfLoggerClass = fqcn;
    this.loggerName = logger.getName();
    this.loggerContext = logger.getLoggerContext();
    this.loggerContextVO = loggerContext.getLoggerContextRemoteView();
    this.level = level;

    this.message = message;
    this.argumentArray = argArray;

    if (throwable == null) {
        throwable = extractThrowableAnRearrangeArguments(argArray);
    }

    if (throwable != null) {
        this.throwableProxy = new ThrowableProxy(throwable);
        LoggerContext lc = logger.getLoggerContext();
        if (lc.isPackagingDataEnabled()) {
            this.throwableProxy.calculatePackagingData();
        }
    }

    timeStamp = System.currentTimeMillis();
}

4-4、日志输出
  1. 这里面的调用链很长,但逻辑都很简单,就直接把代码给出吧
  2. doAppend 方法之后就是一些正常的操作,就不再截图了,可以自行去看(ROOT 里面默认的是ConsoleAppender)
public void callAppenders(ILoggingEvent event) {
    int writes = 0;
    // 循环便利每个层级的Logger,如果当前Logger 有Appender 就输出
    // 上面创建 Logger的时候,其实就知道了只有ROOT_Logger 才有Appender,所里这里也是等到ROOT才会输出
    for (Logger l = this; l != null; l = l.parent) {
        writes += l.appendLoopOnAppenders(event);
        
        // 这个参数默认都是 true 不用管
        if (!l.additive) {
            break;
        }
    }
    // No appenders in hierarchy
    if (writes == 0) {
        loggerContext.noAppenderDefinedWarning(this);
    }
}


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

// ROOT 里面默认的是ConsoleAppender
public int appendLoopOnAppenders(E e) {
    int size = 0;
    Appender<E>[] appenderArray = (Appender[])this.appenderList.asTypedArray();
    int len = appenderArray.length;

    for(int i = 0; i < len; ++i) {
        appenderArray[i].doAppend(e);
        ++size;
    }

    return size;
}

4-4-1、日志格式化

上面看到最终输出日志的是ROOT_Logger,而ROOT中的 layout是 TTLLLayout,日志格式化的时候会调用doLayout 方法

@Override
public String doLayout(ILoggingEvent event) {
    if (!isStarted()) {
        return CoreConstants.EMPTY_STRING;
    }
    StringBuilder sb = new StringBuilder();

    long timestamp = event.getTimeStamp();

    sb.append(cachingDateFormatter.format(timestamp));
    sb.append(" [");
    sb.append(event.getThreadName());
    sb.append("] ");
    sb.append(event.getLevel().toString());
    sb.append(" ");
    sb.append(event.getLoggerName());
    sb.append(" - ");
    sb.append(event.getFormattedMessage());
    sb.append(CoreConstants.LINE_SEPARATOR);
    IThrowableProxy tp = event.getThrowableProxy();
    if (tp != null) {
        String stackTrace = tpc.convert(event);
        sb.append(stackTrace);
    }
    return sb.toString();
}

在这里插入图片描述



三、SpringBoot中的日志

使用SpringBoot就要引入相关的pom文件,这里需要把pom文件替换成下面的

<groupId>cn.xdx</groupId>
<artifactId>logLook</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
</properties>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/>
</parent>

<dependencies>
    <!--Use undertow, 设置服务器,和日志没关系哈-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

启动服务,使用日志功能,发现在不做任何配置的时候,它默认使用的是 logback打印

在这里插入图片描述


1、如何加载、选择日志工厂

1-1、前置:SpringBoot 自动加载流程

在SpringBoot项目启动的时候会自动做很多的操作,这里需要了解两点

  1. SpringFactoriesLoader 这个类会去加载所有 META-INF/spring.factories 文件,loadFactoryNames这个方法就是通过name找到所有spring.factories文件中对应的类。
  2. ApplicationListener 是一个接口,SpringBoot项目在启动的每个阶段都会投递事件到 onApplicationEvent 方法中。

在SpringBoot的 spring.factories 下有三个工厂构造器,启动的时候会把它们三个都加载进去(按照顺序加载第一个就是 logback)

在这里插入图片描述


在SpringBoot的中有一个 LoggingApplicationListener,它的继承关系如下,所以它本质上是一个ApplicationListener,并且重写了onApplicationEvent方法。

在这里插入图片描述


1-2、logback和log4j切换、默认的为何是logback

在原始日志中,知道LoggerFactory的实现类是取决于引用了什么包。而在SpringBoot项目中它默认就引入了 logback的包,所以它默认是用 logback。

如果在SpringBoot中想切换成 log4j,原理也是一样,去除logback的包,引入log4j

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
        <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-log4j</artifactId>
    <version>1.3.8.RELEASE</version>
</dependency>

1-3、准备:初始化LoggerFactory

其实在SpringBoot中 LoggerFactory的构建也是基于上面原生的方式,只不过在原生方式创建了 LoggerFactory之后,SpringBoot再基于自己的配置,去修改、填充LoggerFactory中配置。

  1. 基于原生方式构建出 LoggerFactory
  2. SpringBoot基于启动阶段来做初始化(LoggingApplicationListener)
public void onApplicationEvent(ApplicationEvent event) {
    // 启动时候的事件 —— 进行日志前置初始化
    if (event instanceof ApplicationStartingEvent) {
        this.onApplicationStartingEvent((ApplicationStartingEvent)event);
    } 
    // 环境变量准备好的事件 —— 进行日志初始化
    else if (event instanceof ApplicationEnvironmentPreparedEvent) {
        this.onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event);
    }
    //  全部启动好之后的事件 —— 暂不关注
    else if (event instanceof ApplicationPreparedEvent) {
        this.onApplicationPreparedEvent((ApplicationPreparedEvent)event);
    } else if (event instanceof ContextClosedEvent && ((ContextClosedEvent)event).getApplicationContext().getParent() == null) {
        this.onContextClosedEvent();
    } else if (event instanceof ApplicationFailedEvent) {
        this.onApplicationFailedEvent();
    }
}

1-4、前置:初始化LoggerFactory

  1. 找到 loggingSystem,这个就是SpringBoot对各种日志实现的包装,对应的包装就是对应的实现类
  2. 初始化前置,这里其实是一个空实现,如果有必要可以实现这个接口做一些操作
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
    this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    this.loggingSystem.beforeInitialize();
}

在这里插入图片描述

private static final LoggingSystemFactory SYSTEM_FACTORY = LoggingSystemFactory.fromSpringFactories();

public static LoggingSystem get(ClassLoader classLoader) {
    String loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);
    // 这里默认为 null
    if (StringUtils.hasLength(loggingSystemClassName)) {
        return (LoggingSystem)("none".equals(loggingSystemClassName) ? new LoggingSystem.NoOpLoggingSystem() : get(classLoader, loggingSystemClassName));
    } else {
        // 走这个
        LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);
        Assert.state(loggingSystem != null, "No suitable logging system located");
        return loggingSystem;
    }
}
public interface LoggingSystemFactory {
    LoggingSystem getLoggingSystem(ClassLoader classLoader);

    static LoggingSystemFactory fromSpringFactories() {
        // 创建一个 DelegatingLoggingSystemFactory
        return new DelegatingLoggingSystemFactory((classLoader) -> {
            // 这里就是前面说的,通过name去获取 spring.factories 对应的数据,有兴趣自己去看看
            // 这里获取的数据会按照 @Order 排序,但是这三个实现类的 @Order是一样的,所以是默认顺序
            return SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);
        });
    }
}

循环去遍历每一个factory,找到了就返回,其本质也是看当前项目下有没有具体的类

public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
    List<LoggingSystemFactory> delegates = this.delegates != null ? (List)this.delegates.apply(classLoader) : null;
    if (delegates != null) {
        Iterator var3 = delegates.iterator();

        while(var3.hasNext()) {
            LoggingSystemFactory delegate = (LoggingSystemFactory)var3.next();
            LoggingSystem loggingSystem = delegate.getLoggingSystem(classLoader);
            if (loggingSystem != null) {
                return loggingSystem;
            }
        }
    }

    return null;
}

在这里插入图片描述


logback和log4j的实现,就是判断isPresent中的这个权限定名有没有。

@Order(2147483647)
public static class Factory implements LoggingSystemFactory {
    private static final boolean PRESENT = ClassUtils.isPresent("ch.qos.logback.core.Appender", LogbackLoggingSystem.Factory.class.getClassLoader());

    public Factory() {
    }

    public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
        return PRESENT ? new LogbackLoggingSystem(classLoader) : null;
    }
}

@Order(2147483647)
public static class Factory implements LoggingSystemFactory {
    private static final boolean PRESENT = ClassUtils.isPresent("org.apache.logging.log4j.core.impl.Log4jContextFactory", Log4J2LoggingSystem.Factory.class.getClassLoader());

    public Factory() {
    }

    public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
        return PRESENT ? new Log4J2LoggingSystem(classLoader) : null;
    }
}

1-5、开始:初始化LoggerFactory

前面已经创建好了LoggerFactory > LoggerContext,这里的初始化是对LoggerContext里面的一些数据进行赋值,主要是读取配置文件 自定义的 logback.xml 和 application

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
   // 在前置里面已经生成了 loggingSystem,准确来说是它的实现类  LogbackLoggingSystem
   if (this.loggingSystem == null) {
      this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
   }
   // 初始化就是读取配置文件中的信息,来重新填充LoggerFactory——它的实现类 LoggerContext
   initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
}

如果你配置过logback.xml,那你肯定在application里面配置过它的位置logging.config: classpath:logback.xml

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
   // ... 
   initializeSystem(environment, this.loggingSystem, this.logFile);
   // ...
}


public static final String CONFIG_PROPERTY = "logging.config";

private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
   String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
   try {
      LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
      if (ignoreLogConfig(logConfig)) {
         system.initialize(initializationContext, null, logFile);
      }
      else {
         system.initialize(initializationContext, logConfig, logFile);
      }
   }
   catch (Exception ex) {
      // ...
   }
}

这里用的是 logback,所以最终是:org.springframework.boot.logging.logback.LogbackLoggingSystem#initialize

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
   LoggerContext loggerContext = getLoggerContext();
   if (isAlreadyInitialized(loggerContext)) {
      return;
   }
   super.initialize(initializationContext, configLocation, logFile);
   loggerContext.getTurboFilterList().remove(FILTER);
   markAsInitialized(loggerContext);
   if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {
      getLogger(LogbackLoggingSystem.class.getName()).warn("Ignoring '" + CONFIGURATION_FILE_PROPERTY
            + "' system property. Please use 'logging.config' instead.");
   }
}

2、如何解析配置信息、自定义配置信息


在读取配置文件的时候无非就几种情况,既然有多种情况,那肯定是有一个优先级的——即下面的排序

  1. 指定自己的配置文件 (logging.config: classpath:logback.xml)
  2. 默认读取的配置文件,本质上和【1】一样,只是文件的位置不同
    • logback自己的默认配置文件 (“logback-test.groovy”, “logback-test.xml”, “logback.groovy”, “logback.xml”)
    • 在Spring中 logback默认的配置文件(“logback-test-spring.groovy”, “logback-test-spring.xml”, “logback.groovy”, “logback-spring.xml”)
  1. 使用 application 配置
  2. 没有任何配置文件,默认策略

这也就是为什么说logback的在SpringBoot中的默认配置文件是 logback-spring.xml, 因为其它几个后缀基本上不会有。


从代码角度来看的logback日志配置也可以看成是:

  1. 读取 xml 配置
  2. 读取 application 配置

org.springframework.boot.logging.AbstractLoggingSystem#initialize

public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
   // 读取 logging.config 配置的文件 也就是 logback.xml
   if (StringUtils.hasLength(configLocation)) {
      initializeWithSpecificConfig(initializationContext, configLocation, logFile);
      return;
   }
   // 没有单独的配置文件,读取 默认/application 的配置
   initializeWithConventions(initializationContext, logFile);
}
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
   // 获取logback的默认配置  "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"
   String config = getSelfInitializationConfig();
   if (config != null && logFile == null) {
      // self initialization has occurred, reinitialize in case of property changes
      reinitialize(initializationContext);
      return;
   }
   if (config == null) {
      // 获取Srping的默认配置  logback-test-spring.groovy", "logback-test-spring.xml", "logback.groovy", "logback-spring.xml
      config = getSpringInitializationConfig();
   }
   if (config != null) {
      // 加载配置
      loadConfiguration(initializationContext, config, logFile);
      return;
   }
   // 默认的配置 application
   loadDefaults(initializationContext, logFile);
}

2-1、application 读取

先来看没有自定义配置的情况,也就是没有配置 logging.config,也没有读取到logback默认配置和spring中logback的默认配置


org.springframework.boot.logging.logback.LogbackLoggingSystem#loadDefaults

@Override
protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
   LoggerContext context = getLoggerContext();
   stopAndReset(context);
   boolean debug = Boolean.getBoolean("logback.debug");
   if (debug) {
      StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());
   }
   Environment environment = initializationContext.getEnvironment();
   // 读取 application 中的配置存入 LoggerContext 中
   new LogbackLoggingSystemProperties(environment, context::putProperty).apply(logFile);
   LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)
         : new LogbackConfigurator(context);
   // 基于读取到的配置进行配置
   new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);
   context.setPackagingDataEnabled(true);
}

2-1-1、读取 application 配置

这里的代码很清晰,具体如何去解析字段(比如 logging.pattern.console),可以自行去看。

public final void apply(LogFile logFile) {
   PropertyResolver resolver = getPropertyResolver();
   apply(logFile, resolver);
}

protected void apply(LogFile logFile, PropertyResolver resolver) {
   setSystemProperty(resolver, EXCEPTION_CONVERSION_WORD, "logging.exception-conversion-word");
   setSystemProperty(PID_KEY, new ApplicationPid().toString());
   setSystemProperty(resolver, CONSOLE_LOG_PATTERN, "logging.pattern.console");
   setSystemProperty(resolver, CONSOLE_LOG_CHARSET, "logging.charset.console", getDefaultCharset().name());
   setSystemProperty(resolver, LOG_DATEFORMAT_PATTERN, "logging.pattern.dateformat");
   setSystemProperty(resolver, FILE_LOG_PATTERN, "logging.pattern.file");
   setSystemProperty(resolver, FILE_LOG_CHARSET, "logging.charset.file", getDefaultCharset().name());
   setSystemProperty(resolver, LOG_LEVEL_PATTERN, "logging.pattern.level");
   applyDeprecated(resolver);
   if (logFile != null) {
      logFile.applyToSystemProperties();
   }
}

2-1-2、基于配置文件初始化

在开始之前需要回顾一下logback里面基本内容:

  1. Appender 日志如何输出,里面包含了输出的格式
  2. ROOT_log 日志是以包名来建立的层级,根目录是 ROOT
  3. 所以大部分的配置其实就是围绕上面两个对象

配置的主流程

  1. 把从application 中读取的内容变成配置
  2. 基于配置创建 Appender
  3. 把 Appender 放到 ROOT上
void apply(LogbackConfigurator config) {
   synchronized (config.getConfigurationLock()) {
      defaults(config);
      Appender<ILoggingEvent> consoleAppender = consoleAppender(config);
      if (this.logFile != null) {
         Appender<ILoggingEvent> fileAppender = fileAppender(config, this.logFile.toString());
         config.root(Level.INFO, consoleAppender, fileAppender);
      }
      else {
         config.root(Level.INFO, consoleAppender);
      }
   }
}

配置文件放入 LoggerContext

private void defaults(LogbackConfigurator config) {
   config.conversionRule("clr", ColorConverter.class);
   config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);
   config.conversionRule("wEx", ExtendedWhitespaceThrowableProxyConverter.class);
   config.getContext().putProperty("CONSOLE_LOG_PATTERN", resolve(config, "${CONSOLE_LOG_PATTERN:-"
         + "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) "
         + "%clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} "
         + "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"));
   config.getContext().putProperty("CONSOLE_LOG_CHARSET", resolve(config, "${CONSOLE_LOG_CHARSET:-default}"));
   config.getContext().putProperty("FILE_LOG_PATTERN", resolve(config, "${FILE_LOG_PATTERN:-"
         + "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] "
         + "%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"));
   config.getContext().putProperty("FILE_LOG_CHARSET", resolve(config, "${FILE_LOG_CHARSET:-default}"));
   config.logger("org.apache.catalina.startup.DigesterFactory", Level.ERROR);
   config.logger("org.apache.catalina.util.LifecycleBase", Level.ERROR);
   config.logger("org.apache.coyote.http11.Http11NioProtocol", Level.WARN);
   config.logger("org.apache.sshd.common.util.SecurityUtils", Level.WARN);
   config.logger("org.apache.tomcat.util.net.NioSelectorPool", Level.WARN);
   config.logger("org.eclipse.jetty.util.component.AbstractLifeCycle", Level.ERROR);
   config.logger("org.hibernate.validator.internal.util.Version", Level.WARN);
   config.logger("org.springframework.boot.actuate.endpoint.jmx", Level.WARN);
}

Appender 创建

private Appender<ILoggingEvent> consoleAppender(LogbackConfigurator config) {
   ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
   PatternLayoutEncoder encoder = new PatternLayoutEncoder();
   encoder.setPattern(resolve(config, "${CONSOLE_LOG_PATTERN}"));
   encoder.setCharset(resolveCharset(config, "${CONSOLE_LOG_CHARSET}"));
   config.start(encoder);
   appender.setEncoder(encoder);
   config.appender("CONSOLE", appender);
   return appender;
}

绑定到ROOT

final void root(Level level, Appender<ILoggingEvent>... appenders) {
   Logger logger = this.context.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
   if (level != null) {
      logger.setLevel(level);
   }
   for (Appender<ILoggingEvent> appender : appenders) {
      logger.addAppender(appender);
   }
}

2-2、xml 读取

读取文件的规则可以是很多,而且很复杂,我暂时不想去了解这么复杂的逻辑,所以这里只说明入口,不做深入。

不管是怎么得到的xml,最终解析入口都是

protected abstract void loadConfiguration(LoggingInitializationContext initializationContext, String location,
      LogFile logFile);

如果配置了 logging.config 那就直接读取到了xml,来看看没有配置的时候怎么读取到的默认文件

private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
   String config = getSelfInitializationConfig();
   if (config != null && logFile == null) {
      // self initialization has occurred, reinitialize in case of property changes
      reinitialize(initializationContext);
      return;
   }
   if (config == null) {
      config = getSpringInitializationConfig();
   }
   if (config != null) {
      loadConfiguration(initializationContext, config, logFile);
      return;
   }
   loadDefaults(initializationContext, logFile);
}


// logback默认配置的原因
protected String getSelfInitializationConfig() {
   return findConfig(getStandardConfigLocations());
}
@Override
protected String[] getStandardConfigLocations() {
   return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };
}

// spring默认配置,其实它就是获取logback的默认配置,然后加上一个【-spring】后缀
protected String getSpringInitializationConfig() {
   return findConfig(getSpringConfigLocations());
}
protected String[] getSpringConfigLocations() {
   String[] locations = getStandardConfigLocations();
   for (int i = 0; i < locations.length; i++) {
      String extension = StringUtils.getFilenameExtension(locations[i]);
      locations[i] = locations[i].substring(0, locations[i].length() - extension.length() - 1) + "-spring."
            + extension;
   }
   return locations;
}

3、日志的输出

在原生的日志输出里面已经讲解了整个流程,输出过程都是一样的。


K、扩展


1、log读取MDC和application的数据


在日志输出的时候可能会要设置一些自己的参数

  1. 比如全局 traceId,这种情况可以把它设置到MDC里面去,本质上是放入 ThreadLocal中
  2. 读取application中的数据比如 spring.application.name
spring:
  application:
    name: xdx97

logging:
  pattern:
    console: "[${spring.application.name}] [%X{userId}] [%thread] %-5level %logger{36} - %msg%n"

在这里插入图片描述


2、lomback 的 @Slf4j 注解


在项目中大多数时候并不是直接自己注入log类,而是使用 @Slf4j,其本质上是一样的。

但为什么在没有编译之前就可以使用log这个参数,大概率是idea做了什么操作吧,这个等后续有空再研究。

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值