首頁> Java> java教程> 主體

SpringBoot怎麼實作模組日誌入庫

WBOY
發布: 2023-05-11 09:37:05
轉載
926 人瀏覽過

1.簡述

模組日誌的實作方式大致有三種:

  • #AOP 自訂註解實作

  • 輸出指定格式日誌日誌掃描實作

  • 在介面中透過程式碼侵入的方式,在業務邏輯處理之後,呼叫方法記錄日誌。

這裡我們主要討論下第3種實作方式。

假設我們需要實作一個使用者登入之後記錄登入日誌的動作。

呼叫關係如下:

SpringBoot怎麼實作模組日誌入庫

這裡的核心程式碼是在LoginService.login() 方法中設定了在交易結束後執行:

// 指定事务提交后执行 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { // 不需要事务提交前的操作,可以不用重写这个方法 @Override public void beforeCommit(boolean readOnly) { System.out.println("事务提交前执行"); } @Override public void afterCommit() { System.out.println("事务提交后执行"); } });
登入後複製

在這裡,我們把這段程式碼封裝成了工具類,參考:4.TransactionUtils。

如果在LoginService.login() 方法中開啟了事務,不指定事務提交後指定的話,日誌處理的方法做非同步和做新事務都會有問題:

  • 做非同步:由於主事務可能沒有執行完畢,導致可能讀取不到主事務中新增或修改的資料資訊;

  • 做新事物:可以透過Propagation .REQUIRES_NEW 事務傳播行為來建立新事務,在新事務中執行記錄日誌的操作,可能會導致以下問題:

    • 由於資料庫預設事務隔離等級是可重複讀,表示事物之間讀取不到未提交的內容,所以也會導致讀取不到主事務中新增或修改的資料資訊;

    • 如果開啟的新事務和先前的事務操作了同一個表,就會導致鎖定表。

  • 什麼都不做,直接同步呼叫:問題最多,可能導致以下幾個問題:

    • ##不捕獲異常,直接導致介面所有操作回滾;

    • 捕獲異常,部分資料庫,如:PostgreSQL,同一事務中,只要有一次執行失敗,就算捕獲異常,剩餘的資料庫操作也會全部失敗,拋出例外;

    • 日誌記錄耗時增加介面回應時間,影響使用者體驗。

2.LoginController

@RestController public class LoginController { @Autowired private LoginService loginService; @RequestMapping("/login") public String login(String username, String pwd) { loginService.login(username, pwd); return "succeed"; } }
登入後複製

3.Action

/** * 

@Title Action *

@Description 自定义动作函数式接口 * * @author ACGkaka * @date 2023/4/26 13:55 */ public interface Action { /** * 执行动作 */ void doSomething(); }

登入後複製

4.TransactionUtils

import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; /** * 

@Title TransactionUtils *

@Description 事务同步工具类 * * @author ACGkaka * @date 2023/4/26 13:45 */ public class TransactionUtils { /** * 提交事务前执行 */ public static void beforeTransactionCommit(Action action) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void beforeCommit(boolean readOnly) { // 异步执行 action.doSomething(); } }); } /** * 提交事务后异步执行 */ public static void afterTransactionCommit(Action action) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { // 异步执行 action.doSomething(); } }); } }

登入後複製

5.LoginService

@Service public class LoginService { @Autowired private LoginLogService loginLogService; /** 登录 */ @Transactional(rollbackFor = Exception.class) public void login(String username, String pwd) { // 用户登录 // TODO: 实现登录逻辑.. // 事务提交后执行 TransactionUtil.afterTransactionCommit(() -> { // 异步执行 taskExecutor.execute(() -> { // 记录日志 loginLogService.recordLog(username); }); }); } }
登入後複製

6.LoginLogService

6.1 @Async實作非同步

@Service public class LoginLogService { /** 记录日志 */ @Async @Transactional(rollbackFor = Exception.class) public void recordLog(String username) { // TODO: 实现记录日志逻辑... } }
登入後複製

注意:@Async 需要配合@EnableAsync 使用,@EnableAsync 加入啟動類別、設定類別、自訂執行緒池類上均可。

補充:由於@Async 註解會動態建立一個繼承類別來擴展方法的實現,所以可能會導致目前類別注入Bean容器失敗BeanCurrentlyInCreationException,可以使用以下方式:自訂執行緒池@Autowired

6.2 自訂執行緒池實作非同步

1)自訂執行緒池

AsyncTaskExecutorConfig.java

import com.demo.async.ContextCopyingDecorator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ThreadPoolExecutor; /** * 

@Title AsyncTaskExecutorConfig *

@Description 异步线程池配置 * * @author ACGkaka * @date 2023/4/24 19:48 */ @EnableAsync @Configuration public class AsyncTaskExecutorConfig { /** * 核心线程数(线程池维护线程的最小数量) */ private int corePoolSize = 10; /** * 最大线程数(线程池维护线程的最大数量) */ private int maxPoolSize = 200; /** * 队列最大长度 */ private int queueCapacity = 10; @Bean public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setThreadNamePrefix("MyExecutor-"); // for passing in request scope context 转换请求范围的上下文 executor.setTaskDecorator(new ContextCopyingDecorator()); // rejection-policy:当pool已经达到max size的时候,如何处理新任务 // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setWaitForTasksToCompleteOnShutdown(true); executor.initialize(); return executor; } }

登入後複製

2)複製上下文請求

ContextCopyingDecorator .java

import org.slf4j.MDC; import org.springframework.core.task.TaskDecorator; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import java.util.Map; /** * 

@Title ContextCopyingDecorator *

@Description 上下文拷贝装饰者模式 * * @author ACGkaka * @date 2023/4/24 20:20 */ public class ContextCopyingDecorator implements TaskDecorator { @Override public Runnable decorate(Runnable runnable) { try { // 从父线程中获取上下文,然后应用到子线程中 RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); Map previous = MDC.getCopyOfContextMap(); SecurityContext securityContext = SecurityContextHolder.getContext(); return () -> { try { if (previous == null) { MDC.clear(); } else { MDC.setContextMap(previous); } RequestContextHolder.setRequestAttributes(requestAttributes); SecurityContextHolder.setContext(securityContext); runnable.run(); } finally { // 清除请求数据 MDC.clear(); RequestContextHolder.resetRequestAttributes(); SecurityContextHolder.clearContext(); } }; } catch (IllegalStateException e) { return runnable; } } }

登入後複製

3)自訂執行緒池實作非同步LoginService

import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; @Service public class LoginService { @Autowired private LoginLogService loginLogService; @Qualifier("taskExecutor") @Autowired private TaskExecutor taskExecutor; /** 登录 */ @Transactional(rollbackFor = Exception.class) public void login(String username, String pwd) { // 用户登录 // TODO: 实现登录逻辑.. // 事务提交后执行 TransactionUtil.afterTransactionCommit(() -> { // 异步执行 taskExecutor.execute(() -> { // 记录日志 loginLogService.recordLog(username); }); }); } }
登入後複製

7.其他解決方案

7.1 使用編程式事務來取代@Transactional

我們也可以使用TransactionTemplate來取代@Transactional 註解:

import org.springframework.transaction.support.TransactionTemplate; @Service public class LoginService { @Autowired private LoginLogService loginLogService; @Autowired private TransactionTemplate transactionTemplate; /** 登录 */ public void login(String username, String pwd) { // 用户登录 transactionTemplate.execute(status->{ // TODO: 实现登录逻辑.. }); // 事务提交后异步执行 taskExecutor.execute(() -> { // 记录日志 loginLogService.recordLog(username); }); } }
登入後複製
經過測試:

這種實作方式拋出例外後,交易也可以正常回滾

#正常執行之後也可以讀取到事務執行後的內容,可行。

別看日誌記錄好實現,坑是真的多,這裡記錄的只是目前遇到的問題。

以上是SpringBoot怎麼實作模組日誌入庫的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:yisu.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!