实现自定义SpringBoot框架日志组件の一: LoggingSystem

系列

实现自定义SpringBoot框架日志组件の一:日志系统
实现自定义SpringBoot框架日志组件の二:配置文件
实现自定义SpringBoot框架日志组件の三: 自定义pattern
实现自定义SpringBoot框架日志组件の四: 自适应

目录

前言
日志组件目标
版本
spring boot 日志系统
YellowLog4J2LoggingSystem
多环境支持
激活彩色日志
开启异步
如何加载

前言

最近工作中要实现一个日志组件,实现后觉得效果还不错,所以写一篇博客分享给大家
因为公司屏幕带水印,好多截图无法直接上传,所以只好回家重新实现一遍,代码也传到了 github

项目地址

日志组件目标

  1. 替代spring默认的log,达到一个依赖即可拥有默认级别、输出样式等效果
  2. 针对不同环境,设置不同的配置,如生产环境不输出console,级别为info等
  3. 自定义一个输出 pattern,做一些自定义的规则

本文旨在实现目标1, 且日志组件的选型是 log4j2

版本

spring: 2.6.11
java: 11

spring boot 日志系统

想要实现自定义的日志组件,需要先了解一下 spring boot 的日志系统是怎么玩的

具体流程和源码看这篇博客 Springboot 源码分析之log配置加载
这篇博客spring的版本有点老了,和比较新的spring代码对不上,但是大致流程是一样的,不影响
并且本文用的spring版本也不是最新的,大家实际实现的时候也需要根据spring源码做相应的适配(如果需要的话)

这里简单介绍一下spring的日志系统流程:

  1. 初始化日志系统 LoggingSystem
    入口在 org.springframework.boot.context.logging.LoggingApplicationListener#onApplicationEnvironmentPreparedEvent, 根据实际依赖的日志实现选择不同的日志组件,如logback、log4j2等
  2. 根据不同的日志系统加载不同的日志配置文件
  3. 配置文件是否指定logging.config, 如果指定,直接读取、加载,结束
  4. 没有指定的话,加载LoggingSystem实现类提供的文件名列表(log4j2.xml log4j2.json等),如果存在就加载
  5. 还是没有的话,加载带spring后缀的文件,log4j2-spring.xml,存在就加载
  6. 还是不存在,就加载默认的(loadDefault),配置文件在jar包里

根据上面的流程,我们想要达到目标,实现思路就是:

  1. 自定义一个日志系统
  2. 重载 loadDefault, 加载我们自己的配置文件
  3. 根据不同的环境(spring.profile),加载不同的配置文件(功能点:多环境支持)
  4. 代码里直接开启log4j2的异步功能(默认需要配置文件主动开启)(功能点:异步)
  5. 代码里直接开启log4j2的彩色控制台功能(默认需要配置文件主动开启)(功能点:彩色日志)

YellowLog4J2LoggingSystem

因为我们的日志系统只需要重载 loadDefault,其它功能都不变,那么直接继承Log4J2LoggingSystem就行

我们先看一下Log4J2LoggingSystemloadDefault代码

@Override
    protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
        if (logFile != null) {
            // 配置了日志输出文件
            loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile, getOverrides(initializationContext));
        }
        else {
            // 什么都没有配置
            loadConfiguration(getPackagedConfigFile("log4j2.xml"), logFile, getOverrides(initializationContext));
        }
    }

可以看出,我们要修改的逻辑就是:改变else里面的逻辑

而且我们看源码可以发现,代码里的 log4j2.xml 文件其实在这个jar包里

image.png

到了这里,我们就有了下面的代码

package com.github.hwhaocool;

import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LoggingInitializationContext;
import org.springframework.boot.logging.log4j2.Log4J2LoggingSystem;

import java.util.Collections;
import java.util.List;

/**
 * @author yellowtail
 * @since 2022/8/21 17:48
 */
public class YellowLog4J2LoggingSystem extends Log4J2LoggingSystem {

    public YellowLog4J2LoggingSystem(ClassLoader classLoader) {
        super(classLoader);
    }

    @Override
    protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
        if (logFile != null) {
            // 配置了日志输出文件
            loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile, getOverrides(initializationContext));
        }
        else {
            // 什么都没有配置
            loadConfiguration(getPackagedConfigFile("log4j2.xml"), logFile, getOverrides(initializationContext));
        }
    }

    // 直接复制的,没有做任何改变
    private List<String> getOverrides(LoggingInitializationContext initializationContext) {
        BindResult<List<String>> overrides = Binder.get(initializationContext.getEnvironment())
                .bind("logging.log4j2.config.override", Bindable.listOf(String.class));
        return overrides.orElse(Collections.emptyList());
    }
}

多环境支持

logback 的配置文件里支持标签 SpringProfile, 但是 log4j2 默认是不支持的,所以多环境必须通过多个文件来实现
还好LoggingInitializationContext initializationContext 里面有Environment,里面有当前环境的信息

/**
 * 得到当前的环境(spring.profile.active)
 * @param environment
 * @return
 */
private ProfileEnum getProfile(Environment environment) {
    String[] activeProfiles = environment.getActiveProfiles();

    if (null == activeProfiles || activeProfiles.length == 0) {
        return ProfileEnum.LOCAL;
    }

    switch (activeProfiles[0]) {
        case "local":
            return ProfileEnum.LOCAL;
        case "dev":
        case "develop":
            return ProfileEnum.DEV;
        case "prod":
        case "prd":
        case "product":
        default:
            return ProfileEnum.PROD;
    }
}

static enum ProfileEnum{
    /**
     * 本地debug环境
     */
    LOCAL,
    /**
     * dev环境
     */
    DEV,
    /**
     * 生产 环境
     */
    PROD
}

不同环境使用不同的配置文件

private String getConfigFile(ProfileEnumprofile) {
        switch (profile) {
            case DEV:
                return "log4j2-dev.xml";
            case PROD:
                return "log4j2-prod.xml";
            case LOCAL:
            default:
                return "log4j2-local.xml";
        }
    }

激活彩色日志

默认是不开启的(因为影响性能),需要手动开启

/**
 * 激活彩色日志
 * @param profile
 */
private void enableColor(ProfileEnum profile) {
    if (ProfileEnum.LOCAL == profile || ProfileEnum.DEV == profile) {
        System.setProperty("log4j.skipJansi", "false");
    }
}

开启异步

官方文档
默认是不开启的,需要手动开启
-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector or

用环境变量是无法直接开启的,需要通过配置文件log4j2.component.properties (这个信息我是怎么知道的呢?去 github看源码,看一下log4j2.contextSelector 是从哪里读取的)

log4j2.component.properties

log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

log4j2.formatMsgNoLookups=true

还需要主动引入依赖

<!-- log4j2用到的高性能队列  -->
            <dependency>
                <groupId>com.lmax</groupId>
                <artifactId>disruptor</artifactId>
                <version>3.4.4</version>
            </dependency>

如何加载

现在我们实现完了,那么如何让spring加载它呢?或者说,因为我们是继承Log4J2LoggingSystem的,我们如何先于默认的加载呢?

这里就要借助spring.factories
我们看一下Log4J2LoggingSystem的 useage 就知道了

image.png

# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

可以看到每一行后面还有一个Factory
这里是一个内部静态类,是用来构建日志系统的,spring加载的时候会执行,不写会报错

@Order(Ordered.LOWEST_PRECEDENCE)
    public static class Factory implements LoggingSystemFactory {

        private static final boolean PRESENT = ClassUtils
                .isPresent("org.apache.logging.log4j.core.impl.Log4jContextFactory", Factory.class.getClassLoader());

        @Override
        public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
            if (PRESENT) {
                return new Log4J2LoggingSystem(classLoader);
            }
            return null;
        }

    }

代码很好理解

  • PRESENT, 是否存在日志组件
  • getLoggingSystem, 构建并返回日志系统

且我们看到有一个Order注解,这个就是每个日志组件的优先级,spring提供和的是LOWEST_PRECEDENCE, 就是最低优先级的,我们只需要改一下优先级就可以了

注意:order的优先级是 数值越大,优先级月底;数值越小,优先级越高

所以我们的代码最后如下

package com.github.hwhaocool;

import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LoggingInitializationContext;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemFactory;
import org.springframework.boot.logging.log4j2.Log4J2LoggingSystem;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.util.ClassUtils;

import java.util.Collections;
import java.util.List;

/**
 * @author yellowtail
 * @since 2022/8/21 17:48
 */
public class YellowLog4J2LoggingSystem extends Log4J2LoggingSystem {

    public YellowLog4J2LoggingSystem(ClassLoader classLoader) {
        super(classLoader);
    }

    @Override
    protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
        if (logFile != null) {
            // 配置了日志输出文件
            loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile, getOverrides(initializationContext));
        }
        else {
            // 什么都没有配置

            // 1. 得到当前的环境(spring.profile.active)
            ProfileEnum profile = getProfile(initializationContext.getEnvironment());

            // 2. 当前环境应该加载的配置文件
            String logConfigFile = getConfigFile(profile);

            // 3. local dev 激活彩色console日志
            enableColor(profile);

            // 4. 开启异步
            enableAsync();

            // 5. 加载
            loadConfiguration(getPackagedConfigFile(logConfigFile), logFile, getOverrides(initializationContext));

            // 6. 自适应包名
            autofixPackage();
        }
    }

    private void autofixPackage() {
        // TODO: 待实现
    }

    /**
     * 开启异步
     */
    private void enableAsync() {
        System.setProperty("log4j2.contextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
    }

    /**
     * 激活彩色日志
     * @param profile
     */
    private void enableColor(ProfileEnum profile) {
        if (ProfileEnum.LOCAL == profile || ProfileEnum.DEV == profile) {
            System.setProperty("log4j.skipJansi", "false");
        }
    }

    /**
     * 当前环境应该加载的配置文件
     * @param profile
     * @return
     */
    private String getConfigFile(ProfileEnum profile) {
        switch (profile) {
            case DEV:
                return "log4j2-dev.xml";
            case PROD:
                return "log4j2-prod.xml";
            case LOCAL:
            default:
                return "log4j2-local.xml";
        }
    }

    /**
     * 得到当前的环境(spring.profile.active)
     * @param environment
     * @return
     */
    private ProfileEnum getProfile(Environment environment) {
        String[] activeProfiles = environment.getActiveProfiles();

        if (null == activeProfiles || activeProfiles.length == 0) {
            return ProfileEnum.LOCAL;
        }

        switch (activeProfiles[0]) {
            case "local":
                return ProfileEnum.LOCAL;
            case "dev":
            case "develop":
                return ProfileEnum.DEV;
            case "prod":
            case "prd":
            case "product":
            default:
                return ProfileEnum.PROD;
        }
    }

    // 直接复制的,没有做任何改变
    private List<String> getOverrides(LoggingInitializationContext initializationContext) {
        BindResult<List<String>> overrides = Binder.get(initializationContext.getEnvironment())
                .bind("logging.log4j2.config.override", Bindable.listOf(String.class));
        return overrides.orElse(Collections.emptyList());
    }

    static enum ProfileEnum {
        /**
         * 本地debug环境
         */
        LOCAL,
        /**
         * dev环境
         */
        DEV,
        /**
         * 生产 环境
         */
        PROD
    }

    /**
     * 初始化YellowLog4J2LoggingSystem
     * order 调整一下,稍微加一点优先级
     * @author yellowtail
     */
    @Order(Ordered.LOWEST_PRECEDENCE-100)
    public static class Factory implements LoggingSystemFactory {

        private static final boolean PRESENT = ClassUtils
                .isPresent("org.apache.logging.log4j.core.impl.Log4jContextFactory", Log4J2LoggingSystem.Factory.class.getClassLoader());

        @Override
        public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
            if (PRESENT) {
                return new YellowLog4J2LoggingSystem(classLoader);
            }
            return null;
        }

    }
}

其中6. 自适应包名 暂时没有实现,实现起来有点繁琐,后面讲到,先忽略

自适应包名:我们作为一个框架组件,其实是不知道使用方的 package 的,而我们日志配置文件里又都是通过 pakcage 来指定级别的,所以我们需要去获取包名,然后根据不同的环境去设置日志级别

并且有一个文件 src\main\resources\META-INF\spring.factories

# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
com.github.hwhaocool.YellowLog4J2LoggingSystem.Factory
image.png

到这里自定义日志系统就实现完成了,当然了我们目前还缺一些功能

  1. 各个 Log4j2.xml
  2. 自适应包名

下一篇的博客会讲到

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容