欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot日志配置全過(guò)程

 更新時(shí)間:2025年01月14日 14:40:04   作者:A塵埃  
Spring Boot默認(rèn)使用Logback作為日志框架,可以配置多種日志系統(tǒng),包括JavaUtilLogging、CommonsLogging、Log4J及SLF4J,默認(rèn)日志輸出在控制臺(tái),可以通過(guò)配置文件將日志保存到文件中,日志級(jí)別包括TRACE、DEBUG、INFO、WARN、ERROR和FATAL

SpringBoot日志配置

如果使用Spring Boot Starters,那么默認(rèn)使用的日志框架是Logback。Spring Boot底層對(duì)Java Util Logging、Commons Logging、Log4J及SLF4J日志框架也進(jìn)行了適配,只需相關(guān)配置就可以實(shí)現(xiàn)日志框架的相互切換。

SpringBoot默認(rèn)日志事打印在console控制臺(tái)中,不會(huì)保存到文件中。

實(shí)際項(xiàng)目中必須保存到文件中進(jìn)行日志分析

根據(jù)不同的日志系統(tǒng),可以按如下規(guī)則組織配置文件名,就能被正確加載:

  • Spring Boot官方推薦優(yōu)先使用帶有-spring的文件名作為定義的日志配置(使用logback-spring.xml而不是logback.xml名稱(chēng))
  • 若命名為logback-spring.xml的日志配置文件,Spring Boot可以為它添加一些Spring Boot特有的配置項(xiàng)
  • 建議盡可能不使用Java Util Logging方式,因?yàn)镴ava Util Logging從可執(zhí)行jar運(yùn)行時(shí)會(huì)導(dǎo)致一些已知的類(lèi)加載問(wèn)題

自定義日志配置:

  • 通過(guò)將相應(yīng)的庫(kù)添加到classpath可以激活各種日志系統(tǒng)
  • 在classpath根目錄下提供合適的配置文件可以進(jìn)一步定制日志系統(tǒng)
  • 配置文件也可以通過(guò)Spring Environment的logging.config屬性指定

日志分級(jí):(TRACE < DEBUG < INFO< WARN < ERROR < FATAL)從低到高

  • TRACE,最低級(jí)別的日志記錄,用于輸出最詳細(xì)的調(diào)試信息,通常用于開(kāi)發(fā)調(diào)試目的。在生產(chǎn)環(huán)境中,應(yīng)該關(guān)閉 TRACE 級(jí)別的日志記錄,以避免輸出過(guò)多無(wú)用信息
  • DEBUG,是用于輸出程序中的一些調(diào)試信息,通常用于開(kāi)發(fā)過(guò)程中。像 TRACE 一樣,在生產(chǎn)環(huán)境中應(yīng)該關(guān)閉 DEBUG 級(jí)別的日志記錄。
  • INFO,用于輸出程序正常運(yùn)行時(shí)的一些關(guān)鍵信息,比如程序的啟動(dòng)、運(yùn)行日志等。通常在生產(chǎn)環(huán)境中開(kāi)啟 INFO 級(jí)別的日志記錄。
  • WARN,是用于輸出一些警告信息,提示程序可能會(huì)出現(xiàn)一些異常或者錯(cuò)誤。在應(yīng)用程序中,WARN 級(jí)別的日志記錄通常用于記錄一些非致命性異常信息,以便能夠及時(shí)發(fā)現(xiàn)并處理這些問(wèn)題。
  • ERROR,是用于輸出程序運(yùn)行時(shí)的一些錯(cuò)誤信息,通常表示程序出現(xiàn)了一些不可預(yù)料的錯(cuò)誤。在應(yīng)用程序中,ERROR 級(jí)別的日志記錄通常用于記錄一些致命性的異常信息,以便能夠及時(shí)發(fā)現(xiàn)并處理這些問(wèn)題。

Logback日志不提供FATAL級(jí)別,它被映射到ERROR級(jí)別。Spring Boot只會(huì)輸出比當(dāng)前級(jí)別高的日志,默認(rèn)的日志級(jí)別是INFO,因此低于INFO級(jí)別的日志記錄都不輸出

Spring Boot中默認(rèn)配置ERROR、WARN和INFO級(jí)別的日志輸出到控制臺(tái)。

通過(guò)啟動(dòng)您的應(yīng)用程序—debug標(biāo)志來(lái)啟用“調(diào)試”模式(開(kāi)發(fā)時(shí)推薦開(kāi)啟),以下兩種方式皆可:

  • 在運(yùn)行命令后加入–debug標(biāo)志,例如:java -jar springTest.jar --debug
  • 在application.properties中配置debug=true,該屬性置為true的時(shí)候,核心Logger(包含嵌入式容器、hibernate、spring)會(huì)輸出更多內(nèi)容,但是你自己應(yīng)用的日志并不會(huì)輸出為DEBUG級(jí)別。

除了這五種級(jí)別以外,還有一些日志框架定義了其他級(jí)別,例如 Python 中的 CRITICAL、PHP 中的 FATAL 等。CRITICAL 和 FATAL 都是用于表示程序出現(xiàn)了致命性錯(cuò)誤或者異常,即不可恢復(fù)的錯(cuò)誤。

使用xml配置日志保存

(并不需要pom配置slf4j依賴(lài),使用這個(gè)默認(rèn)不用配置pom依賴(lài),最新的spring-boot-starter-web中已經(jīng)集成了)

啟動(dòng)一個(gè)項(xiàng)目,直接將logback-spring.xml文件復(fù)制到resources目錄下就可以實(shí)現(xiàn)日志文件記錄。

步驟如下:

  1. 在項(xiàng)目resources目錄下創(chuàng)建一個(gè)logback-spring.xml日志配置文件

名稱(chēng)只要是logback開(kāi)頭

備注:要配置logback-spring.xml,springboot會(huì)默認(rèn)加載此文件,為什么不配置logback.xml,因?yàn)閘ogback.xml會(huì)先application.properties加載,而logback-spring.xml會(huì)后于application.properties加載,這樣我們?cè)赼pplication.properties文中設(shè)置日志文件名稱(chēng)和文件路徑才能生效。

  1. 內(nèi)容如下

Spring Boot 默認(rèn)日志輸出如下:

上述輸出的日志信息,從左往右含義解釋如下:

  • 日期時(shí)間:精確到毫秒
  • 日志級(jí)別:ERROR,WARN,INFO,DEBUG or TRACE
  • 進(jìn)程:id
  • 分割符:用于區(qū)分實(shí)際的日志記錄
  • 線程名:括在方括號(hào)中
  • 日志名字:通常是源類(lèi)名
  • 日志信息說(shuō)明

依賴(lài):

<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>
 <!--輸出到控制臺(tái)-->
 <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>
  1. 編寫(xiě)打印日志
@SpringBootTest
public class LoggerTest {

    private static final Logger logger = LoggerFactory.getLogger(LoggerTest.class);

    @Test
    public void test() {
        logger.trace("trace 級(jí)別的日志");
        logger.debug("debug 級(jí)別的日志");
        logger.info("info 級(jí)別的日志");
        logger.warn("warn 級(jí)別的日志");
        logger.error("error 級(jí)別的日志");
    }
}
  1. 啟動(dòng)測(cè)試

在當(dāng)前文件夾下會(huì)創(chuàng)建一個(gè)【poslog/2020-10/22】的文件夾,里面會(huì)按天生成日志:【2020-10-22.log】,例如:

控制臺(tái)輸出:

分類(lèi)logback.xml配置

需在application.properties中設(shè)置logging.file.name或logging.file.path屬性

1)logging.file.name,設(shè)置文件,可以是絕對(duì)路徑,也可以是相對(duì)路徑。例如:

logging.file.name=info.log

2)logging.file.path,設(shè)置目錄,會(huì)在該目錄下創(chuàng)建spring.log文件,并寫(xiě)入日志內(nèi)容,例如:

logging.file.path=/workspace/log

如果只配置logging.file.name,會(huì)在項(xiàng)目的當(dāng)前路徑下生成一個(gè)xxx.log日志文件。如果只配置logging.file.path,在/workspace/log文件夾生成一個(gè)為spring.log日志文件。

二者不能同時(shí)使用,如若同時(shí)使用,則只有l(wèi)ogging.file.name生效。默認(rèn)情況下,日志文件的大小達(dá)到10MB時(shí)會(huì)切分一次,產(chǎn)生新的日志文件,默認(rèn)級(jí)別為:ERROR、WARN、INFO。

所有支持的日志記錄系統(tǒng)都可以在Spring環(huán)境中設(shè)置記錄級(jí)別,格式為:“logging.level.* = LEVEL”。

雖然Spring Boot中application.properties配置文件提供了日志的配置,但是個(gè)人更傾向于logback.xml的配置方式。

日志配置到d盤(pán)了:

根節(jié)點(diǎn)包含的屬性

  • scan:當(dāng)此屬性設(shè)置為true時(shí),配置文件如果發(fā)生改變,將會(huì)被重新加載,默認(rèn)值為true
  • scanPeriod:設(shè)置監(jiān)測(cè)配置文件是否有修改的時(shí)間間隔,如果沒(méi)有給出時(shí)間單位,默認(rèn)單位是毫秒。當(dāng)scan為true時(shí),此屬性生效。默認(rèn)的時(shí)間間隔為1分鐘
  • debug:當(dāng)此屬性設(shè)置為true時(shí),將打印出logback內(nèi)部日志信息,實(shí)時(shí)查看logback運(yùn)行狀態(tài)。默認(rèn)值為false

子節(jié)點(diǎn)

  • root節(jié)點(diǎn)是必選節(jié)點(diǎn),用來(lái)指定最基礎(chǔ)的日志輸出級(jí)別,只有一個(gè)level屬性。
  • level:用來(lái)設(shè)置打印級(jí)別,大小寫(xiě)無(wú)關(guān),其值包含如下:TRACE、DEBUG、INFO、WARN、ERROR、ALL和OFF
  • level不能設(shè)置為INHERITED或者同義詞NULL,默認(rèn)是DEBUG。
  • root節(jié)點(diǎn)中可以包含零個(gè)或多個(gè)元素,標(biāo)識(shí)這個(gè)appender將會(huì)添加到這個(gè)loger

子節(jié)點(diǎn)設(shè)置上下文名稱(chēng)

每個(gè)logger都關(guān)聯(lián)到logger上下文,默認(rèn)上下文名稱(chēng)為“default”。但可以使用設(shè)置成其他名字,用于區(qū)分不同應(yīng)用程序的記錄。

設(shè)置后不能修改,通過(guò)%contextName設(shè)置來(lái)打印日志上下文名稱(chēng),一般來(lái)說(shuō)不用這個(gè)屬性

子節(jié)點(diǎn)

appender用來(lái)格式化日志輸出節(jié)點(diǎn),有兩個(gè)屬性name和class,class用來(lái)指定哪種輸出策略,常用就是控制臺(tái)輸出策略和文件輸出策略。

<?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" />

 <!-- 控制臺(tái)輸出 -->
 <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)政策:基于時(shí)間創(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">
   <!-- 過(guò)濾的級(jí)別 只會(huì)打印debug不會(huì)有info日志-->
<!--            <level>DEBUG</level>-->
   <!-- 匹配時(shí)的操作:接收(記錄) -->
   <onMatch>ACCEPT</onMatch>
   <!-- 不匹配時(shí)的操作:拒絕(不記錄) -->
   <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)政策:基于時(shí)間創(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">
   <!-- 過(guò)濾的級(jí)別 -->
   <level>ERROR</level>
   <!-- 匹配時(shí)的操作:接收(記錄) -->
   <onMatch>ACCEPT</onMatch>
   <!-- 不匹配時(shí)的操作:拒絕(不記錄) -->
   <onMismatch>DENY</onMismatch>
  </filter>
 </appender>
 
 <!-- 用戶(hù)訪問(wèn)日志輸出  -->
 <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)模塊日志級(jí)別控制  -->
 <logger name="com.example" level="debug" />
 <!-- Spring日志級(jí)別控制  -->
 <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)用戶(hù)操作日志-->
 <logger name="sys-user" level="info">
  <appender-ref ref="sys-user"/>
 </logger>
</configuration> 

注:1)控制臺(tái)和日志文件的字符集;2)日志文件的存放位置,須要遵守Linux的命名規(guī)則。

在application.yml中進(jìn)行設(shè)置日志級(jí)別

如果com.example: debug,那么項(xiàng)目com.example包里面的debug以上的日志也會(huì)輸出

logging:
	level:
		com.example: info
		org.springframework: warn

或者properties方式

#com.yoodb.study.demo04包下所有class以DEBUG級(jí)別輸出
logging.level.com.yoodb.study=DEBUG
#用來(lái)指定自己創(chuàng)建的日志文件
logging.config=classpath:logback-spring.xml
#指定輸出文件位置
logging.file.path=D://workspace/log

Controller

注:在添加引用時(shí),日志的包一定是org.slf4j.Logger、org.slf4j.LoggerFactory類(lèi)

@RestController  
public class HelloWorldController {  
   
    protected static Logger logger=LoggerFactory.getLogger(HelloWorldController.class);  
       
    @RequestMapping("/")  
    public String helloworld(){  
        logger.debug("關(guān)注微信公眾號(hào)“Java精選”,Spring Boot系列文章持續(xù)更新中,帶你從入門(mén)到精通,玩轉(zhuǎn)Spring Boot框架。");  
        return "Hello world!";  
    }  
       
    @RequestMapping("/hello/{name}")  
    public String helloName(@PathVariable String name){  
        logger.debug("訪問(wèn) helloName,Name={}",name);  
        return "Hello "+name;  
    }  
}

要解決的核心問(wèn)題:「誰(shuí)」在「什么時(shí)間」對(duì)「什么」做了「什么事」

方案 1:AOP 切面 + 注解

①、定義日志注解,用于標(biāo)記哪些方法需要記錄業(yè)務(wù)操作日志

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable{
	
	String value() default "";
	//可以添加更多的配置屬性,如操作類(lèi)型、級(jí)別
}

②、創(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í)行前的邏輯,例如記錄開(kāi)始事件、方法參數(shù)等
		long start = System.currentTimeMillis();
		try{
			Object result = jointPoint.proceed();//執(zhí)行目標(biāo)方法
			//方法執(zhí)行后的邏輯,例如記錄結(jié)束時(shí)間、返回值等
			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ù)邏輯
	}
}

缺點(diǎn):

  • 日志粒度和詳細(xì)度:切面雖然攔截了我們目標(biāo)方法,但其中能拿到的信息上下文有限,無(wú)法構(gòu)成一條操作日志所需的數(shù)據(jù)信息
  • 業(yè)務(wù)操作場(chǎng)景劃分:切面的定義和使用都是非業(yè)務(wù)化的,所以無(wú)法感知到新的業(yè)務(wù)操作范圍和業(yè)務(wù)的定義劃分邊界是如何處理
  • 級(jí)聯(lián)操作斷檔:當(dāng)業(yè)務(wù)操作是設(shè)計(jì)多表或者多個(gè)服務(wù)間的調(diào)用串聯(lián)時(shí),切面只能單獨(dú)記錄每個(gè)服務(wù)方法級(jí)別的數(shù)據(jù)信息,無(wú)法對(duì)調(diào)用鏈的部分進(jìn)行業(yè)務(wù)串聯(lián)

記錄到的日志數(shù)據(jù)都是固定的模板數(shù)據(jù),如:_XXX 修改了項(xiàng)目,XXX 新建了問(wèn)題數(shù)據(jù),XXX 刪除了風(fēng)險(xiǎn)問(wèn)題,因?yàn)槲覀儫o(wú)法通過(guò)每個(gè)切面對(duì)具體參數(shù)內(nèi)容和業(yè)務(wù)場(chǎng)景進(jìn)行捕獲。那么_如果我們想要在日志內(nèi)容中添加更多的業(yè)務(wù)上下文信息,如:XXX 修改了項(xiàng)目 ID=001 的數(shù)據(jù),XXX 刪除了產(chǎn)品 ID=002 的數(shù)據(jù),這時(shí)候就可以通過(guò)使用 AOP + SpEL 表達(dá)式來(lái)實(shí)現(xiàn)。

方案2:AOP 切面 + SpEL

①、對(duì)方案1的注解進(jìn)行內(nèi)容擴(kuò)展

@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ù)操作場(chǎng)景人

    String type(); // 業(yè)務(wù)場(chǎng)景 模塊范圍

    String subType() default ""; //業(yè)務(wù)子場(chǎng)景,主要是模塊下的功能范圍

    String bizNo(); //業(yè)務(wù)場(chǎng)景的業(yè)務(wù)編號(hào),

    String extra() default "";//一些操作的擴(kuò)展操作

    String actionType(); //業(yè)務(wù)操作類(lèi)型,比如編輯、新增、刪除
}

②、基于注解進(jìn)行定義SpEL的解析器來(lái)對(duì)注解中的字段進(jìn)行解析和使用

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;
	}
}

③、表達(dá)式使用

系統(tǒng)中實(shí)際業(yè)務(wù)操作的使用場(chǎng)景,在注解的內(nèi)容中填充了很多業(yè)務(wù)操作場(chǎng)景的數(shù)據(jù),如果需要涉及操作前后數(shù)據(jù)的內(nèi)容記錄,還可以再次進(jìn)行擴(kuò)充 SpEL 的字段及解析邏輯,可以說(shuō)是有了它,我們可以做的更多了?。ǖ亲⒔庖苍絹?lái)越長(zhǎng)了)

優(yōu)缺點(diǎn):

  • 解決了方案1中冗余重復(fù)代碼層面的侵入,但會(huì)出現(xiàn)大量注解定義的出現(xiàn),也帶有一定的侵入性
  • 日志內(nèi)容還是需要系統(tǒng)自身根據(jù)上報(bào)場(chǎng)景進(jìn)行封裝,需要從產(chǎn)品的業(yè)務(wù)定義到研發(fā)編碼達(dá)成統(tǒng)一共識(shí)
  • 與方案1相比簡(jiǎn)化了一部分代碼集成的復(fù)雜度,只需編寫(xiě)自定義注解即可
  • 與方案1相比擴(kuò)展了對(duì)操作的業(yè)務(wù)數(shù)據(jù)廣度,數(shù)據(jù)范圍大大增加,而且還可根據(jù)自身業(yè)務(wù)定義無(wú)限擴(kuò)展

解決了業(yè)務(wù)操作日志的一個(gè)收集問(wèn)題,能夠清晰的記錄各類(lèi)操作場(chǎng)景、動(dòng)作、數(shù)據(jù)前后的內(nèi)容等

方案3:Binlog + 時(shí)間窗口

怎么從應(yīng)用層對(duì)操作場(chǎng)景、數(shù)據(jù)進(jìn)行抓包、處理邏輯、保存,所以復(fù)雜度都會(huì)集中到應(yīng)用層。既然是這樣我們能不能直接基于底層的 MySQL 本身來(lái)處理這件事兒呢?

Binlog 是數(shù)據(jù)庫(kù)中二進(jìn)制格式的文件,用于記錄用戶(hù)對(duì)數(shù)據(jù)庫(kù)更新的 SQL 語(yǔ)句信息,例如更改數(shù)據(jù)庫(kù)表和更改內(nèi)容的 SQL 語(yǔ)句都會(huì)記錄到 binlog 里。那么 Binlog 能用來(lái)記錄業(yè)務(wù)層面的數(shù)據(jù)變化內(nèi)容嗎?

問(wèn)題 1:無(wú)法對(duì)多表存在級(jí)聯(lián)保存和更新的數(shù)據(jù)進(jìn)行非常好的兼容支持,因?yàn)楸旧韇inlog數(shù)據(jù)是無(wú)序的,并且如果上游數(shù)據(jù)的操作不是包裹在一個(gè)事務(wù)中,也很難處理

解決問(wèn)題 1:由于本身 binlog 的無(wú)序性,所以無(wú)法對(duì)大量 binlog 進(jìn)行有序組合,如果本身是一個(gè)事務(wù)提交的還可以根據(jù)事務(wù) KEY 進(jìn)行組合,如果不是呢?這里可以考慮借鑒 Flink 的時(shí)間窗口機(jī)制:滾動(dòng)的時(shí)間窗口將每個(gè)元素指定給指定窗口大小的窗口,滾動(dòng)窗口具有固定大小,且不重疊。

例如,我們指定一個(gè)大小為 1 分鐘的滾動(dòng)窗口,在這種情況下,我們將每隔 1 分鐘開(kāi)啟一個(gè)新的窗口,其中每一條數(shù)都會(huì)劃分到唯一一個(gè) 1 分鐘的窗口中,如下圖所示:

基于以上的窗口機(jī)制,我們就可以對(duì)數(shù)據(jù)先進(jìn)行范圍的框定,通過(guò)窗口的滑動(dòng)機(jī)制和補(bǔ)償機(jī)制對(duì)窗口中的數(shù)據(jù)進(jìn)行關(guān)聯(lián)處理。但光靠時(shí)間窗口還是無(wú)法對(duì) binlog 進(jìn)行關(guān)聯(lián),那我們就從關(guān)聯(lián)數(shù)據(jù)本身下手,這類(lèi)數(shù)據(jù)關(guān)聯(lián)復(fù)雜主要是涉及表之間的引用關(guān)系,那我們?cè)谶M(jìn)行定義 binlog 解析時(shí)就把前后數(shù)據(jù) + 表之間的引用字段都進(jìn)行指定,這樣在窗口中進(jìn)行滑動(dòng)關(guān)聯(lián)時(shí),就可以進(jìn)行子表的引用字段關(guān)聯(lián)了!這樣關(guān)聯(lián)字段補(bǔ)償更新的機(jī)制就可以解決問(wèn)題 1 了。

//部分的 binlog 數(shù)據(jù)變動(dòng)結(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;
}

問(wèn)題 2:關(guān)于更新人的問(wèn)題,系統(tǒng)進(jìn)行更新時(shí)如果未手動(dòng)更新對(duì)應(yīng)操作人,則系統(tǒng)無(wú)法識(shí)別,需要上游做對(duì)應(yīng)場(chǎng)景的統(tǒng)一改造,但從系統(tǒng)承接來(lái)看,本身系統(tǒng)的操作人就是要跟著業(yè)務(wù)操作一起進(jìn)行聯(lián)動(dòng)的

解決問(wèn)題 2:關(guān)于更新人的問(wèn)題其實(shí)是各系統(tǒng)需要自己排除解決的問(wèn)題,因?yàn)楸旧順I(yè)務(wù)在進(jìn)行數(shù)據(jù)操作時(shí)就是需要留痕更新人信息,比較統(tǒng)一的方案就是基于底層的 ORM 框架來(lái)統(tǒng)一進(jìn)行攔截處理,大家可以自行 GPT。

總結(jié):

  • 基于 binlog 后,我們對(duì)底層的數(shù)據(jù)變動(dòng)感知更明顯了,但是 binlog 的數(shù)據(jù)來(lái)源除了系統(tǒng)應(yīng)用層還有很多其他來(lái)源,比如我們的數(shù)據(jù)庫(kù)工單,日常跑批刷數(shù)等場(chǎng)景,這類(lèi)的數(shù)據(jù)變動(dòng)范圍可能較大,而且感知較弱。
  • 方案 3 的設(shè)計(jì)把方案 2 中的業(yè)務(wù)場(chǎng)景(也就是 actiontype subtype 等)弱化了,所以并不能很好的感知到很細(xì)顆粒度。

項(xiàng)目中應(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實(shí)現(xiàn)類(lèi)中的使用日志

@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名稱(chēng)不能為空!");
		}
		
		if(checkName(request.getSpuName(), null)){
			return Response.toError(ResponseStatus.Code.FAIL_CODE, "spu名稱(chēng)已存在!");
		}

		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失敗");
	}
}

校驗(yàn)名稱(chēng)的唯一性

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));
}

項(xiàng)目中注解和日志的結(jié)合

①、注解

/**
	自定義操作日志記錄注解
*/
@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog{
	
	//模塊
	public String title() default "";

	//功能
	public BusinessType businessType() default BusinessType.OTHER;

	//操作人類(lèi)別
	public OperatorType operatorType() default OperatorType.MANAGE;

	//是否保存請(qǐng)求的參數(shù)
	public boolean isSaveRequestData() default true;
}
/**
 * 業(yè)務(wù)操作類(lèi)型
 *
 * @author ruoyi
 */
public enum BusinessType {
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 刪除
     */
    DELETE,

    /**
     * 授權(quán)
     */
    GRANT,

    /**
     * 導(dǎo)出
     */
    EXPORT,

    /**
     * 導(dǎo)入
     */
    IMPORT,

    /**
     * 強(qiáng)退
     */
    FORCE,

    /**
     * 生成代碼
     */
    GENCODE,

    /**
     * 清空
     */
    CLEAN,
}
/**
 * 操作人類(lèi)別
 *
 * @author ruoyi
 */
public enum OperatorType {
    /**
     * 其它
     */
    OTHER,

    /**
     * 后臺(tái)用戶(hù)
     */
    MANAGE,

    /**
     * 手機(jī)端用戶(hù)
     */
    MOBILE
}

②、切面

@Aspect
@Slf4j
@Document
public class OperLogAspect{
	
	//配置織入點(diǎn)(注解)
	@Pointcut("@annotation(com.ruoyi.system.log.annotation.OperLog)")
	public void logPointCut(){}
	
	//處理完請(qǐng)求后執(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ù)庫(kù)日志=========*//
            OperLog operLog = new OperLog();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 請(qǐng)求的地址
            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è)置方法名稱(chēng)
			String className = joinPoint.getTarget().getClass().getName();
			Strng methodName = joinPoint.getSignature().getName();
			operLog.setMethod(className + "." + methodName + "()");
			
			//設(shè)置請(qǐng)求方式
			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;
	}

	//獲取注解中對(duì)方法的描述信息,用于Controller層注解
	public void getControllerMethodDescription(com.ruoyi.system.log.annotation.OperLog log, OperLog operLog, Object[] args) 
												throws Exception {
		// 設(shè)置action動(dòng)作
        operLog.setBusinessType(log.businessType().ordinal());
        // 設(shè)置標(biāo)題
        operLog.setTitle(log.title());
        // 設(shè)置操作人類(lèi)別
        operLog.setOperatorType(log.operatorType().ordinal());
        // 是否需要保存request,參數(shù)和值
        if (log.isSaveRequestData()) {
            // 獲取參數(shù)的信息,傳入到數(shù)據(jù)庫(kù)中。
            setRequestValue(operLog, args);
        }
	}

	//獲取請(qǐng)求的參數(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));
    }
}

工具類(lèi) 

/**
 * 客戶(hù)端工具類(lèi)
 *
 * @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;
    }

    /**
     * 將字符串渲染到客戶(hù)端
     *
     * @param response 渲染對(duì)象
     * @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異步請(qǐng)求
     *
     * @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 "未知";
    }
}
/**
 * 獲取地址類(lèi)
 *
 * @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)不查詢(xún)
        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;
	}

	//實(shí)現(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);
    }

	//實(shí)現(xiàn)DisposableBean接口, 在Context關(guān)閉時(shí)清理靜態(tài)變量.
	@Override
    @SneakyThrows
    public void destroy() {
        SpringContextHolder.clearHolder();
    }

	//獲取運(yùn)行環(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é)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • 詳解Spring Boot 自定義PropertySourceLoader

    詳解Spring Boot 自定義PropertySourceLoader

    這篇文章主要介紹了詳解Spring Boot 自定義PropertySourceLoader,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-05-05
  • Spring?MVC?請(qǐng)求映射路徑的配置實(shí)現(xiàn)前后端交互

    Spring?MVC?請(qǐng)求映射路徑的配置實(shí)現(xiàn)前后端交互

    在Spring?MVC中,請(qǐng)求映射路徑是指與特定的請(qǐng)求處理方法關(guān)聯(lián)的URL路徑,這篇文章主要介紹了Spring?MVC?請(qǐng)求映射路徑的配置,實(shí)現(xiàn)前后端交互,需要的朋友可以參考下
    2023-09-09
  • Spring實(shí)戰(zhàn)之Bean的后處理器操作示例

    Spring實(shí)戰(zhàn)之Bean的后處理器操作示例

    這篇文章主要介紹了Spring實(shí)戰(zhàn)之Bean的后處理器操作,結(jié)合實(shí)例形式詳細(xì)分析了Bean的后處理器相關(guān)配置、操作方法及使用注意事項(xiàng),需要的朋友可以參考下
    2019-12-12
  • SpringBoot整合MQTT小結(jié)匯總

    SpringBoot整合MQTT小結(jié)匯總

    MQTT 客戶(hù)端是運(yùn)行 MQTT 庫(kù)并通過(guò)網(wǎng)絡(luò)連接到 MQTT 代理的任何設(shè)備,是一種基于發(fā)布/訂閱(publish/subscribe)模式的“輕量級(jí)”通訊協(xié)議,該協(xié)議構(gòu)建于 TCP/IP 協(xié)議上,由 IBM 于 1999 年發(fā)明,對(duì)SpringBoot整合MQTT相關(guān)知識(shí)感興趣的朋友一起看看吧
    2022-01-01
  • 淺試仿?mapstruct實(shí)現(xiàn)微服務(wù)編排框架詳解

    淺試仿?mapstruct實(shí)現(xiàn)微服務(wù)編排框架詳解

    這篇文章主要為大家介紹了淺試仿?mapstruct實(shí)現(xiàn)微服務(wù)編排框架詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • Java 全排列的幾種實(shí)現(xiàn)方法

    Java 全排列的幾種實(shí)現(xiàn)方法

    本文詳細(xì)介紹了Java中全排列問(wèn)題的幾種實(shí)現(xiàn)方法,包括回溯法、字典序排列法和迭代法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2024-11-11
  • Java中jdk1.8和jdk17相互切換實(shí)戰(zhàn)步驟

    Java中jdk1.8和jdk17相互切換實(shí)戰(zhàn)步驟

    之前做Java項(xiàng)目時(shí)一直用的是jdk1.8,現(xiàn)在想下載另一個(gè)jdk版本17,并且在之后的使用中可以進(jìn)行相互切換,下面這篇文章主要給大家介紹了關(guān)于Java中jdk1.8和jdk17相互切換的相關(guān)資料,需要的朋友可以參考下
    2023-05-05
  • Java判斷一個(gè)時(shí)間是否在當(dāng)前時(shí)間區(qū)間代碼示例

    Java判斷一個(gè)時(shí)間是否在當(dāng)前時(shí)間區(qū)間代碼示例

    這篇文章主要給大家介紹了關(guān)于使用Java判斷一個(gè)時(shí)間是否在當(dāng)前時(shí)間區(qū)間的相關(guān)資料,在日常開(kāi)發(fā)中我們經(jīng)常會(huì)涉及到時(shí)間的大小比較或者是判斷某個(gè)時(shí)間是否在某個(gè)時(shí)間段內(nèi),需要的朋友可以參考下
    2023-07-07
  • Springboot視圖解析器ViewResolver使用實(shí)例

    Springboot視圖解析器ViewResolver使用實(shí)例

    這篇文章主要介紹了Springboot視圖解析器ViewResolver使用實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-04-04
  • SpringBoot多環(huán)境日志配置方式

    SpringBoot多環(huán)境日志配置方式

    SpringBoot?默認(rèn)使用LogBack日志系統(tǒng),默認(rèn)情況下,SpringBoot項(xiàng)目的日志只會(huì)在控制臺(tái)輸入,本文給大家介紹SpringBoot多環(huán)境日志配置方式,需要的朋友可以參考下
    2024-08-08

最新評(píng)論