1. 程式人生 > >Spring通過切面記錄日誌

Spring通過切面記錄日誌

這兩天專案使用者越來越多,決定給控制層統一加上日誌,方便對出現的問題資料進行原因查詢。在加日誌的過程中遇到了一些小麻煩(如何獲取方法引數名,內容,列印到指定檔案,不再控制檯列印等),在這裡記錄一下。
首先web.xml下要不要加以下配置,看你的log4j配置檔案是不是在spring的約定位置,如果是的話可加可不加。假如你要加的話要注意的是Log4jConfigListener必須要在Spring的Listener之前。

<!--列印日誌配置檔案位置,可以不配置則去預設位置搜尋,搜尋不到則使用tomcat預設的日誌配置所以此處log4j.properties找不到時,此xml檔案依舊不會報錯-->
  <!--但是隻會掃面src目錄(包括子目錄)下是否有此檔案(先去找log4j.xml,然後再去找log4j.properties)-->
  <context-param>
    <param-name>log4jConfigLoaction</param-name>
    <param-value>classpath:log4j.properties</param-value>
  </context-param>
  <!-- 載入Spring框架中的log4j監聽器Log4jConfigListener -->
  <listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  </listener>

之前在查詢資料的時候發現有人指出log4j.properties和log4j.xml的約定位置不同,說log4j.xml在resources下,log4j.properties在WEB-INF下。我沒有發現這個問題,我的log4j.properties在resources下,也並沒有配置log4jConfigLoaction,依舊可用。

下面是log4j.properties中的配置(底部有部分日誌的配置解釋!底部有部分日誌的配置解釋!底部有部分日誌的配置解釋!)

#log4j.properties載入是自動的但是隻會掃面src目錄(包括子目錄)下是否有此檔案(先去找log4j.xml,然後再去找log4j.properties)可在web.xml中配置配置檔案位置
#<context-param><param-name>log4jConfigLoaction</param-name><param-value>classpath:log4j.properties</param-value></context-param>

#日誌等級由高到低分別為 OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL  (簡介見底部1)
#Log4j建議只使用四個級別,優先順序從高到低分別是 ERROR、WARN、INFO、DEBUG
#程式會列印高於或等於所設定級別的日誌,設定的日誌等級越高,打印出來的日誌就越少。
#下面配置日誌的級別和輸出的配置  第一個為日誌級別,後面的是輸出配置(名字可自定義,多個輸出配置用逗號隔開)
log4j.rootLogger=ERROR,stdout
#下面將定義定義名為stdout的輸出端是哪種型別(詳情見底部2)
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
#下面將定義名為stdout的輸出端的layout是哪種型別(詳情見底部3)
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
#如果使用pattern佈局就要指定的列印資訊的具體格式ConversionPattern(詳情見底部4)
log4j.appender.stdout.layout.ConversionPattern=%d %-5p %l - %m%n
#列印sql的 目前沒研究明白 因還有一些配置不知在哪配置:包括是否輸出查詢結果 而且mapper中也要配上註解
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
#設定sql日誌只打印sql不列印查詢結果
#配置丟失,請自行查詢

#*************************************************************這裡是新加的日誌配置*******start***************************************
log4j.logger.org.redblue.hide.controller.webutil.LogsAspect=info,lanlytest
#log4j.additivity是子Logger是否繼承父Logger的輸出源(appender)的標誌位。如果繼承了那麼列印到這裡的同時也會在父輸出源列印一份<這裡必須寫路徑而不是日誌別名lanlytest>
#log4j.additivity.org.redblue.hide.controller.webutil.LogsAspect = false
log4j.appender.lanlytest=org.apache.log4j.DailyRollingFileAppender
#如果你使用的是tomcat,那麼預設就可以使用${catalina.home}獲得tomcat的根目錄
#log4j.appender.lanlytest.File=${catalina.home}/logs/firestorm.log
#windos下需使用絕對路徑不然會有問題
log4j.appender.lanlytest.File=D\:/lanly/worktool/workspace/ideal-tomcat-log4j-output/logs/firestorm.log
log4j.appender.lanlytest.MaxFileSize=100KB
log4j.appender.lanlytest.MaxBackupIndex=1
#在日誌後面追加還是覆蓋,預設是true追加
log4j.appender.lanlytest.Append = true
log4j.appender.lanlytest.layout=org.apache.log4j.PatternLayout
log4j.appender.lanlytest.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} - %m%n
#*************************************************************這裡是新加的日誌配置*******end***************************************

#1/2/3/4分別對應上面加標註的位置
#1
#日誌等級簡介
#DEBUG Level指出細粒度資訊事件對除錯應用程式是非常有幫助的。
#INFO level表明 訊息在粗粒度級別上突出強調應用程式的執行過程。
#WARN level表明會出現潛在錯誤的情形。
#ERROR level指出雖然發生錯誤事件,但仍然不影響系統的繼續執行。
#FATAL level指出每個嚴重的錯誤事件將會導致應用程式的退出。
#另外,還有兩個可用的特別的日誌記錄級別: (以下描述來自log4j API http://jakarta.apache.org/log4j/docs/api/index.html):
#ALL Level是最低等級的,用於開啟所有日誌記錄。
#OFF Level是最高等級的,用於關閉所有日誌記錄。

#2
#輸出端是型別
#org.apache.log4j.ConsoleAppender(控制檯),
#org.apache.log4j.FileAppender(檔案),
#org.apache.log4j.DailyRollingFileAppender(每天產生一個日誌檔案),
#org.apache.log4j.RollingFileAppender(檔案大小到達指定尺寸的時候產生一個新的檔案)
#org.apache.log4j.WriterAppender(將日誌資訊以流格式傳送到任意指定的地方)

#3
#此句為定義名為stdout的輸出端的layout是哪種型別
#org.apache.log4j.HTMLLayout(以HTML表格形式佈局)
#org.apache.log4j.PatternLayout(可以靈活地指定佈局模式)
#org.apache.log4j.SimpleLayout(包含日誌資訊的級別和資訊字串)
#org.apache.log4j.TTCCLayout(包含日誌產生的時間、執行緒、類別等等資訊)

#4
#log4j.appender.stdout.layout.ConversionPattern= [QC] %p [%t] %C.%M(%L) | %m%n
#如果使用pattern佈局就要指定的列印資訊的具體格式ConversionPattern,列印引數如下:
#%m 輸出程式碼中指定的訊息(為了方便你自己查詢錯誤位置可以自己加內容,當然也可以在程式碼中加固定的日誌訊息<但多人開發可能會忘記加日誌特定訊息>)
#%p 輸出優先順序,即DEBUG,INFO,WARN,ERROR,FATAL
#%r 輸出自應用啟動到輸出該log資訊耗費的毫秒數
#%c 輸出所屬的類目,通常就是所在類的全名
#%t 輸出產生該日誌事件的執行緒名
#%n 輸出一個回車換行符,Windows平臺為“rn”,Unix平臺為“n”
#%d 輸出日誌時間點的日期或時間,預設格式為ISO8601,也可以在其後指定格式,比如:%d{yyyy MMM dd HH:mm:ss,SSS},輸出類似:2002年10月18日 22:10:28,921
#%l 輸出日誌事件的發生位置,包括類目名、發生的執行緒,以及在程式碼中的行數。
#[QC]是log資訊的開頭,可以為任意字元,一般為專案簡稱。

接下來就是用來記錄日誌的切面類(裡面提供了一些獲取引數名以及引數內容的方法,歡迎使用和交流)

package org.redblue.hide.controller.webutil;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.aspectj.lang.JoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @Author lanly
 * @Date 2018/12/25 0025.
 */
public class LogsAspect implements MethodBeforeAdvice, AfterReturningAdvice, ThrowsAdvice {
    private  Logger logger = LoggerFactory.getLogger(LogsAspect.class);

    public LogsAspect() {
    }

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {}
    public void after(JoinPoint point) {}

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        logger.info(getLogString(method,args,target,null));
    }

//這裡沒有@Override 發現ThrowsAdvice 介面中並沒有任何方法,Spirng內部是用反射來實現方法匹配的,需要實現下列介面中的其中1個
/**
 *
 *  public void afterThrowing(Exception ex)
 *  public void afterThrowing(RemoteException)
 *  public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
 *  public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)
 * */
    public void afterThrowing(Method method, Object[] args, Object target, Exception ex) throws Throwable {
        logger.info(getLogString(method,args,target,ex));
    }

    private static String getLogString(Method method, Object[] args, Object target, Exception ex)throws Throwable{
        StringBuilder logString = new StringBuilder();
        String className = target.getClass().getName();
        String methodName = method.getName();
        //這裡不可以重定義logger因為重定義之後專門記錄切面日誌的輸出將認為這不是切面的日誌而是重新定義的類的日誌
        // logger = LoggerFactory.getLogger(target.getClass());
        List<String> paramNames = getParamterName(target.getClass(),methodName);
        List<String> paramValues = new ArrayList<String>();
        StringBuilder paramsString = new StringBuilder();
        try {
            String value = "null";
            //獲取所有的引數
            for (int k = 0; k < args.length; k++) {
                Object arg = args[k];
                if(arg!=null){
                    String argTypeName = arg.getClass().getTypeName();
                    if(isPrimite(argTypeName)){
                        value = new String(arg+"");
                    }else{
                    //這裡使用SerializerFeature.IgnoreNonFieldGetter來防止alibaba.fastjson轉換部分物件時出現異常情況
                        value = JSON.toJSONString(arg, SerializerFeature.IgnoreNonFieldGetter);
                    }
                    paramValues.add(value);
                }else{
                    paramValues.add("null");
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        for (int i =0 ;i<paramNames.size();i++) {
            paramsString.append(paramNames.get(i)+":"+paramValues.get(i)+",");
        }
        logString.append("className ="+className+";    methodName = "+methodName+";");
        if(paramsString.length()>0){
            logString.append("    args="+paramsString.substring(0,paramsString.length()-1)+";");
        }
        if(ex!=null){
            logString.append("     exception="+ex.getMessage()+";");
        }
        return logString.toString();
    }
    /**
     * 判斷是否為基本型別:包括String和基本型別封裝類
     * @param typeName clazz
     * @return  true:是;     false:不是
     */
    private static boolean isPrimite(String typeName){
        for (String t : types) {
            if (t.equals(typeName)) {
                return true;
            }
        }
        return false;
    }

    private static String[] types = {"java.lang.Integer", "java.lang.Double",
            "java.lang.Float", "java.lang.Long", "java.lang.Short",
            "java.lang.Byte", "java.lang.Boolean", "java.lang.Char",
            "java.lang.String", "int", "double", "long", "short", "byte",
            "boolean", "char", "float"
    };
   //獲取方法引數名
        public static List<String> getParamterName(Class clazz, String methodName){
            LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                if (methodName.equals(method.getName())) {
                    String[] params = u.getParameterNames(method);
                    return Arrays.asList(params);
                }
            }
            return null;
        }
    }

遇到的問題包括,獲取引數名,獲取引數內容並轉為字串,控制檯有日誌列印卻輸出不到指定檔案。問題都在文章中解決了,另外輸出都指定檔案時因為多次測試發現log4j的配置檔案在修改後立刻啟動可能會存在未儲存修改的情況,雖然ideal會自動儲存但是有時間間隔,所以建議每次修改後及時儲存。