SpringBoot日志打印實(shí)踐過(guò)程
背景
在項(xiàng)目當(dāng)中,我們經(jīng)常需要打印一些日志埋點(diǎn)信息,這些日志埋點(diǎn)信息,在后續(xù)軟件的運(yùn)維、穩(wěn)定性建設(shè)中發(fā)揮了巨大的作用:
- 問(wèn)題追蹤:通過(guò)埋點(diǎn)日志中的關(guān)鍵信息,幫助定位系統(tǒng)異常原因
- 系統(tǒng)監(jiān)控:通過(guò)日志,監(jiān)控系統(tǒng)的運(yùn)行情況,包括性能指標(biāo)、訪問(wèn)頻率、錯(cuò)誤等
- 數(shù)據(jù)分析:分析用戶行為、系統(tǒng)性能和業(yè)務(wù)趨勢(shì)等
- 調(diào)試:通過(guò)查看日志,幫助開(kāi)發(fā)人員了解程序在執(zhí)行過(guò)程中的狀態(tài)和行為
SpringBoot整合Logback實(shí)現(xiàn)日志打印
SpringBoot默認(rèn)使用Slf4j作為日志門(mén)面,并集成Logback作為日志實(shí)現(xiàn)。
要在springboot中實(shí)現(xiàn)日志打印,只需要引入下列依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>然后在配置文件中,配置對(duì)應(yīng)的日志級(jí)別:
logging:
level:
root: INFO對(duì)某些特定的包,需要指定日志級(jí)別,則配置如下:
logging:
level:
com.example.demo: DEBUG最后,我們創(chuàng)建logback-spring.xml,來(lái)自定義日志的配置信息,包括日志輸出文件、日志格式等
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATH" value="logs"/>
<property name="LOG_FILE" value="${LOG_PATH}/spring-boot-logger.log"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>common.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/spring-boot-logger.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>然后,我們?cè)谛枰蛴∪罩镜念?,加上Slf4j注解,然后使用log來(lái)打印日志信息即可,如下代碼所示:
package com.yang.web.controller;
import com.yang.api.common.ResultT;
import com.yang.api.common.command.RegisterCommand;
import com.yang.api.common.dto.UserDTO;
import com.yang.api.common.facade.UserFacade;
import com.yang.web.request.RegisterRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/user")
@Slf4j
public class UserController {
@Autowired
private UserFacade userFacade;
@GetMapping(value = "/{id}")
public ResultT<UserDTO> queryById(@PathVariable("id") Integer id) {
log.info("queryById===========");
return userFacade.getById(id);
}
@PostMapping(value = "/register")
public ResultT<String> register(@RequestBody RegisterRequest registerRequest) {
RegisterCommand registerCommand = convert2RegisterCommand(registerRequest);
return userFacade.register2(registerCommand);
}
private RegisterCommand convert2RegisterCommand(RegisterRequest registerRequest) {
RegisterCommand registerCommand = new RegisterCommand();
registerCommand.setLoginId(registerRequest.getLoginId());
registerCommand.setEmail(registerRequest.getEmail());
registerCommand.setPassword(registerRequest.getPassword());
registerCommand.setExtendMaps(registerRequest.getExtendMaps());
return registerCommand;
}
}然后訪問(wèn)queryById,打印結(jié)果如下:

日志打印工具類
在logback-spring.xml中,我們雖然能配置日志打印的格式,但是不夠靈活,因此,我們可以添加一個(gè)日志打印工具類,通過(guò)該工具類,來(lái)自定義項(xiàng)目中的日志打印格式,以方便后續(xù)更好地通過(guò)日志排查、定位問(wèn)題。
首先創(chuàng)建一個(gè)日志打印抽象類,定義日志打印的格式:
package com.yang.core.infrastructure.log;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractLogPrinter {
protected String bizCode;
protected List<String> params = new ArrayList<>();
protected String msg;
protected Throwable e;
public AbstractLogPrinter addBizCode(String bizCode) {
this.bizCode = bizCode;
return this;
}
public AbstractLogPrinter addMsg(String msg) {
this.msg = msg;
return this;
}
public AbstractLogPrinter addParam(String key, String value) {
this.params.add(key);
this.params.add(value);
return this;
}
public AbstractLogPrinter addThrowable(Throwable e) {
this.e = e;
return this;
}
public abstract void printBizLog();
public abstract void printErrorLog();
public abstract String getSeparator();
public String commonContent() {
StringBuilder stringBuilder = new StringBuilder();
String separator = getSeparator();
stringBuilder.append("bizCode").append(":")
.append(this.bizCode).append(separator);
if (!CollectionUtils.isEmpty(params)) {
for (int i = 0; i < params.size(); i += 2) {
stringBuilder.append(params.get(i))
.append(":")
.append(params.get(i + 1))
.append(separator);
}
}
if (StringUtils.isNotEmpty(msg)) {
stringBuilder.append("msg").append(":")
.append(msg).append(separator);
}
return stringBuilder.toString();
}
}
然后創(chuàng)建日志打印實(shí)現(xiàn)類,在實(shí)現(xiàn)類中,定制實(shí)現(xiàn)日志打印的級(jí)別、分隔符等內(nèi)容
package com.yang.core.infrastructure.log;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PlatformLogPrinter extends AbstractLogPrinter {
public void printBizLog() {
log.info(commonContent());
}
public void printErrorLog() {
if (e != null) {
log.error(commonContent(), e);
} else {
log.error(commonContent());
}
}
@Override
public String getSeparator() {
return "<|>";
}
}同時(shí),為了方便打印日志,創(chuàng)建一個(gè)日志打印創(chuàng)建者
package com.yang.core.infrastructure.log;
public class PlatformLogger {
public static AbstractLogPrinter build() {
return new PlatformLogPrinter();
}
}上述內(nèi)容準(zhǔn)備完畢后,我們?cè)赾ontroller中,使用PlatformLogger來(lái)打印日志,修改后的代碼如下:
package com.yang.web.controller;
import com.yang.api.common.ResultT;
import com.yang.api.common.command.RegisterCommand;
import com.yang.api.common.dto.UserDTO;
import com.yang.api.common.facade.UserFacade;
import com.yang.core.infrastructure.log.PlatformLogger;
import com.yang.web.request.RegisterRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private UserFacade userFacade;
@GetMapping(value = "/{id}")
public ResultT<UserDTO> queryById(@PathVariable("id") Integer id) {
PlatformLogger.build()
.addBizCode("queryById")
.addParam("id", id.toString())
.addMsg("query by id")
.printBizLog();
return userFacade.getById(id);
}
@GetMapping(value = "/error/{id}")
public ResultT testError(@PathVariable("id") Integer id) {
try {
int i = 1 / 0;
} catch (Throwable t) {
PlatformLogger.build()
.addBizCode("testError")
.addParam("id", id.toString())
.addMsg("test error print")
.addThrowable(t)
.printErrorLog();
}
return ResultT.fail();
}
@PostMapping(value = "/register")
public ResultT<String> register(@RequestBody RegisterRequest registerRequest) {
RegisterCommand registerCommand = convert2RegisterCommand(registerRequest);
return userFacade.register2(registerCommand);
}
private RegisterCommand convert2RegisterCommand(RegisterRequest registerRequest) {
RegisterCommand registerCommand = new RegisterCommand();
registerCommand.setLoginId(registerRequest.getLoginId());
registerCommand.setEmail(registerRequest.getEmail());
registerCommand.setPassword(registerRequest.getPassword());
registerCommand.setExtendMaps(registerRequest.getExtendMaps());
return registerCommand;
}
}
啟動(dòng)項(xiàng)目,分別訪問(wèn)queryById和testError,打印日志內(nèi)容如下:

日志分文件打印
一般情況下,我們的項(xiàng)目會(huì)分為不同的模塊,每一個(gè)模塊承擔(dān)不同的職責(zé),比如bussiness模塊,主要是負(fù)責(zé)業(yè)務(wù)邏輯代碼的實(shí)現(xiàn),業(yè)務(wù)邏輯編排等;web模塊主要負(fù)責(zé)http請(qǐng)求的接收,參數(shù)的校驗(yàn),入?yún)⑥D(zhuǎn)化為業(yè)務(wù)層入?yún)⒌?;而core模塊主要負(fù)責(zé)基礎(chǔ)能力實(shí)現(xiàn),比如持久化數(shù)據(jù)庫(kù)、領(lǐng)域服務(wù)實(shí)現(xiàn)等。
對(duì)于不同的模塊,我們希望將日志輸出到不同的文件當(dāng)中,從而協(xié)助我們后續(xù)定位問(wèn)題以及建設(shè)不同模塊下的監(jiān)控,包括基礎(chǔ)服務(wù)監(jiān)控、業(yè)務(wù)成功率監(jiān)控等。

因此,我們?cè)诓煌哪K下,分別實(shí)現(xiàn)不同的日志打印工具類:
package com.yang.web.log;
import com.yang.core.infrastructure.log.AbstractLogPrinter;
public class WebLogger {
public static AbstractLogPrinter build() {
return new WebLogPrinter();
}
}
package com.yang.web.log;
import com.yang.core.infrastructure.log.AbstractLogPrinter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class WebLogPrinter extends AbstractLogPrinter {
@Override
public void printBizLog() {
log.info(commonContent());
}
@Override
public void printErrorLog() {
if (this.e != null) {
log.error(commonContent(), e);
} else {
log.error(commonContent());
}
}
@Override
public String getSeparator() {
return "<|>";
}
}
package com.yang.business.log;
public class BusinessLogger {
public static BusinessLogPrinter build() {
return new BusinessLogPrinter();
}
}
package com.yang.business.log;
import com.yang.core.infrastructure.log.AbstractLogPrinter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BusinessLogPrinter extends AbstractLogPrinter {
@Override
public void printBizLog() {
log.info(commonContent());
}
@Override
public void printErrorLog() {
if (this.e != null) {
log.error(commonContent(), e);
} else {
log.error(commonContent());
}
}
@Override
public String getSeparator() {
return "<|>";
}
}
然后我們修改logback-spring.xml文件,將不同的日志打印工具類,輸出到不同的日志文件中
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATH" value="logs"/>
<property name="LOG_FILE" value="${LOG_PATH}/spring-boot-logger.log"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>common.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/spring-boot-logger.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
<appender name="PLATFORM_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>platform.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/platform-logger.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
<appender name="BUSINESS_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>business.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/business-logger.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
<appender name="WEB_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>web.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/web-logger.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- 工具類PlatformLogPrinter的logger -->
<logger name="com.yang.core.infrastructure.log.PlatformLogPrinter" level="INFO" additivity="false">
<appender-ref ref="PLATFORM_FILE" />
</logger>
<!-- 工具類BusinessLogPrinter的logger -->
<logger name="com.yang.business.log.BusinessLogPrinter" level="INFO" additivity="false">
<appender-ref ref="BUSINESS_FILE" />
</logger>
<!-- 工具類WebLogPrinter的logger -->
<logger name="com.yang.web.log.WebLogPrinter" level="INFO" additivity="false">
<appender-ref ref="WEB_FILE" />
</logger>
</configuration>最后,分別在web模塊、business模塊和core模塊下,添加埋點(diǎn)日志
// WEB模塊
@GetMapping(value = "/{id}")
public ResultT<UserDTO> queryById(@PathVariable("id") Integer id) {
WebLogger.build()
.addBizCode("userController_queryById")
.addParam("id", id.toString())
.addMsg("query by id")
.printBizLog();
return userFacade.getById(id);
}
// Business模塊
@Override
public ResultT<UserDTO> getById(Integer id) {
UserQueryDomainRequest userQueryDomainRequest = new UserQueryDomainRequest.UserQueryDomainRequestBuilder()
.queryMessage(id.toString())
.userQueryType(UserQueryType.ID)
.build();
UserQueryDomainResponse userQueryDomainResponse = userDomainService.query(userQueryDomainRequest);
List<UserAccount> userAccountList = userQueryDomainResponse.getUserAccountList();
UserDTO userDTO = null;
if (!CollectionUtils.isEmpty(userAccountList)) {
UserAccount userAccount = userAccountList.get(0);
userDTO = userDTOConvertor.convert2DTO(userAccount);
}
BusinessLogger.build()
.addBizCode("userFacade_getById")
.addParam("id", id.toString())
.addParam("userDTO", JSONObject.toJSONString(userDTO))
.addMsg("get by id")
.printBizLog();
return ResultT.success(userDTO);
}
// core模塊
public UserQueryDomainResponse query(UserQueryDomainRequest userQueryDomainRequest) {
UserQueryType userQueryType = userQueryDomainRequest.getUserQueryType();
UserDO userDO = null;
switch (userQueryType) {
case ID:
userDO = queryById(Integer.valueOf(userQueryDomainRequest.getQueryMessage()));
break;
case EMAIL:
userDO = queryByEmail(userQueryDomainRequest.getQueryMessage());
break;
case LOGIN_ID:
userDO = queryByLoginId(userQueryDomainRequest.getQueryMessage());
break;
}
if (userDO == null) {
return new UserQueryDomainResponse();
}
UserAccount userAccount = new UserAccount();
userAccount.setId(userDO.getId());
userAccount.setLoginId(userDO.getLoginId());
userAccount.setEmail(userDO.getEmail());
userAccount.setFeatureMap(FeatureUtils.convert2FeatureMap(userDO.getFeatures()));
userAccount.setCreateTime(userDO.getCreateTime());
userAccount.setUpdateTime(userDO.getUpdateTime());
UserQueryDomainResponse userQueryDomainResponse = new UserQueryDomainResponse();
List<UserAccount> userAccounts = new ArrayList<>();
userAccounts.add(userAccount);
userQueryDomainResponse.setUserAccountList(userAccounts);
PlatformLogger.build()
.addBizCode("userDomainService_query")
.addParam("queryMsg", userQueryDomainRequest.getQueryMessage())
.addParam("queryType", userQueryDomainRequest.getUserQueryType().name())
.printBizLog();
return userQueryDomainResponse;
}啟動(dòng)項(xiàng)目,訪問(wèn)queryById接口,可以看到在web.log,business.log和platform.log下分別打印了不同的日志信息



總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot+Vue3整合SSE實(shí)現(xiàn)實(shí)時(shí)消息推送功能
在日常開(kāi)發(fā)中,我們經(jīng)常需要實(shí)現(xiàn)實(shí)時(shí)消息推送的功能,這篇文章將基于SpringBoot和Vue3來(lái)簡(jiǎn)單實(shí)現(xiàn)一個(gè)入門(mén)級(jí)的例子,下面小編就和大家詳細(xì)介紹一下2025-11-11
JAVA使用POI(XSSFWORKBOOK)讀取EXCEL文件過(guò)程解析
這篇文章主要介紹了JAVA使用POI(XSSFWORKBOOK)讀取EXCEL文件過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08
MyBatis基于pagehelper實(shí)現(xiàn)分頁(yè)原理及代碼實(shí)例
這篇文章主要介紹了MyBatis基于pagehelper實(shí)現(xiàn)分頁(yè)原理及代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
JAVA實(shí)現(xiàn)讀取txt文件內(nèi)容的方法
本篇文章主要介紹了JAVA實(shí)現(xiàn)讀取txt文件內(nèi)容的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01
IDEA中thymeleaf語(yǔ)法沒(méi)有提示的問(wèn)題及解決
這篇文章主要介紹了IDEA中thymeleaf語(yǔ)法沒(méi)有提示的問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
Gradle環(huán)境下導(dǎo)出Swagger為PDF的步驟詳解
這篇文章主要介紹了Gradle環(huán)境下導(dǎo)出Swagger為PDF的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
Mybatis使用collection標(biāo)簽進(jìn)行樹(shù)形結(jié)構(gòu)數(shù)據(jù)查詢時(shí)攜帶外部參數(shù)查詢
這篇文章主要介紹了Mybatis使用collection標(biāo)簽進(jìn)行樹(shù)形結(jié)構(gòu)數(shù)據(jù)查詢時(shí)攜帶外部參數(shù)查詢,需要的朋友可以參考下2023-10-10
關(guān)于Springboot在新增和修改下上傳圖片并顯示的問(wèn)題
這篇文章主要介紹了關(guān)于Springboot在新增和修改下上傳圖片并顯示的問(wèn)題及解決方法,在這里 springboot中已經(jīng)內(nèi)嵌了上傳圖片的依賴包,因此不需要再添加額外依賴,具體實(shí)現(xiàn)代碼跟隨小編一起看看吧2021-04-04

