文章目录
系列文章目录
学习Java日志框架之——搞懂JUL(java.util.logging)
学习Java日志框架之——搞懂log4j
学习Java日志框架之——搞懂日志门面(JCL+SLF4J)
学习日志框架之——搞懂logback
学习日志框架之——log4j2入门
log4j2扩展——打印自定义日志输出格式,将日志输出为json或自定义
一、JUL简介
JUL全称 Java Util Logging,核心类在java.util.logging包下,它是java原生的日志框架,使用时不需要另外引用第三方的类库,相对其他的框架使用方便,学习简单,主要是使用在小型应用中。
二、JUL组件介绍
Logger:被称为记录器,应用程序通过获取Logger对象,调用其API来发布日志信息。Logger通常被认为是访问日志系统的入口程序。
Handler:处理器,每个Logger都会关联一个或者是一组Handler,Logger会将日志交给关联的Handler去做处理,由Handler负责将日志做记录。Handler具体实现了日志的输出位置,比如可以输出到控制台或者是文件中等等。
Filter:过滤器,根据需要定制哪些信息会被记录,哪些信息会被略过。
Formatter:格式化组件,它负责对日志中的数据和信息进行转换和格式化,所以它决定了我们输出日志最终的形式。
Level:日志的输出级别,每条日志消息都有一个关联的级别。我们根据输出级别的设置,用来展现最终所呈现的日志信息。根据不同的需求,去设置不同的级别。
三、代码实例
本文使用的类,都是java.util.logging包下的类。
1、入门案例
// 入门案例
public static void test01() {
// Logger创建方式,参数为当前类全路径字符串com.demo.logger.jul.JULTest
Logger logger = Logger.getLogger(JULTest.class.getCanonicalName());
/*
第一种方式:
直接调用日志级别相关的方法,方法中传递日志输出信息
假设现在我们要输出info级别的日志信息
*/
logger.info("输出info信息");
/*
输出内容:
三月 20, 2023 9:09:38 下午 com.demo.logger.jul.JULTest test01
信息: 输出info信息
*/
/*
第二种方式:
调用通用的log方法,然后在里面通过level类型来定义日志的级别参数,以及搭配日志输出信息的参数
*/
logger.log(Level.WARNING, "输出warning信息");
/*
输出内容:
三月 20, 2023 9:14:09 下午 com.demo.logger.jul.JULTest test01
警告: 输出warning信息
*/
// 动态输出数据,生产日志,使用占位符的方式进行操作
String name = "zhangsan";
int age = 23;
// logger.log(Level.INFO, "姓名:" + name + "年龄:" + age);
logger.log(Level.INFO, "姓名:{0}年龄:{1}", new Object[]{name, age});
/*
{0}和 {1}分别代表第一个占位符和第二个占位符,同时传递一个数组,代表参数的集合
输出内容:
三月 20, 2023 9:18:16 下午 com.demo.logger.jul.JULTest test01
信息: 姓名:zhangsan年龄:23
*/
}
2、日志级别
// 日志级别
public static void test02() {
/*
日志的级别,总共七级
Level.SEVERE:(最高级)错误
Level.WARNING:警告
Level.INFO:(默认级别)消息
Level.CONFIG:配置级别
Level.FINE:详细信息(少)
Level.FINER:详细信息(中)
Level.FINEST:(最低级)详细信息(多)
两个特殊的级别:
Level.OFF;可用来关闭日志记录
Level.ALL:启用所有日志记录
对于日志的级别,我们重点关注的是new对象时第二个参数,是一个数值:
public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);
public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);
public static final Level WARNING = new Level("WARNING", 900, defaultBundle);
public static final Level INFO = new Level("INFO", 800, defaultBundle);
public static final Level CONFIG = new Level("CONFIG", 700, defaultBundle);
public static final Level FINE = new Level("FINE", 500, defaultBundle);
public static final Level FINER = new Level("FINER", 400, defaultBundle);
public static final Level FINEST = new Level("FINEST", 300, defaultBundle);
public static final Level ALL = new Level("ALL", Integer.MIN_VALUE, defaultBundle);
这个数值的意义在于:如果我们设置的日志级别是800,那么最终展现的日志信息,比如是数值大于800的所有日志信息。
*/
Logger logger = Logger.getLogger(JULTest.class.getCanonicalName());
logger.severe("severe信息");
logger.warning("warning信息");
logger.info("info信息");
logger.config("config信息");
logger.fine("fine信息");
logger.finer("finer信息");
logger.finest("finest信息");
/*
输出内容:我们看到,默认是输出info及比info信息级别高的信息
三月 20, 2023 9:47:27 下午 com.demo.logger.jul.JULTest test02
严重: severe信息
三月 20, 2023 9:47:27 下午 com.demo.logger.jul.JULTest test02
警告: warning信息
三月 20, 2023 9:47:27 下午 com.demo.logger.jul.JULTest test02
信息: info信息
*/
}
(1)默认日志级别源码分析
我们进入Logger的getLogger方法:
// java.util.logging.Logger#getLogger(java.lang.String)
@CallerSensitive
public static Logger getLogger(String name) {
// This method is intentionally not a wrapper around a call
// to getLogger(name, resourceBundleName). If it were then
// this sequence:
//
// getLogger("Foo", "resourceBundleForFoo");
// getLogger("Foo");
//
// would throw an IllegalArgumentException in the second call
// because the wrapper would result in an attempt to replace
// the existing "resourceBundleForFoo" with null.
return demandLogger(name, null, Reflection.getCallerClass());
}
// java.util.logging.Logger#demandLogger
private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
LogManager manager = LogManager.getLogManager(); // 获取LogManager
SecurityManager sm = System.getSecurityManager();
if (sm != null && !SystemLoggerHelper.disableCallerCheck) {
if (caller.getClassLoader() == null) {
return manager.demandSystemLogger(name, resourceBundleName);
}
}
return manager.demandLogger(name, resourceBundleName, caller);
// ends up calling new Logger(name, resourceBundleName, caller)
// iff the logger doesn't exist already
}
在初始化LogManager时,初始化了默认的日志级别为Level.INFO:
// java.util.logging.LogManager#ensureLogManagerInitialized
final void ensureLogManagerInitialized() {
final LogManager owner = this;
if (initializationDone || owner != manager) {
// we don't want to do this twice, and we don't want to do
// this on private manager instances.
return;
}
// Maybe another thread has called ensureLogManagerInitialized()
// before us and is still executing it. If so we will block until
// the log manager has finished initialized, then acquire the monitor,
// notice that initializationDone is now true and return.
// Otherwise - we have come here first! We will acquire the monitor,
// see that initializationDone is still false, and perform the
// initialization.
//
synchronized(this) {
// If initializedCalled is true it means that we're already in
// the process of initializing the LogManager in this thread.
// There has been a recursive call to ensureLogManagerInitialized().
final boolean isRecursiveInitialization = (initializedCalled == true);
assert initializedCalled || !initializationDone
: "Initialization can't be done if initialized has not been called!";
if (isRecursiveInitialization || initializationDone) {
// If isRecursiveInitialization is true it means that we're
// already in the process of initializing the LogManager in
// this thread. There has been a recursive call to
// ensureLogManagerInitialized(). We should not proceed as
// it would lead to infinite recursion.
//
// If initializationDone is true then it means the manager
// has finished initializing; just return: we're done.
return;
}
// Calling addLogger below will in turn call requiresDefaultLogger()
// which will call ensureLogManagerInitialized().
// We use initializedCalled to break the recursion.
initializedCalled = true;
try {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
assert rootLogger == null;
assert initializedCalled && !initializationDone;
// Read configuration.
owner.readPrimordialConfiguration();
// Create and retain Logger for the root of the namespace.
owner.rootLogger = owner.new RootLogger();
owner.addLogger(owner.rootLogger);
// 设置默认的日志级别Level defaultLevel = Level.INFO;
if (!owner.rootLogger.isLevelInitialized()) {
owner.rootLogger.setLevel(defaultLevel);
}
// Adding the global Logger.
// Do not call Logger.getGlobal() here as this might trigger
// subtle inter-dependency issues.
@SuppressWarnings("deprecation")
final Logger global = Logger.global;
// Make sure the global logger will be registered in the
// global manager
owner.addLogger(global);
return null;
}
});
} finally {
initializationDone = true;
}
}
}
3、自定义日志级别
JUL对于日志级别的自定义有些麻烦,要先排除掉默认的Handler,然后替换为自定义的Handler。
// 自定义日志级别
public static void test03(){
//日志记录器
Logger logger = Logger.getLogger(JULTest.class.getCanonicalName());
//将默认的日志打印方式关闭掉
//参数设置为false,我们打印日志的方式就不会按照父logger默认的方式去进行操作
logger.setUseParentHandlers(false);
//处理器Handler
//在此我们使用的是控制台日志处理器,取得处理器对象
ConsoleHandler handler = new ConsoleHandler();
//创建日志格式化组件对象
SimpleFormatter formatter = new SimpleFormatter();
//在处理器中设置输出格式
handler.setFormatter(formatter);
//在记录器中添加处理器
logger.addHandler(handler);
//设置日志的打印级别
//此处必须将日志记录器和处理器的级别进行统一的设置,才会达到日志显示相应级别的效果
//logger.setLevel(Level.CONFIG);
//handler.setLevel(Level.CONFIG);
logger.setLevel(Level.ALL);
handler.setLevel(Level.ALL);
logger.severe("severe信息");
logger.warning("warning信息");
logger.info("info信息");
logger.config("config信息");
logger.fine("fine信息");
logger.finer("finer信息");
logger.finest("finest信息");
/*
输出结果:
三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03
严重: severe信息
三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03
警告: warning信息
三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03
信息: info信息
三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03
配置: config信息
三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03
详细: fine信息
三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03
较详细: finer信息
三月 21, 2023 10:48:40 上午 com.demo.logger.jul.JULTest test03
非常详细: finest信息
*/
}
4、将日志输出到文件中
用户使用Logger来进行日志的记录,Logger可以持有多个处理器Handler(日志的记录使用的是Logger,日志的输出使用的是Handler)
添加了哪些handler对象,就相当于需要根据所添加的handler将日志输出到指定的位置上,例如控制台、文件等
public static void test04() throws IOException {
/*
将日志输出到具体的磁盘文件中
这样做相当于是做了日志的持久化操作
*/
Logger logger = Logger.getLogger(JULTest.class.getCanonicalName());
logger.setUseParentHandlers(false);
//文件日志处理器,输出到指定目录下
FileHandler handler = new FileHandler("D:\\test\\myLogTest.log");
SimpleFormatter formatter = new SimpleFormatter();
handler.setFormatter(formatter);
logger.addHandler(handler);
//也可以同时在控制台和文件中进行打印
ConsoleHandler handler2 = new ConsoleHandler();
handler2.setFormatter(formatter);
logger.addHandler(handler2); //可以在记录器中同时添加多个处理器
logger.setLevel(Level.ALL);
handler.setLevel(Level.ALL); // 文件中的日志级别为ALL
handler2.setLevel(Level.CONFIG); // 控制台的日志级别为CONFIG
logger.severe("severe信息");
logger.warning("warning信息");
logger.info("info信息");
logger.config("config信息");
logger.fine("fine信息");
logger.finer("finer信息");
logger.finest("finest信息");
}
5、Logger的父子关系
JUL中Logger之间是存在"父子"关系的,值得注意的是,这种父子关系不是我们普遍认为的类之间的继承关系,这种关系是通过树状结构存储的。
public static void test05(){
/*
从下面创建的两个logger对象看来
我们可以认为logger1是logger2的父亲
*/
//父亲是RootLogger,名称默认是一个空的字符串
//RootLogger可以被称之为所有logger对象的顶层logger
Logger logger1 = Logger.getLogger("com.demo.test");
Logger logger2 = Logger.getLogger("com.demo.test.JULTest");
System.out.println(logger2.getParent()==logger1); //true
// logger1的父Logger引用为:java.util.logging.LogManager$RootLogger@31ef45e3; 名称为com.demo.test; 父亲的名称为
System.out.println("logger1的父Logger引用为:"
+logger1.getParent()+"; 名称为"+logger1.getName()+"; 父亲的名称为"+logger1.getParent().getName());
// logger2的父Logger引用为:java.util.logging.Logger@598067a5; 名称为com.demo.test.JULTest; 父亲的名称为com.demo.test
System.out.println("logger2的父Logger引用为:"
+logger2.getParent()+"; 名称为"+logger2.getName()+"; 父亲的名称为"+logger2.getParent().getName());
/*
父亲所做的设置,也能够同时作用于儿子
对logger1做日志打印相关的设置,然后我们使用logger2进行日志的打印
*/
//父亲做设置
logger1.setUseParentHandlers(false);
ConsoleHandler handler = new ConsoleHandler();
SimpleFormatter formatter = new SimpleFormatter();
handler.setFormatter(formatter);
logger1.addHandler(handler);
handler.setLevel(Level.ALL);
logger1.setLevel(Level.ALL);
//儿子做打印,会输出ALL
logger2.severe("severe信息");
logger2.warning("warning信息");
logger2.info("info信息");
logger2.config("config信息");
logger2.fine("fine信息");
logger2.finer("finer信息");
logger2.finest("finest信息");
}
(1)父子关系源码分析
JUL在初始化时会创建一个顶层RootLogger作为所有Logger的父Logger:
//java.util.logging.LogManager#ensureLogManagerInitialized
owner.rootLogger = owner.new RootLogger();
owner.addLogger(owner.rootLogger);
if (!owner.rootLogger.isLevelInitialized()) {
owner.rootLogger.setLevel(defaultLevel);
}
RootLogger其实是LogManager的内部类,默认的名称是空字符串。
以上的RootLogger对象作为树状结构的根节点存在的,将来自定义的父子关系通过路径来进行关联,父子关系,同时也是节点之间的挂载关系。
通过owner.addLogger(owner.rootLogger);来进行根节点的挂载。
// java.util.logging.LogManager#addLogger
public boolean addLogger(Logger logger) {
final String name = logger.getName();
if (name == null) {
throw new NullPointerException();
}
drainLoggerRefQueueBounded();
LoggerContext cx = getUserContext(); // 用来保存节点的Map关系
if (cx.addLocalLogger(logger)) {
// Do we have a per logger handler too?
// Note: this will add a 200ms penalty
loadLoggerHandlers(logger, name, name + ".handlers");
return true;
} else {
return false;
}
}
class LoggerContext {
// Table of named Loggers that maps names to Loggers.
private final Hashtable<String,LoggerWeakRef> namedLoggers = new Hashtable<>();
// Tree of named Loggers
private final LogNode root;
private LoggerContext() {
this.root = new LogNode(null, this);
}
final class LoggerWeakRef extends WeakReference<Logger> {
private String name; // for namedLoggers cleanup
private LogNode node; // for loggerRef cleanup
private WeakReference<Logger> parentRef; // for kids cleanup
private boolean disposed = false; // avoid calling dispose twice
LoggerWeakRef(Logger logger) {
super(logger, loggerRefQueue);
name = logger.getName(); // save for namedLoggers cleanup
}
private static class LogNode {
HashMap<String,LogNode> children;
LoggerWeakRef loggerRef;
LogNode parent;
final LoggerContext context;
由LoggerContext 的存储数据结构我们也可以看出,是存在父子关系的。
6、使用配置文件
(1)默认配置文件位置
如果我们没有自己添加配置文件,则会使用系统默认的配置文件:
在java.util.logging.LogManager#ensureLogManagerInitialized方法中,执行了owner.readPrimordialConfiguration();方法:
// java.util.logging.LogManager#readPrimordialConfiguration
private void readPrimordialConfiguration() {
if (!readPrimordialConfiguration) {
synchronized (this) {
if (!readPrimordialConfiguration) {
// If System.in/out/err are null, it's a good
// indication that we're still in the
// bootstrapping phase
if (System.out == null) {
return;
}
readPrimordialConfiguration = true;
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws Exception {
readConfiguration(); // 读取配置
// Platform loggers begin to delegate to java.util.logging.Logger
sun.util.logging.PlatformLogger.redirectPlatformLoggers();
return null;
}
});
} catch (Exception ex) {
assert false : "Exception raised while reading logging configuration: " + ex;
}
}
}
}
}
// java.util.logging.LogManager#readConfiguration()
public void readConfiguration() throws IOException, SecurityException {
checkPermission();
// if a configuration class is specified, load it and use it.
String cname = System.getProperty("java.util.logging.config.class");
if (cname != null) {
try {
// Instantiate the named class. It is its constructor's
// responsibility to initialize the logging configuration, by
// calling readConfiguration(InputStream) with a suitable stream.
try {
Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(cname);
clz.newInstance();
return;
} catch (ClassNotFoundException ex) {
Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass(cname);
clz.newInstance();
return;
}
} catch (Exception ex) {
System.err.println("Logging configuration class \"" + cname + "\" failed");
System.err.println("" + ex);
// keep going and useful config file.
}
}
String fname = System.getProperty("java.util.logging.config.file");
if (fname == null) { // 如果配置为null,就会找java.home --> 找到jre文件夹 --> lib --> logging.properties
fname = System.getProperty("java.home");
if (fname == null) {
throw new Error("Can't find java.home ??");
}
File f = new File(fname, "lib");
f = new File(f, "logging.properties");
fname = f.getCanonicalPath();
}
try (final InputStream in = new FileInputStream(fname)) {
final BufferedInputStream bin = new BufferedInputStream(in);
readConfiguration(bin);
}
}
也就是说,如果我们没有指定配置文件的话,JUL也是会读取默认的配置文件,读取的文件是java.homo目录中,jre文件夹下,lib目录中的logging.properties文件。
(2)默认配置文件(去掉注释)
handlers= java.util.logging.ConsoleHandler
.level= INFO
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
com.xyz.foo.level = SEVERE
(3)配置文件解析
# RootLogger使用的处理器,在获取RootLogger对象时进行的设置
# 默认情况下,配置的是控制台处理器,只能在控制台上进行输出操作
# 如果想要其他的处理器,在处理器类后面通过以逗号的形式进行分割
handlers= java.util.logging.ConsoleHandler
# 根节点RootLogger的日志级别
# 默认情况下,这是全局的日志级别,如果不手动配置其他的日志级别,则默认输出下述配置的级别及更高的级别
.level= INFO
# 文件处理器属性的设置
# 输出日志文件的路径
java.util.logging.FileHandler.pattern = %h/java%u.log
# 输出日志文件的限制(默认50000个字节)
java.util.logging.FileHandler.limit = 50000
# 设置日志文件的数量
java.util.logging.FileHandler.count = 1
# 输出日志的格式
# 默认是以XML的方式输出
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
# 控制台处理器的属性设置
# 控制台输出默认级别
java.util.logging.ConsoleHandler.level = INFO
# 控制台默认输出日志的格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 也可以将日志级别设定到具体的某个包下
#com.xyz.foo.level = SEVERE
(4)自定义读取配置文件
可以将logging.properties文件稍作修改,验证有效性。
InputStream input = new FileInputStream("E:\\logging.properties");
//取得日志管理器对象
LogManager logManager = LogManager.getLogManager();
//读取自定义的配置文件
logManager.readConfiguration(input);
Logger logger = Logger.getLogger(JULTest.class.getCanonicalName());
logger.severe("severe信息");
logger.warning("warning信息");
logger.info("info信息");
logger.config("config信息");
logger.fine("fine信息");
logger.finer("finer信息");
logger.finest("finest信息");
(5)自定义日志配置文件
以下配置可以自定义FileHandler的设置:
# 自定义Logger
com.demo.logger.jul.handlers=java.util.logging.FileHandler
# 自定义Logger日志等级
com.demo.logger.jul.level=CONFIG
# 屏蔽掉父Logger的日志设置
com.demo.logger.jul.useParentHandlers=false
默认使用的是XMLFormatter,我们可以改成容易读的:
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
文件默认保存在用户的home目录的java0.log文件。
默认的日志文件是覆盖的方式,我们可以设置为追加的方式:
# 输出日志文件,是否追加
java.util.logging.FileHandler.append=true
四、总结
1.初始化LogManager,加载logging.properties配置文件,将Logger添加到LogManager中。
2.从单例的LogManager获取Logger。
// 构造方法是protected的
protected LogManager() {
this(checkSubclassPermissions());
}
// 只能通过静态方法获取唯一实例
public static LogManager getLogManager() {
if (manager != null) {
manager.ensureLogManagerInitialized();
}
return manager;
}
3.设置日志级别Level,在打印的过程中使用到了日志记录的LogRecord类。
public void log(Level level, String msg) {
if (!isLoggable(level)) {
return;
}
LogRecord lr = new LogRecord(level, msg);
doLog(lr);
}
4.Filter作为过滤器提供了日志级别之外更细粒度的控制。
5.Handler日志处理器,决定日志的输出位置,例如控制台、文件…
6.Formatter是用来格式化输出的。