一、日志的概念
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"); } 结果: 10月 21, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent 严重: severe 10月 21, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent 警告: warning 10月 21, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent 信息: info 10月 21, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent 配置: config 10月 21, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent 详细: fine 10月 21, 2021 12:45:15 下午 com.ydlclass.entity.LoggerTest testLogParent 较详细: finer 10月 21, 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 |
m | 月 | 10 |
d | 日 | 21 |
H | 24小时制的时 | 14 |
l | 12小时制的时 | 2 |
M | 分 | 57 |
S | 秒 | 46 |
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