SpringBoot日志配置全過程
SpringBoot日志配置
如果使用Spring Boot Starters,那么默認使用的日志框架是Logback。Spring Boot底層對Java Util Logging、Commons Logging、Log4J及SLF4J日志框架也進行了適配,只需相關(guān)配置就可以實現(xiàn)日志框架的相互切換。
SpringBoot默認日志事打印在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,最低級別的日志記錄,用于輸出最詳細的調(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)前級別高的日志,默認的日志級別是INFO,因此低于INFO級別的日志記錄都不輸出
Spring Boot中默認配置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依賴,使用這個默認不用配置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會默認加載此文件,為什么不配置logback.xml,因為logback.xml會先application.properties加載,而logback-spring.xml會后于application.properties加載,這樣我們在application.properties文中設(shè)置日志文件名稱和文件路徑才能生效。
- 內(nèi)容如下
Spring Boot 默認日志輸出如下:
上述輸出的日志信息,從左往右含義解釋如下:
- 日期時間:精確到毫秒
- 日志級別: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生效。默認情況下,日志文件的大小達到10MB時會切分一次,產(chǎ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ā)生改變,將會被重新加載,默認值為true
- scanPeriod:設(shè)置監(jiān)測配置文件是否有修改的時間間隔,如果沒有給出時間單位,默認單位是毫秒。當(dāng)scan為true時,此屬性生效。默認的時間間隔為1分鐘
- debug:當(dāng)此屬性設(shè)置為true時,將打印出logback內(nèi)部日志信息,實時查看logback運行狀態(tài)。默認值為false
子節(jié)點
- root節(jié)點是必選節(jié)點,用來指定最基礎(chǔ)的日志輸出級別,只有一個level屬性。
- level:用來設(shè)置打印級別,大小寫無關(guān),其值包含如下:TRACE、DEBUG、INFO、WARN、ERROR、ALL和OFF
- level不能設(shè)置為INHERITED或者同義詞NULL,默認是DEBUG。
- root節(jié)點中可以包含零個或多個元素,標(biāo)識這個appender將會添加到這個loger
子節(jié)點設(shè)置上下文名稱
每個logger都關(guān)聯(lián)到logger上下文,默認上下文名稱為“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ù)邏輯 } }
缺點:
- 日志粒度和詳細度:切面雖然攔截了我們目標(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 的字段及解析邏輯,可以說是有了它,我們可以做的更多了?。ǖ亲⒔庖苍絹碓介L了)
優(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 等)弱化了,所以并不能很好的感知到很細顆粒度。
項目中應(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)文章
詳解Spring Boot 自定義PropertySourceLoader
這篇文章主要介紹了詳解Spring Boot 自定義PropertySourceLoader,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05Spring?MVC?請求映射路徑的配置實現(xiàn)前后端交互
在Spring?MVC中,請求映射路徑是指與特定的請求處理方法關(guān)聯(lián)的URL路徑,這篇文章主要介紹了Spring?MVC?請求映射路徑的配置,實現(xiàn)前后端交互,需要的朋友可以參考下2023-09-09淺試仿?mapstruct實現(xiàn)微服務(wù)編排框架詳解
這篇文章主要為大家介紹了淺試仿?mapstruct實現(xiàn)微服務(wù)編排框架詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08Java中jdk1.8和jdk17相互切換實戰(zhàn)步驟
之前做Java項目時一直用的是jdk1.8,現(xiàn)在想下載另一個jdk版本17,并且在之后的使用中可以進行相互切換,下面這篇文章主要給大家介紹了關(guān)于Java中jdk1.8和jdk17相互切換的相關(guān)資料,需要的朋友可以參考下2023-05-05Java判斷一個時間是否在當(dāng)前時間區(qū)間代碼示例
這篇文章主要給大家介紹了關(guān)于使用Java判斷一個時間是否在當(dāng)前時間區(qū)間的相關(guān)資料,在日常開發(fā)中我們經(jīng)常會涉及到時間的大小比較或者是判斷某個時間是否在某個時間段內(nèi),需要的朋友可以參考下2023-07-07Springboot視圖解析器ViewResolver使用實例
這篇文章主要介紹了Springboot視圖解析器ViewResolver使用實例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-04-04