简介
- 本篇将介绍如何使用
JDK
原生自带的日志框架JUL
。
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
对日志内容进行排版,之后再输出到指定的位置。
入门案例
@Test
public void testQuick() {
// 1.创建JUL Logger对象无法传入class对象,迂回战术可以获取该类之后获取名字
Logger logger = Logger.getLogger(JulTest.class.getName());
// 2.输出日志的两种方式,其中日志级别共有7种,还有2种特殊级别
logger.info("Hello, here is Java Util Logging");
logger.log(Level.INFO, "You can also use this way to output log.");
// 3.尽量不采用拼接字符串的形式,采用占位符的形式,占位符中需要填写索引编号
String name = "dylan";
int age = 12;
logger.log(Level.INFO, "[USER]:{0} [AGE]:{1}", new Object[]{name, age});
}
日志级别
JUL
内置工7
种日志级别,另外有2
中特殊级别:
java.util.logging.Level
中定义了7
种日志级别:
SEVERE
(最高等级)
WARNING
INFO
(默认等级)
CONFIG
FINE
FINER
FINEST
(最低等级)
- 还有
2
种特殊日志级别,用于开关日志:
OFF
:可用于关闭日志记录;
ALL
:启用所有消息的日志记录。
- 示例代码:
- 默认的日志级别是由
RootLogger
决定的,所有的Logger
对象默认都会继承并使用RootLogger
所提供的控制台输出处理器对象ConsoleHandler
;
- 同时,
RootLogger
的默认日志输出等级为INFO
,则所有未经配置的Logger
默认也是使用该日志级别。
/**
* JUL默认的日志级别是Info,所有级别高于等于Info的日志都会被输出控制台。
*/
@Test
public void testLogLevel() {
// 1.获取日志记录器对象,其中getLogger的参数name是此日志对象Logger的名称,可以由logger.getName()取出
final Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");
// 2.使用默认的ConsoleHandler输出各级别的日志,默认情况下只有Server、Warning和Info会输出到控制台
logger.severe("Level Severe.");
logger.warning("Level Warning.");
logger.info("Level Info.");
logger.config("Level Config.");
logger.fine("Level Fine.");
logger.finer("Level Finer.");
logger.finest("Level Finest");
}
- 尽管代码中定义了输出
INFO
等级以下的日志,但实际控制台中并没有相关的日志信息,这是因为此时创建的Logger
对象继承并使用了RootLogger
中的日志等级和处理器对象。
- 考虑以下代码:
@Test
public void testFindDefault() {
final Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");
System.out.println("Default Logger's Level: " + logger.getLevel());
System.out.println("Default Logger's Handlers' Quantity: " + logger.getHandlers().length);
}
- 其中采用更为直观的方式打印当前
Logger
对象的日志等级,和与其关联的处理器对象数量。运行输出,得到:
- 这并不意外,默认情况下由于
Logger
继承并使用RootLogger
中的日志等级与处理器对象,因此对于它自身来说,并不拥有任何的日志等级信息与处理器对象。
- 考虑以下代码:
@Test
public void testParentLogger() {
final Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");
final Logger loggerParent = logger.getParent();
System.out.println("Logger's Default Parent Logger is: " + loggerParent.getClass().getSimpleName());
System.out.println("Parent Logger's Level: " + loggerParent.getLevel());
System.out.println("Parent Logger's Handlers' Quantity: " + loggerParent.getHandlers().length);
for (Handler handler : loggerParent.getHandlers()) {
System.out.println("Default " + handler.getClass().getSimpleName() + "'s Level: " + handler.getLevel());
}
}
- 同样采用更直观的方式打印当前
Logger
的父日志相关信息,得到以下输出:
- 至此,不难推断出
Logger
的日志输出等级取决于RootLogger
。
- 还有一个定论,即
Logger
和Handler
的日志等级是相互牵制的,其中等级较高一方的配置生效。
- 通过更改
RootLogger
中的日志级别及其ConsoleHandler
的日志级别,编写以下代码:
@Test
public void testLevel() {
// 0.准备工作
final Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");
final Logger parent = logger.getParent();
// 1.RootLogger日志等级高于ConsoleHandler日志等级
parent.setLevel(Level.WARNING);
parent.getHandlers()[0].setLevel(Level.CONFIG);
logger.severe("[SEVERE ]Something.");
logger.warning("[WARNING]Something.");
logger.info("[INFO ]Something.");
logger.config("[CONFIG ]Something.");
System.out.println("=============================================");
// 2.RootLogger日志等级小于ConsoleHandler日志等级
parent.setLevel(Level.FINER);
logger.severe("[SEVERE ]Something.");
logger.warning("[WARNING]Something.");
logger.info("[INFO ]Something.");
logger.config("[CONFIG ]Something.");
logger.fine("[FINE ]Something.");
logger.finer("[FINER ]Something.");
}
自定义配置
- 通常会在单独的配置文件中去配置
Logger
的日志等级和处理器类型等,但作为入门,需要了解如何在Java
代码中,通过更改Logger
日志级别和配置自定义ConsoleHandler
的方式,去影响日志输出。
- 如果不希望
Logger
对象使用RootLogger
中的日志级别进行输出,则需要对Logger
进行以下配置:
- 重新设置
Logger
的日志输出等级;
- 重新配置
Logger
的处理器Handler
类型,并不再使用RootLogger
中提供的默认处理器。
- 测试类
testUserDefined
源码如下:
@Test
public void testUserDefined() {
// 1.获取日志记录器对象
Logger logger = Logger.getLogger("cn.dylanphang.jul.JulTest");
// 2.配置Logger使其不再继承使用RootLogger中的所有Handler
logger.setUseParentHandlers(false);
// 3.自定义ConsoleHandler对象,并配置该处理器的日志等级
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.ALL);
// 4.为Logger添加自定义的ConsoleHandler
logger.addHandler(consoleHandler);
// 5.由于Logger默认会使用RootLogger的日志等级,如果希望输出Level.ALL的日志,同时需要设置Logger的日志等级也为Level.ALL
// *.否则,Logger将会使用RootLogger的默认日志等级INFO,最终日志只会输出等级高于或等于INFO的内容
logger.setLevel(Level.ALL);
// 6.分别输出各等级的日志
logger.severe("[SEVERE ]Something.");
logger.warning("[WARNING]Something.");
logger.info("[INFO ]Something.");
logger.config("[CONFIG ]Something.");
logger.fine("[FINE ]Something.");
logger.finer("[FINER ]Something.");
logger.finest("[FINEST ]Something.");
}
- 而开发中较为经常的使用方式,是通过
logging.properties
文件的方式进行配置:
- 程序中需要编写代码去显示加载
logging.properties
文件以启用自定义的配置;
- 配置文件中,
Handler
是单独进行配置的,开发人员可以单独定义控制台输出日志的处理器对象ConsoleHandler
或文件输出日志的处理器对象FileHandler
等;
- 具备相关自定义
Handler
后,需要将Logger
与Handler
进行关联,配置文件支持RootLogger
或指定名称的Logger
与自定义Handler
进行关联;
- 无论任何时候,都需要明确日志最终的输出等级,是同时由
Logger
与其相关联的Handler
所决定的。
# RootLogger的日志级别(默认INFO),所有的Handler都受限于此日志级别,Handler的日志级别可以比RootLogger的日志级别高
.level=ALL
# RootLogger默认的处理器,可以配置多个,所有非手动解除父日志的子日志都将使用这些处理器
handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler
# ConsoleHandler控制台输出处理器配置
# 指定ConsoleHandler默认日志级别
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.encoding=UTF-8
# FileHandler文件输出处理器配置
# 指定FileHandler默认日志级别
java.util.logging.FileHandler.level=INFO
# 日志文件输出路径
java.util.logging.FileHandler.pattern=/dylan%u.log
# 单个日志文件大小,单位是bit,1024bit即为1kb
java.util.logging.FileHandler.limit=1024*1024*10
# 日志文件数量,如果数量为2,则会生成dylan.log.0文件和dylan.log.1文件,总容量为: (limit * count)bit
java.util.logging.FileHandler.count=1
# FileHandler持有的最大并发锁数
java.util.logging.FileHandler.maxLocks=100
# 指定要使用的Formatter类的名称,FileHandler默认使用的是XMLFormatter
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
# 涉及中文日志就最好加上编码集
java.util.logging.FileHandler.encoding=UTF-8
# 是否以追加方式添加日志内容
java.util.logging.FileHandler.append=true
# SimpleFormatter的输出格式配置
java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
# 自定义日志级别,其中”cn.hanna“指的是Logger.getLogger(String name)中的入参name!!!
cn.hanna.handlers=java.util.logging.ConsoleHandler
cn.hanna.level=INFO
# 如果此时不关闭名为cn.hanna的Logger的父日志处理器,则在控制台会同时出现父日志处理器和自定义的处理器,消息将重复输出
cn.hanna.useParentHandlers=false
- 测试类
test()
的加载过程如下:
- 项目是通过
Maven
构建的,默认使用getResourceAsStream(String fileName)
将读取resource
目录下的配置文件;
- 其中
LogManager
对象是用于全局配置日志的管理对象,它是单例的,使用它来加载应用配置文件;
- 配置文件使用
readConfiguration(InputStream is)
加载应用后,RootLogger
的日志级别和与之关联的Handler
及其日志级别就已经配置完毕,与logging.properties
中一致;
- 同时特殊名称
cn.hanna
的Logger
会遵循配置文件中设置的日志等级INFO
,其关闭依赖RootLogger
,同时仅仅将日志输出到控制台中。
@Test
public void testUserDefined() throws IOException {
// 1.读取配置文件
final InputStream is =
Jul2Test.class.getClassLoader().getResourceAsStream("logging.properties");
// 2.获取LogManager,LogManager是单例对象,并加载应用配置文件logging.properties
final LogManager logManager = LogManager.getLogManager();
logManager.readConfiguration(is);
// 4.正常输出日志
final Logger loggerNormal = Logger.getLogger(this.getClass().getName());
loggerNormal.severe("[SEVERE ]Something.");
loggerNormal.warning("[WARNING]Something.");
loggerNormal.info("[INFO ]Something.");
loggerNormal.config("[CONFIG ]Something.");
loggerNormal.fine("[FINE ]Something.");
loggerNormal.finer("[FINER ]Something.");
loggerNormal.finest("[FINEST ]Something.");
System.out.println("=============================================");
// 5.指定日志对象的名称,配置文件中对cn.hanna名称的Logger进行了特殊配置
final Logger loggerSpecial = Logger.getLogger("cn.hanna");
loggerSpecial.severe("[SEVERE ]Something.");
loggerSpecial.warning("[WARNING]Something.");
loggerSpecial.info("[INFO ]Something.");
loggerSpecial.config("[CONFIG ]Something.");
loggerSpecial.fine("[FINE ]Something.");
loggerSpecial.finer("[FINER ]Something.");
loggerSpecial.finest("[FINEST ]Something.");
}
占位符相关
- 留意到配置内
formatter
中的包含了相关占位符,如果希望掌握这一部分的含义,需要首先了解String.format()
的使用。
- 考虑以下代码:
@Test
public void test() {
String name = "Mike";
double score = 89;
Calendar calendar = Calendar.getInstance();
calendar.set(1992, Calendar.JANUARY, 3);
final String formatA = String.format("[%1$-5s] %2$tF score: (%3$-8.2f)", name, calendar, score);
final String formatB = String.format("[%1$-5s] %2$tY-%2$tm-%2$td score: (%3$8.2f)", name, calendar, score);
System.out.println(formatA);
System.out.println(formatB);
}
- 可以看到,
String.format
可以将数据按照指定的格式转换为String
类型的数据,其中需要遵循以下规则:
%[argument_index$][flags][width][.precision]conversion
- 参数说明:
参数 |
描述 |
argument_index$ |
格式化参数在String.format 中的索引位置,从1 开始计算索引 |
flags |
关于此格式化数据的额外格式设定,如左对齐- 、是否显示正负数符号+ |
width |
此格式数据所占用的宽度。当宽度大于参数的实际宽度时,会自动将格式化数据右对齐 |
.precision |
需要格式化的是一个浮点数时,可以自定义该浮点数格式化后所需保留的小数点位数 |
conversion |
表示需要格式的数据类型是什么 |
- 关于
String.format
的详细内容,可以参考以下网址,其中提供了大量的关于各种类型的各种格式化占位符信息:
- 了解
String.format
后,需要知道其实日志记录的格式化,内部使用的也是String.format
,形式如下:
String.format(format, date, source, logger, level, message, thrown);
- 参数说明:
参数 |
描述 |
format |
SimpleFormatter.format 中使用的格式,也是配置文件logging.properties 中定义的格式 |
date |
日志的输出日期 |
source |
日志的调用者,如果不存在则会输出日志对象的名称 |
logger |
日志对象的名称 |
level |
日志等级 |
message |
日志信息 |
thrown |
当有异常时,会在日志信息中包含异常信息,如果不存在则不输出 |
- 关于
SimpleFormatter
的详细内容,可以参考以下网址,其中包含了更详细的说明与示例:
- 初步了解相关原理后,重新审视配置文件中的
%4$s: %5$s [%1$tc]%n
:
%4$s
:索引位4
是level
,表示日志等级,数据类型是String
,使用的conversion
为s
;
%5$s
:索引位5
是message
,表示日志信息,数据类型是String
,使用的conversion
为s
;
%1$tc
:索引位1
是date
,表示日志输出日期,数据类型是Date
,使用的conversion
为tc
;
%n
:换行符。
- 因此,当运行代码
logger.severe("[SEVERE ]Something.");
,将得到日志记录,对比自定义格式:
滤器配置
- 关于
Filter
过滤器,了解即可。
- 代码中需要使用
Logger
对象中的setFilter
方法,配置一个Filter
对象,其源码如下:
@FunctionalInterface
public interface Filter {
/**
* Check if a given log record should be published.
* @param record a LogRecord
* @return true if the log record should be published.
*/
public boolean isLoggable(LogRecord record);
}
- 其中真正控制日志是否运行记录的对象是
LogRecord
,使用其中的getMessage()
可以获取到日志信息。
- 当方法
isLoggable
返回false
时,不记录日志;返回true
则记录日志。
- 测试代码如下:
- 其中使用
lambda
表达式,返回的结果为!record.getMessage().contains("What")
,即如果日志记录包含了关键字What
,则返回false
,表示过滤该条日志信息,不进行记录操作。
@Test
public void test() throws IOException {
final InputStream is =
Jul3Test.class.getClassLoader().getResourceAsStream("logging.properties");
final LogManager logManager = LogManager.getLogManager();
logManager.readConfiguration(is);
final Logger logger = Logger.getLogger(this.getClass().getName());
logger.severe("[SEVERE ]Something What.");
logger.warning("[WARNING]Something.");
logger.info("[INFO ]Something.");
logger.config("[CONFIG ]Something.");
logger.fine("[FINE ]Something What.");
logger.finer("[FINER ]Something.");
logger.finest("[FINEST ]Something What.");
System.out.println("=============================================");
// *.配置过滤器Filter
logger.setFilter((x) -> !x.getMessage().contains("What"));
logger.severe("[SEVERE ]Something What.");
logger.warning("[WARNING]Something.");
logger.info("[INFO ]Something.");
logger.config("[CONFIG ]Something.");
logger.fine("[FINE ]Something What.");
logger.finer("[FINER ]Something.");
logger.finest("[FINEST ]Something What.");
}
Filter
生效并成功将包含What
的日志记录过滤。
完整的配置文件示例
- 最后贴一个
JUL
可用的配置文件示例,其中RootLogger
仅使用了ConsoleHandler
输出日志:
.level=ALL
handlers=java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.encoding=UTF-8
java.util.logging.FileHandler.level=INFO
java.util.logging.FileHandler.pattern=/sample%u.log
java.util.logging.FileHandler.limit=1024*1024*10
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.encoding=UTF-8
java.util.logging.FileHandler.append=true
java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
cn.xyz.handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler
cn.xyz.level=INFO
cn.xyz.useParentHandlers=false
- 关于其他的
Handler
不作演示了,因为实际遇到的情况很少,有需要的时候查阅相关官方资料即可。
总结
- 关于
JUL
的介绍到此为止,总体来说是一款很轻量的框架;
- 在项目体量很小时,可以使用
JUL
记录相关日志,免去使用其他日志门面或框架的繁琐操作,如依赖导入等。