我们提供安全,免费的手游软件下载!
大家好,我是 晓凡
日志的重要性不用我多说了,日志,简单来说就是记录。
用来记录程序运行时发生的事情。比如,程序启动了、执行了某个操作、遇到了问题等等,这些都可以通过日志记录下来。
想象一下,你开了一家店,每天的营业额、顾客的反馈、商品的进出、库存等等,你都会记录下来。这就像是程序的日志。比如:
public class SimpleApp {
public static void main(String[] args) {
System.out.println("程序启动");
// 假设这里是用户输入数据
String userInput = "Hello, World!";
System.out.println("用户输入了: " + userInput);
// 处理数据
String result = processInput(userInput);
System.out.println("处理结果: " + result);
try {
//可能异常的逻辑代码
}catch(Exception e){
e.printStackTrace()
}
// 程序结束
System.out.println("程序结束");
}
private static String processInput(String input) {
// 这里是处理逻辑
return "Processed: " + input;
}
}
上面的代码我们不陌生了吧,我们使用
System.out.println
来打印程序的运行状态,使用
e.printStackTrace()
来打印信息和错误
这就是没有日志框架时,最简单直接的日志打印方式
这种方式简单直接,但也有一些缺点:
所以我们要引入各种功能强大的日志框架进行日志管理
日志框架由日志门面和日志实现构成,具体如下图所示
顾名思义,日志门面,就像是一个团队的领导者一样,只负责制定规则,安排任务,而具体干活的则交给苦逼的打工人(日志具体实现)即可。
日志门面提供了一套标准的日志记录接口,而具体的日志记录工作则由不同的日志框架来完成。
这样做的好处是,可以在不修改代码的情况下,通过配置来切换不同的日志框架。
正如职场中,一个打工人跑路了,在不需要太多成本,不用做太多改变的情况下,新招一个更便宜的打工人也可完成同样的任务实现快速切换,好像有点扯远了
主流的日志门面框架主要有:
SLF4J
:这是一个非常流行的日志门面,它提供了一套简单的日志记录接口,并且可以与多种日志框架(如Log4j、Logback等)配合使用。
JCL
:这是早期的一个日志门面
通过是实现日志门面接口来完成日志记录,实实在在的打工人无疑了
主流的日志实现框架有:
JUL
Java
自带的日志框架 ,功能相对基础,性能一般,但对于简单的日志需求来说足够用了。
Log4j
个非常老牌的日志框架,功能非常强大,可以自定义很多日志的细节,比如日志级别、输出格式、输出目的地等。现由Apache软件基金会维护
Log4j2
也是Apache软件基金会开发,相比
Log4j
,
Log4j2
在性能上有显著提升,同时保持了丰富的功能,支持异步日志处理,适合高性能需求的场景
Logback
由
Log4j
的原开发者之一主导开发,
Spring Boot
默认日志,轻量级,性能优秀,功能也比较全面
Handler
SEVERE
、
WARNING
、
INFO
、
CONFIG
、
FINE
、
FINER
、
FINEST
等。
Logger
实例。
Handler
Handler
设置日志级别(
Level
)和格式输出(
Formatter
)
Filter
过滤器
Logger
实例添加日志处理器(
Handler
)和日志过滤器(
Filter
)
public class LogQuickTest {
@Test
public void testLogQuick(){
//创建日志记录对象
Logger logger = Logger.getLogger("com.xiezhr");
//日志记录输出
logger.info("这是一个info日志");
logger.log(Level.INFO,"这是一个info日志");
String name="程序员晓凡";
Integer age=18;
logger.log(Level.INFO,"姓名:{0},年龄:{1}",new Object[]{name,age});
}
}
日志级别系统,用来区分日志的重要性
SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST
日志级别越高,记录的信息越重要。当你设置一个日志级别时,比如INFO,那么INFO级别以及以上的日志(SEVERE和WARNING)都会被记录,而FINE、FINER和FINEST级别的日志则会被忽略
这里我们按照上面的步骤创建一个日志记录器,将日志文件分别输出到控制台和文件中
public class LoggingExampleTest {
@Test
public void testLogging() {
// 获取日志记录器
Logger logger = Logger.getLogger("LoggingExample");
// 设置日志级别为INFO,这意味着INFO级别及以上的日志会被记录
logger.setLevel(Level.INFO);
// 创建控制台Handler 将日志输出到控制台
// 并设置其日志级别和Formatter
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.WARNING); // 控制台只输出WARNING及以上级别的日志
consoleHandler.setFormatter(new SimpleFormatter() {
@Override
public synchronized String format(LogRecord record) {
// 自定义日志格式
return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
}
});
logger.addHandler(consoleHandler);
// 创建文件Handler 将日志输出到文件
// 并设置其日志级别和Formatter
try {
FileHandler fileHandler = new FileHandler("app.log", true);
fileHandler.setLevel(Level.ALL); // 文件将记录所有级别的日志
fileHandler.setFormatter(new SimpleFormatter() {
@Override
public synchronized String format(LogRecord record) {
// 自定义日志格式
return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
}
});
logger.addHandler(fileHandler);
} catch (IOException e) {
e.printStackTrace();
}
// 创建并设置Filter
Filter filter = new Filter() {
@Override
public boolean isLoggable(LogRecord record) {
// 这里可以添加过滤逻辑,例如只记录包含特定字符串的日志
return record.getMessage().contains("important");
}
};
// 将Filter应用到Logger
//logger.setFilter(filter);
// 记录不同级别的日志
logger.severe("严重错误信息 - 应记录到控制台和文件");
logger.warning("警告信息 - 应记录到控制台和文件");
logger.info("常规信息 - 只记录到文件");
logger.config("配置信息 - 只记录到文件");
logger.fine("详细日志 - 只记录到文件");
// 这条日志将被Filter过滤掉,不会记录
logger.info("这条信息不重要,将被过滤");
// 这条日志将被记录,因为消息中包含"important"
logger.info("这条信息很重要,将被记录到控制台和文件");
}
}
① 控制台日志输出
②日志文件输出
app.log
内容
代码解释 :
LoggingExample
的
Logger
实例。
INFO
,这意味着INFO及以上级别的日志将被记录。
ConsoleHandler
实例,设置其日志级别为
WARNING
,并且自定义了日志的输出格式。
FileHandler
实例,将日志写入到
app.log
文件中,并设置其日志级别为
ALL
,意味着所有级别的日志都将被记录到文件。
SimpleFormatter
,用于定义日志的输出格式。
Filter
接口的匿名内部类,并重写
isLoggable
方法,实现过滤逻辑,这里只记录消息中包含"important"字符串的日志。
以上3.4小节通过硬编码的方式打印输出日志,这样的方式很不利于后期的管理与维护,这小节我们将使用配置文件的方式进行日志输出
① 在resources下面新建
logconfig.properties
文件,内容如下
# 指定日志处理器为:ConsoleHandler,FileHandler 表示同时使用控制台和文件处理器
handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler
#设置默认的日志级别为:ALL
.level= ALL
# 配置自定义 Logger
com.xiezhr.handlers = com.xiezhr.DefConsoleHandler
com.xiezhr.level = CONFIG
# 如果想要使用自定义配置,需要关闭默认配置
com.xiezhr.useParentHanlders =true
# 向日志文件输出的 handler 对象
# 指定日志文件路径 当文件数为1时 日志为/logs/java0.log
java.util.logging.FileHandler.pattern = /logs/java%u.log
# 指定日志文件内容大小,下面配置表示日志文件达到 50000 字节时,自动创建新的日志文件
java.util.logging.FileHandler.limit = 50000
# 指定日志文件数量,下面配置表示只保留 1 个日志文件
java.util.logging.FileHandler.count = 1
# 指定 handler 对象日志消息格式对象
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集为 UTF-8 ,防止出现乱码
java.util.logging.FileHandler.encoding = UTF-8
# 指定向文件中写入日志消息时,是否追加到文件末尾,true 表示追加,false 表示覆盖
java.util.logging.FileHandler.append = true
# 向控制台输出的 handler 对象
# 指定 handler 对象的日志级别
java.util.logging.ConsoleHandler.level =WARNING
# 指定 handler 对象的日志消息格式对象
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8
# 指定日志消息格式
java.util.logging.SimpleFormatter.format = [%1$tF %1$tT] %4$s: %5$s %n
注意: 设置日志消息格式中(后面一小节会详细讲解)
%1$tF
:这个占位符表示日志记录的时间,格式为
yyyy-MM-dd
,其中
1$
表示这是第一个参数
tF
是日期的格式化代码
%1$tT
:这个占位符表示日志记录的时间,格式为
HH:mm:ss.SSS
,即小时:分钟:秒.毫秒
1$
表示这是第一个参数,
tT
是时间的格式化代码
%4$s
: 表示日志级别,
level =WARNING
输出警告 level =INFO 输出消息
%5$s
: 表示日志消息
%n
:这个占位符表示换行符,每条日志记录之后会有一个换行,以便在查看日志时能够清晰地区分每条记录。
② 日志测试
@Test
public void testLogProperties()throws Exception{
// 1、读取配置文件,通过类加载器
InputStream ins = LoggingExampleTest.class.getClassLoader().getResourceAsStream("logconfig.properties");
// 2、创建LogManager
LogManager logManager = LogManager.getLogManager();
// 3、通过LogManager加载配置文件
logManager.readConfiguration(ins);
// 4、创建日志记录器
Logger logger = Logger.getLogger("com.xiezhr");
// 5、记录不同级别的日志
logger.severe("这是一条severe级别信息");
logger.warning("这是一条warning级别信息");
}
执行上面代码后
控制台输出 :
java0.log文件输出:
上面两个小节中,不管是通过编码或者配置文件 都对日志进行了格式化
① 编码设置日志格式
fileHandler.setFormatter(new SimpleFormatter() {
@Override
public synchronized String format(LogRecord record) {
// 自定义日志格式
return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
}
});
② 配置文件指定日志格式
# 指定日志消息格式
java.util.logging.SimpleFormatter.format = [%1$tF %1$tT] %4$s: %5$s %n
上面设置的日志格式设置你看懂了么?
不管是哪种方式设置日志格式,我们看源码最终都是通过
String.format
函数来实现的,所有我们有必要学一学
String
类提供的
format
这个方法的使用
String
的
format
方法
String
的format
方法用来格式化字符串。
format
方法就像是一个模板,你可以在这个模板里插入你想要的数据,然后它就会帮你生成一个格式化好的字符串。
我们先来看看下面这个简单例子
@Test
public void testStringFormatter()throws Exception{
String name = "晓凡";
Integer age = 18;
// 使用String.format()方法格式化字符串
String xiaofan = String.format("%s今年%d岁", name, age);
System.out.println(xiaofan);
}
//输出
晓凡今年18岁
%s
和%d
为占位符,不同类型需要不同占位符,那么还有哪些常用转换符呢?
占位符 | 详细说明 | 示例 |
---|---|---|
%s
|
字符串类型**** | “喜欢晓凡请关注” |
%c
|
字符类型 | ‘x’ |
%b
|
布尔类型 | true |
%d
|
整数类型(十进制) | 666 |
%x
|
整数类型(十六进制) | FF |
%o
|
整数类型(八进制) | 77 |
%f
|
浮点类型 | 8.88 |
%a
|
十六进制浮点类型 | FF.34 |
%e
|
指数类型 | 1.28e+5 |
%n
|
换行符 | |
%tx
|
日期和时间类型(x代表不同的日期与时间转换符) |
符号 | 说明 | 示例 | 结果 |
---|---|---|---|
0 | 指定数字、字符前面补0,用于对齐 | ("%04d",6) | 0006 |
空格 | 指定数字、字符前面补空格,用于对齐 | ("[% 4s]",x) | [ x] |
, | 以“,”对数字分组显示(常用于金额) | ("%,f,666666.66") | 666,666.6600 |
注意: 默认情况下,可变参数是按照顺序依次替换,但是我们可以通过“数字$”来重复利用可变参数
@Test
public void testStringFormatter()throws Exception{
String name = "晓凡";
Integer age = 18;
// 使用String.format()方法格式化字符串
String xiaofan = String.format("%s今年%d岁", name, age);
System.out.println(xiaofan);
//
String xiaofan1 = String.format("%s今年%d岁,%1$s的公众号是:程序员晓凡", name, age);
System.out.println(xiaofan1);
}
//输出
晓凡今年18岁
晓凡今年18岁,晓凡的公众号是:程序员晓凡
上面例子中我们通过
%1$s
重复使用第一个参数
name
上面我们说到%tx,x代表日期转换符,其具体含义如下
符号 | 描述 | 示例 |
---|---|---|
c
|
包含全部日期和时间信息 |
周六 8月 03 17:16:37 CST 2024
|
F
|
"年-月-日" 格式 |
2024-08-03
|
D
|
"月/日/年"格式 |
08/03/24
|
d
|
日 |
03
|
r
|
“
HH:MM:SS PM
”格式(12小时制)
|
05:16:37 下午
|
R
|
“
HH:MM
”格式(24小时制)
|
17:16
|
T
|
“
HH:MM:SS
”格式(24小时制)
|
17:16:37
|
b
|
月份本地化 |
8月
|
y
|
两位年 |
24
|
Y
|
四位年 |
2024
|
m
|
月 |
08
|
H
|
时(24小时制) |
17
|
I
|
时(12小时制) |
05
|
M
|
分 |
16
|
S
|
秒 |
37
|
s
|
秒为单位的时间戳 |
1722677530
|
p
|
上午还是下午 |
下午
|
Log4j 是Apache软件基金组织旗下的一款开源日志框架,是一款比较老的日志框架,目前已出log4j2,它在log4j上做了很大改动,性能提升了不少。但是有些老项目还会在使用,所以我们也来说一说
官网: https://logging.apache.org/log4j/1.x/
注意:
从官网,我们可以看到项目管理委员会宣布
Log4j 1. x
已终止使用。建议用户升级到
Log4j 2
log4j
log4j
1.2.17
junit
junit
4.13.2
test
@Test
public void testLog4jQuick(){
//初始化日志配置信息,不需要配置文件
BasicConfigurator.configure();
//获取日志记录器
Logger logger = Logger.getLogger(Log4jTest.class);
//通过各种日志级别打印日志
logger.fatal("这是一条致命的信息"); // 严重错误,一般会造成系统崩溃
logger.error("这是一条错误的信息"); // 出现错误时,比如出错了但是不影响系统继续运行
logger.warn("这是一条警告的信息"); // 警告级别,比如要告警的时候
logger.info("这是一条普通的信息"); // 一般信息,比如记录普通的方法执行
logger.debug("这是一条调试的信息"); // 调试信息,比如调试的时候打印的信息
logger.trace("这是一条追踪的信息"); // 追踪信息,比如追踪程序运行路径
}
//输出
0 [main] FATAL Log4jTest - 这是一条致命的信息
0 [main] ERROR Log4jTest - 这是一条错误的信息
0 [main] WARN Log4jTest - 这是一条警告的信息
0 [main] INFO Log4jTest - 这是一条普通的信息
0 [main] DEBUG Log4jTest - 这是一条调试的信息
注意:
BasicConfigurator.configure();
为log4j在不添加配置文件的情况下初始化默认日志配置信息,如果既没有默认配置信息,也没有配置文件
会报下面错误
日志级别,就好比是日记本里的不同标记,用来区分信息的重要性。在log4j中,日志级别从低到高分为以下几种:
出了上面的,还有以下两个特殊级别
1. **OFF**: 用来关闭日志记录
1. **ALL**: 启用所有消息的日志记录
Logger
:这个组件就像是日志的大脑,负责记录日志信息。你可以想象它是一个日记本的主人,决定哪些事情值得记录,哪些事情可以忽略。
Appender
:Appender就像是日记本的笔,它决定了日志信息要写到哪里。可以是控制台、文件、数据库,甚至是通过网络发送到远程服务器。每种Appender都有不同的用途和特点。
Layout
:Layout决定了日志的外观,也就是日志的格式。比如,你可以选择日志中包含时间、日志级别、发生日志的类名和方法名,以及日志的具体内容等。Layout就像是给日记本设计外观样式。
Log4j
中有一个特殊的
logger
叫做
root
,它是
logger
的根,其他的
logger
都会直接或者间接的继承自
root
。
入门示例中,我们通过
Logger.getLogger(Log4jTest.class);
获取的就是
root logger
name为
org.apache.commons
的logger会继承name为
org.apache
的logger
用来指定日志记录到哪儿,主要有以下几种
Appender类型 | 作用 |
---|---|
ConsoleAppender
|
将日志输出到控制台 |
FileAppender
|
将日志输出到文件中 |
DailyRollingFileAppender
|
将日志输出到文件中,并且每天输出到一个日志文件中 |
RollingFileAppender
|
将日志输出到文件中,并且指定文件的大小,当文件大于指定大小,会生成一个新的日志文件 |
JDBCAppender
|
将日志保存到数据库中 |
用于控制日志内容输出格式,Log4j常用的有以下几种输出格式
日志格式器 | 说明 |
---|---|
HTMLLayout
|
将日志以html表格形式输出 |
SimpleLayout
|
简单的日志格式输出,例如(info-message) |
PatternLayout
|
最强大的格式化器,也是我们使用最多的一种,我们可以自定义输出格式 |
示例:下面我们通过
PatternLayout
格式化日志
@Test
public void testLog4jLayout(){
//初始化日志配置信息,不需要配置文件
BasicConfigurator.configure();
//获取日志记录器
Logger logger = Logger.getLogger(Log4jTest.class);
Layout patternLayout = new PatternLayout("%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n");// 将自定义的Layout应用到控制台Appender上
ConsoleAppender consoleAppender = new ConsoleAppender(patternLayout);
logger.addAppender(consoleAppender);
// 记录日志
logger.info("这是一条自定义格式的日志信息");
}
//输出
2024-08-04 13:55:35 [INFO] - Log4jTest.testLog4jLayout(Log4jTest.java:44) - 这是一条自定义格式的日志信息
占位符 | 说明 |
---|---|
%m
|
输出代码中指定的日志信息 |
%p
|
输出优先级 |
%n
|
换行符 |
%r
|
输出自应用启用到输出log信息消耗的毫秒数 |
%c
|
输出语句所属的类全名 |
%t
|
输出线程全名 |
%d
|
输出服务器当前时间,%d |
%l
|
输出日志时间发生的位置,包括类名、线程、及在代码中的函数 例如:
Log4jTest.testLog4jLayout(Log4jTest.java:44)
|
%F
|
输出日志消息产生时所在的文件夹名称 |
%L
|
输出代码中的行号 |
%5c
|
category名称不足5位时,左边补充空格,即右对齐 |
%-5c
|
category名称不足5位时,右边补充空格,即左对齐 |
.5c
|
category名称大于5位时,会将左边多出的字符截取掉,小于5位时,以空格补充 |
BasicConfigurator.configure();
上面代码中通过这段代码初始化日志配置信息,这一小节,我们通过配置文件来配置
通过看
LogManager
日志管理器源码,我们知道可以默认加载如下几种格式的配置文件(其中
log4j.xml
和
log4j.properties
是我们最常用的)
log4j.properties
log4j.xml
og4j.configuration
等等
# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制台日志输出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定消息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.Console.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
或者
上面小节中已经说了控制台输出配置,由于篇幅原因,这里不再赘述
① 文件输出配置
# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,File
# 指定文件日志输出appender
log4j.appender.File = org.apache.log4j.FileAppender
# 指定日志文件名
log4j.appender.File.File=D:/logs/testxiezhr.log
# 指定是否在原有日志的基础添加新日志
log4j.appender.File.Append=true
# 指定消息格式器 layout
log4j.appender.File.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.File.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
# 指定日志文件编码格式
log4j.appender.File.encoding=UTF-8
②日志文件根据大小分割输出
# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,RollingFile
# 指定文件日志根据大小分割输出appender
log4j.appender.RollingFile = org.apache.log4j.RollingFileAppender
# 指定日志文件名
log4j.appender.RollingFile.File=D:/logs/testxiezhr.log
# 设置是否在重新启动服务时,在原有日志的基础添加新日志
log4j.appender.RollingFile.Append=true
# 设置最多保存的日志文件个数
log4j.appender.RollingFile.MaxBackupIndex=5
# 设置文件大小,超过这个值,就会再产生一个文件
log4j.appender.RollingFile.maximumFileSize=1
# 指定消息格式器 layout
log4j.appender.RollingFile.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.RollingFile.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
# 指定日志文件编码格式
log4j.appender.RollingFile.encoding=UTF-8
最终生成日志效果如下所示
③ 日志文件根据日期分割
# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,DailyRollingFile
# 指定文件日志根据日期分割输出appender
log4j.appender.DailyRollingFile = org.apache.log4j.DailyRollingFileAppender
# 指定日志文件名
log4j.appender.DailyRollingFile.File=D:/logs/testxiezhr.log
# 设置是否在重新启动服务时,在原有日志的基础添加新日志
log4j.appender.DailyRollingFile.Append=true
# 指定消息格式器 layout
log4j.appender.DailyRollingFile.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.DailyRollingFile.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
# 指定日志文件编码格式
log4j.appender.DailyRollingFile.encoding=UTF-8
最终生成日志效果如下所示
④ 自定义日志配置
当我们想定义自己的日志配置时,可以按照如下配置添加.例如:添加
com.xiezhr
,它也是继承自rootLogger
,所以我们必须要添加
log4j.additivity.com.xiezhr=false
避免日志打印重复
# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,DailyRollingFile
# 自定义日志配置
log4j.logger.com.xiezhr=DEBUG,Console
# 设置日志叠加,这一句配置一定要添加,否则日志会重复输出
log4j.additivity.com.xiezhr=false
⑤ 将日志信息存入数据库
首先,我们新建一个testlog数据库,并在数据库下新建log日志表
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`)
);
其次,新建
JDBCAppender
,并且为
JDBCAppender
设置数据库连接信息,具体代码如下
@Test
public void testLog4j2db(){
//初始化日志配置信息,不需要配置文件
BasicConfigurator.configure();
//获取日志记录器
Logger logger = Logger.getLogger(Log4jTest.class);
// 新建JDBCAppender
JDBCAppender jdbcAppender = new JDBCAppender();
jdbcAppender.setDriver("com.mysql.cj.jdbc.Driver");
jdbcAppender.setURL("jdbc:mysql://localhost:3308/testlog?useSSL=false&serverTimezone=UTC");
jdbcAppender.setUser("root");
jdbcAppender.setPassword("123456");
jdbcAppender.setSql("INSERT INTO log(project_name,create_date,level,category,file_name,thread_name,line,all_category,message) values('晓凡日志测试','%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%t','%L','%l','%m')");
logger.addAppender(jdbcAppender);
// 记录日志
logger.info("这是一条自定义格式的日志信息");
logger.error("这是一条自定义格式的错误日志信息");
}
最后,运行代码,来看一下效果
何为日志门面,我们在第二小节中已经介绍过了,这里就不多说了。
日志门面的引入,使得我们可以面向接口开发,不再依赖具体的实现类,减小代码耦合。
JCL
全称
Jakarta Commons Logging
是Apache提供的一个通用日志
API
,
JCL
中自带一个日志实现
simplelog
,不过这个功能非常简单
① LCL的两个抽象类
② 示例代码
引入依赖
commons-logging
commons-logging
1.2
基本代码
我们没有导入任何日志实现,所以这里默认使用jdk自带
JUL
来实现日志
@Test
public void test(){
Log log = LogFactory.getLog(JclTest.class);
log.error("这是一条error");
log.warn("这是一条warn");
log.info("这是一条info");
log.debug("这是一条debug");
log.trace("这是一条trace");
}
① 导入
log4j
日志依赖
log4j
log4j
1.2.17
② 添加
log4j.properties
配置文件
# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制台日志输出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定消息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.Console.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
③ 测试日志输出
@Test
public void testJclLog4j(){
Log log = LogFactory.getLog(JclLog4jTest.class);
log.error("这是一条error");
log.warn("这是一条warn");
log.info("这是一条info");
log.debug("这是一条debug");
log.trace("这是一条trace");
}
日志输出如下:
我们可以看到,使用了
JCL
日志门面之后,我们从
simplelog
日志框架切换到
log4j
日志框架,没有改过代码。
SLF4j
全称是Simple Logging Facade For Java
Java简单的日志门面 和上一小节说到的JCL
干的一样的活。
在现目前的大多数Java项目中,日志框架基本上会选择
slf4j-api
作为门面,配上具体实现框架
logback
、
log4j
等使用
SLF4j
是目前市面上最流行的日志门面,主要提供了以下两个功能
① 添加依赖
org.slf4j
slf4j-api
2.0.13
org.slf4j
slf4j-simple
2.0.13
②日志输出
//申明日志对象
public final static Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
@Test
public void testSlf4j(){
//打印日志
logger.error("这是error日志");
logger.warn("这是warn日志");
logger.info("这是info日志");
logger.debug("这是debug日志");
logger.trace("这是trace日志");
//使用占位符输出日志信息
String name = "晓凡";
Integer age = 18;
logger.info("{},今年{}岁", name, age);
//将系统异常写入日志
try {
int i = 1/0;
}catch (Exception e){
logger.error("执行出错", e);
}
}
上面代码输出日志如下
下图是从官网薅下来的
slf4j
日志绑定图,对了,官网在这https://www.slf4j.org/
小伙伴看到上图可能会有点懵,全是英文,看不懂。
于是乎,晓凡简单翻译了一下,如下如所示
logback
、
simplelog
、
no-operation
框架遵循
SLF4j
规范 导入jar包即可使用
log4j
、
JUL
属于比较古老日志框架,不遵循
SLF4j
规范,需要引入适配器才能使用
slf4j-nop
后将不会使用任何日志框架
① 引入logback依赖
ch.qos.logback
logback-classic
1.4.14
② 日志输出
快速入门中代码不变,运行后,采用logback日志框架输入日志如下所示
slf4j-nop
① 引入依赖
org.slf4j
slf4j-nop
2.0.13
② 此时控制台将不会输出任何日志
log4j
日志框架
① 导入依赖
org.slf4j
slf4j-log4j12
2.0.13
log4j
log4j
1.2.17
② 添加log4j.properties配置文件
# 指定RootLogger顶级父元素默认配置信息
# 指定日志级别位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制台日志输出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定消息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定消息内容格式
log4j.appender.Console.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
③ 代码不变,日志输出如下
① 引入依赖
org.slf4j
slf4j-jdk14
2.0.13
② 代码不变,日志输出如下
如果你的项目中已经使用了
Log4j 1.x
等老的日志框架,但你想迁移到使用
SLF4J
的
API
,这时候你可以使用
SLF4J
的
Log4j 1.x
桥接器来平滑过渡
上图为SLF4j官网提供的桥接原理图,从图中,我们可以看到,只需要引入不同的桥接器
log4j-over-slf4j
、
jul-to-slf4j
、
jcl-over-slf4j
就可以实现在
不改变原有代码
的情况下,将日志从
log4j
、
jul
、
jcl
迁移到
slf4j
+
logback
日志组合
下面以
Log4j 1.x
迁移到slf4j
+logback
日志组合为例
Log4j 1.x
依赖
添加
SLF4J
提供的桥接组件
为项目添加
SLF4J
的具体实现
官网: https://logback.qos.ch/index.html
① 添加依赖
org.slf4j
slf4j-api
2.0.13
ch.qos.logback
logback-classic
1.4.14
② 打印日志代码
public class LogbackTest {
private static final Logger logger = LoggerFactory.getLogger(LogbackTest.class);
@Test
public void testLogbackQuick(){
logger.error("这是一个错误日志");
logger.warn("这是一个警告日志");
logger.info("这是一个信息日志");
logger.debug("这是一个调试日志");
logger.trace("这是一个跟踪日志");
}
}
Logback可以通过编程式配置(添加配置类的方式),也可以通过配置文件配置。
配置文件是日常开发中最常用的,我们这里就以这种方式配置,如果对配置文件感兴趣的小伙伴可自行到官网查看
Logger
:日志记录器,用来记录不同级别的日志信息,比如错误、警告、信息、调试和追踪。
Appender
:指定日志信息输出到不同的地方。比如,你可以设置一个Appender将日志输出到控制台,另一个Appender将日志写入文件,或者发送到远程服务器。
Encoder
:如果你使用的是文件Appender,Encoder就是用来定义日志文件内容格式的。比如,你可以选择日志的格式是简单文本还是XML。
Layout
:老版本的
Logback
中用来定义日志格式的组件。在新版本中,Encoder已经取代了Layout的功能。
Filter
:指定特定的规则来过滤日志信息,比如只记录错误以上的日志,或者只记录包含特定关键字的日志。
Configuration
:用来配置
Logback
的设置,比如设置日志级别、Appender的类型和参数等。配置可以通过
XML
、
JSON
或者
Groovy
脚本来完成。
Logback会依次读取以下类型配置文件
logback.groovy
logback-test.xml
logback.xml
(
最常用的
)
如果均不存在会采用默认配置
ConsoleAppender
控制台日志输出配置
配置文件
${pattern}
日志输入如下
日志输出格式:在前面几个日志框架中我们已经介绍过,大同小异。这里简单说下常用的几种
符号 | 含义 |
---|---|
%d{pattern}
|
格式化日期 |
%m或者%msg
|
日志信息 |
%M
|
method(方法) |
%L
|
行号 |
%c
|
完整类名称 |
%thread
|
线程名称 |
%n
|
换行 |
%-5level
|
日志级别,并且左对齐 |
FileAppender
将日志输出到文件
配置文件
${pattern}
${log_file}/logback.log
日志输出如下
${pattern}
${log_file}/logback.html
日志输出:
在
d:/logs
目录下生成一个
logback.html
文件
在生产环境中对日志进行按时间、日志大小拆分 且压缩日志 非常非常重要 ,所以单独拿出来说一说
配置文件
${pattern}
${log_file}/roll_logback.log
${log_file}/roll_logback.%d{yyyy-MM-dd}.log%i.gz
1MB
3
日志滚动输出: 按照日期和文件大小进行拆分
我们先来解释下什么是异步日志?
我们将日志输出到文件中,这样会涉及到大量
io
操作,非常耗时,如果需要输出大量的日志,就可能影响正常的主线程业务逻辑。
为了解决这问题,异步日志就出现了。日志信息不是直接写入到日志文件或者控制台,而是先发送到一个队列里,
然后由一个专门的线程去处理这些日志信息的写入工作。
这样做的好处是可以减少日志记录对主程序运行的影响,提高程序的效率。
private static final Logger logger = LoggerFactory.getLogger(LogbackTest.class);
@Test
public void testLogbackQuick(){
//日志输出
logger.error("这是一个错误日志");
logger.warn("这是一个警告日志");
logger.info("这是一个信息日志");
logger.debug("这是一个调试日志");
logger.trace("这是一个跟踪日志");
//这里模拟业务逻辑
System.out.println("晓凡今年18岁了");
System.out.println("晓凡的个人博客是:www.xiezhrspace.cn");
System.out.println("晓凡的个人公众号是:程序员晓凡");
System.out.println("晓凡的个人微信是:xie_zhr");
System.out.println("欢迎关注晓凡,持续输出干货!!!!!");
}
输出结果:
从上面控制台输出看,只有当日志输出完成之后我们的业务逻辑代码才被执行。如果日志耗时比较长,非常影响效率
我们只需在原来的配置文件中添加如下关键配置
日志输出效果:
从上面日志日志输出看,不再是日志输出完再进行业务逻辑代码执行,而是异步执行了
官网: https://logging.apache.org/log4j/2.x/
Log4j2
是
Log4j
的升级版,参考了
Logback
的一些优秀设计,修复了一些bug,性能和功能都带来了极大提升
主要体现在以下几个方面
性能提升:
Log4j2
在多线程环境下表现出更高的吞吐量,比
Log4j 1.x
和
Logback
高出10倍
异步日志
:
Log4j2
支持异步日志记录,可以通过
AsyncAppender
或
AsyncLogger
实现。异步日志可以减少日志记录对主程序性能的影响,尤其是在高并发场景下
自动重载配置
:
Log4j2
支持动态修改日志级别而不需要重启应用,这是借鉴了
Logback
的设计
无垃圾机制
:
Log4j2
大部分情况下使用无垃圾机制,避免因频繁的日志收集导致的
JVM GC2
。
异常处理
:
Log4j2
提供了异常处理机制,Appender 中的异常可以被应用感知到,而
Logback
中的异常不会被应用感知
Log4j2
有这么多优势,所以在未来
SLF4j
+
Log4j2
组合
Log4j2不仅仅是日志实现,同时也是日志门面。在快速入门中,我们就使用Log4j2作为日志门面和日志实现来快速入门
org.apache.logging.log4j
log4j-api
2.23.1
org.apache.logging.log4j
log4j-core
2.23.1
public class Log4j2Test {
private static final Logger logger = LogManager.getLogger(Log4j2Test.class);
@Test
public void Log4j2Test(){
logger.fatal("这是一条致命信息");
logger.error("这是一条错误信息");
logger.warn("这是一条警告信息");
logger.info("这是一条一般信息");
logger.debug("这是一条调试信息");
logger.trace("这是一条追踪信息");
}
}
日志输出结果如下
前面我们提到
SLF4j
+Log4j2
组合会是未来日志发展的大趋势,所以接下来我们就使用这个组合来输出日志
导入依赖
org.apache.logging.log4j
log4j-api
2.23.1
org.apache.logging.log4j
log4j-core
2.23.1
org.slf4j
slf4j-api
2.0.13
org.apache.logging.log4j
log4j-slf4j-impl
2.23.1
日志输出代码
public class Log4j2Test {
//这里我们换成了slf4j的门面接口
private static final Logger logger = LoggerFactory.getLogger(Log4j2Test.class);
@Test
public void Log4j2Test(){
logger.error("这是一条错误信息");
logger.warn("这是一条警告信息");
logger.info("这是一条一般信息");
logger.debug("这是一条调试信息");
logger.trace("这是一条追踪信息");
}
}
日志输出效果
log4j2 默认加载classpath 下的 log4j2.xml 文件中的配置。
下面通过log4j2.xml 配置文件进行测试,配置大同小异,这里就不一一说明了,给出完整的配置
D:/logs
日志输出如下
下面的截图为2024-08-11的日志按日志文件大小1MB拆分成10个并进行压缩,拆分满10个文件后新日志会覆盖旧日志,其他天的类似
Log4j2
最大的特点就是异步日志,就因为异步日志的存在,将性能提升了好多。
下图是官网给的性能对比图,从图中我们可以看出在 全局异步模式 (Loggers all async) 和 混合异步模式 (Loggers mixed sync/async)
性能简直将
Logback
和
Log4j
日志框架甩了一条街。
至于什么时全局异步模式和混合异步模式?我们会在后面详细说明
同步日志 :想象一下你手里有一堆信件要写,每写一封信你都得亲自动手,写完后才能去做别的事情。在这个过程中,你得一封一封地写,不能同时干其他事,这就类似于同步日志。在程序中,同步日志意味着每次记录日志时,程序都得停下来,等待日志写完了才能继续执行其他任务。这样做的好处是不会丢信(日志),但坏处是写信(记录日志)这个过程如果太慢,就会耽误你做其他事情(程序运行)
异步日志 :如果你特别忙,你可能会找个助手来帮你写信。你只需要告诉他要写什么,然后就可以继续忙自己的事情,而助手会帮你把信写好并寄出去。这个过程就像是异步日志。在程序中,异步日志意味着程序可以把要记录的日志信息交给一个专门的“助手”(通常是另外的线程或进程),然后程序就可以继续执行其他任务,而不需要等待日志写完。这样做的好处是可以更快地处理任务,不会耽误正事儿,但偶尔可能会有一两封信(日志)因为意外情况没有寄出去。
全局异步 : 所有日志记录都采用异步的方式记录
混合异步 :以在应用中同时使用同步日志和异步日志,这使得日志配置更加灵活
2、异步日志流程
异步日志的实现一共有两种方式
AsyncAppender
[
生产上几乎不使用,因为性能低下
]
AsyncLogger
[
生产上用得多,因为性能高
]
第一种方式因为用的不多性能也不够好,所以这里就不说了,我们以第二种配置来具体说一说
不管采用哪种方式,首先都要引入异步依赖
com.lmax
disruptor
3.4.4
① 全局异步
只需在
resources
下添加
log4j2.component.properties
,具体内容如下
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
日志输出结果
② 混合异步配置
首先,我们需要关闭全局异步配置,将上面添加的
log4j2.component.properties
内容注释即可
log4j2.xml
配置
D:/logs
输出结果:
注意事项:
AsyncAppender
、全局配置、混合配置 不能同时出现,否则将影响日志性能
includeLocation="false"
关闭日志记录的行号信息 配置一定要加上,否则会降低日志性能
通过上面八小节我们对Java日志框架应该非常熟悉了,并且也知道怎么使用了。但在日志开发中,使用日志还是有写规约需要我们去遵守。
下面式阿里巴巴Java开发手册中的日志规约
❶【
强制
】应用中不可直接使用日志系统(
Log4j
、
Logback
)中的
API
,而应依赖使用日志框架(
SLF4J
、
JCL--Jakarta Commons Logging
)中的
API
,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
说明:日志框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推荐使用 SLF4J)
1)使用
SLF4J
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);
JCL
:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
private static final Log log = LogFactory.getLog(Test.class);
❷【
强制
】所有日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以“应用名
.log
”来保存,
保存在”
/home/admin/应用名/logs/
“目录下,过往日志格式为:
{logname}.log.{保存日期}
,日期格式:
yyyy-MM-dd
正例
:以 aap 应用为例,日志保存在
/home/admin/aapserver/logs/aap.log
,历史日志名称为
aap.log.2021-03-23
❸【 强制 】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不少于六个月,并且进行网络多机备份。
❹【
强制
】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log
。
logType
:日志类型,如
stats/monitor/access
等;
logName
:日志描述。
这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
正例
:
mppserver
应用中单独监控时区转换异常,如:
mppserver_monitor_timeZoneConvert.log
❺ 【 强制 】在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为 String 字符串的拼接会使用
StringBuilder
的append()
方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例:
logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
❻【
强制
】对于
trace
/
debug
/
info
级别的日志输出,必须进行日志级别的开关判断。
说明:虽然在
debug(参数)
的方法体内第一行代码isDisabled(Level.DEBUG_INT)
为真时(Slf4j 的常见实现Log4j 和 Logback),就直接return
,但是参数可能会进行字符串拼接运算。此外,如果debug(getName())
这种参数内有getName()
方法调用,无谓浪费方法调用的开销。
正例:
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) {
logger.debug("Current ID is: {} and name is: {}", id, getName());
}
❼【
强制
】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置
additivity=false
。
正例:
❽ 【强制】生产环境禁止直接使用
System.out
或
System.err
输出日志或使用
e.printStackTrace()
打印异常堆栈 。
说明:标准日志输出与标准错误输出文件每次
Jboss
重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。
❾ 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字
throws
往上抛出。
正例:
logger.error("inputParams:{} and errorMessage:{}", 各类参数或者对象 toString(), e.getMessage(), e);
❿ 【
强制
】日志打印时禁止直接用 JSON 工具将对象转换成
String
。
说明:如果对象里某些
get
方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。
正例:
打印日志时仅打印出业务相关属性值或者调用其对象的
toString()
方法。
⓫ 【
推荐
】谨慎地记录日志。生产环境禁止输出
debug
日志;有选择地输出
info
日志;
如果使用
warn
来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
⓬ 【
推荐
】可以使用
warn
日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。
说明:如非必要,请不要在此场景打出
error
级别,避免频繁报警。 注意日志输出的级别,error
级别只记录系统逻辑出错、异常或者重要的错误信息。
⓭ 【 推荐 】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。
说明:国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息。
本期内容到这儿就结束了 ★,° :.☆( ̄▽ ̄)/$: .°★ 。 希望对您有所帮助
我们下期再见 ヾ(•ω•`)o (●'◡'●)
热门资讯