天天看點

重構-封裝LogUtil 工具類

項目中的日志工具類不友好,不友善跟進項目問題;是以打算重新優化一下

分析項目現狀

  • 工程下有不同的日志工具類有的一樣,有的部分定制
重構-封裝LogUtil 工具類
  • 輸出日志格式 隻有日期和 日志内容;沒有 類方法資訊;問題定位困難
重構-封裝LogUtil 工具類

問題 1 : 解決日志工具類過多問題

  1. 新增通用日志工具類 LogUtils; 整合提取共用日志方法,放置 common 包
  2. 各個工具類引用LogUtils 現有方法進行日志列印, 此時可以友善看出哪些類進行大量定制
  3. 少量定制的日志類 統一合并到通用工具類LogUtils; 可以選擇删除或者重命名區分
  4. 定制了大量日志方法的工具類,進行指令重構 以包名+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接口實作類

重構-封裝LogUtil 工具類

可以通過 commons-logging.properties 配置日志工廠實作類

# 配置日志工廠類
org.apache.commons.logging.LogFactory
  =org.apache.commons.logging.impl.LogFactoryImpl           

日志對象為 org.apache.commons.logging.impl.Log4JLogger

重構-封裝LogUtil 工具類

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 最終運作效果

重構-封裝LogUtil 工具類

推薦閱讀-更全面認識 java 日志架構 認識java日志架構

不多說,項目又出 bug 了趕緊拿上去試試