Maison  >  Article  >  Java  >  Comment SpringBoot implémente le stockage des journaux de module

Comment SpringBoot implémente le stockage des journaux de module

WBOY
WBOYavant
2023-05-11 09:37:05968parcourir

1. Brève description

Il existe environ trois façons d'implémenter les journaux de module :

  • AOP + implémentation d'annotations personnalisées

  • sortie du journal au format spécifié + implémentation de l'analyse des journaux

  • via l'intrusion de code dans l'interface, après traitement de la logique métier, appelez la méthode pour enregistrer le journal.

Ici, nous discutons principalement de la troisième méthode de mise en œuvre.

Supposons que nous devions implémenter une opération d'enregistrement des journaux de connexion après la connexion d'un utilisateur.

La relation d'appel est la suivante :

Comment SpringBoot implémente le stockage des journaux de module

Le code principal ici est défini dans la méthode LoginService.login() à exécuter une fois la transaction terminée :

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

Ici, nous encapsulons ce code dans une classe d'outils, reportez-vous à :4.TransactionUtils.

Si vous activez une transaction dans la méthode LoginService.login() et ne la spécifiez pas après la soumission de la transaction, il y aura des problèmes avec la méthode de traitement asynchrone des journaux et les nouvelles transactions :

  • Asynchrone : car la transaction principale ne peut pas être exécuté Terminé, les informations de données nouvellement ajoutées ou modifiées dans la transaction principale peuvent ne pas être lues

  • Faites de nouvelles choses : vous pouvez créer une nouvelle transaction via le comportement de propagation de la transaction Propagation.REQUIRES_NEW et effectuer l'opération de journalisation ; dans la nouvelle transaction. Cela peut entraîner les problèmes suivants : 

    • Étant donné que le niveau d'isolement des transactions par défaut de la base de données est une lecture répétable, cela signifie que le contenu non validé ne peut pas être lu entre les transactions, ce qui entraînera également l'échec de la lecture. lire des données nouvelles ou nouvelles dans la transaction principale. Informations sur les données modifiées

    • Si la nouvelle transaction ouverte exploite la même table que la transaction précédente, la table sera verrouillée.

  • Ne rien faire, appeler directement de manière synchrone : le plus problématique, qui peut entraîner les problèmes suivants :

    • Ne détecte pas les exceptions, ce qui entraîne directement le rollback de toutes les opérations sur l'interface ; Attrapez les exceptions, certaines bases de données, telles que : PostgreSQL, dans la même transaction, tant qu'une exécution échoue, même si l'exception est interceptée, toutes les opérations de base de données restantes échoueront et des exceptions seront levées

    • Cela prend du temps ; la journalisation augmente le temps de réponse de l’interface et affecte l’expérience utilisateur.

    • 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 implémente asynchrone

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

Remarque : @Async doit coopérer avec @EnableAsync Utilisation, @EnableAsync peut être ajouté à la classe de démarrage, à la classe de configuration ou à la classe de pool de threads personnalisés.

Supplément : étant donné que l'annotation @Async créera dynamiquement une classe héritée pour étendre l'implémentation de la méthode, cela peut entraîner l'échec de l'injection de la classe actuelle dans le conteneur Bean et provoquer une exception BeanCurrentlyInCreation. Vous pouvez utiliser la méthode suivante : thread personnalisé. pool + @Autowired

6.2 Le pool de threads personnalisés implémente l'asynchrone

1) Le pool de threads personnalisé

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) La demande de contexte de copie

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) Le pool de threads personnalisé implémente le LoginService

r asynchrone rreee

7. Autres solutions

7.1 Utiliser des transactions programmatiques pour remplacer @Transactional

Nous pouvons également utiliser TransactionTemplate pour remplacer @Transactional Annotation :

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

Testé :

Après que cette implémentation ait généré une exception, la transaction peut également être annulée normalement.

Exécuter normalement Vous pouvez également lire le contenu une fois la transaction exécutée ultérieurement, ce qui est faisable.

Même s'il est facile de mettre en œuvre la journalisation, il existe de nombreux pièges. Ce qui est enregistré ici ne concerne que les problèmes rencontrés jusqu'à présent.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer