第一章 日志的概念
一、概述
日志文件是用于记录系统操作事件的文件集合,可分为事件日志和消息日志。具有处理历史数据、诊断问题的追踪以及理解系统的活动等重要作用。
在计算机中,日志文件是记录在操作系统或其他软件运行中发生的事件或在通信软件的不同用户之间的消息的文件。记录是保持日志的行为。在最简单的情况下,消息被写入单个日志文件。
二、日志的作用
调试
在Java
项目调试时,查看栈信息可以方便地知道当前程序的运行状态,输出的日志便于记录程序在之前的运行结果。如果你大量使用System.out
或者System.err
,这是一种最方便最有效的方法,但显得不够专业。
错误定位
不要以为项目能正确跑起来就可以高枕无忧,项目在运行一段时候后,可能由于数据问题,网络问题,内存问题等出现异常。这时日志可以帮助开发或者运维人员快速定位错误位置,提出解决方案。
数据分析
大数据的兴起,使得大量的日志分析成为可能,ELK
也让日志分析门槛降低了很多。日志中蕴含了大量的用户数据,包括点击行为,兴趣偏好等,用户画像对于公司下一步的战略方向有一定指引作用。
三、接触过的日志
最简单的日志输出方式,我们每天都在使用:
System.out.println("这个数的结果是:"+ num);
以及错误日志:
System.err.println("此处发生了异常");
此类代码在程序的执行过程中没有什么实质的作用,但是却能打印一些中间变量,辅助我们调试和错误的排查。
日志系统我们也见过:
当我们的程序无法启动或者运行过程中产生问题,会有所记录,比如我的catalina.log
中查看,发现确实有错误信息,这能帮我们迅速定位:
而我们的System.err
只能做到控制台打印日志,所以我们需要更强大日志框架来处理:
四、主流日志框架
- 日志实现(具体干活的):
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
,将输出内容进行排版。
一、入门案例
public static void main(String[] args) {
Logger logger = Logger.getLogger("myLogger");
logger.info("信息");
logger.warning("警告信息");
logger.severe("严重信息");
}
十月 19, 2022 2:13:15 下午 com.example.DemoGitApplicationTests main
信息: 信息
十月 19, 2022 2:13:15 下午 com.example.DemoGitApplicationTests main
警告: 警告信息
十月 19, 2022 2:13:15 下午 com.example.DemoGitApplicationTests main
严重: 严重信息
二、日志的级别
jul
中定义的日志级别,从上述例子中我们也看到使用info
和warning
打印出的日志有不同的前缀,通过给日志设置不同的级别可以清晰的从日志中区分出哪些是基本信息,哪些是调试信息,哪些是严重的异常。
java.util.logging.Level
中定义了日志的级别:
- SEVERE(最高值)
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST(最低值)
再例如:我们查看tomcat
的日志,能明显的看到不同级别的日志,其实tomcat
默认使用的就是JUL
:
还有两个特殊的级别:
OFF
,可用来关闭日志记录。ALL
,启用所有消息的日志记录。
虽然我们测试了7
个日志级别,
public static void main(String[] args) {
Logger logger = Logger.getLogger(DemoGitApplicationTests.class.getName());
logger.severe("severe");
logger.warning("warning");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
我们发现能够打印的只有三行,这是为什么呢?
十月 19, 2022 2:24:57 下午 com.example.DemoGitApplicationTests main
严重: severe
十月 19, 2022 2:24:57 下午 com.example.DemoGitApplicationTests main
警告: warning
十月 19, 2022 2:24:57 下午 com.example.DemoGitApplicationTests main
信息: info
我们找一下这个文件,下图是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
貌似可以处理文件,同时我们确实发现了他有这么一个方法:
三、日志配置
public static void main(String[] args) throws IOException {
// 1.创建日志记录器对象
Logger logger = Logger.getLogger("com.example.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("C:/Users/len/Desktop/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");
}
再次查看结果:
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
严重: severe
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
警告: warning
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
信息: info
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
配置: config
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
详细: fine
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
较详细: finer
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
非常详细: finest
文件中也输出了正确的结果:
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
严重: severe
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
警告: warning
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
信息: info
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
配置: config
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
详细: fine
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
较详细: finer
十月 19, 2022 2:43:16 下午 com.example.DemoGitApplicationTests main
非常详细: finest
四、 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;
}
public static void main(String[] args) throws IOException {
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@2d98a335
logger1.getParent() = java.util.logging.Logger@16b98e56
logger2 = java.util.logging.Logger@16b98e56
logger2.getParent() = java.util.logging.LogManager$RootLogger@7ef20235
true
public static void main(String[] args) throws IOException {
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");
}
十月 19, 2022 2:48:49 下午 com.example.DemoGitApplicationTests main
严重: severe
十月 19, 2022 2:48:49 下午 com.example.DemoGitApplicationTests main
警告: warning
十月 19, 2022 2:48:49 下午 com.example.DemoGitApplicationTests main
信息: info
十月 19, 2022 2:48:49 下午 com.example.DemoGitApplicationTests main
配置: config
十月 19, 2022 2:48:49 下午 com.example.DemoGitApplicationTests main
详细: fine
十月 19, 2022 2:48:49 下午 com.example.DemoGitApplicationTests main
较详细: finer
十月 19, 2022 2:48:49 下午 com.example.DemoGitApplicationTests main
非常详细: finest
五、日志格式化
我们可以独立的实现日志格式化的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
方法了。
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
当做填空题,后边的可变参数当做答案。
常用的转换符
当然不同数据类型需要不同转换符完成字符串的转换,以下是不同类型的转化符列表:
小例子:
System.out.printf("过年了,%s今年%d岁了,今天收了%f元的压岁钱!",
"小明",5,88.88);
结果:
过年了,小明今年5岁了,今天收了88.880000元的压岁钱!
这要比拼写字符串简单多了。
特殊符号
接下来我们看几个特殊字符的常用搭配,可以实现一些高级功能:
System.out.printf("过年了,%s今年%03d岁了,今天收了%,f元的压岁钱!",
"小明",5,8888.88);
结果
过年了,小明今年005岁了,今天收了8,888.880000元的压岁钱!
默认情况下,我们的可变参数是安装顺序依次替换,但是我想重复利用可变参数那该怎么处理呢?
我们可以采用 在转换符中加数字$
完成匹配:
System.out.printf("%1$s %1$s %1$s","小明");
其中1$
就代表第一个参数,那么2$
就代表第二个参数了
小明 小明 小明
日期处理
第一个例子中有说到 %tx x
代表日期转换符 我也顺便列举下日期转换符
我们可以使用以下三个类去进行格式化,其中可能存在不支持的情况,比如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
六、配置文件
我们不妨看看一个文件处理器的源码是怎么读配置项的:
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") );
C:\Users\zn\java0.log
我们将拷贝的文件稍作修改:
.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了:
第三章 log4j2的使用
Apache Log4j2
是对Log4j
的升级版,参考了logback
的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有:
- 异常处理,在
logback
中,Appender
中的异常不会被应用感知到,但是在log4j2
中,提供了一些异常处理机制。 - 性能提升,
log4j2
相较于log4j
和logback
都具有很明显的性能提升,后面会有官方测试的数据。 - 自动重载配置,参考了
logback
的设计,当然会提供自动刷新参数配置,最实用的就是我们在生产 上可以动态的修改日志的级别而不需要重启应用。
官网: https://logging.apache.org/log4j/2.x/
一、Log4j2入门
目前已经有三个门面了,其实不管是哪里都是江湖,都想写一个门面,一统江湖,所以log42
出了提供日志实现以外,也拥有一套自己的独立的门面。
目前市面上最主流的日志门面就是SLF4J
,虽然Log4j2
也是日志门面,因为它的日志实现功能非常强大,性能优越。所以大家一般还是将Log4j2
看作是日志的实现,Slf4j + Log4j2
应该是未来的大势所趋。
使用log4j-api做门面
- 添加依赖
<!-- 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>
- 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");
}
}
- 结果:
使用slf4j做门面
使用slf4j
作为日志的门面,使用log4j2
作为日志的实现。
<!-- 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>
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");
}
结果:
我们看到log4j2
的默认日志级别好像是error
。
二、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},最小粒度为小时,则每一个小时生成一个文件。
三、Log4j2异步日志
异步日志
log4j2
最大的特点就是异步日志,其性能的提升主要也是从异步日志中受益,我们来看看如何使用 log4j2
的异步日志。
- 同步日志
- 异步日志
Log4j2
提供了两种实现日志的方式,一个是通过AsyncAppender
,一个是通过AsyncLogger
,分别对应 前面我们说的Appender
组件和Logger
组件。
注意:配置异步日志需要添加依赖
<!--异步日志依赖-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.4</version>
</dependency>
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>
AsyncLogger方式
AsyncLogger
才是log4j2
的重头戏,也是官方推荐的异步方式。它可以使得调用Logger.log
返回的 更快。你可以有两种选择:全局异步和混合异步。
- 全局异步就是,所有的日志都异步的记录,在配置文件上不用做任何改动,只需要添加一个 l
og4j2.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
,打印位置信息会急剧降低异步日志的性能,比同步日志还要慢。
or (int i = 0; i < 1000000; i++) {
LOGGER.fatal("fatal");
}
long end = System.currentTimeMillis();
System.out.println(end - start);
2970
Log4j2的性能
log4j官网对其性能进行大肆宣扬,但是网上也有专业认识进行测试,log4j在大量日志的情况下有一定的优势,他确实是日后的选择。但是也不必纠结。
第四章:怎么打日志
基本格式
必须使用参数化信息的方式:
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
,需要使用开关进行管理,不能一直开启。
说明 如果代码中出现以下代码,可以进行优化:
//1. 获取用户基本薪资
//2. 获取用户休假情况
//3. 计算用户应得薪资
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());
}
}