01、JUL日志(JDK自带日志框架,包含源码分析)(上)
@[toc]
前言
在平时跟着教程做项目时,对于日志处理往往都是直接引入jar包,添加一个配置文件,对于日志的概念是十分模糊的,在寒假这段时间阅读书籍《Java核心技术》中的日志章节时,感觉并不是特别能够理解,接着就去b站搜了搜相关学习视频,搜到了黑马教程:黑马程序员java日志框架教程,全面深入学习多种java日志框架,花了半个月时间跟着视频学习并结合阅读其他博主的文章输出了多篇博客,也算是入了个门。
JUL
是JDK
自带的一个日志,对于日志的学习可以先从JUL
入手,会有利于之后学习其他的日志框架。
所有博客文件目录索引(包含日志框架系列学习):博客目录索引(持续更新)
一、JUL架构介绍
1.1、认识不同组件
Logger
与Handler
都可设置过滤器Filter
。
日志靠几个组件完成:
Loggers
(日志记录器):Logger
通常是应用程序访问日志系统的入口程序,负责捕捉事件并将其发送给何时的Appender
,其关联着一组Handler
。Appenders
(输出源):也称为Handlers
,负责从Logger
中取出日志消息,并使用Layouts
(即Formatters
)来格式化信息,然后将消息发送出去。一个logger
可以有多个Handlers
。可通过抽象类Handler
下的具体实现类来决定输出位置,可输出多个位置。(控制台、文件或其他日志收集系统)Layouts
(布局器):称为Formatters
,负责对日志事件中的数据进行转换和格式化,通过使用抽象类Formatter
下的指定实例来决定输出格式。Filters
:过滤器,根据需要来定制哪些日志会被记录或放过。
日志输出大致流程:
- 首先获取到
Logger
实例(默认日志等级为INFO
),该实例初始化会自动配置一个rootlogger
(即它的父logger),这个rootlogger
中会有一个Consolehandler
(用于将输出到屏幕)。 - 当使用
Logger
实例来调用方法输出日志时,首先会比对Logger
本身的日志等级以及其过滤器。 - 接着会依次执行
Logger
实例中的handlers
集合中的handler
的publish()
方法来输出日志信息,再此之前会与handler
的日志等级作比较以及handler
的filter
过滤器。(若无handlers则跳过) - 接着会根据
Logger
中的useParentHandlers
布尔值来判断是否执行rootlogger
中的handlers
,操作如同3一样。
注意:Logger
、Handler
都有自己对应的日志level
等级,初始默认都为INFO(800)
。上面的流程初始阶段了解即可,对于详细的执行流程可见最后总结。
1.2、Logger
Logger
:是一个独立的类,它有一个子类为RootLogger
(是LogManager
类的内部类)
包含方法:
static Logger getLogger(String name)
:获取一个Logger实例,其有参构造器为私有的。void log(Level level, String msg)
:用于打印日志信息,第一个参数为日志等级,第二个参数为日志信息。void info(String msg)
:打印Level.INFO
级别的日志,实际上也是调用的log()
方法。对应类似方法还有void warning(String msg)
以及其他几个级别日志方法,更加方便打印日志。void setUseParentHandlers(boolean useParentHandlers)
:不使用默认自带的父类的handler
。void setLevel(Level newLevel)
:设置日志等级。(默认为INFO)void addHandler(Handler handler)
:添加指定类型handler
到handlers
集合中。void setFilter(Filter newFilter)
:设置过滤器。
1.3、Handler
Handler
:是一个抽象类,其中包含了set/get方法。可设置对应的Formatter
、Filter
,并自身带有对应的日志等级。
相关实现类:用于输出到不同位置
StreamHandler
:将日志通过OutputStream
输出。FileHandler
:对应的OutputStream
对象是FileOutputStream
,能够将日志输出到文件。ConsoleHandler
:对应的OutputStream
对象是System.err
,会将日志输出到控制台。SocketHandler
:对应的OutputStream
对象是Socket.getOutputStream()
,会将日志输出到网络套接字。
MemoryHandler
:将日志输出到一个内存缓冲区。
相关方法如下:
synchronized void setLevel(Level newLevel)
:设置Handler
的日志等级。synchronized void setFilter(Filter newFilter)
:设置对应过滤器。synchronized void setFormatter(Formatter newFormatter)
:设置对应的格式化输出器。synchronized void setEncoding(String encoding)
:设置输出时的字符编码。ErrorManager getErrorManager()
:返回错误处理器,errorManager用来处理日志记录过程中发生的异常信息。synchronized void publish(LogRecord record)
:用于输出日志。
二、输出日志信息
JUL
:JDK自带的日志实现依赖。
输出Info
级别日志的三种方式:
@Test
public void test01(){
//方式一:通过Logger.getLogger()方法获取日志记录对象
//有参构造是全限定名
Logger logger = Logger.getLogger("xyz.changlu.LogTest");
logger.info("hello jul");
//方式二:通用方法进行日志输出
logger.log(Level.INFO,"自定义level为INFO,报错");
//方式三:通过占位符方式
String methodName = "test01";
int msg = 2;
logger.log(Level.INFO,"方法名{0},次数为{1}",new Object[]{methodName,msg});
}
- 方式一的
logger.info()
内部实际上也是调用的log()
方法(传INFO
参数即可)。 log()
方法第一个参数可以指定任意级别,后面msg则是输出到控制台信息的内容;重载方法也可以通过占位符方式填入指定要输出的信息。
- 第一行信息自带日期时间,包名及方法都是自动获取输出的;第二行的信息:后则是我们输入的
msg
。
三、自定义日志级别配置
3.1、认识Level类
Level
:是一个枚举类(jdk1.5以前自定义枚举类)。
该类中包含七个不同等级的枚举实例,等级从高到低如下:
SEVERE
:造成了程序的终止,可以使用该等级来记录。(value:1000)WARNING
:记录程序发生的一些问题,但问题不会造成程序终止。(value:900)INFO
:消息记录,记录数据库的连接信息,IO的传递信息等等。(value:800)CONFIG
:配置信息,加载了配置文件,读取配置的一些参数。(value:700)- 下面的三个都是用来
Debug
日志记录的消息,记录程序运行的状态跟执行的流程,参数的传递信息。这三者value级别的大小不同,FINE低一些,FINEST高一些,这三个开发中拿一个即可FINE
:(value:500)FINER
:(value:400)FINEST
:(value:300)
ALL
:所有日志级别都能执行。(value:Integer.MIN_VALUE)OFF
:所有日志都不能执行。(value:Integer.MAX_VALUE)
源码实例:
- value是枚举实例在构造时填入的参数,决定了该Level实例的等级。
3.2、输出不同等级日志
上面枚举实例对应的value
起到什么作用呢?
- 用于当前
logger
实例的日志等级value
值与调用log()
方法输出指定日志级别的value
值对比,有限制日志输出的作用。
我们看下面的例子:同时输出7个不同等级的日志
@Test
public void test01(){
Logger logger = Logger.getLogger("xyz.changlu.test01");
logger.severe("severe");
logger.warning("warning");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
- 可以看到只有
severe
、warning
、info
等级能够输出msg信息。
那么为什么只输出了前三个等级的日志呢?我们看Logger
源码:
//这里调用的是Logger类的log()
public void log(Level level, String msg) {
if (!isLoggable(level)) {//首先会判断是否大于当前logger的等级,一旦小于返回isLoggable()返回false,则会执行return结束方法
return;
}
//传入进来的level符合现有等级,那么就会执行下去通过handler进行输出信息
LogRecord lr = new LogRecord(level, msg);
doLog(lr);
}
public boolean isLoggable(Level level) {
//这里levelValue是现有logger实例中的日志等级默认为INFO(800),一旦小于800或等于Integer.MAX_VALUE(Level.OFF实例值)返回false
if (level.intValue() < levelValue || levelValue == offValue) {
return false;
}
return true;
}
能够看到在调用log()
方法会去与自己本身的日志等级比较(初始默认为INFO),所以只会输出INFO等级及以上。
我们可通过使用void setLevel(Level newLevel)
来设置当前Logger的日志等级:
@Test
public void test01(){
Logger logger = Logger.getLogger("xyz.changlu.test01");
logger.setLevel(Level.WARNING);
logger.severe("severe");
logger.warning("warning");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
- 果然生效了,当设置Logger的当前日志等级为WARNING时,有对应效果。
那我们接着设置低级别的呢,如logger.setLevel(Level.CONFIG);
,结果却并没有输出config
的日志信息。其实原因是因为在进行日志输出时,并不仅仅会比对Logger
本身的日志等级还会比其Handler
的日志等级(初始默认也为INFO
)。
若是我们想要输出所有日志等级的日志信息呢?
- 将
logger
实例以及其handler
的日志等级都设置为ALL
即可。
public void test01(){
Logger logger = Logger.getLogger("xyz.changlu.test01");
//不执行rootlogger中的handlers
logger.setUseParentHandlers(false);
//自定义handler
ConsoleHandler ch = new ConsoleHandler();
ch.setFormatter(new SimpleFormatter());
//将实例logger与对应handler都设置自定义的日志等级
ch.setLevel(Level.ALL);
logger.setLevel(Level.ALL);
//将handler添加到logger的handlers集合中
logger.addHandler(ch);
//输出不同等级的日志
logger.severe("severe");
logger.warning("warning");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
- 获得的
logger
实例中会有一个rootlogger
,我们不自定义handler
时,使用log()
输出到控制台信息就是通过rootlogger
中的ConsoleHandler
输出的。 - 第4行:我们使用该方法,传入false表示不执行
rootlogger
的handlers
。 - 第7行-13行:自定义
handler
并设置对应的转换器,接着将handler
添加到logger
中。其中有两个关键操作是设置自定义handler
的日志等级以及设置logger
实例的日志等级为Level.ALL
。ALL对应的值为Integer.MIN_VALUE
,所以所有级别的日志都能够输出显示。
3.2、自定义日志级别(console与文件输出)
@Test
public void test01() throws IOException {
//输出日志到调试窗口
Logger logger = Logger.getLogger("xyz.changlu.test01");
//不将记录发送到父类处理器中
logger.setUseParentHandlers(false);
//1、定义窗口处理器ConsoleHandler
ConsoleHandler ch = new ConsoleHandler();
ch.setFormatter(new SimpleFormatter());//ConsoleHandler设置简单格式化输出器
//2、定义文件处理器
FileHandler fh = new FileHandler("jul.log");//指定路径(这里就是工程目录下)
fh.setFormatter(new SimpleFormatter());
//logger器中添加两个handler
logger.addHandler(ch);
logger.addHandler(fh);
//输出的所有等级的日志信息到对应handler指定的区域(窗口以及文件)
logger.severe("severe");
logger.warning("warning");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
- 第4、6行:通过
Logger
静态方法getLogger()
会获取到一个Logger
实例,它有个rootlogger
,并自带handlers
(ConsoleHandler
,其使用的SimpleFormatter
转换器),由于不使用rootlogger
中的handlers
,所以我们调用setUseParentHandlers(false)
方法传入参数false
表示不执行rootlogger
的handlers
(防止重复打印)。 - 第9、12行:就是设置不同的
handler
(输出到窗口、文件),并将对应的转换器放置到hander
中。 - 第15、16行:将多个
handler
依次放置到logger
实例中,一旦执行log()
方法就会执行其所有的handler
。
四、Logger的子父类关系
4.1、认识根Logger
探究初始创建Logger与根Logger的关系
JUL中的Logger
之间存在子父类关系,当我们初始化一个Logger
时,会默认创建一个rootlogger(一般情况下,除非下面①)
①直接创建一个rootlogger
(name=""
就是):
①我们先创建一个顶层Logger
(即name=""
):
@Test
public void test01(){
//传入空字符串获取Logger实例
Logger logger2 = Logger.getLogger("");
System.out.println(logger2);
System.out.println(logger2.getParent());//打印其父类Logger
}
- 当传入的name为
""
时,就会默认获取一个RootLogger
实例,即为根Logger
,因为是最顶层的了,所以就没有父Logger
了。
②我们接着传入包名来获取Logger
实例
@Test
public void test01(){
Logger logger = Logger.getLogger("xyz.changlu");
System.out.println(logger+",name="+logger.getName());
System.out.println(logger.getParent()+",name="+logger.getParent().getName());
}
- 可以看到当我们创建一个name为
"xyz.changlu"
的Logger
实例时,其父类则是根Logger
,其name=""
。
③创建有层级关系的Logger
实例:
@Test
public void test01(){
Logger logger = Logger.getLogger("xyz.changlu");
//传入name为xyz.changlu的上级包名为xyz的Logger实例
Logger logger2 = Logger.getLogger("xyz");
System.out.println("logger="+logger+",name="+logger.getName());
System.out.println("logger2="+logger.getParent()+",name="+logger.getParent().getName());
System.out.println(logger.getParent() == logger2);
System.out.println("logger2的根looger="+logger2.getParent()+",name="+logger2.getParent().getName());
}
- 注意当我们创建了
logger
的上级Logger
实例(name为xyz
)时,该实例的上级Logger
默认会变为logger2
的实例了,此时Logger2
的父类则为根Logger
了。(可以注意到当定义了有层级相关的Logger
时会自动继承相关关系,也与包的层级有关)
总结:
- 当我们初始化一个
Logger
实例时(name!=""
情况下),会默认自带一个rootlogger
实例,可通过getParent()
获取到,其name=""
。 - 当我们初始化一个
Logger
实例时,设置其name=""
会直接获得一个rootlogger
实例,此时就没有父Logger
了。 - 当我们定义多个
Logger
实例时他们的name
具有层级关系,例如:xyz.changlu
、xyz
,那么他们会自动建立关联,name=xyz.changlu
的Logger
实例其父Logger
则为刚刚定义的name=xyz
的Logger
实例,name=xyz
的父Logger
则为根Logger
。
查看
rootlogger
中的handler、formatter、level等级
@Test
public void test01(){
Logger logger = Logger.getLogger("xyz.changlu");
Logger parent = logger.getParent();//为根Logger实例
System.out.println("rootLogger:"+parent);
System.out.println("rootlogger的name:"+parent.getName());
System.out.println("rootlogger的filter:"+parent.getFilter());
System.out.println("rootlogger的level:"+parent.getLevel());
System.out.println("handler:"+parent.getHandlers()[0]);
System.out.println("handler的Formatter:"+parent.getHandlers()[0].getFormatter());
System.out.println("handler的level:"+parent.getHandlers()[0].getLevel());
System.out.println("handler的filter:"+parent.getHandlers()[0].getFilter());
}
+
- 获得
rootlogger
还有一种方式:Logger.getLogger("")
空字符串即可。 - 可以看到
rootlogger
的level
与该logger
的handler
的level
都为INFO
;Handler
为ConsoleHandler
,Formatter
为SimpleFormatter
。
此时对于logge
r算是有一些了解了,不过还不够继续往下看。
4.2、Logger的info(msg)执行流程*
这里默认name=xyz.changlu
的logger
实例,其父Logger
为rootlogger
@Test
public void test01(){
Logger logger = Logger.getLogger("xyz.changlu");
logger.info("报错啦");
}
过程如下:
- 比较该
logger
实例的level
是否高于INFO
。(这里由于logger本身初始化并没有设定级别,所以使用的是rootlogger的日志级别)。 - 发现
>=INFO
,则继续,接着调用logger
的Filter
实例的isLoggable()
,若没有添加,就不进行过滤。 - 接着开始依次调用
logger
的handlers
(即handler集合),由于初始化没有指定handler
,就不进行日志输出。 - 判断
useParentHandlers
boolean参数是否为true
,默认为true
,接着调用rootlogger
的handler
实例的publish()
方法。 - 在
rootlogger
的handler
的publish()
方法中判断该handler
的级别是否>=INFO
,由于默认为INFO
,大于继续执行。 - 接着调用
rootlogger
的handler
的isLoggable()
进行过滤,没有则不过滤。 - 使用
rootlogger
的ConsoleHandler
的getFormatter().format(LogRecord)
进行格式化(默认为SimpleFormatter
)。 - 最终调用
write()
将格式化后数据输出到控制台。
注意注意:在抛到上层的logger
时并不会判断该logger
的level
,仅仅是handlers
的level
。
针对于Logger
与handler
规范:
一个Logger对应1个Level,对应0个或1个Filter,对应0个或多个Handler,对应0个或1个ParentLogger 。
一个Handler对应1个Level,对应0个或1个Filter,对应1个Formatter。
通过上面说明,我们来看下面例子,请说出打印的内容有些什么:
public class LogTest {
private static final Logger logger1 = Logger.getLogger("");
private static final Logger logger2 = Logger.getLogger("cn");
private static final Logger logger3 = Logger.getLogger("cn.codecrazy");
static {
logger2.addHandler(new ConsoleHandler());
logger3.addHandler(new ConsoleHandler());
logger2.setLevel(Level.WARNING);
logger3.setLevel(Level.INFO);
}
public static void main(String[] args) {
System.out.println(logger2.getLevel());
System.out.println(logger2.getHandlers()[0].getLevel());
logger1.info("logger1");
logger2.info("logger2");
logger3.info("logger3");
}
}
- 若是创建一个
ConsoleHandler
,其会自动初始化一个INFO
的日志等级(剧透一下是因为读取了默认的配置文件设置的等级)。
介绍整个流程:首先定义了三个日志实例,互相包含层级关系,在static
代码块中将logger2
中的日志等级设置为WARNING
,并且添加了一个ConsoleHandler
(该handler
会自动赋予自己的一个INFO
日志等级);接着给logger3
中的日志等级设置为INFO
,并也添加了一个ConsoleHandler
。
开始进入main()方法,之前设置了logger2
的等级,第一行则输出WARNING
,logger2中的handler
就是刚刚的consolehandler
,默认等级为INFO。接着开始INFO
打印输出,
①首先是第15行,对logger1
进行日志输出(其等级为INFO
),由于该logger
实例是rootlogger
其handler
默认日志等级都为INFO
,所以直接打印。
②接着是16行,首先会比对logger2
中的日志等级(由于代码块中设置为WARNING
),所以直接方法过程中直接return
了,没有打印。
③接着是17行,首先会比对logger3
的日志等级,本身为INFO
,所以通过,接着会遍历logger3
的handlers
,其中consolehandler
日志等级默认为INFO
通过,所以在loggers
中打印了第一次;接着会调用执行父logger
(即为logger2
)中的handlers
,比对其handler
的日志等级为INFO
,所以再次通过打印第二次;最后再次调用父logger
(这次为rootlogger
)的handlers
,由于是consolehandler
,所以日志等级还是INFO
,所以再次打印。
所以logger1
打印了1次,logger3
打印了3次。
本部分参考案例来源: Java Logging之JUL系列——Logger Hierarchy
4.3、设置日志等级
logger实例本身设置高的日志等级,logger尝试打印
@Test
public void test01(){
Logger logger = Logger.getLogger("xyz.changlu");
Logger parent = logger.getParent();//rootlogger
//logger实例本身设置日志等级
logger.setLevel(Level.WARNING);
System.out.println(parent.getLevel());
System.out.println(logger.getLevel());
logger.info("cl报错啦");
}
- 该logger在进行日志等级判定时就会判定<INFO,所以无打印。
rootlogger设置WARNING等级,logger来打印日志
@Test
public void test01(){
Logger logger = Logger.getLogger("xyz.changlu");
Logger parent = logger.getParent();//rootlogger
//logger实例本身设置日志等级
parent.setLevel(Level.WARNING);
logger.info("cl报错啦");
}
当rootlogger
设置等级时则会影响到其子logger
的level
等级。
五、日志的配置文件
5.1、初探源码(读取配置文件部分,前)*
当我们使用Logger.getLogger("")
来获取一个Logger实例时,会默认在根Logger
中添加一个Conslehandler
(带有SimpleFormatter
转换器)。
- 对于
rootlogger
默认添加的handler
以及formatter
实际上是通过一个配置文件来进行配置的,其配置文件是JDK自带的,名称为logging.properteis
。
接下来我们来看源码中的内容,根据数字的顺序:只针对于读取配置文件
private static final Logger logger1 = Logger.getLogger("");//调用该方法开始读源码
public class Logger {
//1、getLogger()获取实例的方法
@CallerSensitive
public static Logger getLogger(String name) {、
return demandLogger(name, null, Reflection.getCallerClass());//去到2
}
//2、demandLogger():日志记录器
private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
LogManager manager = LogManager.getLogManager();//去到3
....
//该方法会创建logger实例的操作,包含其中的handlers(根据配置文件是否有反射创建实例)以及level等...,其level若没有会去拿到父类的level
return manager.demandLogger(name, resourceBundleName, caller);//返回logger实例对象
}
}
public class LogManager {
private static final LogManager manager;//该类是一个单例实例
//3、获取到LogManager的单例属性
public static LogManager getLogManager() {
if (manager != null) {
manager.ensureLogManagerInitialized();//去往4
}
return manager;
}
//4、进行LogManager读取配置文件与初始化操作
final void ensureLogManagerInitialized() {
final LogManager owner = this;
if (initializationDone || owner != manager) {
return;
}
synchronized(this) {
final boolean isRecursiveInitialization = (initializedCalled == true);
assert initializedCalled || !initializationDone
if (isRecursiveInitialization || initializationDone) {
return;
}
initializedCalled = true;
try {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
assert rootLogger == null;
assert initializedCalled && !initializationDone;
//注意这里
owner.readPrimordialConfiguration();//去往5
//own就是logManager实例,对rootLogger进行初始化
owner.rootLogger = owner.new RootLogger();
//add方法中会实例化LoggerContext,其中包含了一个hashtable实例,键为logger名称,LoggerWeakRef引用(属性有name、LogNode、parentRef,分别为名称、节点以及父节点引用,logNode存储一个完整的logger信息)
owner.addLogger(owner.rootLogger);
if (!owner.rootLogger.isLevelInitialized()) {
owner.rootLogger.setLevel(defaultLevel);
}
//这里是获取一个logger实例(为了向下兼容1.7的)
final Logger global = Logger.global;
owner.addLogger(global);
return null;
}
});
} finally {
initializationDone = true;
}
}
}
//5、读取原始配置文件
private void readPrimordialConfiguration() {
if (!readPrimordialConfiguration) {
synchronized (this) {
if (!readPrimordialConfiguration) {
if (System.out == null) {
return;
}
readPrimordialConfiguration = true;
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws Exception {
//继续调用读取配置文件方法(很重要的地方)
readConfiguration();//去往6
// 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;
}
}
}
}
}
//6、读取配置文件(本部分最关键地方)
public void readConfiguration() throws IOException, SecurityException {
checkPermission();
//自定义配置类,负责完成配置信息的初始化。
//这里cname就是对应的配置类名称,方便下面通过类加载来加载对应类进行配置信息。
String cname = System.getProperty("java.util.logging.config.class");
if (cname != null) {
try {
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.
}
}
//自定义配置文件
//获取System里Properties实例中的键
String fname = System.getProperty("java.util.logging.config.file");
//若是没有获取到,fname为null,则会读取jdk目录中官方的logging.properties
if (fname == null) {
//获取jdk中的配置文件路径
//获取系统参数,Java 安装目录,我的是C:\Program Files\Java\jdk1.8.0_201\jre
fname = System.getProperty("java.home");
//若没有对应参数,则会报错
if (fname == null) {
throw new Error("Can't find java.home ??");
}
//此时f路径为:C:\Program Files\Java\jdk1.8.0_201\jre\lib
File f = new File(fname, "lib");
//在添加一个子目录:C:\Program Files\Java\jdk1.8.0_201\jre\lib\logging.properties
f = new File(f, "logging.properties");
fname = f.getCanonicalPath();
}
//使用文件输入流来读取该配置文件:logging.properties
try (final InputStream in = new FileInputStream(fname)) {
//添加缓冲流,加快读取速度
final BufferedInputStream bin = new BufferedInputStream(in);
//读取之前读取的配置文件输入流
readConfiguration(bin);
}
}
//7、读取文件输入流操作
public void readConfiguration(InputStream ins) throws IOException, SecurityException {
checkPermission();
reset();
//该props是一个properties实例,这里将参数中的输入流放置到其中,为之后键值对读取做准备。
props.load(ins);
...
}
}
- 上面的源码分析暂时只到配置文件为止,不再往下深入更细节的内容,我们只要知道,在不进行自定义配置文件时,会自动读取
jre/lib
目录中的logging.properties
到一个输入流中,再调用LoggerManager
实例(单例)的readConfiguration(bin);
,bin则为对应的配置文件输入流。 - 第122行:
fname
指的就是对应配置文件的路径,这里为:C:\Program Files\Java\jdk1.8.0_201\jre\lib\logging.properties。
若是我们不进行自定义配置类或自定义配置文件,那么就会自动读取jre/lib
目录下官方提供的配置文件!
源码看完了,我们就去看一下这个logging.properties
里面的内容吧:
- 确实有
logging.properties
配置文件。
清理了注释后内容如下:
# RootLogger 顶级父元素指定的默认处理器 ConsoleHandler,可添加多个用,隔开
handlers= java.util.logging.ConsoleHandler
# RootLogger 顶级父元素默认的日志级别为INFO
.level= INFO
# 若是上面handlers中多加了一个FileHandler就生效8-11行配置
java.util.logging.FileHandler.pattern = %h/java%u.log # ①%h表示当前用户目录路径,我的为C:\Users\93997;②%u对 # 应下面的count,范围为[0,count)
java.util.logging.FileHandler.limit = 50000 # 输出日志文件限制大小(50000字节)
java.util.logging.FileHandler.count = 1 # 定义上面%h的范围
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter # 设置FileHandler中的转换器为 # XMLFormatter (XML格式)
# 对应上面handlers中的ConsoleHandler进行配置
java.util.logging.ConsoleHandler.level = INFO # handler的日志等级为INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # 该handler转换器为 # SimpleForamtter
# 特殊logger日志的默认等级,当设置name为com.xyz.foo的logger实例,其日志等级为SEVERE
com.xyz.foo.level = SEVERE #
所以我们刚开始获取一个logger
实例会自带一个handler
及formatter
,与这个配置文件关系很大。
5.2、自定义配置文件
首先要定义一个自定义配置文件,接着通过调用方法或在java命令中添加参数(目的就是让LogManager的单例加载到该配置文件),对于配置文件我们可以借鉴jre/lib
下的logging.properties
,在其基础上进行自定义配置。
名字可以随意如:xxx.properteis
,后缀应当使用properteis,因为源码中读取配置文件的输入流使用的Properties
的load()
方法。
我们来自定义一个吧,还是logging.properties
,放置的位置若是Maven
项目的话放置在resource
目录下;若是普通java工程放置在src
目录下即可:
# ①额外增加了一个FileHandler,用于输出到文件中
handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler
# ②默认日志等级为ALL(那么)
.level= ALL
# ④在.log文件上层多添加了一个logs目录
java.util.logging.FileHandler.pattern = %h/logs/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
# ⑤转换器更改为简单转换
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# ③设置handler的日志等级为ALL
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
com.xyz.foo.level = SEVERE
- 增添内容如上数字标记部分。
因为创建的是Maven
工程,所以放在resources
目录中:
方式如下:
方式一:使用
System.setProperties()
来配置文件
System.setProperty("java.util.logging.config.file",
"C:\\Users\\93997\\Desktop\\工程文件\\logdemo\\src\\main\\resources\\logging.properties");
解析:我们之前看源码时,针对于自定义配置文件是通过System.getProperty()
来获取对应的文件路径的,所以我们在main()方法前添加对应的配置文件路径之后调用Logger.getLogger()
方法就会加载我们的自定义配置文件。
方式二:在执行字节码时添加命令参数
-D java.util.logging.config.file="C:\Users\93997\Desktop\projectexers\logdemo\src\main\resources\logging.properties"
,值应当为详细路径。
添加该命令之后,运行程序则会自动添加到系统参数中。
方式三:使用
LogManager
的单例实例调用readConfiguration()
方法读取配置文件输入流
@Test
public void test01() throws IOException {
//1、通过系统类加载器的getResourceAsStream()来加载配置文件,返回输入流
InputStream is = LogTest.class.getClassLoader().getResourceAsStream("logging.properties");
//2、使用LogManager的单例来读取该配置文件(与源码读取官方logging.properties方法类似)
LogManager.getLogManager().readConfiguration(is);
//测试
Logger logger = Logger.getLogger("xyz.changlu");
logger.severe("severe");
logger.warning("warning");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
}
- 这种方式与源码中的使用文件流读取配置文件类似,源码使用的是
FileInputStream
来读取配置文件,最后都是使用的LogManager
实例(该实例是单例)的对应方法加载输入流。
- 配置文件中%h表示的是当前用户目录路径。
OK,三种方式都可以进行自定义配置文件,其实也可以使用配置类进行来进行自定义配置,这里就不作描述。
- 点赞
- 收藏
- 关注作者
评论(0)