定义:
JUL全称Java util logging,是java原生的日志框架,使用时不需要另外引入第三方类库,相对于其他框架使用方便,学习简单,能够在小型的应用中灵活使用。
架构:
Application:我们的程序应用。
LogManager:管理Logger,是个单例Bean。
Logger:日志记录器,我们的应用程序通过获取日志记录器Logger对象,调用其API来发布日志信息,Logger通常是应用程序访问日志系统的入口。
Handler:日志处理器,每个Logger会关联持有多个Handler,Logger会把日志交给Handler进行处理,由Handler负责日志记录。Handler在这里是一个抽象,其具体实现决定了日志输出的位置,比如控制台,文件等。
Formattter:日志格式转化器,负责对日志的格式进行处理。决定了输出的日志的最终形式。
Level:每条日志信息都有一个关联的日志级别,该级别处理指导了日志信息的重要性。我们可以将Level作用于Logger和Handler上。以便于我们过滤消息。
Filter:过滤器,根据规则过滤掉某些日志信息。
总体流程:
初始化LogManager,加载logging.properties配置文件,添加Logger到LogManager中。
从单例Bean LogManager获取Logger。
设置日志级别Level。
Handler处理日志输出。
Formatter处理日志格式。
日志的级别:
JUL日志级别由类java.util.logging.Level记录,总共有七个日志级别,由高到低分别是:
SEVERE //错误信息,一般是记录导致系统终止的信息。
WARNING //警告信息,一般记录程序的问题,该问题不会导致系统终止,但是却值得我们关注。
INFO // 一般信息,一般记录一些连接信息,访问信息等。(这是JUL的默认级别)
CONFIG // 一般记录加载配置文件等日志信息。
FINE // Debug日志信息,一般记录程序一般运行的状态。执行的流程参数的传递等信息。
FINER //与FINE 类似,只是记录的颗粒度要高一些。
FINEST //与上面两个类似,只是记录的颗粒度要高一些。
还有两个特殊的级别:
OFF: 可用来关闭日志信息,不输出任何级别的日志。
ALL: 记录所有级别的日志信息
源码
Logger.getLogger
LogManager manager = LogManager.getLogManager();
return manager.demandLogger(name, resourceBundleName, caller);
LogManager的创建
忽略一些权限校验
LogManager就是java.util.logging.manager
static {
manager = AccessController.doPrivileged(new PrivilegedAction<LogManager>() {
@Override
public LogManager run() {
LogManager mgr = null;
String cname = null;
try {
cname = System.getProperty("java.util.logging.manager");
if (cname != null) {
try {
@SuppressWarnings("deprecation")
Object tmp = ClassLoader.getSystemClassLoader()
.loadClass(cname).newInstance();
mgr = (LogManager) tmp;
} catch (ClassNotFoundException ex) {
@SuppressWarnings("deprecation")
Object tmp = Thread.currentThread()
.getContextClassLoader().loadClass(cname).newInstance();
mgr = (LogManager) tmp;
}
}
} catch (Exception ex) {
System.err.println("Could not load Logmanager \"" + cname + "\"");
ex.printStackTrace();
}
if (mgr == null) {
mgr = new LogManager();
}
return mgr;
}
});
}
LogManager的初始化
final void ensureLogManagerInitialized() {
final LogManager owner = this;
//已经初始化就直接退出
if (initializationDone || owner != manager) {
return;
}
//锁
configurationLock.lock();
try {
final boolean isRecursiveInitialization = (initializedCalled == true);
//如果被调用或者已经执行完毕,直接报错
assert initializedCalled || !initializationDone
: "Initialization can't be done if initialized has not been called!";
//执行完了直接返回
if (isRecursiveInitialization || initializationDone) {
return;
}
//忽略权限的代码,就是设置RootLogger,名字是空串,是系统log
initializedCalled = true;
try {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
assert rootLogger == null;
assert initializedCalled && !initializationDone;
// create root logger before reading primordial
// configuration - to ensure that it will be added
// before the global logger, and not after.
final Logger root = owner.rootLogger = owner.new RootLogger();
// Read configuration.
owner.readPrimordialConfiguration();
// Create and retain Logger for the root of the namespace.
owner.addLogger(root);
// Initialize level if not yet initialized
if (!root.isLevelInitialized()) {
root.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
initializationDone = true;
}
} finally {
configurationLock.unlock();
}
}
getLogger
Logger demandLogger(String name, String resourceBundleName, Module module) {
//1.从userContext里面获取log,userContext里面有一个map,WeakHashMap<Object, LoggerContext> contextsMap
Logger result = getLogger(name);
if (result == null) {
// 没有直接new一个
Logger newLogger = new Logger(name, resourceBundleName,
module, this, false);
do {
//加入userContext
if (addLogger(newLogger)) {
return newLogger;
}
result = getLogger(name);
} while (result == null);
}
return result;
}
log.info
public void log(Level level, String msg) {
//检查级别
if (!isLoggable(level)) {
return;
}
//LogRecord
LogRecord lr = new LogRecord(level, msg);
doLog(lr);
}
//一些国际化设置
private void doLog(LogRecord lr) {
lr.setLoggerName(name);
final LoggerBundle lb = getEffectiveLoggerBundle();
final ResourceBundle bundle = lb.userBundle;
final String ebname = lb.resourceBundleName;
if (ebname != null && bundle != null) {
lr.setResourceBundleName(ebname);
lr.setResourceBundle(bundle);
}
log(lr);
}
doLog
public void log(LogRecord record) {
//级别不够返回
if (!isLoggable(record.getLevel())) {
return;
}
//调用filter.isLoggable方法
Filter theFilter = config.filter;
if (theFilter != null && !theFilter.isLoggable(record)) {
return;
}
//
Logger logger = this;
while (logger != null) {
final Handler[] loggerHandlers = isSystemLogger
? logger.accessCheckedHandlers()
: logger.getHandlers();
for (Handler handler : loggerHandlers) {
//执行loggerHandlers的publish方法
handler.publish(record);
}
final boolean useParentHdls = isSystemLogger
? logger.config.useParentHandlers
: logger.getUseParentHandlers();
//不使用ParentHandler直接返回
if (!useParentHdls) {
break;
}
//使用ParentHandler,logger设置为parent,继续打印日志,这里能解释为什么有的日志打印好几份
logger = isSystemLogger ? logger.parent : logger.getParent();
}
}
handler.publish(record);
handler.publish(record);
简单看一下ConsoleHandler extends StreamHandler
@Override
public synchronized void publish(LogRecord record) {
String msg;
try {
//格式化组件
msg = getFormatter().format(record);
} catch (Exception ex) {
reportError(null, ex, ErrorManager.FORMAT_FAILURE);
return;
}
//这里面的write就是System.err,所以默认的都是红色日志
try {
if (!doneHeader) {
writer.write(getFormatter().getHead(this));
doneHeader = true;
}
writer.write(msg);
} catch (Exception ex) {
// We don't want to throw an exception here, but we
// report the exception to any registered ErrorManager.
reportError(null, ex, ErrorManager.WRITE_FAILURE);
}
}
Formatter
简单看一下SimpleFormatter
核心就是调用String.format
@Override
public String format(LogRecord record) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(
record.getInstant(), ZoneId.systemDefault());
String source;
if (record.getSourceClassName() != null) {
source = record.getSourceClassName();
if (record.getSourceMethodName() != null) {
source += " " + record.getSourceMethodName();
}
} else {
source = record.getLoggerName();
}
String message = formatMessage(record);
String throwable = "";
if (record.getThrown() != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println();
record.getThrown().printStackTrace(pw);
pw.close();
throwable = sw.toString();
}
//系统默认的格式化format %1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%6$s%n
return String.format(format,
zdt,
source,
record.getLoggerName(),
record.getLevel().getLocalizedLevelName(),
message,
throwable);
}
扩展点
1.指定配置文件或者修改默认的
2.手工加载配置文件
LogManager logManager = LogManager.getLogManager();
logManager.readConfiguration(自己的配置文件inputStreaqm);
3.手动实现以下format和handler
public class MyConsoleHandler extends Handler {
public MyConsoleHandler() {
setFormatter(new MyFormatter());
}
@Override
public void publish(LogRecord record) {
if(record==null) return;
String msg = getFormatter().format(record);
if(record.getLevel().intValue()> Level.WARNING.intValue()){
System.err.print(msg);
}else{
System.out.print(msg);
}
}
@Override
public void flush() {
}
@Override
public void close() throws SecurityException {
}
}
public class MyFormatter extends Formatter {
public static final String format = "%1$tF %1$tT.%1$tL %2$s%n%4$s: %5$s%6$s%n";
@Override
public String format(LogRecord record) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(
record.getInstant(), ZoneId.systemDefault());
String source;
if (record.getSourceClassName() != null) {
source = record.getSourceClassName();
if (record.getSourceMethodName() != null) {
source += " " + record.getSourceMethodName();
}
} else {
source = record.getLoggerName();
}
String message = formatMessage(record);
String throwable = "";
if (record.getThrown() != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println();
record.getThrown().printStackTrace(pw);
pw.close();
throwable = sw.toString();
}
return String.format(format,
zdt,
source,
record.getLoggerName(),
record.getLevel().getLocalizedName(),
message,
throwable);
}
}
使用记录
Logger logger = Logger.getLogger("TestLog.class");
//logger.addHandler(new MyConsoleHandler());
//用了自定义的可以考虑不适用父级的
//logger.setUseParentHandlers(false);
//2.打印日志
logger.info("hello jul");
logger.log(Level.INFO,"jul level.info");
logger.severe("姓名:李四,年龄:"+33);
String name = "张三";
int age = 18;
logger.log(Level.INFO,"姓名:{0},年龄:{1}",new Object[]{
name,age});
使用自定义的之后
转换符 | 说明 | 示例 |
%s | 字符串类型 | “mingrisoft” |
%c | 字符类型 | ‘m’ |
%b | 布尔类型 | true |
%d | 整数类型(十进制) | 99 |
%x | 整数类型(十六进制) | FF |
%o | 整数类型(八进制) | 77 |
%f | 浮点类型 | 99.99 |
%a | 十六进制浮点类型 | FF.35AE |
%e | 指数类型 | 9.38e+5 |
%g | 通用浮点类型(f和e类型中较短的) | |
%h | 散列码,Integer.toHexString(arg.hashCode()) | |
%% | 百分比类型 | % |
%n | 换行符 | |
%tx | 日期与时间类型(x代表不同的日期与时间转换符 |
日期格式化
转换符 | 说明 | 示例 |
---|---|---|
c | 包括全部日期和时间信息 | 星期六 十月 27 14:21:20 CST 2007 |
F | “年-月-日”格式 | 2007-10-27 |
D | “月/日/年”格式 | 10/27/07 |
r | “HH:MM:SS PM”格式(12时制) | 02:25:51 下午 |
T | “HH:MM:SS”格式(24时制) | 14:28:16 |
R | “HH:MM”格式(24时制) | 14:28 |
时间格式化
转换符 | 说明 | 示例 |
---|---|---|
H | 2位数字24时制的小时(不足2位前面补0) | 15 |
I | 2位数字12时制的小时(不足2位前面补0) | 03 |
K | 2位数字24时制的小时(前面不补0) | 15 |
L | 2位数字12时制的小时(前面不补0) | 3 |
M | 2位数字的分钟(不足2位前面补0) | 03 |
S | 2位数字的秒(不足2位前面补0) | 09 |
L | 3位数字的毫秒(不足3位前面补0) | 015 |
N | 9位数字的毫秒数(不足9位前面补0) | 562000000 |
p | 小写字母的上午或下午标记 | 中:下午,英:pm |
z | 相对于GMT的RFC822时区的偏移量 | +0800 |
Z | 时区缩写字符串 | CST |
S | 1970-1-1 00:00:00 到现在所经过的秒数 | 1193468128 |
Q | 1970-1-1 00:00:00 到现在所经过的毫秒数 | 1193468128984 |
%1 t b tb %1 tbtd, %1 t Y tY %1 tYtl:%1 t M : tM:%1 tM:tS %1 T p Tp %2 Tps%n%4 s : s: %5 s:s%6$s%n
String format = "%1$tF %1$tT.%1$tL %2$s%n%4$s: %5$s%6$s%n";
2021-11-29 16:34:25.177 com.git.log.TestLog testJUL
信息: hello jul
标Cprintf
转换符 | 含义 |
---|---|
%A | 浮点数、十六进制数字和p-记法(C99) |
%c | 一个字符 |
%d | 有符号十进制整数 |
%e | 浮点数、e-记数法 |
%E | 浮点数、E-记数法 |
%f | 浮点数、十进制记数法 |
%g | 根据数值不同自动选择%f或%e. |
%G | 根据数值不同自动选择%f或%e. |
%i | 有符号十进制数(与%d相同) |
%o | 无符号八进制整数 |
%p | 指针 |
%s | 字符串 |
%u | 无符号十进制整数 |
%x | 使用十六进制数字0f的无符号十六进制整数 |
%X | 使用十六进制数字0f的无符号十六进制整数 |
%% | 打印一个百分号 |
符号 作用
──────────────────────────
%d 十进制有符号整数
%u 十进制无符号整数
%f 浮点数
%s 字符串
%c 单个字符
%p 指针的值
%e 指数形式的浮点数
%x, %X 无符号以十六进制表示的整数
%0 无符号以八进制表示的整数
%g 自动选择合适的表示法
\n 换行
\f 清屏并换页
\r 回车
\t Tab符
\xhh 表示一个ASCII码用16进表示,
其中hh是1到2个16进制数
━━━━━━━━━━━━━━━━━━━━━━━━━━
# RootLogger 顶级父元素指定的默认处理器为:ConsoleHandler
handlers= java.util.logging.FileHandler
# RootLogger 顶级父元素默认的日志级别为:ALL
.level= ALL
# 自定义 Logger 使用
com.leon.handlers = java.util.logging.ConsoleHandler
com.leon.level = CONFIG
# 关闭默认配置
com.leon.useParentHanlders = false
# 向日志文件输出的 handler 对象
# 指定日志文件路径 /logs/java0.log
java.util.logging.FileHandler.pattern = /logs/java%u.log
# 指定日志文件内容大小
java.util.logging.FileHandler.limit = 50000
# 指定日志文件数量
java.util.logging.FileHandler.count = 1
# 指定 handler 对象日志消息格式对象
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 指定以追加方式添加日志内容
java.util.logging.FileHandler.append = true
# 向控制台输出的 handler 对象
# 指定 handler 对象的日志级别
java.util.logging.ConsoleHandler.level = ALL
# 指定 handler 对象的日志消息格式对象
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8
# 指定日志消息格式
java.util.logging.SimpleFormatter.format = %4$s: %5$s [%1$tc]%n
参考:
Java日志框架之JUL(java util logging)详解 引用其中一些概念