SpringBoot日志打印實(shí)踐過程
背景
在項(xiàng)目當(dāng)中,我們經(jīng)常需要打印一些日志埋點(diǎn)信息,這些日志埋點(diǎn)信息,在后續(xù)軟件的運(yùn)維、穩(wěn)定性建設(shè)中發(fā)揮了巨大的作用:
- 問題追蹤:通過埋點(diǎn)日志中的關(guān)鍵信息,幫助定位系統(tǒng)異常原因
- 系統(tǒng)監(jiān)控:通過日志,監(jiān)控系統(tǒng)的運(yùn)行情況,包括性能指標(biāo)、訪問頻率、錯(cuò)誤等
- 數(shù)據(jù)分析:分析用戶行為、系統(tǒng)性能和業(yè)務(wù)趨勢(shì)等
- 調(diào)試:通過查看日志,幫助開發(fā)人員了解程序在執(zhí)行過程中的狀態(tài)和行為
SpringBoot整合Logback實(shí)現(xiàn)日志打印
SpringBoot默認(rèn)使用Slf4j作為日志門面,并集成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,來自定義日志的配置信息,包括日志輸出文件、日志格式等
<?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來打印日志信息即可,如下代碼所示:
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;
}
}然后訪問queryById,打印結(jié)果如下:

日志打印工具類
在logback-spring.xml中,我們雖然能配置日志打印的格式,但是不夠靈活,因此,我們可以添加一個(gè)日志打印工具類,通過該工具類,來自定義項(xiàng)目中的日志打印格式,以方便后續(xù)更好地通過日志排查、定位問題。
首先創(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來打印日志,修改后的代碼如下:
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)目,分別訪問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)⒌龋欢鴆ore模塊主要負(fù)責(zé)基礎(chǔ)能力實(shí)現(xiàn),比如持久化數(shù)據(jù)庫(kù)、領(lǐng)域服務(wù)實(shí)現(xiàn)等。
對(duì)于不同的模塊,我們希望將日志輸出到不同的文件當(dāng)中,從而協(xié)助我們后續(xù)定位問題以及建設(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)目,訪問queryById接口,可以看到在web.log,business.log和platform.log下分別打印了不同的日志信息



總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
解決Nacos集群?jiǎn)?dòng)失敗:java版本問題
這篇文章主要介紹了解決Nacos集群?jiǎn)?dòng)失敗:java版本問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06
SpringBoot實(shí)現(xiàn)文件下載的限速功能
在SpringBoot項(xiàng)目中,實(shí)現(xiàn)文件下載的限速功能可以有效控制服務(wù)器帶寬的占用,并防止單個(gè)用戶消耗過多的資源,本文將通過具體的代碼示例和詳細(xì)的流程解釋,介紹如何在SpringBoot項(xiàng)目中實(shí)現(xiàn)文件下載的限速功能,需要的朋友可以參考下2024-07-07
Java泛型與數(shù)據(jù)庫(kù)應(yīng)用實(shí)例詳解
這篇文章主要介紹了Java泛型與數(shù)據(jù)庫(kù)應(yīng)用,結(jié)合實(shí)例形式詳細(xì)分析了java繼承泛型類實(shí)現(xiàn)增刪改查操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-08-08
Java 中一個(gè)類提供一個(gè)默認(rèn)對(duì)象的多種方法
這篇文章主要介紹了Java 中一個(gè)類提供一個(gè)默認(rèn)對(duì)象的多種方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07
詳述 DB2 分頁(yè)查詢及 Java 實(shí)現(xiàn)的示例
本篇文章主要介紹了詳述 DB2 分頁(yè)查詢及 Java 實(shí)現(xiàn)的示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09
Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的定時(shí)器代碼解析
這篇文章主要介紹了Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的定時(shí)器代碼解析,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12
java編程實(shí)現(xiàn)獲取服務(wù)器IP地址及MAC地址的方法
這篇文章主要介紹了java編程實(shí)現(xiàn)獲取機(jī)器IP地址及MAC地址的方法,實(shí)例分析了Java分別針對(duì)單網(wǎng)卡及多網(wǎng)卡的情況下獲取服務(wù)器IP地址與MAC地址的相關(guān)技巧,需要的朋友可以參考下2015-11-11

