- Session 認證和 Token 認證
- 過濾器和攔截器
- SpringBoot統一傳回和統一異常處理
- SpringBoot項目logback日志配置
程式運作出現錯誤時,第一時間想到的是甩鍋還是日志?通過檢視日志定位出問題的位置,才能更好的甩鍋,今天就來學習 springBoot 日志如何配置。
一、日志架構
Java 中的日志架構分為兩種,分别為日志抽象/門面、日志實作。
日志門面不負責日志具體實作,它隻是為所有日志架構提供一套标準、規範的API架構。其主要意義在于提供接口,具體的實作可以交由其它日志架構,例如 log4j和logback等。
當今主流的的日志門面是SLF4J,SpringBoot 中推薦使用該門面技術。
1.1、SLF4J
SLF4J官網位址:https://www.slf4j.org/
SLF4J(Simple Logging Facade For Java),即簡單日志門面,它用作各種日志架構(例如Java.util.Logging、logback、log4j)的簡單門面或抽象,允許最終使用者在部署時插入所需的日志架構。
它和JDBC 差不多,JDBC 不關心具體的資料庫實作,同樣的,SLF4J 也不關心具體日志架構實作。
application 下面的 SLF4JAPI 表示 SLF4J 的日志門面,包含以下三種情況:
- 如果隻是導入 slf4j 日志門面,沒有導入對應的日志實作架構,則日志功能預設是關閉的,不會進行日志輸出。
- 藍色圖裡 Logback、slf4j-simple、slf4j-nop 遵循 slf4j 的 API 規範,隻要導入對應的日志實作架構,來實作開發
- 中間兩個日志架構 slf4j-reload4、JUL(slf4j-jdk14) 沒有遵循 slf4j 的 API 規範,所有無法直接使用,中間需要增加一個适配層 (Adaptation layer),通過對應的擴充卡來适配具體的日志實作架構。
1.2、日志實作架構
Java 中的日志實作架構,主流的有以下幾種:
- log4j :老牌日志架構,已經多年不更新了,性能比 logback、log4j2 差。
- logback :log4j 創始人建立的另一個開源日志架構,SpringBoot 預設的日志架構。
- log4j2 :Apache 官方項目,傳聞性能優于 logback,它是 log4j 的新版本。
- JUL :(Java.Util.Logging), jdk 内置。
在項目中,一般都是日志門面+日志實作架構組合使用,這樣更靈活,适配起來更簡單。
前面提到logback作為Spring Boot預設的日志架構 ,肯定有相應的考量,我司也是使用logback 作為 Spring Boot 項目中的日志實作架構,下面我們就詳細說說 logback。
二、SpringBoot 日志架構 logback
2.1、logback 是什麼?
logback 是 log4j 團隊建立的開源日志元件。與 log4j 類似,但是比 log4j 更強大,是log4j 的改良版本。
logback 主要包含三個子產品:
- logback-core :所有 logback 子產品的基礎。
- logback-classic :是 log4j 的改良版本,完整實作了slf4j API 。
- logback-access :通路子產品和 servlet 容器內建,提供通過 http 來通路日志的功能。
2.2、logback 的日志級别有哪些?
日志級别(log level):用來控制日志資訊的輸出,從高到低共分為七個等級。
- OFF :最高等級,用于關閉所有資訊。
- FATAL :災難級的,系統級别,程式無法列印。
- ERROR :錯誤資訊
- WARN :告警資訊
- INFO :普通的列印資訊
- DEBUG :調試,對調試應用程式有幫助。
- TRACE :跟蹤
如果項目中日志級别設定為 INFO,則比它更低級别的日志資訊将看不到了,即 DEBUG 日志不會顯示。 預設情況下,Spring Boot 會用Logback 來記錄日志,并用 INFO 級别輸出到控制台。
2.3、SpringBoot 中如何使用日志?
首先建立一個 SpringBoot 項目 log ,我們看到 SpringBoot 預設已經引入 logback 依賴。
啟動項目,日志列印如下:
從圖中可以看出,輸出的日志預設元素如下:
- 時間日期:精确到毫秒。
- 日志級别:預設是 INFO 。
- 程序 Id
- 分隔符:---辨別日志開始的地方。
- 線程名稱:方括号括起來的。
- Logger 名稱:源代碼的類名。
- 日志内容
在業務中輸出日志,常見的有兩種方式。
方式一:在業務代碼裡添加如下代碼
private final Logger log = LoggerFactory.getLogger(LoginController.class);
package com.duan.controller;
import com.duan.pojo.Result;
import com.duan.pojo.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author db
* @version 1.0
* @description LoginController
* @since 2023/12/19
*/
@RestController
public class LoginController {
private final Logger log = LoggerFactory.getLogger(LoginController.class);
@PostMapping("/login")
public Result login(@RequestBody User user){
log.info("這是正常日志");
if("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())){
return Result.success("ok");
}
return Result.error();
}
}
每個類中都要添加這行代碼才能輸出日志,這樣代碼會很備援。
方式二:使用 lomback 中的 @Slf4j 注解,但是需要在 pom 中引用 lomback 依賴
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
使用時隻需要在類上标注一個 @Slf4j 注解即可
package com.duan.controller;
import com.duan.pojo.Result;
import com.duan.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author db
* @version 1.0
* @description LoginController
* @since 2023/12/19
*/
@RestController
@Slf4j
public class LoginController {
@PostMapping("/login")
public Result login(@RequestBody User user){
log.info("這是正常日志");
if("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())){
return Result.success("ok");
}
return Result.error();
}
}
2.4、如何指定具體的日志級别?
前面我們提到, SpringBoot 預設的日志級别是 INFO,根據需要我們還可以具體的日志級别,如下:
logging:
level:
root: ERROR
将所有的日志級别都改為了 ERROR,同時 SpringBoot 還支援包級别的日志調整,如下:
logging:
level:
com:
duan:
controller: ERROR
com.duan.controller 是項目包名。
2.5、日志如何輸出到指定檔案
SpringBoot 預設是把日志輸出到控制台,生成環境中是不行的,需要把日志輸出到檔案中。 其中有兩個重要配置如下:
- logging.file.path :指定日志檔案的路徑
- logging.file.name :日志的檔案名,預設為 spring.log 注意:官方文檔說這兩個屬性不能同時配置,否則不生效,是以隻需要配置一個即可。
指定日志輸出檔案存在目前路徑的 log 檔案夾下,預設生成的檔案為 spring.log
logging:
file:
path: ./logs
2.6、自定義日志配置
SpringBoot 官方優先推薦使用帶有 -spring 的檔案名稱作為項目日志配置,是以隻需要在 src/resource 檔案夾下建立 logback-spring.xml 即可,配置檔案内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- logback預設每60秒掃描該檔案一次,如果有變動則用變動後的配置檔案。 -->
<configuration scan="false">
<!-- ==============================================開發環境=========================================== -->
<springProfile name="dev">
<!-- 控制台輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級别從左顯示5個字元寬度%msg:日志消息,%n是換行符-->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志輸出級别 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</springProfile>
<!-- ==============================================生産環境=========================================== -->
<springProfile name="prod">
<!--定義日志檔案的存儲位址 勿在 LogBack 的配置中使用相對路徑-->
<property name="LOG_HOME" value="./log"/>
<!-- 控制台輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級别從左顯示5個字元寬度%msg:日志消息,%n是換行符-->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志檔案 -->
<appender name="INFO_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名稱,如果沒有File 屬性,那麼隻會使用FileNamePattern的檔案路徑規則
如果同時有<File>和<FileNamePattern>,那麼當天日志是<File>,明天會自動把今天
的日志改名為今天的日期。即,<File> 的日志都是當天的。
-->
<file>${LOG_HOME}/info.log</file>
<!--滾動政策,按照大小時間滾動 SizeAndTimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志檔案輸出的檔案名-->
<FileNamePattern>${LOG_HOME}/info.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--隻保留最近30天的日志-->
<MaxHistory>30</MaxHistory>
<!--用來指定日志檔案的上限大小,那麼到了這個值,就會删除舊的日志-->
<totalSizeCap>1GB</totalSizeCap>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<!--日志輸出編碼格式化-->
<encoder>
<charset>UTF-8</charset>
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級别從左顯示5個字元寬度%msg:日志消息,%n是換行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<!--過濾器,隻有過濾到指定級别的日志資訊才會輸出,如果level為ERROR,那麼控制台隻會輸出ERROR日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!-- 按照每天生成日志檔案 -->
<appender name="ERROR_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名稱,如果沒有File 屬性,那麼隻會使用FileNamePattern的檔案路徑規則
如果同時有<File>和<FileNamePattern>,那麼當天日志是<File>,明天會自動把今天
的日志改名為今天的日期。即,<File> 的日志都是當天的。
-->
<file>${LOG_HOME}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志檔案輸出的檔案名-->
<FileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
<!--用來指定日志檔案的上限大小,那麼到了這個值,就會删除舊的日志-->
<totalSizeCap>1GB</totalSizeCap>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級别從左顯示5個字元寬度%msg:日志消息,%n是換行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!--指定最基礎的日志輸出級别-->
<root level="INFO">
<!--appender将會添加到這個loger-->
<appender-ref ref="STDOUT"/>
<appender-ref ref="INFO_APPENDER"/>
<appender-ref ref="ERROR_APPENDER"/>
</root>
</springProfile>
</configuration>
最基本配置是一個 configuration 裡面有零個或多個 appender,零個或多個 logger 和最多一個 root 标簽組成。(logback 對大小寫敏感)
configuration 節點:根節點,屬性如下:
- scan :此屬性為 true 時,配置檔案發生改變,将會被重新加載,預設為true。
- scanPeriod :監測配置檔案是否有修改的時間間隔,機關毫秒,當 scan 為 true 時,此屬性生效。預設的時間間隔為1分鐘 。
- debug :此屬性為 true 時,列印出 logback 内部日志資訊,實時檢視 logback 運作狀态,預設 false。
root 節點:必須的節點,用來指定基礎的日志級别,隻有一個屬性。該節點可以包含零個或者多個元素,子節點是 appender-ref ,标記 appender 将會添加到這個 logger 中。
- level :預設值 DEBUG
contextName 節點:辨別一個上下文名稱,預設 default ,一般用不到。
property 節點:标記一個上下文變量,屬性有 name 和 value,定義變量之後用 ${} 擷取值。
appender 節點:<appender> 是 <configuration> 的子節點,主要用于格式化日志輸出節點,屬性有 name 和 class,class 用來指定那種輸出政策,常用的就是控制台輸出政策和檔案輸出政策。有幾個子節點比較重要。
- filter :日志輸出攔截器,沒特殊要求就使用系統自帶的,若要将日志分開,比如将 ERROR 級别的日志輸出到一個檔案中,其他級别的日志輸出到另一個檔案中,這時候就要用到 filter 。
- encoder :和 pattern 節點組合用于具體輸出日志的格式和編碼方式。
- file :用來指定日志檔案輸出位置,絕對路徑或者相對路徑。
- rollingPolicy :日志復原政策,常見的就是按照時間復原政策(TimeBasedRollingPolicy) 和按照大小時間復原政策 (SizeAndTimeBasedRollingPolicy)。
- maxHistory :可選節點,控制保留日志檔案的最大數量,超出數量就删除舊檔案。
- totalSizeCap :可選節點,指定日志檔案的上限大小。
logger 節點:可選節點,用來指定某一個包或者具體某一個類的日志列印級别。
- name :指定包名。
- level :可選,日志的級别。
- addtivity :可選,預設為 true,此 logger 的資訊向上傳遞。
springProfile :多環境輸出日志檔案,根據配置檔案激活參數 (active) 選擇性的包含和排查部配置設定置資訊。根據不同環境來定義不同的日志輸出。
logback 中一般有三種過濾器 Filter
- LevelFilter :級别過濾器,根據日志級别進行過濾,如果日志級别等于配置級别,過濾器會根據onMath 和 onMismatch 接受或者拒絕日志。有以下子節點
- level :設定過濾級别
- onMath :配置符合過濾條件的操作
- onMismath :配置不符合過濾條件的操作
<!-- 在檔案中出現級别為INFO的日志内容 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 在檔案中出現級别為INFO、ERROR的日志内容 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<level>ERROR</level>
</filter>
- ThresholdFilter :臨界值過濾器,過濾掉低于臨界值的日志,當日志級别等于或高于臨界值時,過濾器傳回 NEUTRAL ;當日志級别低于臨界值時,日志會被拒絕。
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 過濾掉 TRACE 和 DEBUG 級别的日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger{30} - %msg%n
</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
- EvaluatorFilter :求值過濾器,評估、鑒别日志是否符合指定條件。
如果不使用 SpringBoot 推薦的名字,想用自己定制的也可以,隻需要在配置檔案中配置。
logging:
config: logging-config.xml
2.7、異步日志
之前都是用同步去記錄日志,這樣代碼效率會大大降低,logback 提供異步記錄日志功能。
原理:
系統會為日志操作單獨配置設定一個線程,原來用來執行目前方法是主線程會繼續向下執行,線程1:系統業務代碼執行。線程2:列印日志
<!-- 異步輸出 -->
<appender name ="async-file-info" class= "ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.預設的,如果隊列的80%已滿,則會丢棄TRACT、DEBUG、INFO級别的日志 -->
<discardingThreshold >0</discardingThreshold>
<!-- 更改預設的隊列的深度,該值會影響性能.預設值為256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多隻能添加一個 -->
<appender-ref ref ="INFO_APPENDER"/>
</appender>
<root level="INFO">
<!-- 引入appender -->
<appender-ref ref="async-file-info"/>
</root>
2.8、如何定制日志格式?
上面我們已經看到預設的日志格式,實際項目代碼中的日志格式不會是 logback 預設的格式,要根據項目業務要求,進行修改,下面我們來看如何定制日志格式。
# 常見的日志格式
2023-12-21 10:39:44.631----[應用名|主機ip|用戶端ip|使用者uuid|traceid]----{}
解釋
2023-12-21 10:39:44.631:時間,格式為yyyy-MM-dd HH:mm:ss.SSS
應用名稱:辨別項目應用名稱,一般就是項目名
主機ip:本機IP
用戶端ip:請求IP
使用者uuid:根據使用者uuid可以知道是誰調用的
traceid:追溯目前鍊路記錄檔的一種有效手段
建立自定義格式轉換符有兩步:
- 首先必須繼承 ClassicConverter 類,ClassicConverter 對象負責從 ILoggingEvent提取資訊,并産生一個字元串。
- 然後要讓 logback 知道新的 Converter,方法是在配置檔案裡聲明新的轉換符。
在 config 包中建立 HostIpConfig 類、RequestIpConfig 類、UUIDConfig 類,代碼如下:
HostIpConfig.java
package com.duan.config;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.duan.utils.LocalIP;
/**
* @author db
* @version 1.0
* @description HostIpConfig 獲得主機IP位址
* @since 2024/1/9
*/
public class HostIpConfig extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
String hostIP = LocalIP.getIpAddress();
return hostIP;
}
}
RequestIpConfig.java
package com.duan.config;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.duan.utils.IpUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @author db
* @version 1.0
* @description RequestIpConfig 獲得請求IP
* @since 2024/1/9
*/
public class RequestIpConfig extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return "127.0.0.1";
}
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
String requestIP = IpUtils.getIpAddr(request);
return requestIP;
}
}
UUIDConfig.java
package com.duan.config;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
/**
* @author db
* @version 1.0
* @description UUIDConfig
* @since 2024/1/9
*/
public class UUIDConfig extends ClassicConverter {
@Override
public String convert(ILoggingEvent iLoggingEvent) {
// 這裡作為示範,直接生成的一個String,實際項目中可以Servlet獲得使用者資訊
return "12344556";
}
}
工具類代碼如下:
package com.duan.utils;
import com.google.common.base.Strings;
import javax.servlet.http.HttpServletRequest;
// 請求IP
public class IpUtils {
private IpUtils(){
}
public static String getIpAddr(HttpServletRequest request) {
String xIp = request.getHeader("X-Real-IP");
String xFor = request.getHeader("X-Forwarded-For");
if (!Strings.isNullOrEmpty(xFor) && !"unKnown".equalsIgnoreCase(xFor)) {
//多次反向代理後會有多個ip值,第一個ip才是真實ip
int index = xFor.indexOf(",");
if (index != -1) {
return xFor.substring(0, index);
} else {
return xFor;
}
}
xFor = xIp;
if (!Strings.isNullOrEmpty(xFor) && !"unKnown".equalsIgnoreCase(xFor)) {
return xFor;
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("Proxy-Client-IP");
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("WL-Proxy-Client-IP");
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("HTTP_CLIENT_IP");
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (Strings.nullToEmpty(xFor).trim().isEmpty() || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(xFor) ? "127.0.0.1" : xFor;
}
}
package com.duan.utils;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
// 獲得主機IP
public class LocalIP {
public static InetAddress getLocalHostExactAddress() {
try {
InetAddress candidateAddress = null;
// 從網卡中擷取IP
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface iface = networkInterfaces.nextElement();
// 該網卡接口下的ip會有多個,也需要一個個的周遊,找到自己所需要的
for (Enumeration<InetAddress> inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) {
InetAddress inetAddr = inetAddrs.nextElement();
// 排除loopback回環類型位址(不管是IPv4還是IPv6 隻要是回環位址都會傳回true)
if (!inetAddr.isLoopbackAddress()) {
if (inetAddr.isSiteLocalAddress()) {
// 如果是site-local位址,就是它了 就是我們要找的
// ~~~~~~~~~~~~~絕大部分情況下都會在此處傳回你的ip位址值~~~~~~~~~~~~~
return inetAddr;
}
// 若不是site-local位址 那就記錄下該位址當作候選
if (candidateAddress == null) {
candidateAddress = inetAddr;
}
}
}
}
// 如果出去loopback回環地之外無其它位址了,那就回退到原始方案吧
return candidateAddress == null ? InetAddress.getLocalHost() : candidateAddress;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String getIpAddress() {
try {
//從網卡中擷取IP
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
InetAddress ip;
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
//用于排除回送接口,非虛拟網卡,未在使用中的網絡接口
if (!netInterface.isLoopback() && !netInterface.isVirtual() && netInterface.isUp()) {
//傳回和網絡接口綁定的所有IP位址
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
ip = addresses.nextElement();
if (ip instanceof Inet4Address) {
return ip.getHostAddress();
}
}
}
}
} catch (Exception e) {
System.err.println("IP位址擷取失敗" + e.toString());
}
return "";
}
}
traceId :用于辨別摸一次具體的請求 Id,通過 traceId 可以把一次使用者請求在系統中的調用路徑串聯起來。
logback 自定義日志格式 traceId 使用 MDC 進行實作。
MDC(Mapped Diagnostic Context) 映射診斷環境,是 log4j 和 logback 提供的一種友善線上多線程條件下記錄日志的功能,可以看成是一個與目前線程綁定的 ThreadLocal。
public class MDC {
// 添加 key-value
public static void put(String key, String val) {...}
// 根據 key 擷取 value
public static String get(String key) {...}
// 根據 key 删除映射
public static void remove(String key) {...}
// 清空
public static void clear() {...}
}
用攔截器或者過濾器實作 MDC,在這裡使用攔截器實作,首先在 interceptor 包中建立 TraceInterceptor 類并實作 HandlerInterceptor 方法。
package com.duan.interceptor;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @author db
* @version 1.0
* @description TraceInterceptor
* @since 2024/1/9
*/
@Component
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
MDC.put("traceid", UUID.randomUUID().toString());
return true;
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,Object handler,Exception e) throws Exception {
MDC.remove("traceid");
}
}
在 config 包中建立 WebConfig 類并繼承 WebMvcConfigurerAdapter,把 TraceInterceptor 攔截器注入。
package com.duan.config;
import com.duan.interceptor.TraceInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* @author db
* @version 1.0
* @description WebConfig
* @since 2024/1/9
*/
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
private TraceInterceptor traceInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(traceInterceptor);
}
}
第二步,在 logback-spring.xml 配置檔案中進行配置,配置檔案如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- logback預設每60秒掃描該檔案一次,如果有變動則用變動後的配置檔案。 -->
<configuration scan="false">
<!-- ==============================================開發環境=========================================== -->
<springProfile name="dev">
<conversionRule conversionWord="hostIp" converterClass="com.duan.config.HostIpConfig"/>
<conversionRule conversionWord="requestIp" converterClass="com.duan.config.RequestIpConfig"/>
<conversionRule conversionWord="uuid" converterClass="com.duan.config.UUIDConfig"/>
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss.SSS})----[%magenta(cxykk)|%magenta(%hostIp)|%magenta(%requestIp)|%magenta(%uuid)|%magenta(%X{traceid})]----%cyan(%msg%n)"/>
<!-- 控制台輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化輸出-->
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 日志輸出級别 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</springProfile>
<!-- ==============================================生産環境=========================================== -->
<springProfile name="prod">
<!--定義日志檔案的存儲位址 勿在 LogBack 的配置中使用相對路徑-->
<property name="LOG_HOME" value="./log"/>
<!-- 控制台輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級别從左顯示5個字元寬度%msg:日志消息,%n是換行符-->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志檔案 -->
<appender name="INFO_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名稱,如果沒有File 屬性,那麼隻會使用FileNamePattern的檔案路徑規則
如果同時有<File>和<FileNamePattern>,那麼當天日志是<File>,明天會自動把今天
的日志改名為今天的日期。即,<File> 的日志都是當天的。
-->
<file>${LOG_HOME}/info.log</file>
<!--滾動政策,按照大小時間滾動 SizeAndTimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志檔案輸出的檔案名-->
<FileNamePattern>${LOG_HOME}/info.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--隻保留最近30天的日志-->
<MaxHistory>30</MaxHistory>
<!--用來指定日志檔案的上限大小,那麼到了這個值,就會删除舊的日志-->
<totalSizeCap>1GB</totalSizeCap>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<!--日志輸出編碼格式化-->
<encoder>
<charset>UTF-8</charset>
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級别從左顯示5個字元寬度%msg:日志消息,%n是換行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<!--過濾器,隻有過濾到指定級别的日志資訊才會輸出,如果level為ERROR,那麼控制台隻會輸出ERROR日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!-- 按照每天生成日志檔案 -->
<appender name="ERROR_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志檔案輸出的檔案名-->
<FileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
<!--用來指定日志檔案的上限大小,那麼到了這個值,就會删除舊的日志-->
<totalSizeCap>1GB</totalSizeCap>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級别從左顯示5個字元寬度%msg:日志消息,%n是換行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!--指定最基礎的日志輸出級别-->
<root level="INFO">
<!--appender将會添加到這個loger-->
<appender-ref ref="STDOUT"/>
<appender-ref ref="INFO_APPENDER"/>
<appender-ref ref="ERROR_APPENDER"/>
</root>
</springProfile>
</configuration>
啟動項目,通過 postman 調用 login 接口,檢視結果輸出日志格式。
代碼位址:https://gitee.com/duan138/practice-code/tree/dev/logback
三、總結
SpringBoot 中日志講解就到這裡,上面提到的知識點都是項目中常用的,比如日志怎麼配置、根據日志級别把日志輸出到不同的檔案裡、或者将 INFO 和 ERROR 級别的日志輸出到同一個檔案中、或者定制日志格式等等。
作者:程式員康康
連結:https://juejin.cn/post/7330104253824729097