首頁  >  文章  >  Java  >  SpringBoot怎麼實作模組日誌入庫

SpringBoot怎麼實作模組日誌入庫

WBOY
WBOY轉載
2023-05-11 09:37:05968瀏覽

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

/**
 * <p> @Title Action
 * <p> @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;
/**
 * <p> @Title TransactionUtils
 * <p> @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;
/**
 * <p> @Title AsyncTaskExecutorConfig
 * <p> @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;
/**
 * <p> @Title ContextCopyingDecorator
 * <p> @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<String, String> 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刪除