Spring Boot與Logback的運用(自定義異常+AOP)
在開發以及除錯過程中,程式設計師對日誌的需求是非常大的,出了什麼問題,都要通過日誌去進行排查,但是如果日誌不清或者雜亂無章,則不利於維護
這邊就比較詳細的列舉幾種型別的日誌,供大家參考
首先明白logback日誌是Spring Boot自帶的,不需要引入額外的包
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-access</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback.version}</version> </dependency>
點進pom裡的核心依賴,就能看見上面幾個,是由Spring Boot自動依賴配置好的,我們只要直接使用就好了
比較簡單的是直接在application的配置檔案裡 寫引數配置就行了,他提供了日誌級別,日誌輸出路徑等,也能滿足基本的日誌輸出
我們這通過xml檔案進行配置 logback-spring.xml
這樣就能直接引用到xml了,但是為什麼能引用到了
就是在logback裡有個預設的機制,內部會有幾種標準的檔案格式,在LogbackLoggingSystem裡標註了
@Override protected String[] getStandardConfigLocations() {return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" }; }
所以最為標準的為這裡面的四種檔案格式,但是如果專案中沒有,他還提供了擴充套件檔案格式 就是在後面拼上-spring,例如logback.xml 擴充套件為logback-spring.xml
ok
下面看下xml裡面的內容:
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!--定義日誌檔案的儲存地址 可以在LogBack 的配置中使用相對路徑--> <property name="LOG_HOME" value="logs" /> <!-- 彩色日誌 --> <!-- 彩色日誌依賴的渲染類 --> <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" /> <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /> <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" /> <!-- 彩色日誌格式 --> <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" /> <!-- Console 輸出設定 --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> <charset>utf8</charset> </encoder> </appender> <!-- 按照每天生成日誌檔案 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--日誌檔案輸出的檔名--> <FileNamePattern>${LOG_HOME}/category-server-log.%d{yyyy-MM-dd}.log</FileNamePattern> <!--日誌檔案保留天數--> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> <!--日誌檔案最大的大小 <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy>--> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>WARN</level> <onMatch>DENY</onMatch> <onMismatch>NEUTRAL</onMismatch> </filter> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>DENY</onMatch> <onMismatch>NEUTRAL</onMismatch> </filter> </appender> <!-- 出錯日誌 appender --> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 按天回滾 daily --> <!-- log.dir 在maven profile裡配置 --> <FileNamePattern>${LOG_HOME}/category-server-error-log.%d{yyyy-MM-dd}.log</FileNamePattern> <!-- 日誌最大的歷史 60天 --> <maxHistory>60</maxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>WARN</level> </filter> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!-- 自己列印的日誌檔案,用於記錄重要日誌資訊 --> <appender name="MY_INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!--日誌檔案輸出的檔名--> <FileNamePattern>${LOG_HOME}/category-server-myinfo-log.%d{yyyy-MM-dd}.%i.log</FileNamePattern> <!--日誌檔案保留天數--> <MaxHistory>15</MaxHistory> <!--日誌檔案最大的大小--> <MaxFileSize>10MB</MaxFileSize> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>DEBUG</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <logger name="my_info" additivity="true"> <appender-ref ref="MY_INFO_FILE"/> </logger> <!--myibatis log configure--> <logger name="com.example.demo" level="TRACE"/> <logger name="java.sql.Connection" level="DEBUG"/> <logger name="java.sql.Statement" level="DEBUG"/> <logger name="java.sql.PreparedStatement" level="DEBUG"/> <!-- 日誌輸出級別 --> <root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="FILE" /> <appender-ref ref="ERROR_FILE" /> </root> </configuration>
這裡一共有四塊內容,第一是console的日誌輸出,第二是系統執行日誌,第三是警告以上的日誌輸出(基本上是程式出錯日誌),第四種是自定義日誌
每一塊日誌由一個appender標籤引入
CONSOLE是控制檯日誌輸出,只要規定個格式就行了
FILE是系統執行日誌,系統的所有執行資訊都會保留,正常我們會把這部分資訊儲存在硬碟日誌檔案中,按天按檔案大小儲存,因為這個內容實在是比較多
ERROR_FILE是WARN級別以上的日誌,這塊是開發人員和運維人員最多關注的,因為基本上所有的bug都會在這個裡面體現
MY_INFO_FILE是自定義日誌,想定義自己的日誌檔案,記錄一些重要的資訊
這裡的日誌都是以檔案的形式儲存在本地,當然像WARN級別以上日誌可以非同步儲存到資料庫
日誌檔案定義好後,接下來就要開始定義業務邏輯了
在針對一些異常日誌,我們想盡可能完整準確的丟擲異常,一眼就能知道是什麼問題,這裡我們就需要自定義異常,最多的就是像空指標,陣列越界等常見異常
定義基礎異常類BaseException繼承他的父類RuntimeException
public class BaseException extends RuntimeException { private static final long serialVersionUID = 1L; public BaseException() { super(); // TODO Auto-generated constructor stub } public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); // TODO Auto-generated constructor stub } public BaseException(String message, Throwable cause) { super(message, cause); // TODO Auto-generated constructor stub } public BaseException(String message) { super(message); // TODO Auto-generated constructor stub } public BaseException(Throwable cause) { super(cause); // TODO Auto-generated constructor stub } }
然後全域性異常處理類:GlobalExceptionHandler
@CrossOrigin @RestControllerAdvice public class GlobalExceptionHandler{ private static Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class); private static final String APPLICATION_JSON = "application/json"; private static final String UTF_8 = "UTF-8"; /** * BaseException 處理類 * @Title: HandleBaseException * @Description: TODO * @param @param e * @param @return * @return ResponseMsg * @throws */ @ExceptionHandler(BaseException.class) @ResponseBody public ResponseMsg HandleBaseException(RuntimeException e){ //只能輸出捕獲到的異常,未捕獲到的異常不輸出到日誌,或者通過aop攔截器攔截所有方法 LOGGER.error(getExceptionDetail(e)); //返回失敗資訊 Route route = new Route(); ResponseMsg responseMsg = new ResponseMsg(route,ReturnMsgEnum.INTERNAL_ERROR.getCode(), ReturnMsgEnum.INTERNAL_ERROR.getMsg(), ""); return responseMsg; } @ExceptionHandler(GlobalException.class) @ResponseBody public ResponseMsg HandleGlobalException(Exception e){ //只能輸出捕獲到的異常,未捕獲到的異常不輸出到日誌,或者通過aop攔截器攔截所有方法 LOGGER.error(getExceptionDetail(e)); //返回失敗資訊 Route route = new Route(); ResponseMsg responseMsg = new ResponseMsg(route,ReturnMsgEnum.INTERNAL_ERROR.getCode(), ReturnMsgEnum.INTERNAL_ERROR.getMsg(), "系統未捕獲該異常"); return responseMsg; } public String getExceptionDetail(Exception e) { StringBuffer stringBuffer = new StringBuffer(e.toString() + "\n"); StackTraceElement[] messages = e.getStackTrace(); int length = messages.length; for (int i = 0; i < length; i++) { stringBuffer.append("\t"+messages[i].toString()+"\n"); } return stringBuffer.toString(); } }
@RestControllerAdvice:表明他是一個Controller 並且是異常攔截的統一處理類定義針對自定義異常的處理方法:用@ExceptionHandler(BaseException.class)註解標註BaseException就是剛才的自定義異常之後所有丟擲的BaseException都會由他處理自定義異常我們都能輕鬆捕獲到了,並且輸出到日誌裡瞭如果有些異常我們沒有捕獲到,我們就可以定義一個切面,讓所有方法都經過這個切面處理
/** * 處理未捕獲到的異常 * @ClassName: SpringAOP * @author Mr.Chengjq * @date 2018年10月17日 * @Description: TODO */ @Aspect @Configuration public class SpringAOP { private static final Logger logger = LoggerFactory.getLogger(SpringAOP.class); /** * 定義切點Pointcut * 第一個*號:表示返回型別, *號表示所有的型別 * 第二個*號:表示類名,*號表示所有的類 * 第三個*號:表示方法名,*號表示所有的方法 * 後面括弧裡面表示方法的引數,兩個句點表示任何引數 */ @Pointcut("execution(* com.example.demo..*.*(..))") public void executionService() { } /** * 方法呼叫之前呼叫 * @param joinPoint */ @Before(value = "executionService()") public void doBefore(JoinPoint joinPoint){ //新增日誌列印 String requestId = String.valueOf(UUID.randomUUID()); MDC.put("requestId",requestId); logger.info("=====>@Before:請求引數為:{}",Arrays.toString(joinPoint.getArgs())); } /** * 方法之後呼叫 * @param joinPoint * @param returnValue 方法返回值 */ @AfterReturning(pointcut = "executionService()",returning="returnValue") public void doAfterReturning(JoinPoint joinPoint,Object returnValue){ logger.info("=====>@AfterReturning:響應引數為:{}",returnValue); // 處理完請求,返回內容 MDC.clear(); } /** * 統計方法執行耗時Around環繞通知 * @param joinPoint * @return */ @Around("executionService()") public Object timeAround(ProceedingJoinPoint joinPoint) throws Throwable{ //獲取開始執行的時間 long startTime = System.currentTimeMillis(); // 定義返回物件、得到方法需要的引數 Object obj = null; //Object[] args = joinPoint.getArgs(); try { obj = joinPoint.proceed(); } catch (Throwable e) { // TODO: handle exception logger.error(getExceptionDetail(e)); throw new GlobalException(); } // 獲取執行結束的時間 long endTime = System.currentTimeMillis(); //MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //String methodName = signature.getDeclaringTypeName() + "." + signature.getName(); // 列印耗時的資訊 logger.info("=====>處理本次請求共耗時:{} ms",endTime-startTime); return obj; } public String getExceptionDetail(Throwable e) { StringBuffer stringBuffer = new StringBuffer(e.toString() + "\n"); StackTraceElement[] messages = e.getStackTrace(); int length = messages.length; for (int i = 0; i < length; i++) { stringBuffer.append("\t"+messages[i].toString()+"\n"); } return stringBuffer.toString(); } }
這個切面裡未捕獲到的異常也全部做特定處理