Java 内部的log的使用

介绍

JDK自带的log的使用与运行方式

使用方法

最简单的使用

代码示例

1
2
3
4
5
6
public static void baseUse()
{
Logger logger = Logger.getLogger(Main.class.getName());
logger.info("this is info");
logger.warning("this is warning ");
}

会按默认格式,在console中输出日志

控制输出级别

级别控制可以在代码中直接写。
也可以在配置文件中写。是等效的。后设置的起作用。就是代码中的优先级高。
配置文件中的.level(全局的)和java.util.logging.ConsoleHandler.level是共同起作用,按最严格的输出。

1
2
3
4
5
6
7
public static void withLevelContral()
{
Logger logger = Logger.getLogger(Main.class.getName());
logger.setLevel(Level.WARNING);
logger.info("this is info msg");
logger.warning("this is warning ");
}

logger.setLevel(Level.WARNING);.level = WARNING 等效
logger.getHandlers()[0].setLevel(Level.FINE);java.util.logging.ConsoleHandler.level=FINE等效

读取自定义的配置文件的日志输出

除代码方式外,还可以通过java命令行的方式指定配置文件。
如在启动时,指定参数java -Djava.util.logging.config.file="abc.properties"

1
2
3
4
5
6
7
8
public static void baseUseWithSelfConfig()
{
// 会读取项目内的logging.properties 做为日志配置文件,要放在getLogger之前,才有效
System.setProperty("java.util.logging.config.file","logging.properties");
Logger logger = Logger.getLogger(Main.class.getName());
logger.info("this is info msg");
logger.warning("this is warning ");
}

输出到文件

1
2
3
4
5
6
7
8
9
public static void withFileHandler() throws SecurityException, IOException
{
Logger logger = Logger.getLogger(Main.class.getName());
// 默认的的格式为xml
FileHandler filehandler = new FileHandler("log.txt");
logger.addHandler(filehandler);
logger.info("this is info");
logger.warning("this is warning ");
}

默认的的格式为xml,可以设置fromatter来改变格式

handler

Handler对象从Logger中获取日志信息,并将这些信息导出。例如,它可将这些信息写入控制台或文件中,也可以将这些信息发送到网络日志服务中,或将其转发到操作系统日志中。

Logger默认的输出处理者是ConsoleHandler。ConsoleHandler的输出是使用System.err对象,而信息的默认等级是INFO,这可以在JRE安装目录下lib目录的logging.properties中看到配置。

Java SE实现了5个Handler:

  • java.util.logging.ConsoleHandler 以System.err输出日志。
  • java.util.logging.FileHandler 将信息输出到文件。
  • java.util.logging.!StreamHandler以指定的!OutputStream实例输出日志。
  • java.util.logging.!SocketHandler将信息通过Socket传送至远程主机。
  • java.util.logging.!MemoryHandler将信息暂存在内存中。

FileHandler默认的输出格式是XML格式。输出格式由java.util.logging.Formatter来控制

用户可以定制自己输出媒介控制器,继承Handler即可。

Formatter

Formatter为格式化LogRecords提供支持。
一般来说,每个Handler都有关联的Formatter。Formatter接受LogRecord,并将它转换为一个字符串。
默认提供了两种Formatter:java.util.logging.SimpleFormatter':标准日志格式,java.util.logging.XMLFormatter’: XML形式的日志格式。

也可以自定义日志的输出格式,只要继承抽象类Formatter,并重新定义其format()方法即可。format()方法会传入一个java.util.logging.LogRecord对象作为参数,可以使用它来取得一些与程序执行有关的信息。

Filter

实现java.util.logging.FilterisLoggable的方法。按LogRecord对象来返回是否过滤。
再配置到配置文件中。

JDK内置Logger 如何读取的默认配置文件

默认情况下,JDK的LogManager会在java.home/lib/logging.properties这个文件中读取配置。。
如何读取的呢,核心代码在java.util.logging.LogManager
下面简化readConfiguration中的代码(除去了非正常的分支)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void readConfiguration() throws IOException, SecurityException {
checkPermission();
String cname = System.getProperty("java.util.logging.config.class");
if (cname != null) {
Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(cname);
clz.newInstance();
return;
}
String fname = System.getProperty("java.util.logging.config.file");
if (fname == null) {
fname = System.getProperty("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);
}
}

读取之后,会把文件的key-value读取成Properties对象。

如何修改日志格式

SimpleFormatter中的默认格式为%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%6$s%n

SimpleFormatter中关键代码为:

1
2
3
4
5
6
7
return String.format(format,
dat,
source,
record.getLoggerName(),
record.getLevel().getLocalizedLevelName(),
message,
throwable);

输出示例:

1
2
九月 08, 2017 12:10:18 下午 com.xx.logtest.Main methodname
信息: this is info

关于String格式化的详见,比如%1$tb对第一个参数,进行月份简称的格式化。

通过修改logging.properties,java.util.logging.SimpleFormatter.format中的参数,就是传到SimpleFormatter的中format参数。
当然只能格式化上面有的信息,上面没有的信息,就不能输出的,比如说线程id,序列号就输出不了。

如何配置logging.properties文件

一些关于java.util.logging的日志配置文件的说明,都比较零散,还没有文件中的注释说明写得清楚。
了解这个配置文件怎么使用的,最容易的就看日志代码中怎么处理这个解析这个文件的,怎么使用这些配置的。

这里我就从源码入手,看系统怎么读取并初始化日志配置的。

比如:

1
2
# By default we only configure a ConsoleHandler
handlers= java.util.logging.ConsoleHandler

因为logging.properties文件读取成Properties对象,给代码中其它位置使用,所以必定handlers会是一个key。
logging包下搜索下"handlers"
发现LogManager.java文件中的1578行,initializeGlobalHandlers中发生了调用loadLoggerHandlers(rootLogger, null, "handlers");(用的jdk1.8.0_101的源码),从函数名可以看到是找对位置了。
loadLoggerHandlers中做了什么事呢
简化后的代码:

1
2
3
4
5
6
7
8
9
10
11
String names[] = parseClassNames(handlersPropertyName);
for (int i = 0; i < names.length; i++) {
String word = names[i];
Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(word);
Handler hdl = (Handler) clz.newInstance();
String levs = getProperty(word + ".level");
Level l = Level.findLevel(levs);
hdl.setLevel(l);
logger.addHandler(hdl);
}
}

parseClassNames通key拿到value,按,分割,返回数组对象,也就是多个hander的类名。
loadLoggerHandlers进行newInstance,同时读取类名加”.level”的key,设置为handler的level。
然后增加到root的logger中。
这样,下面的关于level的配置就读取并配置好了。

1
2
3
# 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

那formatter是怎么读取的呢。按".formatter"搜索。
可以看到在ConsoleHandler初始化的时候,调用configure()方法,里面有对level,filter,formatter,encoding的配置的使用。filter和formatter都是可以自定义,代码中通过newInstance来初始化。

小插曲
在上面logger.addHandler(hdl);处理中,还会以调用者的class类名加上”.handler”的配置再加一次handler。
就是说在配置文件中加上com.xyz.foo.handlers的handler, 也加进来

同样的道理。
其它的handler与filter及formatter也可以这样配置

总结

看起来很平常的日志框架,读起源码的实现,还是能学到不少前人对模块设计的智慧。
JDK自带日志框架是个很基础但重要的功能。熟悉它的原理,对其它的日志框架的学习是非常有借鉴作用的。
对于个性化配置tomcat内部日志有一些帮助。

参考

遇到的问题

missing line number attributes

参考 http://www.cnblogs.com/wavky/p/3802537.html 解决了

正确的情况应该是引入JDK本身而不是JRE,点击Edit,更改Location指向正确的JDK目录,点击Restore Default更新左侧所有jar包的引用目录(指向jdk文件夹下的jre目录),确认必需的jar包已配置源代码包路径(主要是rt.jar),Finish确认。

调试时无法查看局部变量

参考 http://blog.csdn.net/appleprince88/article/details/21873807

首先我们要明白JDK source为什么在debug的时候无法观察局部变量,因为在jdk中,sun对rt.jar中的类编译时,去除了调试信息,这样在eclipse中就不能看到局部变量的值。这样的话,如果在debug的时候查看局部变量,就必须自己编译相应的源码使之拥有调试信息。