1.简述

模块日志的实现方式大致有三种:

这里我们主要讨论下第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() 方法中开启了事务,不指定事务提交后指定的话,日志处理的方法做异步和做新事务都会有问题:

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

经测试:

这种实现方式抛出异常后,事务也可以正常回滚

正常执行之后也可以读取到事务执行后的内容,可行。

别看日志记录好实现,坑是真的多,这里记录的只是目前遇到的问题。

本文转载于:https://www.yisu.com/zixun/803271.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。