項目中的日志工具類不友好,不友善跟進項目問題;是以打算重新優化一下
分析項目現狀
- 工程下有不同的日志工具類有的一樣,有的部分定制
- 輸出日志格式 隻有日期和 日志内容;沒有 類方法資訊;問題定位困難
問題 1 : 解決日志工具類過多問題
- 新增通用日志工具類 LogUtils; 整合提取共用日志方法,放置 common 包
- 各個工具類引用LogUtils 現有方法進行日志列印, 此時可以友善看出哪些類進行大量定制
- 少量定制的日志類 統一合并到通用工具類LogUtils; 可以選擇删除或者重命名區分
- 定制了大量日志方法的工具類,進行指令重構 以包名+LogUtil 進行特别區分
問題 2 : 完善日志輸出資訊
- 針對不同日志級别輸出不同資訊
info 日志: 保留現狀,簡潔輸出
debug 和 error 日志: 輸出調用類.方法名.代碼行
參考配置如下:
log4j.rootLogger=CONSOLE,DEBUG,INFO,,ERRORFILE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=cn.javabus.common.util.LogPatternLayout
# 簡單日志格式: 日期 日志内容
log4j.appender.CONSOLE.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%p] %m%n
log4j.appender.ERRORFILE=org.apache.log4j.RollingFileAppender
log4j.appender.ERRORFILE.layout=com.szkingdom.common.util.LogPatternLayout
# 輸出 輸出調用類.方法名.代碼行 線程
log4j.appender.ERRORFILE.layout.ConversionPattern= [%-5p] %d{HH:mm:ss,SSS} %m %l [%T] %n
log4j.appender.ERRORFILE.Threshold=ERROR
log4j.appender.ERRORFILE.file=./log/ERRORFILE/log.txt
log4j.appender.ERRORFILE.MaxBackupIndex=100
log4j.appender.ERRORFILE.MaxFileSize=10MB
...
log4j.logger.cn.javabus.common.util.LogUtils=DEBUG,CONSOLE
解決 LogUtils 輸出的調用方不正确問題(重點)
public class LogUtils {
protected static Log log = LogFactory.getLog(LogUtils.class);
public static void debug(String msg) {
log.debug(msg);
}
...
}
//通過以上方式輸出的日志 代碼行如下,代碼行一直顯示的 LogUtils 不符号期望
... LogUtils.debug(LogUtil.java:26) ...
那麼如何使得日志架構, 調整輸出的代碼位置呢?
1 調試LogFactory.getLog()得知log為Log4JLogger接口實作類
可以通過 commons-logging.properties 配置日志工廠實作類
# 配置日志工廠類
org.apache.commons.logging.LogFactory
=org.apache.commons.logging.impl.LogFactoryImpl
日志對象為 org.apache.commons.logging.impl.Log4JLogger
2 找到日志代碼行輸出位置 : new LocationInfo()
// 1 org.apache.commons.logging.impl.Log4JLogger#info()
public void info(Object message) {
//FQCN 全限定名稱英文縮寫,後面确定代碼行調用位置就需要他
this.getLogger()
.log(FQCN, Priority.INFO, message, (Throwable)null);
}
//1.1 getLogger()傳回 Logger執行個體對象,
public Logger getLogger() {
if (this.logger == null) {
this.logger = Logger.getLogger(this.name);
}
return this.logger;
}
// 1.2 Logger 繼承了 Category
public class Logger extends Category {
private static final String FQCN;
...
}
//2 接下來 Category#log() 方法,會調用forcedLog()
protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
// new LoggingEvent() 建立一個實體對象,但是沒有包含調用位置資訊
// 接下來看 callAppenders()
this.callAppenders(new LoggingEvent(fqcn, this, level, message, t));
}
//2.1 callAppenders 看着有點複雜,是在循環Appenders
public void callAppenders(LoggingEvent event) {
int writes = 0;
for(Category c = this; c != null; c = c.parent) {
synchronized(c) {
if (c.aai != null) {
writes += c.aai.appendLoopOnAppenders(event);
}
if (!c.additive) {
break;
}
}
}
...
}
//2.2 看 appender.doAppend(event);
public int appendLoopOnAppenders(LoggingEvent event) {
int size = 0;
if (this.appenderList != null) {
size = this.appenderList.size();
for(int i = 0; i < size; ++i) {
Appender appender = (Appender)this.appenderList.elementAt(i);
appender.doAppend(event);
}
}
return size;
}
//2.3 看 this.append(event);
public synchronized void doAppend(LoggingEvent event) {
if (this.closed) {
LogLog.error("Attempted to append to closed appender named [" + this.name + "].");
} else if (this.isAsSevereAsThreshold(event.getLevel())) {
Filter f = this.headFilter;
while(true) {
...
}
//多實作類 看 WriterAppender#append
this.append(event);
return;
}
}
}
//2.4 subAppend() 中this.layout.format(event) 是一個重要入口
protected void subAppend(LoggingEvent event) {
//layout 多實作,本次看 PatternLayout
this.qw.write(this.layout.format(event));
...
}
//3 從org.apache.log4j.PatternLayout#format(event)開始
//3.1
public String format(LoggingEvent event) {
...
//周遊各種PatternConverter 處理我們的 日志内容
for(PatternConverter c = this.head; c != null; c = c.next) {
c.format(this.sbuf, event);
}
return this.sbuf.toString();
}
//3.2
public void format(StringBuffer sbuf, LoggingEvent e) {
// 可以看到有 LocationPatternConverter
String s = this.convert(e);
...
}
//3.3 内部類LocationPatternConverter
private class LocationPatternConverter extends PatternConverter {
public String convert(LoggingEvent event) {
// 對象 get方法會 new LocationInfo() 對象
LocationInfo locationInfo = event.getLocationInformation();
...
}
}
//new LocationInfo()
public LocationInfo(Throwable t, String fqnOfCallingClass) {
if (t != null && fqnOfCallingClass != null) {
String s;
int i;
if (getLineNumberMethod != null) {
try {
s = null;
//通過堆棧資訊 擷取調用方
Object[] elements = (Object[])((Object[])getStackTraceMethod.invoke(t, s));
String prevClass = "?";
for(i = elements.length - 1; i >= 0; --i) {
String thisClass = (String)getClassNameMethod.invoke(elements[i], s);
if (fqnOfCallingClass.equals(thisClass)) {
// 如果等于FQCN 就,把下一個作為調用方
int caller = i + 1;
if (caller < elements.length) {
this.className = prevClass;
this.methodName = (String)getMethodNameMethod.invoke(elements[caller], s);
this.fileName = (String)getFileNameMethod.invoke(elements[caller], s);
if (this.fileName == null) {
this.fileName = "?";
}
int line = (Integer)getLineNumberMethod.invoke(elements[caller], s);
if (line < 0) {
this.lineNumber = "?";
} else {
this.lineNumber = String.valueOf(line);
}
StringBuffer buf = new StringBuffer();
buf.append(this.className);
buf.append(".");
buf.append(this.methodName);
buf.append("(");
buf.append(this.fileName);
buf.append(":");
buf.append(this.lineNumber);
buf.append(")");
this.fullInfo = buf.toString();
}
3 調整代碼,想辦法調整FQCN 便能實作列印需要的調用代碼行資訊
網上看到用動态代理方式的,比較複雜,有興趣的可以參考
https://www.ydisp.cn/developer/143972.html
以下是我的調整方式,實作起來相對簡單
public class LogUtil {
protected static Log log = LogFactory.getLog(LogUtil.class);
private static final String FQCN = LogUtil.class.getName();
public static void debug(String msg) {
//log = getCallerLogger();
if (log.isDebugEnabled()) {
log(msg);
}
}
public static void log(Object msg) {log(msg, null); }
//核心調整工具方法
public static void log(Object msg, Throwable t) {
//1 需要根據不同日志實作類特殊判斷處理
if (log instanceof Log4JLogger) {
Log4JLogger log4JLogger = (Log4JLogger) LogUtil.log;
//強制轉換為日志實作類.實作改變 FQCN
Logger logger = log4JLogger.getLogger();
((Category) logger).log(FQCN, Priority.INFO, msg, t);
}
}
//ToDo 作用待補充(其他項目中看到擷取調用方資訊方式,留作備忘)
private static final CallerContext CALLER_CONTEXT
= new LogUtil.CallerContext();
private static Log getCallerLogger() {
return LogFactory.getLog(CALLER_CONTEXT.getCallerClass().getName());
}
private static final class CallerContext extends SecurityManager {
private CallerContext() {
}
Class<?> getCallerClass() {
return super.getClassContext()[4];
}
}
4 最終運作效果
推薦閱讀-更全面認識 java 日志架構 認識java日志架構
不多說,項目又出 bug 了趕緊拿上去試試