1.背景
為了便於分析和記錄系統的運行,一個系統輸出其運行的關鍵日志是非常必要的
比如輸出:請求參數、請求url、請求方式、執行的sql、重要操作的日志、響應結果等
而這些日志中,大部分不需要我們手動對每個接口去輸出,主需要統一配置就可以了
2.實際生產運用步驟
步驟一:在resources下添加logback文件,這個問價基本上每個項目都是一樣的,拷貝過來就可以使用

<?xml version="1.0" encoding="UTF-8"?> <!-- scan:當此屬性設置為true時,配置文件如果發生改變,將會被重新加載,默認值為true。 scanPeriod:設置監測配置文件是否有修改的時間間隔,如果沒有給出時間單位,默認單位是毫秒;當scan為true時,此屬性生效。默認的時間間隔為1分鍾。 debug:當此屬性設置為true時,將打印出logback內部日志信息,實時查看logback運行狀態。默認值為false。 --> <configuration scan="false" scanPeriod="60 seconds" debug="false"> <!-- property:定義變量,相當於java中定義一個 String name="張無忌"; 變量名LOG_HOME,用於日志根目錄文件夾定義,可以任意命名 變量名appName,用於日志文件名定義,可以任意命名 變量名logLevel,用於日志輸出級別定義,可以為 debug,info,error --> <property name="LOG_HOME" value="logs"/> <property name="appName" value="log"></property> <property name="logLevel" value="info"></property> <!-- 獲取當前日期,一般生產上按照每天一個文件夾,文件夾的名稱就是年月日 <timestamp key="dateTime" datePattern="yyyy-MM-dd"/> --> <!-- 1.ch.qos.logback.core.ConsoleAppender 表示控制台輸出 2.name 可以任意取名字 3.每個appender為一個日志類型 --> <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender"> <!-- 日志輸出格式: %d表示日期時間, %thread表示線程名, %-5level:級別從左顯示5個字符寬度 %logger{50} 表示logger名字最長50個字符,否則按照句點分割。 %msg:日志消息, %n是換行符 一般不輸出年份 案例一:<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==> [%thread] ==> %-5level %logger{50} - %msg%n</pattern> --> <encoder> <pattern>%d{MM-dd HH:mm:ss.SSS} [%thread] %level %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 滾動記錄文件,先將日志記錄到指定文件,當符合某個條件時,將日志記錄到其他文件 --> <appender name="all" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--append:如果是 true,日志被追加到文件結尾,如果是 false,清空現存文件,默認是true。--> <append>true</append> <!-- 日志輸出等級 如果需要輸出mybatis執行的sql日志,需要使用debug --> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>debug</level> </filter> <!-- 指定日志文件的名稱與輸出位置 --> <file>${LOG_HOME}/${appName}-all.log</file> <!-- 歸檔日志處理規則 當發生滾動時,決定 RollingFileAppender 的行為,涉及文件移動和重命名 TimeBasedRollingPolicy: 最常用的滾動策略,它根據時間來制定滾動策略,既負責滾動也負責出發滾動。 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 滾動時產生的文件的存放位置及文件名稱 %d{yyyy-MM-dd}:按天進行日志滾動 %i:當文件大小超過maxFileSize時,按照i進行文件滾動 .zip:表示生成的歸檔文件進行壓縮 --> <fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/${appName}-all.%d.%i.log.zip</fileNamePattern> <!-- 可選節點,控制保留的歸檔文件的最大數量,超出數量就刪除舊文件。假設設置每天滾動, 且maxHistory是30,則只保存最近30天的文件,刪除之前的舊文件。注意,刪除舊文件是, 那些為了歸檔而創建的目錄也會被刪除。 實際生中一般日志保留30天,可以根據特殊業務設置保留時間 --> <MaxHistory>30</MaxHistory> <!-- 當日志文件超過maxFileSize指定的大小時,根據上面提到的%i進行日志文件滾動 注意此處配置SizeBasedTriggeringPolicy是無法實現按文件大小進行滾動的,必須配置timeBasedFileNamingAndTriggeringPolicy 一般10M一個文件 這里為了測試滾動產生日志文件,設置為10KB --> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>10KB</maxFileSize> <!--<maxFileSize>10MB</maxFileSize>--> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <!-- 日志輸出格式: %d表示日期時間, %thread表示線程名, %-5level:級別從左顯示5個字符寬度 %logger{50} 表示logger名字最長50個字符,否則按照句點分割。 %msg:日志消息, %line: 行號顯示,建議刪除,這個有很多的性能損耗,報錯的時候有堆棧信息 %n是換行符 案例: <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern> --> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{MM-dd HH:mm:ss.SSS} [%thread] %level - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--輸出到error--> <appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender"> <append>true</append> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>error</level> </filter> <file>${LOG_HOME}/${appName}-error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/${appName}-error.%d.%i.log.zip</fileNamePattern> <MaxHistory>30</MaxHistory> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>10MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{MM-dd HH:mm:ss.SSS} [%thread] %level - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- logger主要用於存放日志對象,也可以定義日志類型、級別 name:表示匹配的logger類型前綴,也就是包的前半部分 level:要記錄的日志級別,包括 TRACE < DEBUG < INFO < WARN < ERROR additivity:作用在於children-logger是否使用 rootLogger配置的appender進行輸出, false:表示只用當前logger的appender-ref,true: 表示當前logger的appender-ref和rootLogger的appender-ref都有效 root與logger是父子關系,沒有特別定義則默認為root,任何一個類只會和一個logger對應, 要么是定義的logger,要么是root,判斷的關鍵在於找到這個logger,然后判斷這個logger的appender和level。 --> <root level="${logLevel}"> <appender-ref ref="stdout"/> <appender-ref ref="all"/> <appender-ref ref="error"/> </root> </configuration>
步驟二:添加一個過濾器,在過濾器中統一輸出日志

package com.ldp.user.common.interceptor; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.servlet.ServletUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Copyright (C) XXXXX科技有限公司 * @Author: LI DONG PING * @Date: 2019/4/22 17:47 * @Description: */ @Component @Slf4j public class HttpServletRequestWrapperFilter implements Filter { private final String CHARSET = "UTF-8"; @Override public void doFilter(ServletRequest request, final ServletResponse response, FilterChain chain) throws IOException, ServletException { // 自定義線程名稱 Thread.currentThread().setName(RandomUtil.randomString(10)); MultiReadHttpServletRequest requestWrapper = new MultiReadHttpServletRequest(request); ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) response, CHARSET); String contentType = requestWrapper.getHeader(HttpHeaders.CONTENT_TYPE); String url = requestWrapper.getRequestURL().toString(); if (url.contains("actuator") || url.contains("swagger") || url.contains("csrf")) { chain.doFilter(requestWrapper, responseWrapper); byte[] bytes = responseWrapper.getBytes(); response.getOutputStream().write(bytes); return; } if (!url.contains("actuator")) { log.info("ContentType: {}", contentType); log.info("請求地址: {}", url); log.info("請求方法: {}", requestWrapper.getMethod().toUpperCase()); if (isTextContentType(contentType)) { String params = requestWrapper.getQueryString(); if (!StrUtil.isEmpty(params)) { log.info("請求參數: {}", params); } String body = ServletUtil.getBody(requestWrapper); if (!StrUtil.isEmpty(body)) { log.info("請求參數[body]: {}", body); } } long start = System.currentTimeMillis(); chain.doFilter(requestWrapper, responseWrapper); long end = System.currentTimeMillis(); byte[] bytes = responseWrapper.getBytes(); if (isTextContentType(response.getContentType())) { try { log.info("響應結果: {}", new String(bytes, CHARSET)); } catch (Exception ex) { log.error("響應異常", ex); } } log.info("HTTP狀態: {}", responseWrapper.getStatus()); long diff = end - start; if (diff < 1000) { log.info("處理時長: {}毫秒", diff); } else { log.error("處理時長: {}毫秒,url:{}", diff, url); } response.getOutputStream().write(bytes); } else { chain.doFilter(requestWrapper, responseWrapper); byte[] bytes = responseWrapper.getBytes(); response.getOutputStream().write(bytes); } } private boolean isTextContentType(String contentType) { return contentType != null && (contentType.contains(MediaType.TEXT_PLAIN_VALUE) || contentType.contains(MediaType.TEXT_XML_VALUE) || contentType.contains(MediaType.TEXT_HTML_VALUE) || contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE) || contentType.contains(MediaType.APPLICATION_JSON_VALUE) || contentType.contains(MediaType.APPLICATION_XML_VALUE)); } }
步驟三:引入日志輸出對象
// 方式一:lombok日志輸出對象引入,變量名為log (推薦使用) @Slf4j // 方式二:日志輸出對象引入 變量名為logger private final static Logger logger = LoggerFactory.getLogger(UserOrderController.class);
步驟四:使用
/** * 測試統一日志輸出 * * @param message * @return */ @GetMapping("/log") public BaseResponse log(String message) { // 注意這個System.out.println的輸出,只會輸出到控制台,不能輸出到日志文件, // 而且效率很低,只用於平時開發,實際生產幾乎不可能使用 System.out.println("message=" + message); logger.debug("debug日志輸出,message={}", message); // 這兩種寫法一樣 logger.debug("debug日志輸出,message=" + message); logger.info("info日志輸出,message={}", message); logger.error("error 日志輸出,message={}", message); // 模擬輸出錯誤堆棧信息 try { if ("abc".equals(message)) { Integer value = Integer.valueOf(message); logger.info("value=" + value); } } catch (Exception e) { logger.error("數字轉換異常:", e.getMessage()); logger.error("數字轉換異常:", e); } // 不處理的異常 Integer value2 = Integer.valueOf(message); logger.info("value2=" + value2); return ResponseBuilder.success("統一參數檢查....."); }
步驟五:模擬請求測試

/** * 統一日志輸出測試 */ @Test void logTest() { String url = urlLocal + "/userOrder/log"; System.out.println("請求地址:" + url); HttpRequest request = HttpUtil.createRequest(Method.GET, url); Map<String, Object> map = new TreeMap<>(); // 業務參數 map.put("message", "1000"); // 公用參數 map.put("appid", "1001"); map.put("sequenceId", "seq" + System.currentTimeMillis()); map.put("timeStamp", System.currentTimeMillis()); map.put("sign", signApi(map, "123456")); request.form(map); System.out.println("請求參數:" + map); request.header("Authorization", token); request.setConnectionTimeout(60 * 1000); String response = request.execute().body(); System.out.println("請求結果:" + response); }
日志輸出結果:
01-02 08:59:51.760 [rptudgh22m] INFO com.ldp.user.controller.UserOrderController - info日志輸出,message=1000
01-02 08:59:51.760 [rptudgh22m] ERROR com.ldp.user.controller.UserOrderController - error 日志輸出,message=1000
01-02 08:59:51.760 [rptudgh22m] INFO com.ldp.user.controller.UserOrderController - value2=1000
01-02 08:59:51.796 [rptudgh22m] INFO c.l.u.c.i.HttpServletRequestWrapperFilter - 響應結果: {"message":"success","code":100,"data":"統一參數檢查....."}
01-02 08:59:51.797 [rptudgh22m] INFO c.l.u.c.i.HttpServletRequestWrapperFilter - HTTP狀態: 200
01-02 08:59:51.798 [rptudgh22m] INFO c.l.u.c.i.HttpServletRequestWrapperFilter - 處理時長: 275毫秒
生成的日志文件夾
3.mybatis中sql日志輸出
步驟一:配置
步驟二:sql輸出
/** * 測試統一sql日志輸出 * * @param message * @return */ @GetMapping("/logSql") public BaseResponse logSql(String message) { logger.info("message=" + message); List<UserOrder> list = service.list(null); return ResponseBuilder.success(list); }
步驟三:測試

/** * 統一日志輸出測試 */ @Test void logTest() { for (int i = 100; i < 200; i++) { String url = urlLocal + "/userOrder/logSql"; System.out.println("請求地址:" + url); HttpRequest request = HttpUtil.createRequest(Method.GET, url); Map<String, Object> map = new TreeMap<>(); // 業務參數 map.put("message", i); // 公用參數 map.put("appid", "1001"); map.put("sequenceId", "seq" + System.currentTimeMillis()); map.put("timeStamp", System.currentTimeMillis()); map.put("sign", signApi(map, "123456")); request.form(map); System.out.println("請求參數:" + map); request.header("Authorization", token); request.setConnectionTimeout(60 * 1000); String response = request.execute().body(); System.out.println("請求結果:" + response); } }
測試結果
01-02 09:47:34.119 [l280fegb2i] INFO com.ldp.user.controller.UserOrderController - message=199 01-02 09:47:34.119 [l280fegb2i] DEBUG com.ldp.user.mapper.UserOrderMapper.selectList - ==> Preparing: SELECT id,`order_no`,`buy_account`,`status`,`product_name`,`price`,`pay_status`,`update_time`,`create_time`,`version`,`deleted` FROM `user_order` 01-02 09:47:34.119 [l280fegb2i] DEBUG com.ldp.user.mapper.UserOrderMapper.selectList - ==> Parameters: 01-02 09:47:34.134 [l280fegb2i] DEBUG com.ldp.user.mapper.UserOrderMapper.selectList - <== Total: 5 01-02 09:47:34.134 [l280fegb2i] INFO c.l.u.c.i.HttpServletRequestWrapperFilter - 響應結果: {"message":"success","code":100,"data":[{"id":1,"orderNo":"LDP001","buyAccount":"string","status":0,"productName":"string","price":0.0,"payStatus":0,"updateTime":"2020-12-16T10:28:25","createTime":"2020-12-16T10:28:25","version":0,"deleted":0},{"id":2,"orderNo":"LDP001","buyAccount":"string","status":0,"productName":"string","price":0.0,"payStatus":0,"updateTime":"2020-12-16T10:28:25","createTime":"2020-12-16T10:28:25","version":0,"deleted":0},{"id":3,"orderNo":"LDP001","buyAccount":"string","status":0,"productName":"string","price":0.0,"payStatus":0,"updateTime":"2020-12-16T10:28:25","createTime":"2020-12-16T10:28:25","version":0,"deleted":0},{"id":4,"orderNo":"LDP003","buyAccount":"wx001","status":null,"productName":"蘋果手機","price":6990.0,"payStatus":null,"updateTime":null,"createTime":null,"version":0,"deleted":0},{"id":5,"orderNo":"NO003","buyAccount":"lidongping","status":null,"productName":"iPhone12","price":8000.0,"payStatus":null,"updateTime":null,"createTime":null,"version":0,"deleted":0}]} 01-02 09:47:34.134 [l280fegb2i] INFO c.l.u.c.i.HttpServletRequestWrapperFilter - HTTP狀態: 200 01-02 09:47:34.134 [l280fegb2i] INFO c.l.u.c.i.HttpServletRequestWrapperFilter - 處理時長: 15毫秒
文字描述內容有限,如果還是不明白可以直接問我,或者在看視頻講解與演示