前言

在这里插入图片描述

本文为 【Java日志框架】 相关知识,之前已经介绍了Java日志框架的发展历史:Java日志框架的发展历史
这篇文章将对日志的概念与作用JUL日志框架Log4j日志框架Logback日志框架Log4j2日志框架日志门面Slf4j日志门面JCL日志门面等进行详尽介绍~

📌博主主页:小新要变强 的主页
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
👉Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)


目录

在这里插入图片描述

一、日志的概念

1️⃣日志概述

日志文件是用于记录系统操作事件的文件集合,可分为事件日志和消息日志。具有处理历史数据、诊断问题的追踪以及理解系统的活动等重要作用。

​在计算机中,日志文件是记录在操作系统或其他软件运行中发生的事件或在通信软件的不同用户之间的消息的文件。记录是保持日志的行为。在最简单的情况下,消息被写入单个日志文件。

2️⃣日志的作用

  • 调试。 在Java项目调试时,查看栈信息可以方便地知道当前程序的运行状态,输出的日志便于记录程序在之前的运行结果。如果你大量使用System.out或者System.err,这是一种最方便最有效的方法,但显得不够专业。
  • 错误定位。 不要以为项目能正确跑起来就可以高枕无忧,项目在运行一段时候后,可能由于数据问题,网络问题,内存问题等出现异常。这时日志可以帮助开发或者运维人员快速定位错误位置,提出解决方案。
  • 数据分析。 大数据的兴起,使得大量的日志分析成为可能,ELK也让日志分析门槛降低了很多。日志中蕴含了大量的用户数据,包括点击行为,兴趣偏好等,用户画像对于公司下一步的战略方向有一定指引作用。

3️⃣接触过的日志

🍀最简单的日志输出方式

最简单的日志输出方式,我们每天都在使用:

System.out.println("这个数的结果是:"+ num);

以及错误日志:

System.err.println("此处发生了异常");

此类代码在程序的执行过程中没有什么实质的作用,但是却能打印一些中间变量,辅助我们调试和错误的排查。

🍀Tomcat中的日志系统

日志系统我们在tomcat中也见过:
在这里插入图片描述

当我们的程序无法启动或者运行过程中产生问题,会有所记录,比如我的catalina.log中查看,发现确实有错误信息,这能帮我们迅速定位:
在这里插入图片描述
而我们的System.err只能做到控制台打印日志,所以我们需要更强大日志框架来处理

4️⃣主流日志框架

  • 日志实现(具体干活的): JUL(java util logging)、logback、log4j、log4j2
  • 日志门面(指定规则的): JCL(Jakarta Commons Logging)、slf4j( Simple Logging Facade for Java)

二、JUL日志框架

JUL全称Java util Logging是java原生的日志框架,使用时不需要另外引用第三方类库,相对其他日志框 架使用方便,学习简单,能够在小型应用中灵活使用。

在JUL中有以下组件,我们先做了解,慢慢学习:

  • Loggers: 被称为记录器,应用程序通过获取Logger对象,调用其API来来发布日志信息。Logger 通常时应用程序访问日志系统的入口程序。
  • Appenders: 也被称为Handlers,每个Logger都会关联一组Handlers,Logger会将日志交给关联 Handlers处理,由Handlers负责将日志做记录。Handlers在此是一个抽象,其具体的实现决定了日志记录的位置可以是控制台、文件、网络上的其他日志服务或操作系统日志等。
  • Layouts: 也被称为Formatters,它负责对日志事件中的数据进行转换和格式化。Layouts决定了 数据在一条日志记录中的最终形式。
  • Level: 每条日志消息都有一个关联的日志级别。该级别粗略指导了日志消息的重要性和紧迫,我 可以将Level和Loggers,Appenders做关联以便于我们过滤消息。
  • Filters: 过滤器,根据需要定制哪些信息会被记录,哪些信息会被放过。

总结一下就是:

​用户使用Logger来进行日志记录,Logger持有若干个Handler,日志的输出操作是由Handler完成的。 在Handler在输出日志前,会经过Filter的过滤,判断哪些日志级别过滤放行哪些拦截,Handler会将日志内容输出到指定位置(日志文件、控制台等)。Handler在输出日志时会使用Layout,将输出内容进行排版。

1️⃣JUL入门

public static void main(String[] args) {
    Logger logger = Logger.getLogger("myLogger");
    logger.info("信息");
    logger.warning("警告信息");
    logger.severe("严重信息");
}

在这里插入图片描述

2️⃣日志的级别

​ jul中定义的日志级别,从上述例子中我们也看到使用info和warning打印出的日志有不同的前缀,通过给日志设置不同的级别可以清晰的从日志中区分出哪些是基本信息,哪些是调试信息,哪些是严重的异常。

java.util.logging.Level中定义了日志的级别:

  • (1)SEVERE(最高值)
  • (2)WARNING
  • (3)INFO (默认级别)
  • (4)CONFIG
  • (5)FINE
  • (6)FINER
  • (7)FINEST(最低值)

还有两个特殊的级别:

  • (8)OFF,可用来关闭日志记录
  • (9)ALL,启用所有消息的日志记录

我们测试一下7个日志级别:

@Test
public void testLogger() {
    Logger logger = Logger.getLogger(LoggerTest.class.getName());
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

我们发现能够打印的只有三行,这是为什么呢?

在这里插入图片描述

我们找一下如下图jdk11的日志配置文件:
在这里插入图片描述

或者在jdk1.8中:
在这里插入图片描述

就可以看到系统默认在控制台打印的日志级别了,系统配置我们暂且不动。

但是我们可以简单的看看这个日志配置了哪些内容:

.level= INFO

############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################

# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
# Default number of locks FileHandler can obtain synchronously.
# This specifies maximum number of attempts to obtain lock file by FileHandler
# implemented by incrementing the unique field %u as per FileHandler API documentation.
java.util.logging.FileHandler.maxLocks = 100
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

在日志中我们发现了,貌似可以给这个日志对象添加各种handler就是处理器,比如ConsoleHandler专门处理控制台日志,FileHandler貌似可以处理文件,同时我们确实发现了他有这么一个方法:

在这里插入图片描述

3️⃣日志的配置

对日志进行相关配置:

@Test
public void testLogConfig() throws Exception {
    // 1.创建日志记录器对象
    Logger logger = Logger.getLogger("com.ydlclass.log.JULTest");
    // 一、自定义日志级别
    // a.关闭系统默认配置
    logger.setUseParentHandlers(false);
    // b.创建handler对象
    ConsoleHandler consoleHandler = new ConsoleHandler();
    // c.创建formatter对象
    SimpleFormatter simpleFormatter = new SimpleFormatter();
    // d.进行关联
    consoleHandler.setFormatter(simpleFormatter);
    logger.addHandler(consoleHandler);
    // e.设置日志级别
    logger.setLevel(Level.ALL);
    consoleHandler.setLevel(Level.ALL);
    // 二、输出到日志文件
    FileHandler fileHandler = new FileHandler("d:/logs/jul.log");
    fileHandler.setFormatter(simpleFormatter);
    logger.addHandler(fileHandler);
    // 2.日志记录输出
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

再看一下打印结果:

10月 21, 2021 11:50:01 上午 com.ydlclass.entity.LoggerTest testConfig
严重: severe
10月 21, 2021 11:50:01 上午 com.ydlclass.entity.LoggerTest testConfig
警告: warning
10月 21, 2021 11:50:01 上午 com.ydlclass.entity.LoggerTest testConfig
信息: info
10月 21, 2021 11:50:01 上午 com.ydlclass.entity.LoggerTest testConfig
配置: config
10月 21, 2021 11:50:01 上午 com.ydlclass.entity.LoggerTest testConfig
详细: fine
10月 21, 2021 11:50:01 上午 com.ydlclass.entity.LoggerTest testConfig
较详细: finer
10月 21, 2021 11:50:01 上午 com.ydlclass.entity.LoggerTest testConfig
非常详细: finest

Process finished with exit code 0

文件中也输出了同样的结果:

在这里插入图片描述

4️⃣ Logger之间的父子关系

JUL中Logger之间存在父子关系,这种父子关系通过树状结构存储,JUL在初始化时会创建一个顶层 RootLogger作为所有Logger父Logger,存储上作为树状结构的根节点。并父子关系通过名称来关联。默认子Logger会继承父Logger的属性。

在这里插入图片描述
所有的logger实例都是由LoggerManager统一管理,不妨我们点进getLogger方法:

private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
    LogManager manager = LogManager.getLogManager();
    if (!SystemLoggerHelper.disableCallerCheck) {
        if (isSystem(caller.getModule())) {
            return manager.demandSystemLogger(name, resourceBundleName, caller);
        }
    }
    return manager.demandLogger(name, resourceBundleName, caller);
    // ends up calling new Logger(name, resourceBundleName, caller)
    // iff the logger doesn't exist already
}

我们可以看到LogManager是单例的:

public static LogManager getLogManager() {
    if (manager != null) {
        manager.ensureLogManagerInitialized();
    }
    return manager;
}
@Test
public void testLogParent() throws Exception {
    Logger logger1 = Logger.getLogger("com.ydlclass.service");
    Logger logger2 = Logger.getLogger("com.ydlclass");
    System.out.println("logger1 = " + logger1);
    System.out.println("logger1.getParent() = " + logger1.getParent());
    System.out.println("logger2 = " + logger2);
    System.out.println("logger2.getParent() = " + logger2.getParent());
    System.out.println(logger1.getParent() == logger2);
}

结果:
logger1 = java.util.logging.Logger@2b4bac49
logger1.getParent() = java.util.logging.Logger@fd07cbb
logger2 = java.util.logging.Logger@fd07cbb
logger2.getParent() = java.util.logging.LogManager$RootLogger@3571b748
true
@Test
public void testLogParent() throws Exception {
    Logger logger1 = Logger.getLogger("com.ydlclass.service");
    Logger logger2 = Logger.getLogger("com.ydlclass");
    // 一、对logger2进行独立的配置
    // 1.关闭系统默认配置
    logger2.setUseParentHandlers(false);
    // 2.创建handler对象
    ConsoleHandler consoleHandler = new ConsoleHandler();
    // 3.创建formatter对象
    SimpleFormatter simpleFormatter = new SimpleFormatter();
    // 4.进行关联
    consoleHandler.setFormatter(simpleFormatter);
    logger2.addHandler(consoleHandler);
    // 5.设置日志级别
    logger2.setLevel(Level.ALL);
    consoleHandler.setLevel(Level.ALL);
    // 测试logger1是否被logger2影响
    logger1.severe("severe");
    logger1.warning("warning");
    logger1.info("info");
    logger1.config("config");
    logger1.fine("fine");
    logger1.finer("finer");
    logger1.finest("finest");
}
    
结果:    
1021, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent
严重: severe
1021, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent
警告: warning
1021, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent
信息: info
1021, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent
配置: config
1021, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent
详细: fine
1021, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent
较详细: finer
1021, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent
非常详细: finest

Process finished with exit code 0

5️⃣日志格式化

我们可以独立的实现日志格式化的Formatter,而不使用SimpleFormatter,我们可以做如下处理,最后返回的结果我们可以随意拼写:

Formatter myFormatter = new Formatter(){
    @Override
    public String format(LogRecord record) {
        return record.getLoggerName()+"." +record.getSourceMethodName() + " " + LocalDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault())+"\r\n"
            +record.getLevel()+": " +record.getMessage() + "\r\n";
    }
};

日志打印结果为:

在这里插入图片描述

当然我们参考一下SimpleFormatter的该方法的实现:

// format string for printing the log record
static String getLoggingProperty(String name) {
    return LogManager.getLogManager().getProperty(name);
}

private final String format =
    SurrogateLogger.getSimpleFormat(SimpleFormatter::getLoggingProperty);

ZonedDateTime zdt = ZonedDateTime.ofInstant(
    record.getInstant(), ZoneId.systemDefault());
return String.format(format,
                     zdt,
                     source,
                     record.getLoggerName(),
                     record.getLevel().getLocalizedLevelName(),
                     message,
                     throwable);

这个写法貌似比我们的写法高级一点,所以我们必须好好学一下String的format方法了。

🍀(1)String的format方法

String类的format()方法用于创建格式化的字符串以及连接多个字符串对象。

format()方法有两种重载形式:

public static String format(String format, Object... args) {
    return new Formatter().format(format, args).toString();
}

public static String format(Locale l, String format, Object... args) {
    return new Formatter(l).format(format, args).toString();
}

在这个方法中我们可以定义字符串模板,然后使用类似填空的方式将模板格式化成我们想要的结果字符串:

String java = String.format("hello %s", "world");

得到的结果就是hello world,我们可以把第一个参数当做模板, %s当做填空题,后边的可变参数当做答案。

🍀(2)常用的转换符

当然不同数据类型需要不同转换符完成字符串的转换,以下是不同类型的转化符列表:

转换符详细说明示例
%s字符串类型
%c字符类型‘m’
%b布尔类型true
%d整数类型(十进制)88
%x整数类型(十六进制)FF
%o整数类型(八进制)77
%f浮点类型8.888
%a十六进制浮点类型FF.35AE
%e指数类型9.38e+5
%n换行符
%tx日期与时间类型(x代表不同的日期与时间转换符)后边详细说

小例子:

System.out.printf("过年了,%s今年%d岁了,今天收了%f元的压岁钱!",
                "小明",5,88.88);

结果:
    过年了,小明今年5岁了,今天收了88.880000元的压岁钱!

🍀(3)特殊符号

接下来我们看几个特殊字符的常用搭配,可以实现一些高级功能:

标志说明示例结果
+为正数或者负数添加符号,因为一般整数不会主动加符号(“%+d”,15)+15
0数字前面补0,用于对齐(“%04d”, 99)0099
空格在整数之前添加指定数量的空格(“%4d”, 99)99
,以“,”对数字分组(常用显示金额)(“%,f”, 9999.99)9,999.990000
(使用括号包含负数(“%(f”, -99.99)(99.990000)
System.out.printf("过年了,%s今年%03d岁了,今天收了%,f元的压岁钱!",
                "小明",5,8888.88);
                
结果:
    过年了,小明今年005岁了,今天收了8,888.880000元的压岁钱!  

默认情况下,我们的可变参数是安装顺序依次替换,但是我想重复利用可变参数那该怎么处理呢?

我们可以采用在转换符中加数字$完成匹配:

System.out.printf("%1$s %1$s %1$s","小明");

其中1$就代表第一个参数,那么2$就代表第二个参数了:

结果:
    小明 小明 小明

🍀(4)日期处理

第一个例子中有说到 %tx,其中x代表日期转换符,下面顺便列举下日期转换符:

标志说明示例
c包括全部日期和时间信息周四 10月 21 14:52:10 GMT+08:00 2021
F“年-月-日”格式2021-10-21
D“月/日/年”格式10/21/21
r“HH:MM:SS PM”格式(12时制)02:53:20 下午
T“HH:MM:SS”格式(24时制)14:53:39
R“HH:MM”格式(24时制)14:53
b月份本地化10月
y两位的年21
Y四位的年2021
m10
d21
H24小时制的时14
l12小时制的时2
M57
S46
s秒为单位的时间戳1634799527
p上午还是下午下午

我们可以使用以下三个类去进行格式化,其中可能存在不支持的情况,比如LocalDateTime不支持c:

System.out.printf("%tc",new Date());
System.out.printf("%tc",ZonedDateTime.now());
System.out.printf("%tF",LocalDateTime.now());

此时我们使用debug查看,默认情况下的fomat,我们看一看:

在这里插入图片描述

10月 21, 2021 2:23:42 下午 com.ydlclass.entity.LoggerTest testLogParent
警告: warning

6️⃣配置文件

我们看看一个文件处理器的源码是怎么读配置项的:

private void configure() {
        LogManager manager = LogManager.getLogManager();

        String cname = getClass().getName();

        pattern = manager.getStringProperty(cname + ".pattern", "%h/java%u.log");
        limit = manager.getLongProperty(cname + ".limit", 0);
        if (limit < 0) {
            limit = 0;
        }
        count = manager.getIntProperty(cname + ".count", 1);
        if (count <= 0) {
            count = 1;
        }
        append = manager.getBooleanProperty(cname + ".append", false);
        setLevel(manager.getLevelProperty(cname + ".level", Level.ALL));
        setFilter(manager.getFilterProperty(cname + ".filter", null));
        setFormatter(manager.getFormatterProperty(cname + ".formatter", new XMLFormatter()));
        // Initialize maxLocks from the logging.properties file.
        // If invalid/no property is provided 100 will be used as a default value.
        maxLocks = manager.getIntProperty(cname + ".maxLocks", MAX_LOCKS);
        if(maxLocks <= 0) {
            maxLocks = MAX_LOCKS;
        }
        try {
            setEncoding(manager.getStringProperty(cname +".encoding", null));
        } catch (Exception ex) {
            try {
                setEncoding(null);
            } catch (Exception ex2) {
                // doing a setEncoding with null should always work.
                // assert false;
            }
        }
    }

可以从以下源码中看到配置项:

public class FileHandler extends StreamHandler {
    private MeteredStream meter;
    private boolean append;
    // 限制文件大小
    private long limit;       // zero => no limit.
    // 控制日志文件的数量
    private int count;
    // 日志文件的格式化方式
    private String pattern;
    private String lockFileName;
    private FileChannel lockFileChannel;
    private File files[];
    private static final int MAX_LOCKS = 100;
    // 可以理解为同时可以有多少个线程打开文件,源码中有介绍
    private int maxLocks = MAX_LOCKS;
    private static final Set<String> locks = new HashSet<>();
}

我们已经知道系统默认的配置文件的位置,那我们能不能自定义呢?当然可以了,我们从jdk中赋值一个配置文件过来:

.level= INFO

# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1

# Default number of locks FileHandler can obtain synchronously.
# This specifies maximum number of attempts to obtain lock file by FileHandler
# implemented by incrementing the unique field %u as per FileHandler API documentation.
java.util.logging.FileHandler.maxLocks = 100
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
pattern = manager.getStringProperty(cname + ".pattern", "%h/java%u.log");
static File generate(String pat, int count, int generation, int unique)
            throws IOException
{
    Path path = Paths.get(pat);
    Path result = null;
    boolean sawg = false;
    boolean sawu = false;
    StringBuilder word = new StringBuilder();
    Path prev = null;
    for (Path elem : path) {
        if (prev != null) {
            prev = prev.resolveSibling(word.toString());
            result = result == null ? prev : result.resolve(prev);
        }
        String pattern = elem.toString();
        int ix = 0;
        word.setLength(0);
        while (ix < pattern.length()) {
            char ch = pattern.charAt(ix);
            ix++;
            char ch2 = 0;
            if (ix < pattern.length()) {
                ch2 = Character.toLowerCase(pattern.charAt(ix));
            }
            if (ch == '%') {
                if (ch2 == 't') {
                    String tmpDir = System.getProperty("java.io.tmpdir");
                    if (tmpDir == null) {
                        tmpDir = System.getProperty("user.home");
                    }
                    result = Paths.get(tmpDir);
                    ix++;
                    word.setLength(0);
                    continue;
                } else if (ch2 == 'h') {
                    result = Paths.get(System.getProperty("user.home"));
                    if (jdk.internal.misc.VM.isSetUID()) {
                        // Ok, we are in a set UID program.  For safety's sake
                        // we disallow attempts to open files relative to %h.
                        throw new IOException("can't use %h in set UID program");
                    }
                    ix++;
                    word.setLength(0);
                    continue;
                } else if (ch2 == 'g') {
                    word = word.append(generation);
                    sawg = true;
                    ix++;
                    continue;
                } else if (ch2 == 'u') {
                    word = word.append(unique);
                    sawu = true;
                    ix++;
                    continue;
                } else if (ch2 == '%') {
                    word = word.append('%');
                    ix++;
                    continue;
                }
            }
            word = word.append(ch);
        }
        prev = elem;
    }

    if (count > 1 && !sawg) {
        word = word.append('.').append(generation);
    }
    if (unique > 0 && !sawu) {
        word = word.append('.').append(unique);
    }
    if (word.length() > 0) {
        String n = word.toString();
        Path p = prev == null ? Paths.get(n) : prev.resolveSibling(n);
        result = result == null ? p : result.resolve(p);
    } else if (result == null) {
        result = Paths.get("");
    }

    if (path.getRoot() == null) {
        return result.toFile();
    } else {
        return path.getRoot().resolve(result).toFile();
    }
}
System.out.println(System.getProperty("user.home") );

我们将拷贝的文件稍作修改:

.level= INFO

# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = D:/log/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.maxLocks = 100
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
@Test
public void testProperties() throws Exception {
    // 读取自定义配置文件
    InputStream in =
        JULTest.class.getClassLoader().getResourceAsStream("logging.properties");
    // 获取日志管理器对象
    LogManager logManager = LogManager.getLogManager();
    // 通过日志管理器加载配置文件
    logManager.readConfiguration(in);
    Logger logger = Logger.getLogger("com.ydlclass.log.JULTest");
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

配置文件:

handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler
.level= INFO

java.util.logging.FileHandler.pattern = D:/logs/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.maxLocks = 100
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

在这里插入图片描述
文件中也出现了:

在这里插入图片描述

打开日志发现是xml,因为这里用的就是XMLFormatter:

在这里插入图片描述

上边我们配置了两个handler给根Logger,我们还可以给其他的Logger做独立的配置:

handlers = java.util.logging.ConsoleHandler
.level = INFO
# 对这个logger独立配置
com.ydlclass.handlers = java.util.logging.FileHandler
com.ydlclass.level = ALL
com.ydlclass.useParentHandlers = false

# 修改了名字
java.util.logging.FileHandler.pattern = D:/logs/ydl-java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.maxLocks = 100
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 文件使用追加方式
java.util.logging.FileHandler.append = true

# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# 修改日志格式
java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

执行发现控制台没有内容,文件中有了,说明没有问题OK了:

在这里插入图片描述

日志出现以下内容:

在这里插入图片描述

三、Log4j日志框架

Log4j是Apache下的一款开源的日志框架。 官方网站:http://logging.apache.org/log4j/1.2/ ,这是一款比较老的日志框架,目前新的log4j2做了很大的改动,任然有一些项目在使用log4j。

1️⃣Log4j入门

🍀(1)建立maven工程

🍀(2)添加相关依赖

<dependencies>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>${maven.compiler.source}</source>
                <target>${maven.compiler.target}</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

🍀(3)编写java代码

@Test
public void testLogger() {
    Logger logger = Logger.getLogger(Log4jTest.class);
    // 日志记录输出
    logger.info("hello log4j");
    // 日志级别
    logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃和终止运行
    logger.error("error"); // 错误信息,但不会影响系统运行
    logger.warn("warn"); // 警告信息,可能会发生问题
    logger.info("info"); // 程序运行信息,数据库的连接、网络、IO操作等
    logger.debug("debug"); // 调试信息,一般在开发阶段使用,记录程序的变量、参数等
    logger.trace("trace"); // 追踪信息,记录程序的所有流程信息
}

发现会有一些警告,JUL可以直接在控制台输出是因为他有默认的配置文件,而这个独立的第三方的日志框架却没有配置文件:

log4j:WARN No appenders could be found for logger (com.wang.entity.Log4jTest).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

我们在执行代码之前,加上以下代码,它会初始化一个默认配置:

BasicConfigurator.configure();

🍀(4)结果与分析

0 [main] INFO com.wang.entity.Log4jTest  - hello log4j
1 [main] FATAL com.wang.entity.Log4jTest  - fatal
1 [main] ERROR com.wang.entity.Log4jTest  - error
1 [main] WARN com.wang.entity.Log4jTest  - warn
1 [main] INFO com.wang.entity.Log4jTest  - info
1 [main] DEBUG com.wang.entity.Log4jTest  - debug

从源码看,这一行代码给我们的RootLogger加入一个控制台的输出源,就和JUL中的handler一样:

public static void configure() {
    Logger root = Logger.getRootLogger();
    root.addAppender(new ConsoleAppender(new PatternLayout("%r [%t] %p %c %x - %m%n")));
}

log4j定义了以下的日志的级别,和JUL的略有不同:

  • fatal 指出每个严重的错误事件将会导致应用程序的退出。
  • error 指出虽然发生错误事件,但仍然不影响系统的继续运行。
  • warn 表明会出现潜在的错误情形。
  • info 一般和在粗粒度级别上,强调应用程序的运行全程。
  • debug 一般用于细粒度级别上,对调试应用程序非常有帮助。
  • trace 是程序追踪,可以用于输出程序运行中的变量,显示执行的流程。
  • 和JUL一样,log4j还有两个特殊的级别:OFF,可用来关闭日志记录。 ALL,启用所有消息的日志记录。

一般情况下,我们只使用4个级别,优先级从高到低为:ERROR > WARN > INFO > DEBUG。

2️⃣Log4j组件讲解

​ Log4J 主要由 Loggers (日志记录器)、Appenders(输出端)和 Layout(日志格式化器)组成。其中 Loggers 控制日志的输出级别与日志是否输出;Appenders 指定日志的输出方式(输出到控制台、文件 等);Layout 控制日志信息的输出格式。

🍀(1)Loggers

日志记录器:负责收集处理日志记录,实例的命名就是类“XX”的full quailied name(类的全限定名), Logger的名字大小写敏感,其命名有继承机制:例如:name为com.ydlclass.service的logger会继承 name为com.ydlclass的logger,和JUL一致。

Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接 或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。 JUL是不是也有一个名为.的根。

🍀(2)Appenders

Appender和JUL的Handler很像,用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。Log4j 常用的输出目的地 有以下几种:

输出端类型作用
ConsoleAppender将日志输出到控制台
FileAppender将日志输出到文件中
DailyRollingFileAppender将日志输出到一个日志文件,并且每天输出到一个新的文件
RollingFileAppender将日志信息输出到一个日志文件,并且指定文件的尺寸,当文件大 小达到指定尺寸时,会自动把文件改名,同时产生一个新的文件
JDBCAppender把日志信息保存到数据库中
// 配置一个控制台输出源
ConsoleAppender consoleAppender = new ConsoleAppender();
consoleAppender.setName("ydl");
consoleAppender.setWriter(new PrintWriter(System.out));
logger.addAppender(consoleAppender);

🍀(3)Layouts

Layout layout = new Layout() {
    @Override
    public String format(LoggingEvent loggingEvent) {
        return loggingEvent.getLoggerName() + " "
            +loggingEvent.getMessage() + "\r\n";
    }

    @Override
    public boolean ignoresThrowable() {
        return false;
    }

    @Override
    public void activateOptions() {

    }
};

有一些默认的实现类:

Layout layout = new SimpleLayout();

在这里插入图片描述

3️⃣Log4j配置

log4j不仅仅可以在控制台,文件文件中输出日志,甚至可以在数据库中,我们先使用配置的方式完成日志的输入:

#指定日志的输出级别与输出端
log4j.rootLogger=INFO,Console,ydl
# 控制台输出配置
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
# 文件输出配置
log4j.appender.ydl = org.apache.log4j.DailyRollingFileAppender
#指定日志的输出路径
log4j.appender.ydl.File = D:/logs/ydl.log
log4j.appender.ydl.Append = true
#使用自定义日志格式化器
log4j.appender.ydl.layout = org.apache.log4j.PatternLayout
#指定日志的输出格式
log4j.appender.ydl.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] -[%p] %m%n
#指定日志的文件编码
log4j.appender.ydl.encoding=UTF-8

有了这个配置文件我们些代码就简单了一些:

@Test
public void testConfig(){
    // 获取一个logger
    Logger logger = Logger.getLogger(TestLog4j.class);
    logger.warn("warning");
}

结果:
    2021-10-21 21:37:06,705 [main] WARN  [com.wang.TestLog4j] - warning

同时日志文件也会产生:
在这里插入图片描述
日志文件内容如下:
在这里插入图片描述
当然日志配置文件是什么时候读取的呢?每一个logger都是LogManager创建的,而LogManager有一个静态代码块帮助我们解析配置文件。

我们还可以直接添加一个数据源,将日志输出到数据库中,就是一个和数据库链接的输出源。

加入一个数据库的日志输出源:

#mysql
log4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.logDB.layout=org.apache.log4j.PatternLayout
log4j.appender.logDB.Driver=com.mysql.cj.jdbc.Driver
log4j.appender.logDB.URL=jdbc:mysql://localhost:3306/ssm
log4j.appender.logDB.User=root
log4j.appender.logDB.Password=root
log4j.appender.logDB.Sql=INSERT INTO log(project_name,create_date,level,category,file_name,thread_name,line,all_category,message) values('ydlclass','%d{yyyy-MM-ddHH:mm:ss}','%p','%c','%F','%t','%L','%l','%m')

需要创建保存日志的数据表:

CREATE TABLE `log` (
    `log_id` int(11) NOT NULL AUTO_INCREMENT,
    `project_name` varchar(255) DEFAULT NULL COMMENT '目项名',
    `create_date` varchar(255) DEFAULT NULL COMMENT '创建时间',
    `level` varchar(255) DEFAULT NULL COMMENT '优先级',
    `category` varchar(255) DEFAULT NULL COMMENT '所在类的全名',
    `file_name` varchar(255) DEFAULT NULL COMMENT '输出日志消息产生时所在的文件名称 ',
    `thread_name` varchar(255) DEFAULT NULL COMMENT '日志事件的线程名',
    `line` varchar(255) DEFAULT NULL COMMENT '号行',
    `all_category` varchar(255) DEFAULT NULL COMMENT '日志事件的发生位置',
    `message` varchar(4000) DEFAULT NULL COMMENT '输出代码中指定的消息',
    PRIMARY KEY (`log_id`)
);

在pom.xml中添加驱动:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

再次执行,发现除了控制台,文件,数据库中也有了日志了:

4️⃣自定义Logger

# RootLogger配置
log4j.rootLogger = trace,console
# 自定义Logger
log4j.logger.com.ydlclass= WARN,logDB
log4j.logger.org.apache = error

由此我们发现,我们可以很灵活的自定义,组装不同logger的实现,接下来我们写代码测试:

@Test
public void testDefineLogger() throws Exception {
    Logger logger1 = Logger.getLogger(Log4jTest.class);
    logger1.fatal("fatal"); // 严重错误,一般会造成系统崩溃和终止运行
    logger1.error("error"); // 错误信息,但不会影响系统运行
    logger1.warn("warn"); // 警告信息,可能会发生问题
    logger1.info("info"); // 程序运行信息,数据库的连接、网络、IO操作等
    logger1.debug("debug"); // 调试信息,一般在开发阶段使用,记录程序的变量、参数等
    logger1.trace("trace"); // 追踪信息,记录程序的所有流程信息
    // 自定义 org.apache
    Logger logger2 = Logger.getLogger(Logger.class);
    logger2.fatal("fatal logger2"); // 严重错误,一般会造成系统崩溃和终止运行
    logger2.error("error logger2"); // 错误信息,但不会影响系统运行
    logger2.warn("warn logger2"); // 警告信息,可能会发生问题
    logger2.info("info logger2"); // 程序运行信息,数据库的连接、网络、IO操作等
    logger2.debug("debug logger2"); // 调试信息,一般在开发阶段使用,记录程序的变量、参数等
    logger2.trace("trace logger2"); // 追踪信息,记录程序的所有流程信息
}

我们发现logger1的日志级别成了warn,并且在数据库中有了日志,logger2级别成了error,他们其实都继承了根logger的一些属性。

四、日志门面

当我们的系统变的复杂的之后,难免会集成其他的系统,不同的系统之间可能会使用不同的日志系统。那么在一个系统中,我们的日志框架可能会出现多个,会出现混乱,而且随着时间的发展,可能会出现新的效率更高的日志系统,如果我们想切换代价会非常的大。如果我们的日志系统能和jdbc一样,有一套自己的规范,其他实现均按照规范去实现,就能很灵活的使用日志框架了。

​ 日志门面就是为了解决这个问题而出现的一种技术,日志门面是规范,其他的实现按照规范实现各自的日志框架即可,我们程序员基于日志门面编程即可。举个例子:日志门面就好比菜单,日志实现就好比厨师,我们去餐馆吃饭按照菜单点菜即可,厨师是谁其实不重要,但是有一个符合我口味的厨师当然会更好。

常见的日志门面: JCL、slf4j

常见的日志实现: JUL、log4j、logback、log4j2

日志框架出现的历史顺序: log4j -->JUL–>JCL–> slf4j --> logback --> log4j2

在这里插入图片描述

1️⃣Slf4j日志门面

​简单日志门面(Simple Logging Facade For Java) SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。 当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的Java项目而言,日志框架 会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接。官方网站: https://www.slf4j.org/

SLF4J是目前市面上最流行的日志门面。现在的项目中,基本上都是使用SLF4J作为我们的日志系统。

SLF4J日志门面主要提供两大功能:

  • 日志框架的绑定
  • 日志框架的桥接

🍀(1)阿里日志规约

  • (1)应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API。使用门面模式的日志框架,有利于维护和各个类的日志处理方法统一。
  • (2)日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。
  • (3)应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType为日志类型,推荐分类有stats/monitor/visit等;
  • (4)logName为日志描述。这种命名的好处:通过文件名就可以知道日志文件属于哪个应用,哪种类型,有什么目的,这也有利于归类查找。
  • (5)对trace/debug/info级别的日志输出,必须使用条件输出形式或者占位符的方式。
  • (6)避免重复打印日志,否则会浪费磁盘空间。务必在日志配置文件中设置additivity=false。
  • (7)异常信息应该包括两类:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字向上抛出。
  • (8)谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免吧服务器磁盘撑爆,并及时删除这些观察日志。
  • (9)可以使用warn日志级别记录用户输入参数错误的情况,避免当用户投诉时无所适从。

🍀(2)SLF4J实战

(1)添加依赖

<!--slf4j core 使用slf4j必須添加-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.27</version>
</dependency>
<!--slf4j 自带的简单日志实现 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.27</version>
</dependency>

(2)编写测试代码

public class TestSlf4j {

    // 声明日志对象
    public final static Logger LOGGER =
            LoggerFactory.getLogger(TestSlf4j.class);
    @Test
    public void testSlfSimple()  {
        //打印日志信息
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
        // 使用占位符输出日志信息
        String name = "lucy";
        Integer age = 18;
        LOGGER.info("{}今年{}岁了!", name, age);
        // 将系统异常信息写入日志
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            // e.printStackTrace();
            LOGGER.info("出现异常:", e);
        }
    }
}

🍀(3)绑定其他日志的实现(Binding)

如前所述,SLF4J支持各种日志框架。SLF4J发行版附带了几个称为“SLF4J绑定”的jar文件,每个绑定对应一个受支持的框架。

使用slf4j的日志绑定流程:

  • (1)添加slf4j-api的依赖
  • (2)使用slf4j的API在项目中进行统一的日志记录
  • (3)绑定具体的日志实现框架
    • a. 绑定已经实现了slf4j的日志框架,直接添加对应依赖
    • b. 绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
  • (4)slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现)

绑定JUL的实现:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.27</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.7.25</version>
</dependency>

绑定log4j的实现:

<!--slf4j core 使用slf4j必須添加-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.27</version>
</dependency>
<!-- log4j-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.27</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

​要切换日志框架,只需替换类路径上的slf4j绑定。例如,要从java.util.logging切换到log4j,只需将 slf4j-jdk14-1.7.27.jar替换为slf4j-log4j12-1.7.27.jar即可。

​SLF4J不依赖于任何特殊的类装载。实际上,每个SLF4J绑定在编译时都是硬连线的, 以使用一个且只有 一个特定的日志记录框架。例如,slf4j-log4j12-1.7.27.jar绑定在编译时绑定以使用log4j。

🍀(4)桥接旧的日志框架(Bridging)

​通常,您依赖的某些组件依赖于SLF4J以外的日志记录API。您也可以假设这些组件在不久的将来不会切换到SLF4J。为了解决这种情况,SLF4J附带了几个桥接模块,这些模块将对log4j,JCL和 java.util.logging API的调用重定向,就好像它们是对SLF4J API一样。

​就是你还用log4j的api写代码,但是具体的实现给你抽离了,我们依赖了一个中间层,这个层其实是用旧的api操作slf4j,而不是操作具体的实现。

桥接解决的是项目中日志的遗留问题,当系统中存在之前的日志API,可以通过桥接转换到slf4j的实现:

  • (1)先去除之前老的日志框架的依赖,必须去掉。
  • (2)添加SLF4J提供的桥接组件,这个组件就是模仿之前老的日志写了一套相同的api,只不过这个api是在调用slf4j的api。
  • (3)为项目添加SLF4J的具体实现。

迁移的方式:

<!-- 桥接的组件 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.27</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.27</version>
</dependency>

SLF4J提供的桥接器:

<!-- log4j-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<!-- jul -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<!--jcl -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>

注意问题:

  • (1)jcl-over-slf4j.jar和 slf4j-jcl.jar不能同时部署。前一个jar文件将导致JCL将日志系统的选择委托给SLF4J,后一个jar文件将导致SLF4J将日志系统的选择委托给JCL,从而导致无限循环
  • (2)log4j-over-slf4j.jar和slf4j-log4j12.jar不能同时出现
  • (3)jul-to-slf4j.jar和slf4j-jdk14.jar不能同时出现
  • (4)所有的桥接都只对Logger日志记录器对象有效,如果程序中调用了内部的配置类或者是Appender,Filter等对象,将无法产生效果

🍀(5)SLF4J原理解析

  • (1)SLF4J通过LoggerFactory加载日志具体的实现对象
  • (2)LoggerFactory在初始化的过程中,会通过performInitialization()方法绑定具体的日志实现
  • (3)在绑定具体实现的时候,通过类加载器,加载org/slf4j/impl/StaticLoggerBinder.class
  • (4)所以,只要是一个日志实现框架,在org.slf4j.impl包中提供一个自己的StaticLoggerBinder类,在其中提供具体日志实现的LoggerFactory就可以被SLF4J所加载

在slf4j中创建logger的方法是:

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

继续进入查看,核心就是performInitialization();:

public static ILoggerFactory getILoggerFactory() {
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                performInitialization();
            }
        }
    }
}

继续进入查看,核心就是bind(),这个方法应该就能绑定日志实现了:

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

来到这里,看看绑定的方法:

private final static void bind() {
        try {
            ...
            // 以下内容就绑定成功了
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
            fixSubstituteLoggers();
            replayEvents();
            // release all resources in SUBST_FACTORY
            SUBST_FACTORY.clear();
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            String msg = nsme.getMessage();
            if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
                Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
                Util.report("Your binding is version 1.5.5 or earlier.");
                Util.report("Upgrade your binding to version 1.6.x.");
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

每一个日志实现的中间包都有一个StaticLoggerBinder:

在这里插入图片描述

public class StaticLoggerBinder implements LoggerFactoryBinder {

    /**
     * The unique instance of this class.
     * 
     */
    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

    /**
     * Return the singleton of this class.
     * 
     * @return the StaticLoggerBinder singleton
     */
    public static final StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }

    /**
     * Declare the version of the SLF4J API this implementation is compiled against. 
     * The value of this field is modified with each major release. 
     */
    // to avoid constant folding by the compiler, this field must *not* be final
    public static String REQUESTED_API_VERSION = "1.6.99"; // !final

    private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();

    /**
     * The ILoggerFactory instance returned by the {@link #getLoggerFactory}
     * method should always be the same object
     */
    private final ILoggerFactory loggerFactory;

    private StaticLoggerBinder() {
        loggerFactory = new Log4jLoggerFactory();
        try {
            @SuppressWarnings("unused")
            Level level = Level.TRACE;
        } catch (NoSuchFieldError nsfe) {
            Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
        }
    }

    public ILoggerFactory getLoggerFactory() {
        return loggerFactory;
    }

    public String getLoggerFactoryClassStr() {
        return loggerFactoryClassStr;
    }
}

2️⃣JCL日志门面

​全称为Jakarta Commons Logging,是Apache提供的一个通用日志API。 改日志门面的使用并不是很广泛。

​它是为 "所有的Java日志实现"提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常常弱 (SimpleLog)。所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具: Log4j, Jdk 自带的日志(JUL)

JCL 有两个基本的抽象类:Log(基本记录器)和LogFactory(负责创建Log实例)。

🍀JCL入门

(1)建立maven工程

(2)添加依赖

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

(3)入门代码

public class JULTest {
    @Test
    public void testQuick() throws Exception {
        // 创建日志对象
        Log log = LogFactory.getLog(JULTest.class);
        // 日志记录输出
        log.fatal("fatal");
        log.error("error");
        log.warn("warn");
        log.info("info");
        log.debug("debug");
    }
}

我们为什么要使用日志门面:

  • 面向接口开发,不再依赖具体的实现类。减少代码的耦合
  • 项目通过导入不同的日志实现类,可以灵活的切换日志框架
  • 统一API,方便开发者学习和使用
  • 统一配置便于项目日志的管理

🍀JCL原理

在这里插入图片描述
(1)通过LogFactory动态加载Log实现类

在这里插入图片描述

(2)日志门面支持的日志实现数组

private static final String[] classesToDiscover =
    new String[]{"org.apache.commons.logging.impl.Log4JLogger",
                 "org.apache.commons.logging.impl.Jdk14Logger",
                 "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
                 "org.apache.commons.logging.impl.SimpleLog"};

(3)获取具体的日志实现

for(int i = 0; i < classesToDiscover.length && result == null; ++i) {
    result = this.createLogFromClass(classesToDiscover[i], logCategory,
                                     true);
}

五、Logback日志框架

Logback是由log4j创始人设计的另一个开源日志组件,性能比log4j要好。官方网站:https://logback.qos.ch/index.html

Logback主要分为三个模块:

  • logback-core:其它两个模块的基础模块
  • logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API
  • logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能
    后续的日志代码都是通过SLF4J日志门面搭建日志系统,所以在代码是没有区别,主要是通过修改配置文件和pom.xml依赖

1️⃣Logback入门

🍀(1)添加依赖

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

🍀(2)java代码

public class TestLogback {

    private final static Logger logger = LoggerFactory.getLogger(TestLog4j.class);
    
    @Test
    public void testLogback(){
        //打印日志信息
        logger.error("error");
        logger.warn("warn");
        logger.info("info");
        logger.debug("debug");
        logger.trace("trace");
    }
}

其实我们发现即使项目中没有引入slf4j我们这里也是用的slf4j门面进行编程。

在这里插入图片描述
从logback’的pom依赖中我们看到slf4j,依赖会进行传递

在这里插入图片描述

2️⃣Logback源码解析

🍀(1)spi机制

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。他是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

主要是使用,java.util包下的ServiceLoader实现:

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}

🍀(2)源码解析

源码看一下启动过程:

我们从日志工厂的常见看起,这里是slf4j的实现:

private final static Logger logger = LoggerFactory.getLogger(TestLog4j.class);

核心方法只有一句:

public static Logger getLogger(Class<?> clazz) {
    Logger logger = getLogger(clazz.getName());
    ...中间的逻辑判断省略掉
    return logger;
}

看一下getLogger方法,这里是先获取日志工厂,在从工厂中提取日志对象,我们不考虑日志对象,主要看看日志工厂的环境怎么初始化的:

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

日志工厂的创建方法:

public static ILoggerFactory getILoggerFactory() {
	...去掉其他的代码,从这一行看。
    return StaticLoggerBinder.getSingleton().getLoggerFactory();

}

这里就进入了,StaticLoggerBinder这个对象,这是日志实现用来和slf4j进行绑定的类,从此就进入日志实现中了。

StaticLoggerBinder.getSingleton()这里看到出来是一个单例,来到这个类当中,我们看到,直接返回了defaultLoggerContext。

public ILoggerFactory getLoggerFactory() {
        if (!initialized) {
            return defaultLoggerContext;
        }
... 省略其他
        
    }

这是个日志上下文,一定保存了我们的环境,配置内容一定在这个里边,那么哪里初始化他了呢,我们能想到的就是静态代码块了:

我们发现这个类中还真有:

static {
    SINGLETON.init();
}

我们看到init()方法中,有一个autoConfig(),感觉就像在自动配置:

void init() {
    try {
        try {
            new ContextInitializer(defaultLoggerContext).autoConfig();
        } catch (JoranException je) {
            Util.report("Failed to auto configure default logger context", je);
        }
        ...其他省略
    }
}

默认配置:ContextInitializer类是初始化的关键。

自动配置是这么玩的,先找配置文件。

public void autoConfig() throws JoranException {
        StatusListenerConfigHelper.installIfAsked(loggerContext);
        // 这就是去找配置文件
        URL url = findURLOfDefaultConfigurationFile(true);
        if (url != null) {
            // 解析配置
            configureByResource(url);
        } else {
            // 没有找到文件,就去使用spi机制找一个配置类,这个配置类是在web中用的
            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);
            }
        }
    }

寻找配置文件的过程:

final public static String GROOVY_AUTOCONFIG_FILE = "logback.groovy";
final public static String AUTOCONFIG_FILE = "logback.xml";
final public static String TEST_AUTOCONFIG_FILE = "logback-test.xml";

public URL findURLOfDefaultConfigurationFile(boolean updateStatus) {
    ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this);
    URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus);
    if (url != null) {
        return url;
    }

    url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus);
    if (url != null) {
        return url;
    }

    url = getResource(GROOVY_AUTOCONFIG_FILE, myClassLoader, updateStatus);
    if (url != null) {
        return url;
    }

    return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus);
}
 public void configureByResource(URL url) throws JoranException {
        if (url == null) {
            throw new IllegalArgumentException("URL argument cannot be null");
        }
        final String urlString = url.toString();
        if (urlString.endsWith("groovy")) {
            if (EnvUtil.isGroovyAvailable()) {
                // avoid directly referring to GafferConfigurator so as to avoid
                // loading groovy.lang.GroovyObject . See also http://jira.qos.ch/browse/LBCLASSIC-214
                GafferUtil.runGafferConfiguratorOn(loggerContext, this, url);
            } else {
                StatusManager sm = loggerContext.getStatusManager();
                sm.add(new ErrorStatus("Groovy classes are not available on the class path. ABORTING INITIALIZATION.", loggerContext));
            }
        } else if (urlString.endsWith("xml")) {
            JoranConfigurator configurator = new JoranConfigurator();
            configurator.setContext(loggerContext);
            configurator.doConfigure(url);
        } else {
            throw new LogbackException("Unexpected filename extension of file [" + url.toString() + "]. Should be either .groovy or .xml");
        }
    }

基础配置的代码:

public class BasicConfigurator extends ContextAwareBase implements Configurator {

    public BasicConfigurator() {
    }

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

我们先不说配置的事情,从源码中我们可以看出有几种配置,因为有了

我们先模仿BasicConfigurator写一个类,只做略微的改动:

public class MyConfigurator extends ContextAwareBase implements Configurator {
    public MyConfigurator() {
    }

    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");
        PatternLayout layout = new PatternLayout();
        layout.setPattern("%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n");

        layout.setContext(lc);
        layout.start();
        encoder.setLayout(layout);

        ca.setEncoder(encoder);
        ca.start();

        Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
        rootLogger.addAppender(ca);
    }
}

在resource中新建META-INF目录,下边在新建services文件夹,再新建一个名字叫ch.qos.logback.classic.spi.Configurator的文件,内容是:com.ydlclass.MyConfigurator

在这里插入图片描述

3️⃣Logback组件

🍀(1)appender

输出源,一个日志可以后好几个输出源

🍀(2)encoder

一个appender有一个encoder,负责将一个event事件转换成一组byte数组,并将转换后的字节数据输出到文件中。

Encoder负责把事件转换为字节数组,并把字节数组写到合适的输出流。因此,encoder可以控制在什么时候、把什么样的字节数组写入到其拥有者维护的输出流中。Encoder接口有两个实现类,LayoutWrappingEncoder与PatternLayoutEncoder。

注意:在logback 0.9.19 版之前没有 encoder。

在之前的版本里,多数 appender 依靠 layout 来把事件转换成字符串并用 java.io.Writer 把字符串输出。在之前的版本里,用户需要在 FileAppender里嵌入一个 PatternLayout。

🍀(3)layout

格式化数据将event事件转化为字符串,解析的过程。

🍀(4)filter 过滤器

LevelFilter levelFilter = new LevelFilter();
        levelFilter.setOnMatch(FilterReply.DENY);
        levelFilter.setLevel(Level.WARN);
        levelFilter.start();
        ca.addFilter(levelFilter);
  • %-5level
  • %d{yyyy-MM-dd HH:mm:ss.SSS} 日期
  • %c 类的完整名称
  • %M 为method
  • %L 为行号
  • %thread 线程名称
  • %m或者%msg 为信息
  • %n换行

4️⃣Logback配置

🍀(1)基本配置信息

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度
    %msg:日志消息,%n是换行符-->
    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]
                                    %-5level %msg%n"/>
    <!--
    	Appender: 设置日志信息的去向,常用的有以下几个
        ch.qos.logback.core.ConsoleAppender (控制台)
        ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
        ch.qos.logback.core.FileAppender (文件)
        -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.err</target>
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
    <!--
  用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
                    <loger>仅有一个name属性,一个可选的level和一个可选的addtivity属性
                    name:
        用来指定受此logger约束的某一个包或者具体的某一个类。
            level:
        用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和
            OFF,
            如果未设置此属性,那么当前logger将会继承上级的级别。
            additivity:
        是否向上级loger传递打印信息。默认是true。
            <logger>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个
            logger
            -->
    <!--
            也是<logger>元素,但是它是根logger。默认debug
            level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL
                和 OFF,
                <root>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个
                logger。
        -->
    <root level="ALL">
        <appender-ref ref="console"/>
    </root>
</configuration>

🍀(2)FileAppender配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 自定义属性 可以通过${name}进行引用-->
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M
                                    %L [%thread] %m %n"/>
    <!--
    日志输出格式:
    %d{pattern}日期
    %m或者%msg为信息
    %M为method
    %L为行号
    %c类的完整名称
    %thread线程名称
    %n换行
    %-5level
    -->
    <!-- 日志文件存放目录 -->
    <property name="log_dir" value="d:/logs"></property>
    <!--控制台输出appender对象-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.err</target>
        <!--日志格式配置-->
        <encoder
                 class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
    <!--日志文件输出appender对象-->
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/logback.log</file>
    </appender>
    <!-- 生成html格式appender对象 -->
    <appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="ch.qos.logback.classic.html.HTMLLayout">
                <pattern>%level%d{yyyy-MM-dd HH:mm:ss}%c%M%L%thread%m</pattern>
            </layout>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/logback.html</file>
    </appender>
    <!--RootLogger对象-->
    <root level="all">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
        <appender-ref ref="htmlFile"/>
    </root>
</configuration>

🍀(3)RollingFileAppender配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 自定义属性 可以通过${name}进行引用-->
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M
                                    %L [%thread] %m %n"/>
    <!--
    日志输出格式:
    %d{pattern}日期
    %m或者%msg为信息
    %M为method
    %L为行号
    %c类的完整名称
    %thread线程名称
    %n换行
    %-5level
    -->
    <!-- 日志文件存放目录 -->
    <property name="log_dir" value="d:/logs"></property>
    <!--控制台输出appender对象-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.err</target>
        <!--日志格式配置-->
        <encoder
                 class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
    <!-- 日志文件拆分和归档的appender对象-->
    <appender name="rollFile"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日志格式配置-->
        <encoder
                 class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/roll_logback.log</file>
        <!--指定日志文件拆分和压缩规则-->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--通过指定压缩文件名称,来确定分割文件方式-->
            <fileNamePattern>${log_dir}/rolling.%d{yyyy-MMdd}.log%i.gz</fileNamePattern>
            <!--文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
        </rollingPolicy>
    </appender>
    <!--RootLogger对象-->
    <root level="all">
        <appender-ref ref="console"/>
        <appender-ref ref="rollFile"/>
    </root>
</configuration>

🍀(4)Filter和异步日志配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 自定义属性 可以通过${name}进行引用-->
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M
                                    %L [%thread] %m %n"/>
    <!--
    日志输出格式:
    %d{pattern}日期
    %m或者%msg为信息
    %M为method
    %L为行号
    %c类的完整名称
    %thread线程名称
    %n换行
    %-5level
    -->
    <!-- 日志文件存放目录 -->
    <property name="log_dir" value="d:/logs/"></property>
    <!--控制台输出appender对象-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.err</target>
        <!--日志格式配置-->
        <encoder
                 class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
    <!-- 日志文件拆分和归档的appender对象-->
    <appender name="rollFile"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日志格式配置-->
        <encoder
                 class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}roll_logback.log</file>
        <!--指定日志文件拆分和压缩规则-->
        <rollingPolicy
                       class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--通过指定压缩文件名称,来确定分割文件方式-->
            <fileNamePattern>${log_dir}rolling.%d{yyyy-MMdd}.log%i.gz</fileNamePattern>
            <!--文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
        </rollingPolicy>
        <!--filter配置-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--设置拦截日志级别-->
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--异步日志-->
    <appender name="async" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="rollFile"/>
    </appender>
    <!--RootLogger对象-->
    <root level="all">
        <appender-ref ref="console"/>
        <appender-ref ref="async"/>
    </root>
    <!--自定义logger additivity表示是否从 rootLogger继承配置-->
    <logger name="com.ydlclass" level="debug" additivity="false">
        <appender-ref ref="console"/>
    </logger>
</configuration>

5️⃣Logback-access的使用

在server.xml里的< host>标签下加上如下所示就可以了。

<Valve className="org.apache.catalina.valves.AccessLogValve"  directory="logs" prefix="localhost_access_log." suffix=".txt"  pattern="common" resolveHosts="false"/>

下面咱们逐一分析各个参数:

className想配置访问日志?这就必须得写成这样。
directory这个东西是日志文件放置的目录,在tomcat下面有个logs文件夹,那里面是专门放置日志文件的,当然你也可以修改,我就给改成了D:\
prefix这个是日志文件的名称前缀,我的日志名称为localhost_access_log.2007-09-22.txt,前面的前缀就是这个localhost_access_log
suffix这就是后缀名啦,可以改成别的
pattern这个是最主要的参数了,具体的咱们下面讲,这个参数的内容比较丰富。
resolveHosts如果这个值是true的话,tomcat会将这个服务器IP地址通过DNS转换为主机名,如果是false,就直接写服务器IP地址啦

logback-access模块与Servlet容器(如Tomcat和Jetty)集成,以提供HTTP访问日志功能。我们可以使 用logback-access模块来替换tomcat的访问日志。

(1)将logback-access.jar与logback-core.jar复制到$TOMCAT_HOME/lib/目录下;

(2)修改$TOMCAT_HOME/conf/server.xml中的Host元素中添加:

<Valve className="ch.qos.logback.access.tomcat.LogbackValve"/>

(3)logback默认会在$TOMCAT_HOME/conf下查找文件 logback-access.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- always a good activate OnConsoleStatusListener -->
    <statusListener
                    class="ch.qos.logback.core.status.OnConsoleStatusListener"/>
    <property name="LOG_DIR" value="${catalina.base}/logs"/>
    <appender name="FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_DIR}/access.log</file>
        <rollingPolicy
                       class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>access.%d{yyyy-MM-dd}.log.zip</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <!-- 访问日志的格式 -->
            <pattern>combined</pattern>
        </encoder>
    </appender>
    <appender-ref ref="FILE"/>
</configuration>
%h %l %u %user %date "%r" %s %b

在这里插入图片描述

官方配置: https://logback.qos.ch/access.html#configuration

六、 Log4j2日志框架

前已经有三个门面了,其实不管是哪里都是江湖,都想写一个门面,一统江湖,所以log42出了提供日志实现以外,也拥有一套自己的独立的门面。

​目前市面上最主流的日志门面就是SLF4J,虽然Log4j2也是日志门面,因为它的日志实现功能非常强大,性能优越。所以大家一般还是将Log4j2看作是日志的实现,Slf4j + Log4j2应该是未来的大势所趋。

1️⃣Log4j2入门

🍀(1)使用log4j-api做门面

(1)添加依赖

<!-- Log4j2 门面API-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.14.1</version>
</dependency>
<!-- Log4j2 日志实现 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.1</version>
</dependency>

(2)JAVA代码

public class TestLog4j2 {

    private static final Logger LOGGER = LogManager.getLogger(TestLog4j2.class);

    @Test
    public void testLog(){
        LOGGER.fatal("fatal");
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
    }

}

(3)结果

在这里插入图片描述

🍀(2)使用slf4j做门面

使用slf4j作为日志的门面,使用log4j2作为日志的实现。

(1)添加依赖

<!-- Log4j2 门面API-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.14.1</version>
</dependency>
<!-- Log4j2 日志实现 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.1</version>
</dependency>
<!--使用slf4j作为日志的门面,使用log4j2来记录日志 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
<!--为slf4j绑定日志实现 log4j2的适配器 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.12.1</version>
</dependency>

(2)JAVA代码

private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(TestLog4j2.class);

@Test
public void testSlf4j(){
    LOG.error("error");
    LOG.warn("warn");
    LOG.debug("debug");
    LOG.info("info");
    LOG.trace("trace");
}

(3)结果

在这里插入图片描述

我们看到log4j2的默认日志级别好像是error。

2️⃣Log4j2配置

🍀默认配置

DefaultConfiguration类中提供的默认配置将设置,

通过debug可以在LoggerContext类中发现

private volatile Configuration configuration = new DefaultConfiguration();

可以看到默认的root日志的layout

在这里插入图片描述

我们也能看到他的日志级别:

在这里插入图片描述

我们能从默认配置类中看到一些默认的配置:

protected void setToDefault() {
    // LOG4J2-1176 facilitate memory leak investigation
    setName(DefaultConfiguration.DEFAULT_NAME + "@" + Integer.toHexString(hashCode()));
    final Layout<? extends Serializable> layout = PatternLayout.newBuilder()
        .withPattern(DefaultConfiguration.DEFAULT_PATTERN)
        .withConfiguration(this)
        .build();
    final Appender appender = ConsoleAppender.createDefaultAppenderForLayout(layout);
    appender.start();
    addAppender(appender);
    final LoggerConfig rootLoggerConfig = getRootLogger();
    rootLoggerConfig.addAppender(appender, null, null);

    final Level defaultLevel = Level.ERROR;
    final String levelName = PropertiesUtil.getProperties().getStringProperty(DefaultConfiguration.DEFAULT_LEVEL,
                                                                              defaultLevel.name());
    final Level level = Level.valueOf(levelName);
    rootLoggerConfig.setLevel(level != null ? level : defaultLevel);
}

🍀自定义配置文件位置

log4j2默认在classpath下查找配置文件,可以修改配置文件的位置。在非web项目中:

public static void main(String[] args) throws IOException {
	File file = new File("D:/log4j2.xml");
	BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
	final ConfigurationSource source = new ConfigurationSource(in);
	Configurator.initialize(null, source);
	
	Logger logger = LogManager.getLogger("mylog");
}

如果是web项目,在web.xml中添加:

<context-param>
    <param-name>log4jConfiguration</param-name>
    <param-value>/WEB-INF/conf/log4j2.xml</param-value>
</context-param>
<listener>
    <listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class>
</listener>

log4j2默认加载classpath下的 log4j2.xml 文件中的配置。事实上log4j2可以通过 XML、JSON、YAML 或properties格式进行配置:https://logging.apache.org/log4j/2.x/manual/configuration.html

如果找不到配置文件,Log4j 将提供默认配置。DefaultConfiguration 类中提供的默认配置将设置:

  • %d{HH:mm:ss.SSS} ,表示输出到毫秒的时间
  • %t,输出当前线程名称
  • %-5level,输出日志级别,-5表示左对齐并且固定输出5个字符,如果不足在右边补0
  • %logger,输出logger名称,因为Root Logger没有名称,所以没有输出
  • %msg,日志文本
  • %n,换行

其他常用的占位符有:

  • %F,输出所在的类文件名,如Client.java
  • %L,输出行号
  • %M,输出所在方法名
  • %l,输出语句所在的行数, 包括类名、方法名、文件名、行数
private void reconfigure(final URI configURI) {
    Object externalContext = externalMap.get(EXTERNAL_CONTEXT_KEY);
    final ClassLoader cl = ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null;
    LOGGER.debug("Reconfiguration started for context[name={}] at URI {} ({}) with optional ClassLoader: {}",
                 contextName, configURI, this, cl);
    final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl);
    if (instance == null) {
        LOGGER.error("Reconfiguration failed: No configuration found for '{}' at '{}' in '{}'", contextName, configURI, cl);
    } else {
        setConfiguration(instance);
        /*
             * instance.start(); Configuration old = setConfiguration(instance); updateLoggers(); if (old != null) {
             * old.stop(); }
             */
        final String location = configuration == null ? "?" : String.valueOf(configuration.getConfigurationSource());
        LOGGER.debug("Reconfiguration complete for context[name={}] at URI {} ({}) with optional ClassLoader: {}",
                     contextName, location, this, cl);
    }
}

ConfigurationFactory:

for (final ConfigurationFactory factory : getFactories()) {
    final String[] types = factory.getSupportedTypes();
    if (types != null) {
        for (final String type : types) {
            if (type.equals(ALL_TYPES)) {
                final Configuration config = factory.getConfiguration(loggerContext, name, configLocation);
                if (config != null) {
                    return config;
                }
            }
        }
    }
}

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" monitorInterval="5">
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L -
                                    -- %m%n" />
        </Console>
        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l
                                    %c{36} - %m%n" />
        </File>
        <RandomAccessFile name="accessFile" fileName="${LOG_HOME}/myAcclog.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l
                                    %c{36} - %m%n" />
        </RandomAccessFile>
        <RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
                     filePattern="D:/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyyMM-dd-HH-mm}-%i.log">
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l
                                    %c{36} - %msg%n" />
            <Policies>
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="10 MB" />
                <TimeBasedTriggeringPolicy />
            </Policies>
            <DefaultRolloverStrategy max="30" />
        </RollingFile>
        <RollingRandomAccessFile name="MyFile"
			fileName="${LOG_HOME}/${FILE_NAME}.log"
			filePattern="${LOG_HOME}/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd HH-mm}-%i.log">
			<PatternLayout
				pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
			<Policies>
				<TimeBasedTriggeringPolicy interval="1" />
				<SizeBasedTriggeringPolicy size="10 MB" />
			</Policies>
			<DefaultRolloverStrategy max="20" />
		</RollingRandomAccessFile>
    </Appenders>
    <Loggers>
        <Logger name="mylog" level="trace" additivity="false">
			<AppenderRef ref="MyFile" />
		</Logger>
		<Root level="error">
			<AppenderRef ref="Console" />
		</Root>
    </Loggers>
</Configuration>

注意根节点增加了一个monitorInterval属性,含义是每隔300秒重新读取配置文件,可以不重启应用的情况下修改配置,还是很好用的功能。

RollingRandomAccessFile的属性:

  • fileName 指定当前日志文件的位置和文件名称
  • filePattern 指定当发生Rolling时,文件的转移和重命名规则
  • SizeBasedTriggeringPolicy 指定当文件体积大于size指定的值时,触发Rolling
  • DefaultRolloverStrategy 指定最多保存的文件个数
  • TimeBasedTriggeringPolicy 这个配置需要和filePattern结合使用,注意filePattern中配置的文件重命名规则是${FILE_NAME}-%d{yyyy-MM-dd HH-mm}-%i,最小的时间粒度是mm,即分钟。
  • TimeBasedTriggeringPolicy指定的size是1,结合起来就是每1分钟生成一个新文件。如果改成%d{yyyy-MM-dd
    HH},最小粒度为小时,则每一个小时生成一个文件。

3️⃣Log4j2异步日志

log4j2最大的特点就是异步日志,其性能的提升主要也是从异步日志中受益,我们来看看如何使用 log4j2的异步日志。

🍀同步日志
在这里插入图片描述

🍀异步日志
在这里插入图片描述
在这里插入图片描述
Log4j2提供了两种实现日志的方式,一个是通过AsyncAppender,一个是通过AsyncLogger,分别对应前面我们说的Appender组件和Logger组件。

注意:配置异步日志需要添加依赖

<!--异步日志依赖-->
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.3.4</version>
</dependency>

(1)AsyncAppender方式

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <Appenders>
        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout>
                <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
            </PatternLayout>
        </File>
        <Async name="Async">
            <AppenderRef ref="file"/>
        </Async>
    </Appenders>
    <Loggers>
        <Root level="error">
            <AppenderRef ref="Async"/>
        </Root>
    </Loggers>
</Configuration>

(2)AsyncLogger方式

AsyncLogger才是log4j2 的重头戏,也是官方推荐的异步方式。它可以使得调用Logger.log返回的 更快。你可以有两种选择:全局异步和混合异步。

  • 全局异步就是,所有的日志都异步的记录,在配置文件上不用做任何改动,只需要添加一个log4j2.component.properties 配置。
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
  • 混合异步就是,你可以在应用中同时使用同步日志和异步日志,这使得日志的配置方式更加灵活。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <Appenders>
        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout>
                <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
            </PatternLayout>
        </File>
        <Async name="Async">
            <AppenderRef ref="file"/>
        </Async>
    </Appenders>
    <Loggers>
        <AsyncLogger name="com.ydlclass" level="trace"
                     includeLocation="false" additivity="false">
            <AppenderRef ref="file"/>
        </AsyncLogger>
        <Root level="info" includeLocation="true">
            <AppenderRef ref="file"/>
        </Root>
    </Loggers>
</Configuration>

如上配置: com.ydlclass 日志是异步的,root日志是同步的。

使用异步日志需要注意的问题:

  • 如果使用异步日志,AsyncAppender、AsyncLogger和全局日志,不要同时出现。性能会和AsyncAppender一致,降至最低。
  • 设置includeLocation=false ,打印位置信息会急剧降低异步日志的性能,比同步日志还要慢。
for (int i = 0; i < 1000000; i++) {
    LOGGER.fatal("fatal");

}
long end = System.currentTimeMillis();
System.out.println(end - start);

2970

七、怎么打印日志

🍀基本格式

必须使用参数化信息的方式:

logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol);

不要进行字符串拼接,那样会产生很多String对象,占用空间,影响性能。反例(不要这么做):

logger.debug("Processing trade with id: " + id + " symbol: " + symbol);

使用[]进行参数变量隔离,如有参数变量,应该写成如下写法:

logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol)

这样的格式写法,可读性更好,对于排查问题更有帮助。

不同级别的使用如下:

🍀ERROR,影响到程序正常运行、当前请求正常运行的异常情况

  • 打开配置文件失败
  • 所有第三方对接的异常(包括第三方返回错误码)
  • 所有影响功能使用的异常,包括:SQLException和除了业务异常之外的所有异常(RuntimeException和Exception)
  • 不应该出现的情况,比如要使用阿里云传图片,但是未响应
  • 如果有Throwable信息,需要记录完成的堆栈信息
log.error("获取用户[{}]的用户信息时出错",userName,e);

说明,如果进行了抛出异常操作,请不要记录error日志,由最终处理方进行处理:

反例(不要这么做):

try{
    ....
}catch(Exception ex){
    String errorMessage=String.format("Error while reading information of user [%s]",userName);
    logger.error(errorMessage,ex);
    throw new UserServiceException(errorMessage,ex);
}

🍀WARN,不应该出现但是不影响程序、当前请求正常运行的异常情况

  • 有容错机制的时候出现的错误情况
  • 找不到配置文件,但是系统能自动创建配置文件
  • 即将接近临界值的时候,例如:缓存池占用达到警告线,业务异常的记录,比如:用户锁定异常

🍀INFO,系统运行信息

  • Service方法中对于系统/业务状态的变更
  • 主要逻辑中的分步骤:1,初始化什么 2、加载什么
  • 外部接口部分
  • 客户端请求参数(REST/WS)
  • 调用第三方时的调用参数和调用结果
  • 对于复杂的业务逻辑,需要进行日志打点,以及埋点记录,比如电商系统中的下订单逻辑,以及OrderAction操作(业务状态变更)
  • 调用其他第三方服务时,所有的出参和入参是必须要记录的(因为你很难追溯第三方模块发生的问题)

说明:并不是所有的service都进行出入口打点记录,单一、简单service是没有意义的(job除外,job需要记录开始和结束,)。

反例(不要这么做):

public List listByBaseType(Integer baseTypeId) {
    log.info("开始查询基地");
    BaseExample ex=new BaseExample();
    BaseExample.Criteria ctr = ex.createCriteria();
    ctr.andIsDeleteEqualTo(IsDelete.USE.getValue());
    Optionals.doIfPresent(baseTypeId, ctr::andBaseTypeIdEqualTo);
    log.info("查询基地结束");
    return baseRepository.selectByExample(ex);

}

🍀DEBUG,可以填写所有的想知道的相关信息(但不代表可以随便写,debug信息要有意义,最好有相关参数)

  • 生产环境需要关闭DEBUG信息
  • 如果在生产情况下需要开启DEBUG,需要使用开关进行管理,不能一直开启

说明:如果代码中出现以下代码,可以进行优化:

  • 获取用户基本薪资
  • 获取用户休假情况
  • 计算用户应得薪资
logger.debug("开始获取员工[{}] [{}]年基本薪资",employee,year);
logger.debug("获取员工[{}] [{}]年的基本薪资为[{}]",employee,year,basicSalary);
logger.debug("开始获取员工[{}] [{}]年[{}]月休假情况",employee,year,month);
logger.debug("员工[{}][{}]年[{}]月年假/病假/事假为[{}]/[{}]/[{}]",employee,year,month,annualLeaveDays,sickLeaveDays,noPayLeaveDays);
logger.debug("开始计算员工[{}][{}]年[{}]月应得薪资",employee,year,month);
logger.debug("员工[{}] [{}]年[{}]月应得薪资为[{}]",employee,year,month,actualSalary);

🍀TRACE,特别详细的系统运行完成信息,业务代码中,不要使用.(除非有特殊用意,否则请使用DEBUG级别替代)

规范示例说明:

@Override
@Transactional
public void createUserAndBindMobile(@NotBlank String mobile, @NotNull User user) throws CreateConflictException{
    boolean debug = log.isDebugEnabled();
    if(debug){
        log.debug("开始创建用户并绑定手机号. args[mobile=[{}],user=[{}]]", mobile, LogObjects.toString(user));
    }
    try {
        user.setCreateTime(new Date());
        user.setUpdateTime(new Date());
        userRepository.insertSelective(user);
        if(debug){
            log.debug("创建用户信息成功. insertedUser=[{}]",LogObjects.toString(user));
        }
        UserMobileRelationship relationship = new UserMobileRelationship();
        relationship.setMobile(mobile);
        relationship.setOpenId(user.getOpenId());
        relationship.setCreateTime(new Date());
        relationship.setUpdateTime(new Date());
        userMobileRelationshipRepository.insertOnDuplicateKey(relationship);
        if(debug){
            log.debug("绑定手机成功. relationship=[{}]",LogObjects.toString(relationship));
        }
        log.info("创建用户并绑定手机号. userId=[{}],openId=[{}],mobile[{}]",user.getId(),user.getOpenId(),mobile); 		// 如果考虑安全,手机号记得脱敏
    }catch(DuplicateKeyException e){
        log.info("创建用户并绑定手机号失败,已存在相同的用户. openId=[{}],mobile=[{}]",user.getOpenId(),mobile);
        throw new CreateConflictException("创建用户发生冲突, openid=[%s]",user.getOpenId());
    }
}

后记

在这里插入图片描述
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
👉Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)

更多推荐