SpringBoot日志配置全過程
SpringBoot日志配置
如果使用Spring Boot Starters,那么默認(rèn)使用的日志框架是Logback。Spring Boot底層對Java Util Logging、Commons Logging、Log4J及SLF4J日志框架也進行了適配,只需相關(guān)配置就可以實現(xiàn)日志框架的相互切換。
SpringBoot默認(rèn)日志事打印在console控制臺中,不會保存到文件中。
實際項目中必須保存到文件中進行日志分析
根據(jù)不同的日志系統(tǒng),可以按如下規(guī)則組織配置文件名,就能被正確加載:
- Spring Boot官方推薦優(yōu)先使用帶有-spring的文件名作為定義的日志配置(使用logback-spring.xml而不是logback.xml名稱)
- 若命名為logback-spring.xml的日志配置文件,Spring Boot可以為它添加一些Spring Boot特有的配置項
- 建議盡可能不使用Java Util Logging方式,因為Java Util Logging從可執(zhí)行jar運行時會導(dǎo)致一些已知的類加載問題

自定義日志配置:
- 通過將相應(yīng)的庫添加到classpath可以激活各種日志系統(tǒng)
- 在classpath根目錄下提供合適的配置文件可以進一步定制日志系統(tǒng)
- 配置文件也可以通過Spring Environment的logging.config屬性指定
日志分級:(TRACE < DEBUG < INFO< WARN < ERROR < FATAL)從低到高
- TRACE,最低級別的日志記錄,用于輸出最詳細(xì)的調(diào)試信息,通常用于開發(fā)調(diào)試目的。在生產(chǎn)環(huán)境中,應(yīng)該關(guān)閉 TRACE 級別的日志記錄,以避免輸出過多無用信息
- DEBUG,是用于輸出程序中的一些調(diào)試信息,通常用于開發(fā)過程中。像 TRACE 一樣,在生產(chǎn)環(huán)境中應(yīng)該關(guān)閉 DEBUG 級別的日志記錄。
- INFO,用于輸出程序正常運行時的一些關(guān)鍵信息,比如程序的啟動、運行日志等。通常在生產(chǎn)環(huán)境中開啟 INFO 級別的日志記錄。
- WARN,是用于輸出一些警告信息,提示程序可能會出現(xiàn)一些異?;蛘咤e誤。在應(yīng)用程序中,WARN 級別的日志記錄通常用于記錄一些非致命性異常信息,以便能夠及時發(fā)現(xiàn)并處理這些問題。
- ERROR,是用于輸出程序運行時的一些錯誤信息,通常表示程序出現(xiàn)了一些不可預(yù)料的錯誤。在應(yīng)用程序中,ERROR 級別的日志記錄通常用于記錄一些致命性的異常信息,以便能夠及時發(fā)現(xiàn)并處理這些問題。
Logback日志不提供FATAL級別,它被映射到ERROR級別。Spring Boot只會輸出比當(dāng)前級別高的日志,默認(rèn)的日志級別是INFO,因此低于INFO級別的日志記錄都不輸出
Spring Boot中默認(rèn)配置ERROR、WARN和INFO級別的日志輸出到控制臺。
通過啟動您的應(yīng)用程序—debug標(biāo)志來啟用“調(diào)試”模式(開發(fā)時推薦開啟),以下兩種方式皆可:
- 在運行命令后加入–debug標(biāo)志,例如:java -jar springTest.jar --debug
- 在application.properties中配置debug=true,該屬性置為true的時候,核心Logger(包含嵌入式容器、hibernate、spring)會輸出更多內(nèi)容,但是你自己應(yīng)用的日志并不會輸出為DEBUG級別。
除了這五種級別以外,還有一些日志框架定義了其他級別,例如 Python 中的 CRITICAL、PHP 中的 FATAL 等。CRITICAL 和 FATAL 都是用于表示程序出現(xiàn)了致命性錯誤或者異常,即不可恢復(fù)的錯誤。
使用xml配置日志保存
(并不需要pom配置slf4j依賴,使用這個默認(rèn)不用配置pom依賴,最新的spring-boot-starter-web中已經(jīng)集成了)
啟動一個項目,直接將logback-spring.xml文件復(fù)制到resources目錄下就可以實現(xiàn)日志文件記錄。
步驟如下:
- 在項目resources目錄下創(chuàng)建一個logback-spring.xml日志配置文件
名稱只要是logback開頭
備注:要配置logback-spring.xml,springboot會默認(rèn)加載此文件,為什么不配置logback.xml,因為logback.xml會先application.properties加載,而logback-spring.xml會后于application.properties加載,這樣我們在application.properties文中設(shè)置日志文件名稱和文件路徑才能生效。
- 內(nèi)容如下
Spring Boot 默認(rèn)日志輸出如下:

上述輸出的日志信息,從左往右含義解釋如下:
- 日期時間:精確到毫秒
- 日志級別:ERROR,WARN,INFO,DEBUG or TRACE
- 進程:id
- 分割符:用于區(qū)分實際的日志記錄
- 線程名:括在方括號中
- 日志名字:通常是源類名
- 日志信息說明
依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency>
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<!--輸出到控制臺-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
<!-- <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} -%5p ${PID:-} [%15.15t] %-30.30C{1.} : %m%n</pattern>-->
</encoder>
</appender>
<!--按天生成日志-->
<appender name="logFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>
poslog/%d{yyyy-MM-dd}/%d{yyyy-MM-dd}.log
</FileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
</Pattern>
</layout>
</appender>
<root level="INFO">
<appender-ref ref="console" />
<appender-ref ref="logFile" />
</root>
</configuration>- 編寫打印日志
@SpringBootTest
public class LoggerTest {
private static final Logger logger = LoggerFactory.getLogger(LoggerTest.class);
@Test
public void test() {
logger.trace("trace 級別的日志");
logger.debug("debug 級別的日志");
logger.info("info 級別的日志");
logger.warn("warn 級別的日志");
logger.error("error 級別的日志");
}
}- 啟動測試
在當(dāng)前文件夾下會創(chuàng)建一個【poslog/2020-10/22】的文件夾,里面會按天生成日志:【2020-10-22.log】,例如:
控制臺輸出:

分類logback.xml配置
需在application.properties中設(shè)置logging.file.name或logging.file.path屬性
1)logging.file.name,設(shè)置文件,可以是絕對路徑,也可以是相對路徑。例如:
logging.file.name=info.log
2)logging.file.path,設(shè)置目錄,會在該目錄下創(chuàng)建spring.log文件,并寫入日志內(nèi)容,例如:
logging.file.path=/workspace/log
如果只配置logging.file.name,會在項目的當(dāng)前路徑下生成一個xxx.log日志文件。如果只配置logging.file.path,在/workspace/log文件夾生成一個為spring.log日志文件。
二者不能同時使用,如若同時使用,則只有l(wèi)ogging.file.name生效。默認(rèn)情況下,日志文件的大小達到10MB時會切分一次,產(chǎn)生新的日志文件,默認(rèn)級別為:ERROR、WARN、INFO。
所有支持的日志記錄系統(tǒng)都可以在Spring環(huán)境中設(shè)置記錄級別,格式為:“logging.level.* = LEVEL”。
雖然Spring Boot中application.properties配置文件提供了日志的配置,但是個人更傾向于logback.xml的配置方式。
日志配置到d盤了:
根節(jié)點包含的屬性
- scan:當(dāng)此屬性設(shè)置為true時,配置文件如果發(fā)生改變,將會被重新加載,默認(rèn)值為true
- scanPeriod:設(shè)置監(jiān)測配置文件是否有修改的時間間隔,如果沒有給出時間單位,默認(rèn)單位是毫秒。當(dāng)scan為true時,此屬性生效。默認(rèn)的時間間隔為1分鐘
- debug:當(dāng)此屬性設(shè)置為true時,將打印出logback內(nèi)部日志信息,實時查看logback運行狀態(tài)。默認(rèn)值為false
子節(jié)點
- root節(jié)點是必選節(jié)點,用來指定最基礎(chǔ)的日志輸出級別,只有一個level屬性。
- level:用來設(shè)置打印級別,大小寫無關(guān),其值包含如下:TRACE、DEBUG、INFO、WARN、ERROR、ALL和OFF
- level不能設(shè)置為INHERITED或者同義詞NULL,默認(rèn)是DEBUG。
- root節(jié)點中可以包含零個或多個元素,標(biāo)識這個appender將會添加到這個loger
子節(jié)點設(shè)置上下文名稱
每個logger都關(guān)聯(lián)到logger上下文,默認(rèn)上下文名稱為“default”。但可以使用設(shè)置成其他名字,用于區(qū)分不同應(yīng)用程序的記錄。
設(shè)置后不能修改,通過%contextName設(shè)置來打印日志上下文名稱,一般來說不用這個屬性
子節(jié)點
appender用來格式化日志輸出節(jié)點,有兩個屬性name和class,class用來指定哪種輸出策略,常用就是控制臺輸出策略和文件輸出策略。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路徑 -->
<property name="log.path" value="d:/logback" />
<!-- 日志輸出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制臺輸出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系統(tǒng)日志輸出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file>
<!-- 循環(huán)政策:基于時間創(chuàng)建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的歷史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 過濾的級別 只會打印debug不會有info日志-->
<!-- <level>DEBUG</level>-->
<!-- 匹配時的操作:接收(記錄) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配時的操作:拒絕(不記錄) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<!-- 循環(huán)政策:基于時間創(chuàng)建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的歷史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 過濾的級別 -->
<level>ERROR</level>
<!-- 匹配時的操作:接收(記錄) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配時的操作:拒絕(不記錄) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 用戶訪問日志輸出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-user.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滾 daily -->
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的歷史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系統(tǒng)模塊日志級別控制 -->
<logger name="com.example" level="debug" />
<!-- Spring日志級別控制 -->
<logger name="org.springframework" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<!--系統(tǒng)操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref=&##34;file_error" />
</root>
<!--系統(tǒng)用戶操作日志-->
<logger name="sys-user" level="info">
<appender-ref ref="sys-user"/>
</logger>
</configuration> 注:1)控制臺和日志文件的字符集;2)日志文件的存放位置,須要遵守Linux的命名規(guī)則。
在application.yml中進行設(shè)置日志級別
如果com.example: debug,那么項目com.example包里面的debug以上的日志也會輸出
logging: level: com.example: info org.springframework: warn
或者properties方式
#com.yoodb.study.demo04包下所有class以DEBUG級別輸出 logging.level.com.yoodb.study=DEBUG #用來指定自己創(chuàng)建的日志文件 logging.config=classpath:logback-spring.xml #指定輸出文件位置 logging.file.path=D://workspace/log
Controller
注:在添加引用時,日志的包一定是org.slf4j.Logger、org.slf4j.LoggerFactory類
@RestController
public class HelloWorldController {
protected static Logger logger=LoggerFactory.getLogger(HelloWorldController.class);
@RequestMapping("/")
public String helloworld(){
logger.debug("關(guān)注微信公眾號“Java精選”,Spring Boot系列文章持續(xù)更新中,帶你從入門到精通,玩轉(zhuǎn)Spring Boot框架。");
return "Hello world!";
}
@RequestMapping("/hello/{name}")
public String helloName(@PathVariable String name){
logger.debug("訪問 helloName,Name={}",name);
return "Hello "+name;
}
}要解決的核心問題:「誰」在「什么時間」對「什么」做了「什么事」
方案 1:AOP 切面 + 注解
①、定義日志注解,用于標(biāo)記哪些方法需要記錄業(yè)務(wù)操作日志
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable{
String value() default "";
//可以添加更多的配置屬性,如操作類型、級別
}②、創(chuàng)建AOP切面
@Aspect
@Component
public class LoggingAspect{
@Autowired
private Logger logger;//SLF4j獲取
@Around("@annotation(loggable)")
public Object logBusinessOperation(Proceeding joinPoint,Loggable loggable)throws Throwable{
//方法執(zhí)行前的邏輯,例如記錄開始事件、方法參數(shù)等
long start = System.currentTimeMillis();
try{
Object result = jointPoint.proceed();//執(zhí)行目標(biāo)方法
//方法執(zhí)行后的邏輯,例如記錄結(jié)束時間、返回值等
return result;
}catch(Exception e){
// 異常處理邏輯,如記錄異常信息
throw e;
}finally{
long executionTime = System.currentTimeMillis() - start;
// 構(gòu)建日志信息并記錄
logger.info("{} executed in {} ms", joinPoint.getSignature(), executionTime);
}
}
}③、配置SpringAOP+標(biāo)記注解
@Configuration
@EnableAspectJAutoProxy
public class AopConfig{
//可能還需要其他的配置或bean
}④、業(yè)務(wù)中使用注解
public class SomeService{
@Loggable
public void someBusinessMethod(Object someParam){
//業(yè)務(wù)邏輯
}
}缺點:
- 日志粒度和詳細(xì)度:切面雖然攔截了我們目標(biāo)方法,但其中能拿到的信息上下文有限,無法構(gòu)成一條操作日志所需的數(shù)據(jù)信息
- 業(yè)務(wù)操作場景劃分:切面的定義和使用都是非業(yè)務(wù)化的,所以無法感知到新的業(yè)務(wù)操作范圍和業(yè)務(wù)的定義劃分邊界是如何處理
- 級聯(lián)操作斷檔:當(dāng)業(yè)務(wù)操作是設(shè)計多表或者多個服務(wù)間的調(diào)用串聯(lián)時,切面只能單獨記錄每個服務(wù)方法級別的數(shù)據(jù)信息,無法對調(diào)用鏈的部分進行業(yè)務(wù)串聯(lián)
記錄到的日志數(shù)據(jù)都是固定的模板數(shù)據(jù),如:_XXX 修改了項目,XXX 新建了問題數(shù)據(jù),XXX 刪除了風(fēng)險問題,因為我們無法通過每個切面對具體參數(shù)內(nèi)容和業(yè)務(wù)場景進行捕獲。那么_如果我們想要在日志內(nèi)容中添加更多的業(yè)務(wù)上下文信息,如:XXX 修改了項目 ID=001 的數(shù)據(jù),XXX 刪除了產(chǎn)品 ID=002 的數(shù)據(jù),這時候就可以通過使用 AOP + SpEL 表達式來實現(xiàn)。
方案2:AOP 切面 + SpEL
①、對方案1的注解進行內(nèi)容擴展
@Repeatable(LogRecords.class)
@Target({ElementType.METHOD,ElmenetType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogRecord{
String success();
String fail() default "";
String operator() default ""; //業(yè)務(wù)操作場景人
String type(); // 業(yè)務(wù)場景 模塊范圍
String subType() default ""; //業(yè)務(wù)子場景,主要是模塊下的功能范圍
String bizNo(); //業(yè)務(wù)場景的業(yè)務(wù)編號,
String extra() default "";//一些操作的擴展操作
String actionType(); //業(yè)務(wù)操作類型,比如編輯、新增、刪除
}②、基于注解進行定義SpEL的解析器來對注解中的字段進行解析和使用
public class LogRecordParser{
public static Map<String,Object> parseLogRecord(Annotation logRecordAnnotation){
Map<String,Object> result = new HashMap<>();
ExpressionParser parser = new ExpressionParser(new SpelFunction("parseLogRecord", LogRecordParser.class, "parseLogRecord"));
for(String attribute : logRecordAnnotation.getAttributeNames()){
Object value = logRecordAnnotation.getAttribute(attribute);
Expression expression = parser.parseExpression(attribute);
TypeResolutionContext typeResolutionContext = new TypeResolutionContext();
typeResolutionContext.setMethod(new Method(null, null, null));
Object parsedValue = expression.getValue(typeResolutionContext);
result.put(attribute, parsedValue);
}
return result;
}
}③、表達式使用
系統(tǒng)中實際業(yè)務(wù)操作的使用場景,在注解的內(nèi)容中填充了很多業(yè)務(wù)操作場景的數(shù)據(jù),如果需要涉及操作前后數(shù)據(jù)的內(nèi)容記錄,還可以再次進行擴充 SpEL 的字段及解析邏輯,可以說是有了它,我們可以做的更多了!(但是注解也越來越長了)

優(yōu)缺點:
- 解決了方案1中冗余重復(fù)代碼層面的侵入,但會出現(xiàn)大量注解定義的出現(xiàn),也帶有一定的侵入性
- 日志內(nèi)容還是需要系統(tǒng)自身根據(jù)上報場景進行封裝,需要從產(chǎn)品的業(yè)務(wù)定義到研發(fā)編碼達成統(tǒng)一共識
- 與方案1相比簡化了一部分代碼集成的復(fù)雜度,只需編寫自定義注解即可
- 與方案1相比擴展了對操作的業(yè)務(wù)數(shù)據(jù)廣度,數(shù)據(jù)范圍大大增加,而且還可根據(jù)自身業(yè)務(wù)定義無限擴展
解決了業(yè)務(wù)操作日志的一個收集問題,能夠清晰的記錄各類操作場景、動作、數(shù)據(jù)前后的內(nèi)容等
方案3:Binlog + 時間窗口
怎么從應(yīng)用層對操作場景、數(shù)據(jù)進行抓包、處理邏輯、保存,所以復(fù)雜度都會集中到應(yīng)用層。既然是這樣我們能不能直接基于底層的 MySQL 本身來處理這件事兒呢?
Binlog 是數(shù)據(jù)庫中二進制格式的文件,用于記錄用戶對數(shù)據(jù)庫更新的 SQL 語句信息,例如更改數(shù)據(jù)庫表和更改內(nèi)容的 SQL 語句都會記錄到 binlog 里。那么 Binlog 能用來記錄業(yè)務(wù)層面的數(shù)據(jù)變化內(nèi)容嗎?

問題 1:無法對多表存在級聯(lián)保存和更新的數(shù)據(jù)進行非常好的兼容支持,因為本身binlog數(shù)據(jù)是無序的,并且如果上游數(shù)據(jù)的操作不是包裹在一個事務(wù)中,也很難處理
解決問題 1:由于本身 binlog 的無序性,所以無法對大量 binlog 進行有序組合,如果本身是一個事務(wù)提交的還可以根據(jù)事務(wù) KEY 進行組合,如果不是呢?這里可以考慮借鑒 Flink 的時間窗口機制:滾動的時間窗口將每個元素指定給指定窗口大小的窗口,滾動窗口具有固定大小,且不重疊。
例如,我們指定一個大小為 1 分鐘的滾動窗口,在這種情況下,我們將每隔 1 分鐘開啟一個新的窗口,其中每一條數(shù)都會劃分到唯一一個 1 分鐘的窗口中,如下圖所示:

基于以上的窗口機制,我們就可以對數(shù)據(jù)先進行范圍的框定,通過窗口的滑動機制和補償機制對窗口中的數(shù)據(jù)進行關(guān)聯(lián)處理。但光靠時間窗口還是無法對 binlog 進行關(guān)聯(lián),那我們就從關(guān)聯(lián)數(shù)據(jù)本身下手,這類數(shù)據(jù)關(guān)聯(lián)復(fù)雜主要是涉及表之間的引用關(guān)系,那我們在進行定義 binlog 解析時就把前后數(shù)據(jù) + 表之間的引用字段都進行指定,這樣在窗口中進行滑動關(guān)聯(lián)時,就可以進行子表的引用字段關(guān)聯(lián)了!這樣關(guān)聯(lián)字段補償更新的機制就可以解決問題 1 了。
//部分的 binlog 數(shù)據(jù)變動結(jié)構(gòu)的 RowChange 定義如下:
@Data
public static class RowChange {
private int tableId;
private List<RowDatas> rowDatas;
private String eventType;
private boolean isDdl;
}
@Data
public static class RowDatas {
private List<DataColumn> afterColumns;
private List<DataColumn> beforeColumns;
}
@Data
public static class DataColumn {
private int sqlType;
private boolean isNull;
private String mysqlType;
private String name;
private boolean isKey;
private int index;
private boolean updated;
private String value;
}
問題 2:關(guān)于更新人的問題,系統(tǒng)進行更新時如果未手動更新對應(yīng)操作人,則系統(tǒng)無法識別,需要上游做對應(yīng)場景的統(tǒng)一改造,但從系統(tǒng)承接來看,本身系統(tǒng)的操作人就是要跟著業(yè)務(wù)操作一起進行聯(lián)動的
解決問題 2:關(guān)于更新人的問題其實是各系統(tǒng)需要自己排除解決的問題,因為本身業(yè)務(wù)在進行數(shù)據(jù)操作時就是需要留痕更新人信息,比較統(tǒng)一的方案就是基于底層的 ORM 框架來統(tǒng)一進行攔截處理,大家可以自行 GPT。

總結(jié):
- 基于 binlog 后,我們對底層的數(shù)據(jù)變動感知更明顯了,但是 binlog 的數(shù)據(jù)來源除了系統(tǒng)應(yīng)用層還有很多其他來源,比如我們的數(shù)據(jù)庫工單,日常跑批刷數(shù)等場景,這類的數(shù)據(jù)變動范圍可能較大,而且感知較弱。
- 方案 3 的設(shè)計把方案 2 中的業(yè)務(wù)場景(也就是 actiontype subtype 等)弱化了,所以并不能很好的感知到很細(xì)顆粒度。
項目中應(yīng)用日志
①、bootstrap.yml配置文件
mybatis-plus:
type-aliases-package: quick.pager.shop.model
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [${spring.application.name}] [traceId:%X{X-B3-TraceId}][spanId:%X{X-B3-SpanId}][parentSpanId:%X{X-B3-ParentSpanId}] --- [%t] - [%class:%method: %line] - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [${spring.application.name}] [traceId:%X{X-B3-TraceId}][spanId:%X{X-B3-SpanId}][parentSpanId:%X{X-B3-ParentSpanId}] --- [%t] - [%class:%method: %line] - %msg%n"
level:
org.springframework: error
com.alibaba: error
org.apache.ibatis: error
io.seata: error
file:
path: ./logs/${spring.application.name}
max-size: 50MB
name: ${spring.application.name}②、service實現(xiàn)類中的使用日志
@Service
@Slf4j //lombok:1.18.12
public class GoodsSpuServiceImpl extends ServiceImpl<GoodsSpuMapper, GoodsSpu> implements GoodsSpuService {
@Autowired
private GoodsClassMapper goodsClassMapper;
@Autowired
private BannerClient bannerClient;
@Override
public Response<Long> create(GoodsSpuSaveRequest request){
if(StringUtils.isBlank(request.getSpuName())){
return Response.toError(ResponseStatus.Code.FAIL_CODE, "spu名稱不能為空!");
}
if(checkName(request.getSpuName(), null)){
return Response.toError(ResponseStatus.Code.FAIL_CODE, "spu名稱已存在!");
}
GoodsSpu spu = this.conv(request);
spu.setCreateTime(DateUtils.dateTime());
spu.setDeleteStatus(Boolean.FALSE);
if (this.baseMapper.insert(spu) > 0) {
return Response.toResponse(spu.getId());
}
//添加日志
log.error("新增SPU失敗 result = {}",JSON.toJSONString(request));
return Response.toError(ResponseStatus.Code.FAIL_CODE, "新增SPU失敗");
}
@Override
public Response<Long> delete(final Long id){
int delete = this.baseMapper.deleteById(id);
if(delete>0){
return Response.toResponse(id);
}
//添加日志
log.error("刪除SPU失敗 id={}",id);
return Response.toError(ResponseStatus.Code.FAIL_CODE, "刪除SPU失敗");
}
}校驗名稱的唯一性
private Boolean checkName(final String name,final Long id){
List<GoodsSpu> spus = this.baseMapper.selectList(new LambdaQueryWrapper<GoodsSpu>()
.eq(GoodsSpu::getSpuName, name));
if(CollectionUtils.isEmpty(spus)){
return Boolean.FALSE;
}
return spus.stream()
.filter(item->Objects.isNull(id)?Boolean.TRUE:IConsts.ZERO!=item.getId().compareTo(id))
.anyMatch(item->item.getSpuName().equals(name));
}項目中注解和日志的結(jié)合
①、注解
/**
自定義操作日志記錄注解
*/
@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog{
//模塊
public String title() default "";
//功能
public BusinessType businessType() default BusinessType.OTHER;
//操作人類別
public OperatorType operatorType() default OperatorType.MANAGE;
//是否保存請求的參數(shù)
public boolean isSaveRequestData() default true;
}/**
* 業(yè)務(wù)操作類型
*
* @author ruoyi
*/
public enum BusinessType {
/**
* 其它
*/
OTHER,
/**
* 新增
*/
INSERT,
/**
* 修改
*/
UPDATE,
/**
* 刪除
*/
DELETE,
/**
* 授權(quán)
*/
GRANT,
/**
* 導(dǎo)出
*/
EXPORT,
/**
* 導(dǎo)入
*/
IMPORT,
/**
* 強退
*/
FORCE,
/**
* 生成代碼
*/
GENCODE,
/**
* 清空
*/
CLEAN,
}
/**
* 操作人類別
*
* @author ruoyi
*/
public enum OperatorType {
/**
* 其它
*/
OTHER,
/**
* 后臺用戶
*/
MANAGE,
/**
* 手機端用戶
*/
MOBILE
}②、切面
@Aspect
@Slf4j
@Document
public class OperLogAspect{
//配置織入點(注解)
@Pointcut("@annotation(com.ruoyi.system.log.annotation.OperLog)")
public void logPointCut(){}
//處理完請求后執(zhí)行
@AfterReturning(pointcut = "logPointCut")
public void doAfterReturning(JoinPoint joinPoint){
handleLog(joinPoint,null);
}
//攔截異常操作
@AfterThrowing(value = "logPointCut()",throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint,Exception e){
handleLog(joinPoint,e);
}
protected void handleLog(final JoinPoint joinPoint,final Exception e){
try{
// 獲得注解
com.ruoyi.system.log.annotation.OperLog controllerLog = getAnnotationLog(joinPoint);
if (controllerLog == null) {
return;
}
// *========數(shù)據(jù)庫日志=========*//
OperLog operLog = new OperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 請求的地址
HttpServletRequest request = ServletUtils.getRequest();
String ip = IpUtils.getIpAddr(request);
operLog.setOperIp(ip);
operLog.setOperUrl(request.getRequestURI());
operLog.setOperLocation(AddressUtils.getRealAddressByIP(ip));
String username = request.getHeader(Constants.CURRENT_USERNAME);
operLog.setOperName(username);
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
//設(shè)置方法名稱
String className = joinPoint.getTarget().getClass().getName();
Strng methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
//設(shè)置請求方式
operLog.setRequestMethod(request.getMethod());
//處理設(shè)置注解上的參數(shù)
Object[] args = joinPoint.getArgs();
getControllerMethodDescription(controllerLog, operLog, args);
//發(fā)布事件
SpringContextHolder.publishEvent(new OperLogEvent(operLog));
}catch(Exception exp){
//記錄本地異常日志
log.error("==前置通知異常==");
log.error("異常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
//是否存在注解,如果存在就獲取
private com.ruoyi.system.log.annotation.OperLog getAnnotationLog(JoinPoint joinPoint) throws Exception {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(com.ruoyi.system.log.annotation.OperLog.class);
}
return null;
}
//獲取注解中對方法的描述信息,用于Controller層注解
public void getControllerMethodDescription(com.ruoyi.system.log.annotation.OperLog log, OperLog operLog, Object[] args)
throws Exception {
// 設(shè)置action動作
operLog.setBusinessType(log.businessType().ordinal());
// 設(shè)置標(biāo)題
operLog.setTitle(log.title());
// 設(shè)置操作人類別
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,參數(shù)和值
if (log.isSaveRequestData()) {
// 獲取參數(shù)的信息,傳入到數(shù)據(jù)庫中。
setRequestValue(operLog, args);
}
}
//獲取請求的參數(shù),放到log中
private void setRequestValue(OperLog operLog, Object[] args) throws Exception {
List<?> param = new ArrayList<>(Arrays.asList(args)).stream().filter(p -> !(p instanceof ServletResponse))
.collect(Collectors.toList());
log.debug("args:{}", param);
String params = JSON.toJSONString(param, true);
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
}
}工具類
/**
* 客戶端工具類
*
* @author ruoyi
*/
public class ServletUtils {
/**
* 獲取String參數(shù)
*/
public static String getParameter(String name) {
return getRequest().getParameter(name);
}
/**
* 獲取String參數(shù)
*/
public static String getParameter(String name, String defaultValue) {
return Convert.toStr(getRequest().getParameter(name), defaultValue);
}
/**
* 獲取Integer參數(shù)
*/
public static Integer getParameterToInt(String name) {
return Convert.toInt(getRequest().getParameter(name));
}
/**
* 獲取Integer參數(shù)
*/
public static Integer getParameterToInt(String name, Integer defaultValue) {
return Convert.toInt(getRequest().getParameter(name), defaultValue);
}
/**
* 獲取request
*/
public static HttpServletRequest getRequest() {
return getRequestAttributes().getRequest();
}
/**
* 獲取response
*/
public static HttpServletResponse getResponse() {
return getRequestAttributes().getResponse();
}
/**
* 獲取session
*/
public static HttpSession getSession() {
return getRequest().getSession();
}
public static ServletRequestAttributes getRequestAttributes() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
/**
* 將字符串渲染到客戶端
*
* @param response 渲染對象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 是否是Ajax異步請求
*
* @param request
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
String accept = request.getHeader("accept");
if (accept != null && accept.indexOf("application/json") != -1) {
return true;
}
String xRequestedWith = request.getHeader("X-Requested-With");
if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1) {
return true;
}
String uri = request.getRequestURI();
if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) {
return true;
}
String ajax = request.getParameter("__ajax");
if (StringUtils.inStringIgnoreCase(ajax, "json", "xml")) {
return true;
}
return false;
}
}/**
* 獲取IP方法
*
* @author ruoyi
*/
public class IpUtils {
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip.split(",")[0];
}
public static boolean internalIp(String ip) {
byte[] addr = textToNumericFormatV4(ip);
if (null != addr) {
return internalIp(addr) || "127.0.0.1".equals(ip);
}
return false;
}
private static boolean internalIp(byte[] addr) {
final byte b0 = addr[0];
final byte b1 = addr[1];
// 10.x.x.x/8
final byte SECTION_1 = 0x0A;
// 172.16.x.x/12
final byte SECTION_2 = (byte) 0xAC;
final byte SECTION_3 = (byte) 0x10;
final byte SECTION_4 = (byte) 0x1F;
// 192.168.x.x/16
final byte SECTION_5 = (byte) 0xC0;
final byte SECTION_6 = (byte) 0xA8;
switch (b0) {
case SECTION_1:
return true;
case SECTION_2:
if (b1 >= SECTION_3 && b1 <= SECTION_4) {
return true;
}
case SECTION_5:
switch (b1) {
case SECTION_6:
return true;
}
default:
return false;
}
}
/**
* 將IPv4地址轉(zhuǎn)換成字節(jié)
*
* @param text IPv4地址
* @return byte 字節(jié)
*/
public static byte[] textToNumericFormatV4(String text) {
if (text.length() == 0) {
return null;
}
byte[] bytes = new byte[4];
String[] elements = text.split("\\.", -1);
try {
long l;
int i;
switch (elements.length) {
case 1:
l = Long.parseLong(elements[0]);
if ((l < 0L) || (l > 4294967295L)) {
return null;
}
bytes[0] = (byte) (int) (l >> 24 & 0xFF);
bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
bytes[3] = (byte) (int) (l & 0xFF);
break;
case 2:
l = Integer.parseInt(elements[0]);
if ((l < 0L) || (l > 255L)) {
return null;
}
bytes[0] = (byte) (int) (l & 0xFF);
l = Integer.parseInt(elements[1]);
if ((l < 0L) || (l > 16777215L)) {
return null;
}
bytes[1] = (byte) (int) (l >> 16 & 0xFF);
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
bytes[3] = (byte) (int) (l & 0xFF);
break;
case 3:
for (i = 0; i < 2; ++i) {
l = Integer.parseInt(elements[i]);
if ((l < 0L) || (l > 255L)) {
return null;
}
bytes[i] = (byte) (int) (l & 0xFF);
}
l = Integer.parseInt(elements[2]);
if ((l < 0L) || (l > 65535L)) {
return null;
}
bytes[2] = (byte) (int) (l >> 8 & 0xFF);
bytes[3] = (byte) (int) (l & 0xFF);
break;
case 4:
for (i = 0; i < 4; ++i) {
l = Integer.parseInt(elements[i]);
if ((l < 0L) || (l > 255L)) {
return null;
}
bytes[i] = (byte) (int) (l & 0xFF);
}
break;
default:
return null;
}
} catch (NumberFormatException e) {
return null;
}
return bytes;
}
public static String getHostIp() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
}
return "127.0.0.1";
}
public static String getHostName() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
}
return "未知";
}
}/**
* 獲取地址類
*
* @author ruoyi
*/
public class AddressUtils {
private static final Logger log = LoggerFactory.getLogger(AddressUtils.class);
public static final String IP_URL = "http://ip-api.com/json/%s?lang=zh-CN";
public static String getRealAddressByIP(String ip) {
String address = "XX XX";
// 內(nèi)網(wǎng)不查詢
if (IpUtils.internalIp(ip)) {
return "內(nèi)網(wǎng)IP";
}
String rspStr = HttpUtil.get(String.format(IP_URL, ip));
if (StringUtils.isEmpty(rspStr)) {
log.error("獲取地理位置異常 {}", ip);
return address;
}
JSONObject obj;
try {
obj = JSON.unmarshal(rspStr, JSONObject.class);
address = obj.getStr("country") + "," + obj.getStr("regionName") + "," + obj.getStr("city");
} catch (Exception e) {
log.error("獲取地理位置異常 {}", ip);
}
return address;
}
}
系統(tǒng)日志事件
public class OperLogEvent extends ApplicationEvent {
private static final long serialVersionUID = 8905017895058642111L;
public OperLogEvent(OperLog source) {
super(source);
}
}@Slf4j
@Service
@Lazy(false)
public class SpringContextHolder implements ApplicationContextAware,DisposableBean{
private static ApplicationContext applicationContext = null;
//取得存在在靜態(tài)變量中的ApplicationContext
public static ApplicationCotnext getApplicationCotnext(){
return applicationContext;
}
//實現(xiàn)ApplicationContextAware接口, 注入Context到靜態(tài)變量中
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SpringContextHolder.applicationContext = applicationContext;
}
//清除SpringContextHolder中的ApplicationContext為Null
public static void clearHolder() {
if (log.isDebugEnabled()) {
log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
}
applicationContext = null;
}
//發(fā)布事件 SpringContextHolder.publishEvent(new OperLogEvent(operLog));
public static void publishEvent(ApplicationEvent event) {
if (applicationContext == null) {
return;
}
applicationContext.publishEvent(event);
}
//實現(xiàn)DisposableBean接口, 在Context關(guān)閉時清理靜態(tài)變量.
@Override
@SneakyThrows
public void destroy() {
SpringContextHolder.clearHolder();
}
//獲取運行環(huán)境
public static String getActiveProfile() {
return applicationContext.getEnvironment().getActiveProfiles()[0];
}
}③、使用
//新增保存通知公告
@HasPermissions("system:notice:add")
@OperLog(title = "通知公告", businessType = BusinessType.INSERT)
@PostMapping("save")
public R addSave(@ReqeustBody Notice notice){
notice.setParkId(getParkId());
notice.setCreateBy(getLoginName());
return toAjax(noticeService.insertNotice(notice));
}總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用springboot實現(xiàn)上傳文件時校驗文件是否有病毒
在SpringBoot中實現(xiàn)文件上傳時的病毒校驗,可以使用ClamAV、Metascan或VirusTotal等工具,這些工具通過掃描上傳的文件,可以有效地檢測和阻止惡意軟件的傳播,安裝和配置ClamAV服務(wù)的步驟如下:下載并安裝ClamAV二進制文件,配置clamd.conf文件2025-01-01
Java網(wǎng)絡(luò)編程之UDP網(wǎng)絡(luò)通信詳解
這篇文章主要為大家詳細(xì)介紹了Java網(wǎng)絡(luò)編程中的UDP網(wǎng)絡(luò)通信的原理與實現(xiàn),文中的示例代碼講解詳細(xì),具有一定的借鑒價值,需要的可以參考一下2022-09-09
淺談System.getenv()和System.getProperty()的區(qū)別
這篇文章主要介紹了System.getenv()和System.getProperty()的區(qū)別,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
java定時任務(wù)Timer和TimerTask使用詳解
這篇文章主要為大家詳細(xì)介紹了java定時任務(wù)Timer和TimerTask使用方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-02-02
java實現(xiàn)文件和base64相互轉(zhuǎn)換
這篇文章主要為大家詳細(xì)介紹了java如何實現(xiàn)文件和base64相互轉(zhuǎn)換,文中的示例代碼講解詳細(xì),具有一定的參考價值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-11-11

