Java日誌持久化之AOP方式+Logback方式
閱讀本文可能會解決的問題:
① AOP簡單瞭解
② AOP實現日誌管理
③ springboot+logback整合
④ logback日誌入庫+檔案記錄
⑤ ...
七月份的尾巴,巴拉巴拉,最近很忙每天11點下班已經成了常態,剛看了個開頭的《三體》也擱一邊無暇顧及,有時候中午趕著出來吃個飯又屁顛回去加班幹活。一到深夜就在想這樣一種生活狀態是不是算病態,因為感覺完全不合理。但是面對程式碼的時候又很興奮,一開幹就根本停下來,惶惶終日卻又自我滿足,實在是無奈...
不廢話了說主題,最近專案需要一個日誌功能,也是大家實在無法忍受沒log除錯的痛苦遂將此重任交給我,還是springboot還是jpa,下面就說下和logback的整合和我遇到的一些坑。環境:
Springboot + jpa + logback
一、什麼是AOP
其實老早就在用但是一直沒怎麼實際接觸過,像shiro就是典型的AOP實現。通過註解實現全域性攔截處理在實際的應用中很常見。
對於AOP只要知道這幾點就好:
1,spring最核心的兩個功能是aop和ioc,即面向切面,控制反轉。
2,aop的實現是:通過切入點將切面注入業務
3,通俗說:很多業務疊在一起,需要一個針對幾乎全部業務統一處理的這樣一個過程,aop就是將業務一刀切開,在業務側面引入統一處理機制。
所以說日誌系統非常符合aop這樣一種設計方式,另外還有許可權系統包括token校驗等也同樣適用。
二、AOP實現記錄日誌
下面的程式碼部分參考了這裡,在其基礎上增加和優化了一些內容,具體看後面說明
1,首先我們要知道的是通過aop方式進行日誌處理我們需要建立自定義註解,因為最終我們是通過在方法和類上加入註解實現面向切面程式設計的日誌操作。
(1) 先定義@LogEvent註解:
import java.lang.annotation.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface LogEvent { ModuleType module() default ModuleType.DEFAULT; // 日誌所屬的模組 EventType event() default EventType.DEFAULT; // 日誌事件型別 String infos() default ""; // 描述資訊 }
使用:在類或方法上加入 @LogEvent(module = ModuleType.DEFAULT)
示例:
@LogEvent(module = ModuleType.DEFAULT, event = EventType.ADD, infos = "為使用者充值")
註解引申:該註解需要定義內容:日誌模組、日誌事件、描述資訊,這三項在類和方法上非必須,可省略,最終在存庫時會參考這三個引數以最為日誌內容記錄,所以建議別怕麻煩都寫上,需要注意的是類上加入此註解是為在方法上加入此註解提供預設參考值,即在方法上此註解未加入引數則會自動獲取類上的該註解內容以後續儲存。以下為該註解內容
① ModuleType模組型別為列舉型,可以根據實際業務增加業務模組
/**
* 模組型別
*/
public enum ModuleType {
DEFAULT("預設值"), // 預設值
CONTACTS("通訊錄"),// 通訊錄模組
private ModuleType(String index){
this.module = index;
}
private String module;
public String getModule(){
return this.module;
}
}
② EventType事件型別為列舉型,可以根據實際業務增加事件型別
/**
* 事件型別
*/
public enum EventType {
DEFAULT("預設", "default"), ADD("新增", "add"), FIND("查詢", "find"), UPDATE("更新", "update"), DELETE_SINGLE("刪除", "delete-single"),
LOGIN("登入","login"),LOGIN_OUT("登出","login_out");
private EventType(String index, String name){
this.name = name;
this.event = index;
}
private String event;
private String name;
public String getEvent(){
return this.event;
}
public String getName() {
return name;
}
}
③ infos描述資訊型別為字串,用以定義方法/類完成的具體業務內容,預設為空。
(2) 定義@LogEnable註解:
import java.lang.annotation.*;
/**
* 日誌開關
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface LogEnable {
/**
* 如果為true,則類下面的LogEvent啟作用,否則忽略
* @return
*/
boolean logEnable() default true;
}
使用:在類上加入此註解@LogEnable以開啟@LogEvent註解生效,因此,此註解為總開關,決定是否記錄日誌
示例:加在類上即可,需要在@LogEvent註解之前
(3)定義@LogKey註解
import java.lang.annotation.*;
/**
* 此註解可以註解到成員變數,也可以註解到方法的引數上
*/
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogKey {
String keyName() default ""; // key的名稱
boolean isUserId() default false; // 此欄位是否是本次操作的userId,這裡略
boolean isLog() default true; // 是否加入到日誌中
boolean isClass() default false; // 是否類物件
}
使用:在方法的引數或者成員變數上加入此註解@LogKey
示例1(引數為物件):
@RequestMapping(value="/getToken",method= RequestMethod.POST, produces="application/json;charset=UTF-8")
@ResponseBody
@LogEvent(module = ModuleType.TOKEN, event = EventType.getToken, infos = "getToken") // 新增日誌標識
public Map<String , String> getToken(@LogKey(keyName = "user", isClass = true) UserInfo user){
...
}
註解引申:該註解需要定義內容:引數名稱keyName、引數是否加入日誌isLog、引數是否為類物件isClass,這三項可根據自己的方法引數自己改動。原博中說這個註解在類上可用是錯誤的,請大家注意。還有在此註解中我加入了isClass引數,用以判斷輸入引數是否為物件,為true則會序列化此物件最終存庫。
示例2(引數為非物件)
@LogEvent(module = ModuleType.DEFAULT, event = EventType.ADD, infos = "為使用者充值")
public int chargeMoney(@LogKey(keyName = "sum") double sum,
@LogKey(keyName = "remarks") String remarks,
@LogKey(keyName = "updateDate") Date updateDate,
@LogKey(keyName = "uid") String uid,
@LogKey(keyName = "price") double price) {
// TODO Auto-generated method stub
...
}
至此註解部分全部完成了,接下來需要完成aop的切面實現邏輯
2,實現AOP
(1)首先為了方便後續對日誌操作,需要一個日誌實體(節省文字已省略get/set,自行新增)
/**
* 日誌資訊類
*/
@Table(name="sys_log")
@Entity
public class LogAdmModel {
@Id
@GeneratedValue
private Long id;
private String userId; // 操作使用者
private String userName;
private String admModel; // 模組
private String admEvent; // 操作
private Date createDate = new Date(); //建立時間
@Lob
private String admOptContent; // 操作內容
private String targetMethod; // 目標方法名
private String targetClass; // 目標方法所屬類的類名
private String errorMsg; // 錯誤資訊
private String infos; // 備註
}
注意:操作內容admOptContent欄位我設定為大文字,因為後續引數增多以及物件序列化內容過大會導致存不下報錯,所以該欄位加入@Lob註解,後續我例項化存庫所以都有指定表明和標註實體類註解。
(2)對應的我們需要完成Service、ServiceImpl、Dao的程式碼(持久化我使用Jpa)
① LogManager
import com.base.entity.LogAdmModel;
/**
* 日誌處理模組
* 1. 可以將日誌存入資料
* 2. 可以將日誌傳送到開中介軟體,如果redis, mq等等
*/
public interface LogManager {
/**
* 日誌處理模組
* @param paramLogAdmBean
*/
void dealLog(LogAdmModel paramLogAdmBean);
/**
* 列印異常資訊
* @param e
*/
String printException(Exception e);
}
包括處理日誌介面和列印異常介面(列印異常介面非必須,寫這個是為了後面我方便操作Exception)
② LogManagerImpl
/**
* 將日誌存入資料庫
*/
@Service
@Component
public class LogManagerImpl implements LogManager {
@Autowired
LogManagerDao dBLogManagerDao;
@Override
public void dealLog(LogAdmModel paramLogAdmBean) {
UserInfo user = (UserInfo) SecurityUtils.getSubject().getSession().getAttribute("user");
System.out.println("將日誌存入資料庫,日誌內容如下: " + JSON.toJSONString(paramLogAdmBean));
if(user!=null){
paramLogAdmBean.setUserId(String.valueOf(user.getUid()));
paramLogAdmBean.setUserName(user.getUsername());
}
dBLogManagerDao.save(paramLogAdmBean);
}
@Override
public String printException(Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw,true));
e.getMessage();
String str = sw.toString();
return str;
}
}
包括處理日誌操作和列印異常的實現方法
③ LogManagerDao
import com.base.entity.LogAdmModel;
import org.springframework.data.repository.CrudRepository;
import javax.transaction.Transactional;
@Transactional
public interface LogManagerDao extends CrudRepository<LogAdmModel,Long> {
}
jpa的dao只要實現jpa介面即可
(3)LogInfoGeneration日誌註解處理類
import com.alibaba.fastjson.JSONObject;
import com.base.common.annotation.LogKey;
import com.base.controller.BaseController;
import com.base.entity.LogAdmModel;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@Component
public class LogInfoGeneration extends BaseController{
public void processingManagerLogMessage(ProceedingJoinPoint jp, LogAdmModel logBean, Method method) {
Object[] args = jp.getArgs();
if(args.length > 0){
JSONObject msgJson = new JSONObject();
// 獲取方法上引數的註解
Annotation[][] methodAnnotations = method.getParameterAnnotations();
for(int i = 0; i < args.length; i++){
Object arg = args[i];
// 如果引數被 LogKey 註解了,則直接返回內容
if(checkArgAnnotationWithIsLogKey(arg, i, methodAnnotations, msgJson)){
continue;
}
Field[] fs = arg.getClass().getDeclaredFields();
for (Field f : fs) {
Annotation[] ans = f.getAnnotations();
for (Annotation an : ans) {
if ((an instanceof LogKey) && (((LogKey) an).isLog())) {
String fieldName = f.getName();
// 如果註解的有定義keyName,則覆蓋物件成員變數名稱
String fieldNameLogkey = ((LogKey)an).keyName();
if(!StringUtils.isEmpty(fieldNameLogkey)){
fieldName = fieldNameLogkey;
}
try {
f.setAccessible(true);
Object fieldValue = f.get(arg);
msgJson.put(fieldName, fieldValue);
} catch (IllegalAccessException e) {
e.printStackTrace();
} finally {
f.setAccessible(false);
}
}
}
}
logBean.setAdmOptContent(msgJson.toJSONString());
}
}
}
/**
* 如果方法引數被Logkey註解,則將獲取整個類的資訊
* @param index
* @param methodAnnotations
* @param msgJson
* @return
*/
private boolean checkArgAnnotationWithIsLogKey(Object arg, int index, Annotation[][] methodAnnotations, JSONObject msgJson) {
for(Annotation annotation : methodAnnotations[index]){
if(annotation instanceof LogKey){
LogKey logKey = ((LogKey)annotation);
if(logKey.isLog()){
String keyName = logKey.keyName();
if(logKey.isClass()){
try {
net.sf.json.JSONObject json = net.sf.json.JSONObject.fromObject(arg);
msgJson.put(keyName, json.toString());
}catch (Exception e){
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw,true));
logger.error(sw.toString());
}
}else{
msgJson.put(keyName, arg.toString());
}
}
break;
}
}
return false;
}
}
注:BaseController類我只是引用進來logger需要列印一些資訊,非必須。
(4)配置AOP
import com.base.common.annotation.*;
import com.base.common.aop.LogInfoGeneration;
import com.base.entity.LogAdmModel;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
@Component
@Aspect
public class LogAspect {
@Autowired
private LogInfoGeneration logInfoGeneration;
@Autowired
private LogManager ilogManager;
@Pointcut("execution(* com.base.service..*.*(..))")
public void basePoint() {
}
@Pointcut("execution(* com.business.service..*.*(..))")
public void businessPoint() {
}
@Pointcut("execution(public * com.business.controller.*.*(..))")
public void controllerPoint() {
}
@Pointcut("basePoint() || businessPoint() || controllerPoint()")
public void managerLogPoint() {
}
@Around("managerLogPoint()")
public Object aroundManagerLogPoint(ProceedingJoinPoint jp) throws Throwable {
printJoinPoint(jp);
Class target = jp.getTarget().getClass();
// 獲取LogEnable
LogEnable logEnable = (LogEnable) target.getAnnotation(LogEnable.class);
if(logEnable == null || !logEnable.logEnable()){
return jp.proceed();
}
// 獲取類上的LogEvent做為預設值
LogEvent logEventClass = (LogEvent) target.getAnnotation(LogEvent.class);
Method method = getInvokedMethod(jp);
if(method == null){
return jp.proceed();
}
// 獲取方法上的LogEvent
LogEvent logEventMethod = method.getAnnotation(LogEvent.class);
if(logEventMethod == null){
return jp.proceed();
}
String optEvent = logEventMethod.event().getEvent();
String optModel = logEventMethod.module().getModule();
String desc = logEventMethod.infos();
if(logEventClass != null){
// 如果方法上的值為預設值,則使用全域性的值進行替換
optEvent = optEvent.equals(EventType.DEFAULT) ? logEventClass.event().getEvent() : optEvent;
optModel = optModel.equals(ModuleType.DEFAULT.getModule()) ? logEventClass.module().getModule() : optModel;
}
LogAdmModel logBean = new LogAdmModel();
if((jp.getSignature().getName()).equals("dealLog")){}else{
logBean.setTargetMethod(jp.getSignature().getName());
logBean.setTargetClass(jp.getSignature().getDeclaringTypeName());
}
logBean.setAdmModel(optModel);
logBean.setAdmEvent(optEvent);
logBean.setInfos(desc);
logBean.setCreateDate(new Date());
logInfoGeneration.processingManagerLogMessage(jp,
logBean, method);
Object returnObj = jp.proceed();
if(optEvent.equals(EventType.LOGIN)){
//TODO 如果是登入,還需要根據返回值進行判斷是不是成功了,如果成功了,則執行新增日誌。這裡判斷比較簡單
if(returnObj != null) {
this.ilogManager.dealLog(logBean);
}
}else {
this.ilogManager.dealLog(logBean);
}
return returnObj;
}
/**
* 列印節點資訊
* @param jp
*/
private void printJoinPoint(ProceedingJoinPoint jp) {
//System.out.println("=======");
//System.out.println("目標方法名為:" + jp.getSignature().getName());
//System.out.println("目標方法所屬類的簡單類名:" + jp.getSignature().getDeclaringType().getSimpleName());
// jp.getSignature().getDeclaringType(): 呼叫類的型別,通常為介面
//System.out.println("目標方法所屬類的類名:" + jp.getSignature().getDeclaringTypeName());
//System.out.println("目標方法宣告型別:" + Modifier.toString(jp.getSignature().getModifiers()));
//獲取傳入目標方法的引數
/*Object[] args = jp.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i+1) + "個引數為:" + args[i]);
}*/
// jp.getTarget() 實際類,通常為jp.getSignature().getDeclaringType()的實現類
//System.out.println("被代理的物件:" + jp.getTarget());
//System.out.println("代理物件自己:" + jp.getThis());
//System.out.println("=======111");
/*for (Method method : jp.getSignature().getDeclaringType().getMethods()) {
System.out.println("==:" + method);
System.out.println("getAnnotations ==:" + Arrays.toString(method.getAnnotations()));
}*/
}
/**
* 獲取請求方法
*
* @param jp
* @return
*/
public Method getInvokedMethod(JoinPoint jp) {
// 呼叫方法的引數
List classList = new ArrayList();
for (Object obj : jp.getArgs()) {
classList.add(obj.getClass());
// if (obj instanceof ArrayList)
// classList.add(List.class);
// else if (obj instanceof LinkedList)
// classList.add(List.class);
// else if (obj instanceof HashMap)
// classList.add(Map.class);
// else if (obj instanceof HashSet)
// classList.add(Set.class);
// else if (obj == null)
// classList.add(null);
// else {
// classList.add(obj.getClass());
// }
}
Class[] argsCls = (Class[]) classList.toArray(new Class[0]);
// 被呼叫方法名稱
String methodName = jp.getSignature().getName();
Method method = null;
try {
method = jp.getTarget().getClass().getMethod(methodName, argsCls);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return method;
}
}
上面這個類是最重要的,因為這就是aop的切面實現類,@Aspect指定這是一個切面,@Pointcut指定我們的切入點,我這裡配置了兩個,controller層和service層,可按自己的系統自己配置切入點路徑。@Around環繞通知,主要是完成對切入物件的操作。
至此已經完成了AOP日誌入庫的所有邏輯,最終我們呼叫被註解的類方法將會在表中按照規則存入如下記錄:
那麼這樣的日誌記錄形式有什麼應用場景呢?我認為對於我們的系統敏感操作最適用,可以在使用者充值,後臺內容操作都加入日誌,最後將表資料傳遞到web頁面,這樣我們就能清晰明瞭的看到是誰在什麼時候在系統哪個模組做了什麼操作,這在系統安全性和系統健壯性上是一個很大的提升。接下來我們還要解決一個問題,對於一些相對不敏感的操作,比如只是日常記錄使用者操作等該怎麼辦呢?那就是logback出馬要解決的問題了。
三、springboot+logback整合
1,關於系統日誌SLF4J
SLF4J是一個對於日誌框架抽象,常用的有java.util.logging, log4j, logback,commons-logging
logback是log4j框架的作者開發的新一代日誌框架,它效率更高、能夠適應諸多的執行環境,同時天然支援SLF4J。
2,日誌級別
日誌級別從低到高分為TRACE < DEBUG < INFO < WARN < ERROR < FATAL
3,引入配置檔案
為了將記錄的日誌存庫需要引入以下maven依賴
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
配置檔案引入只需要在resources下加入logback-spring.xml即可,springboot會自動讀取,檔名稱必須一致。
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">
<!--定義日誌檔案的儲存地址 勿在 LogBack 的配置中使用相對路徑-->
<property name="LOG_NAME" value="C:/mysoftware/mylog"></property>
<!-- %m輸出的資訊,%p日誌級別,%t執行緒名,%d日期,%c類的全名,,,, -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--<pattern>%d %p (%file:%line\)- %m%n</pattern>-->
<!--格式化輸出:%d:表示日期 %thread:表示執行緒名 %-5level:級別從左顯示5個字元寬度 %msg:日誌訊息 %n:是換行符-->
<pattern>1-%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %msg%n</pattern>
<charset>GBK</charset>
</encoder>
</appender>
<!--<include resource="org/springframework/boot/logging/logback/base.xml"/>-->
<contextName>RestAPI</contextName>
<property name="LOG_PATH" value=".logs"/>
<!--設定系統日誌目錄-->
<property name="APPDIR" value="app"/>
<!--
說明:
1、日誌級別及檔案
日誌記錄採用分級記錄,級別與日誌檔名相對應,不同級別的日誌資訊記錄到不同的日誌檔案中
例如:error級別記錄到log_error_xxx.log或log_error.log(該檔案為當前記錄的日誌檔案),而log_error_xxx.log為歸檔日誌,
日誌檔案按日期記錄,同一天內,若日誌檔案大小等於或大於2M,則按0、1、2...順序分別命名
例如log-level-2013-12-21.0.log
其它級別的日誌也是如此。
2、檔案路徑
若開發、測試用,在Eclipse中執行專案,則到Eclipse的安裝路徑查詢logs資料夾,以相對路徑../logs。
若部署到Tomcat下,則在Tomcat下的logs檔案中
3、Appender
FILEERROR對應error級別,檔名以log-error-xxx.log形式命名
FILEWARN對應warn級別,檔名以log-warn-xxx.log形式命名
FILEINFO對應info級別,檔名以log-info-xxx.log形式命名
FILEDEBUG對應debug級別,檔名以log-debug-xxx.log形式命名
CONSOLE將日誌資訊輸出到控制上,為方便開發測試使用
-->
<!-- 日誌記錄器,日期滾動記錄 -->
<appender name="FILEERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在記錄的日誌檔案的路徑及檔名 -->
<file>${LOG_NAME}/${LOG_PATH}/${APPDIR}/log_error.log</file>
<!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 歸檔的日誌檔案的路徑,例如今天是2013-12-21日誌,當前寫的日誌檔案路徑為file節點指定,可以將此檔案與file指定檔案路徑設定為不同路徑,從而將當前日誌檔案或歸檔日誌檔案置不同的目錄。
而2013-12-21的日誌檔案在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
<fileNamePattern>${LOG_NAME}/${LOG_PATH}/${APPDIR}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 除按日誌記錄之外,還配置了日誌檔案不能超過2M,若超過2M,日誌檔案會以索引0開始,
命名日誌檔案,例如log-error-2013-12-21.0.log -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>2MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 追加方式記錄日誌 -->
<append>true</append>
<!-- 日誌檔案的格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>===%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!-- 此日誌檔案只記錄info級別的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 日誌記錄器,日期滾動記錄 -->
<appender name="FILEWARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在記錄的日誌檔案的路徑及檔名 -->
<file>${LOG_NAME}/${LOG_PATH}/${APPDIR}/log_warn.log</file>
<!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 歸檔的日誌檔案的路徑,例如今天是2013-12-21日誌,當前寫的日誌檔案路徑為file節點指定,可以將此檔案與file指定檔案路徑設定為不同路徑,從而將當前日誌檔案或歸檔日誌檔案置不同的目錄。
而2013-12-21的日誌檔案在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
<fileNamePattern>${LOG_NAME}/${LOG_PATH}/${APPDIR}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 除按日誌記錄之外,還配置了日誌檔案不能超過2M,若超過2M,日誌檔案會以索引0開始,
命名日誌檔案,例如log-error-2013-12-21.0.log -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>2MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 追加方式記錄日誌 -->
<append>true</append>
<!-- 日誌檔案的格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>===%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!-- 此日誌檔案只記錄info級別的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 日誌記錄器,日期滾動記錄 -->
<appender name="FILEINFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在記錄的日誌檔案的路徑及檔名 -->
<file>${LOG_NAME}/${LOG_PATH}/${APPDIR}/log_info.log</file>
<!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 歸檔的日誌檔案的路徑,例如今天是2013-12-21日誌,當前寫的日誌檔案路徑為file節點指定,可以將此檔案與file指定檔案路徑設定為不同路徑,從而將當前日誌檔案或歸檔日誌檔案置不同的目錄。
而2013-12-21的日誌檔案在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
<fileNamePattern>${LOG_NAME}/${LOG_PATH}/${APPDIR}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 除按日誌記錄之外,還配置了日誌檔案不能超過2M,若超過2M,日誌檔案會以索引0開始,
命名日誌檔案,例如log-error-2013-12-21.0.log -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>2MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 追加方式記錄日誌 -->
<append>true</append>
<!-- 日誌檔案的格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>===%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!-- 此日誌檔案只記錄info級別的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--日誌非同步到資料庫 -->
<appender name="DBAPPENDER" class="ch.qos.logback.classic.db.DBAppender">
<connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
<dataSource class="org.apache.commons.dbcp.BasicDataSource">
<driverClassName>com.mysql.jdbc.Driver</driverClassName>
<url>jdbc:mysql://localhost:3306/ussd?characterEncoding=UTF-8</url>
<username>root</username>
<password>1234</password>
<!--<sqlDialect class="ch.qos.logback.core.db.dialect.MySQLDialect" />-->
</dataSource>
</connectionSource>
<!-- 過濾error -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger Line:%-3L - %msg%n</pattern>
<!--<charset>GBK</charset>-->
</encoder>
</appender>
<logger name="com" level="INFO"/>
<logger name="org.springframework.data.mybatis" level="DEBUG"/>
<logger name="org.springframework.aop.aspectj" level="ERROR"/>
<logger name="javax.activation" level="WARN"/>
<logger name="javax.mail" level="WARN"/>
<logger name="javax.xml.bind" level="WARN"/>
<logger name="ch.qos.logback" level="INFO"/>
<logger name="com.codahale.metrics" level="WARN"/>
<logger name="com.ryantenney" level="WARN"/>
<logger name="com.sun" level="WARN"/>
<logger name="com.zaxxer" level="WARN"/>
<logger name="io.undertow" level="WARN"/>
<logger name="net.sf.ehcache" level="WARN"/>
<logger name="org.apache" level="WARN"/>
<logger name="org.apache.catalina.startup.DigesterFactory" level="OFF"/>
<logger name="org.bson" level="WARN"/>
<logger name="org.hibernate.validator" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
<logger name="org.hibernate.ejb.HibernatePersistence" level="OFF"/>
<logger name="org.springframework.web" level="INFO"/>
<logger name="org.springframework.security" level="WARN"/>
<logger name="org.springframework.cache" level="WARN"/>
<logger name="org.thymeleaf" level="WARN"/>
<logger name="org.xnio" level="WARN"/>
<logger name="springfox" level="WARN"/>
<logger name="sun.rmi" level="WARN"/>
<logger name="liquibase" level="WARN"/>
<logger name="sun.rmi.transport" level="WARN"/>
<logger name="jdbc.connection" level="ERROR"/>
<logger name="jdbc.resultset" level="ERROR"/>
<logger name="jdbc.resultsettable" level="INFO"/>
<logger name="jdbc.audit" level="ERROR"/>
<logger name="jdbc.sqltiming" level="ERROR"/>
<logger name="jdbc.sqlonly" level="INFO"/>
<!--<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">-->
<!--<resetJUL>true</resetJUL>-->
<!--</contextListener>-->
<springProfile name="production">
<root level="DEBUG">
<!--<appender-ref ref="FILEERROR"/>-->
<!--<appender-ref ref="FILEWARN"/>-->
<!--<appender-ref ref="FILEINFO"/>-->
<!--<appender-ref ref="DBAPPENDER"/>-->
<appender-ref ref="STDOUT"/>
</root>
</springProfile>
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="FILEERROR"/>
<appender-ref ref="FILEWARN"/>
<appender-ref ref="FILEINFO"/>
<appender-ref ref="DBAPPENDER"/>
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>
配置說明:
(1)本地日誌檔案路徑:<property name="LOG_NAME" value="C:/mysoftware/mylog"></property>
(2)開啟日誌:在application.properties中加入 spring.profiles.active=dev,可以再定義一個生產環境的,切換即可,主要是根據不同環境方便我們使用,配置檔案中的springProfile就是了,可以根據不同環境開啟不同的日誌記錄規則。
以上很簡單,基本只要引入就能用了,啟動專案會在控制檯列印相關日誌,C:/mysoftware/mylog下會自動生成一個日誌資料夾,可以看到配置了三個級別的日誌檔案:
4,日誌存庫,我摘出來主要看以下配置資訊
<appender name="DBAPPENDER" class="ch.qos.logback.classic.db.DBAppender">
<connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
<dataSource class="org.apache.commons.dbcp.BasicDataSource">
<driverClassName>com.mysql.jdbc.Driver</driverClassName>
<url>jdbc:mysql://localhost:3306/ussd?characterEncoding=UTF-8</url>
<username>root</username>
<password>1234</password>
<!--<sqlDialect class="ch.qos.logback.core.db.dialect.MySQLDialect" />-->
</dataSource>
</connectionSource>
<!-- 過濾error -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
同時還需要在資料庫下手動生成日誌表,執行以下SQL即可
BEGIN;
DROP TABLE IF EXISTS logging_event_property;
DROP TABLE IF EXISTS logging_event_exception;
DROP TABLE IF EXISTS logging_event;
COMMIT;
BEGIN;
CREATE TABLE logging_event
(
timestmp BIGINT NOT NULL,
formatted_message TEXT NOT NULL,
logger_name VARCHAR(254) NOT NULL,
level_string VARCHAR(254) NOT NULL,
thread_name VARCHAR(254),
reference_flag SMALLINT,
arg0 VARCHAR(254),
arg1 VARCHAR(254),
arg2 VARCHAR(254),
arg3 VARCHAR(254),
caller_filename VARCHAR(254) NOT NULL,
caller_class VARCHAR(254) NOT NULL,
caller_method VARCHAR(254) NOT NULL,
caller_line CHAR(4) NOT NULL,
event_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
);
COMMIT;
BEGIN;
CREATE TABLE logging_event_property
(
event_id BIGINT NOT NULL,
mapped_key VARCHAR(254) NOT NULL,
mapped_value TEXT,
PRIMARY KEY(event_id, mapped_key),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);
COMMIT;
BEGIN;
CREATE TABLE logging_event_exception
(
event_id BIGINT NOT NULL,
i SMALLINT NOT NULL,
trace_line VARCHAR(254) NOT NULL,
PRIMARY KEY(event_id, i),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);
COMMIT;
(1)異常處理,為什麼先說異常處理呢,因為理論上來說只要我們正常引入依賴和配置應該不會有什麼錯誤,但是我還是遇到了問題,具體報錯就不貼了,看過之後發現是找不到class的問題,然後發現配置裡的class點不進去,最終發現是引入了依賴但是IDE工具沒有引入專案,很奇怪,最後重新導了一遍專案正常了,接下來是另外一個異常,提示DataSource的配置資訊名稱有誤(以上配置是我改正之後的),其實點到org.apache.commons.dbcp.BasicDataSource原始碼裡能看到實際定義的一些配置名稱,對比網上其他版本我發現有把url寫成jdbcurl的,可能是因為版本的問題,所以大家留意一下,最好去看原始碼就不會錯了。
(2)分級記錄問題
現在我們配置的日誌記錄包括控制檯輸出、檔案記錄、資料庫表記錄,在這種情況下不對日誌進行分級分類,可能造成控制檯列印的同時檔案記錄了,同時表也儲存了,這樣非常混亂,本來是為了方便我們的日誌變得更復雜了,所以我們首先就應該想好不同等級的日誌要分開記錄,分類標準大致是:debug除錯的資訊控制檯輸出,使用者操作資訊info檔案記錄,系統異常error等使用資料庫表記錄。
具體分級方法我們要用到logback的過濾器Filter
(3)Filter的使用
Filter包含三種狀態:DENY,NEUTRAL,ACCEPT
返回DENY,日誌將立即被拋棄不再經過其他過濾器;
返回NEUTRAL,有序列表裡的下個過濾器過接著處理日誌;
返回ACCEPT,日誌會被立即處理,不再經過剩餘過濾器
這裡我簡單說明下,假設error錯誤日誌我既要記錄檔案,又要記錄到資料庫表裡,那麼在FILEERROR中我需要如下配置
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>NEUTRAL</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
onMatch配置為NEUTRAL表示符合error的日誌將在這裡處理但不銷燬,繼續由下一個appender的過濾器處理,根據我的配置先後順序:
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="FILEERROR"/>
<appender-ref ref="FILEWARN"/>
<appender-ref ref="FILEINFO"/>
<appender-ref ref="DBAPPENDER"/>
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
執行FILEERROR後下一個過濾error的是DBAPPENDER,所以最終又會傳遞到資料庫表裡過濾處理。
最終在資料庫表裡會記錄類似如下異常日誌:
最後差點忘了logger的使用,在類開頭引入
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
在方法中呼叫
//日誌級別從低到高分為TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果設定為WARN,則低於WARN的資訊都不會輸出。
logger.trace("日誌輸出 trace");
logger.debug("日誌輸出 debug");
logger.info("日誌輸出 info");
logger.warn("日誌輸出 warn");
logger.error("日誌輸出 error");
本文完 2018-8-6
有問題歡迎大家留言指正