Ne marchez pas sur la fosse! L'ordre d'exécution des aspects Spring AOP est un grand trou

Préface

L'entreprise a lancé un tout nouveau projet au cours des deux derniers mois, et le calendrier du projet a expiré, mais il est encore nécessaire d'apprendre. Non, quand j'ai monté un projet pour l'entreprise, j'ai marché sur une fosse Spring AOP.

Points forts de cet article:

  • Description du problème
  • Ordre d'exécution Spring AOP
  • Explorez la vérité sur le mauvais ordre
  • Vérification du code
  • en conclusion

Description du problème

Le nouveau projet de l'entreprise doit créer un nouveau service HTTP séparé de l'avant à l'arrière. J'ai choisi le SpringBoot Web familier pour créer rapidement un système utilisable.

Lu Xun a dit, ne vous contentez pas de mettre à niveau la version stable . Je ne crois pas à ce mal, j'utilise Spring depuis si longtemps, alors comment ne pas me précipiter. Sans oublier que la dernière version de SprinBoot 2.3.4.RELEASE a été directement introduite, et le projet a commencé à être construit.

Au début, l'introduction de la plupart des composants s'est bien déroulée. Je pensais que c'était sur le point d'être fait, mais je ne m'attendais pas à tomber sur la section du journal .

En tant que service d'interface, afin de faciliter l'interrogation de l'état des appels d'interface et de localiser les problèmes, le journal des requêtes est généralement imprimé et l'AOP de Spring est pris en charge en tant qu'aspect, ce qui répond parfaitement aux besoins de journalisation.

Dans le projet précédent, l'effet de l'exécution de l'enregistrement de journal d'aspect correct est le suivant:

Ne marchez pas sur la fosse!  L'ordre d'exécution des aspects Spring AOP est un grand trou

 

Vous pouvez voir qu'un appel de méthode dans la figure affichera l'url de la demande, les paramètres d'entrée et de sortie, et l'adresse IP de la demande, etc.

J'ai mis cette classe d'implémentation dans un nouveau projet, mais l'implémentation ressemble à ceci:

Ne marchez pas sur la fosse!  L'ordre d'exécution des aspects Spring AOP est un grand trou

 

Je me suis frotté les yeux et j'ai examiné de plus près l'ancien code copié. La version simplifiée est la suivante:

/**
 * 在切点之前织入
 * @param joinPoint
 * @throws Throwable
 */
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
    // 开始打印请求日志
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();

    // 初始化traceId
    initTraceId(request);

    // 打印请求相关参数
    LOGGER.info("========================================== Start ==========================================");
    // 打印请求 url
    LOGGER.info("URL            : {}", request.getRequestURL().toString());
    // 打印 Http method
    LOGGER.info("HTTP Method    : {}", request.getMethod());
    // 打印调用 controller 的全路径以及执行方法
    LOGGER.info("Class Method   : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
    // 打印请求的 IP
    LOGGER.info("IP             : {}", IPAddressUtil.getIpAdrress(request));
    // 打印请求入参
    LOGGER.info("Request Args   : {}", joinPoint.getArgs());
}

/**
 * 在切点之后织入
 * @throws Throwable
 */
@After("webLog()")
public void doAfter() throws Throwable {
    LOGGER.info("=========================================== End ===========================================");
}

/**
 * 环绕
 * @param proceedingJoinPoint
 * @return
 * @throws Throwable
 */
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    Object result = proceedingJoinPoint.proceed();
    // 打印出参
    LOGGER.info("Response Args  : {}", result);
    // 执行耗时
    LOGGER.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
    return result;
}

Le code estime qu'il n'y a aucun problème, y a-t-il un bogue dans la nouvelle version de SpringBoot.

Évidemment, les frameworks matures ne feront pas d'erreur dans cette direction générale. Se pourrait-il que la nouvelle version de SpringBoot inverse l'ordre de @After et @Around?

En fait, les choses ne sont pas aussi simples.

Ordre d'exécution Spring AOP

Examinons d'abord la séquence d'exécution de Spring AOP.

Nous recherchons des informations sur l'ordre d'exécution de SpringAop sur Internet. La plupart du temps, vous trouverez les réponses suivantes:

situation normale

Ne marchez pas sur la fosse!  L'ordre d'exécution des aspects Spring AOP est un grand trou

 

situation anormale

Ne marchez pas sur la fosse!  L'ordre d'exécution des aspects Spring AOP est un grand trou

 

Aspects multiples

Ne marchez pas sur la fosse!  L'ordre d'exécution des aspects Spring AOP est un grand trou

 

Donc @Around devrait être avant @After, mais dans la version SprinBoot 2.3.4.RELEASE, @Around est en fait implémenté après @After.

Lorsque j'ai essayé de revenir à la version 2.2.5.RELEASE, l'ordre d'exécution est retourné à @Around -> @ After

Explorez la vérité sur le mauvais ordre

Maintenant que vous savez que le problème (ou le changement de séquence) est causé par la mise à niveau de la version de SpringBoot, voyons quelle bibliothèque a changé l'ordre d'exécution d'AOP. Après tout, SpringBoot n'est qu'une «forme», et le véritable cœur est Spring.

Nous ouvrons le fichier pom.xml, utilisons le plug-in pour vérifier la version de spring-aop et constatons que l'AOP utilisé par la version SpringBoot 2.3.4.RELEASE est spring-aop-5.2.9.RELEASE.

Et 2.2.5.RELEASE correspond à spring-aop-5.2.4.RELEASE

Je suis donc allé sur le site officiel pour rechercher la documentation. Je dois dire que parce que Spring est trop volumineux, la documentation sur le site officiel a atteint le point de la lourdeur, mais je l'ai finalement trouvée:

https://docs.spring.io/spring-framework/docs/5.2.9.RELEASE/spring-framework-reference/core.html#aop-ataspectj-advice-ordering

Ne marchez pas sur la fosse!  L'ordre d'exécution des aspects Spring AOP est un grand trou

 

Depuis Spring Framework 5.2.7, les méthodes de conseil définies dans la même classe @Aspect qui doivent s'exécuter au même point de jointure reçoivent une priorité en fonction de leur type de conseil dans l'ordre suivant, de la priorité la plus élevée à la plus basse: @Around, @Before , @After, @AfterReturning, @AfterThrowing.

Permettez-moi de traduire brièvement les points clés:

À partir de Spring 5.2.7, dans la même classe @Aspect, la méthode de notification sera exécutée en fonction de son type de priorité élevée à faible: @Around, @Before, @After, @AfterReturning, @AfterThrowing.

De cette façon, la comparaison n'est pas évidente. Revenons à l'ancienne version, qui est spring-aop-5.2.4.RELEASE correspondant à 2.2.5.RELEASE. Le document à l'époque était écrit comme ceci:

Que se passe-t-il lorsque plusieurs conseils veulent tous s'exécuter au même point de jointure? Spring AOP suit les mêmes règles de priorité qu'AspectJ pour déterminer l'ordre d'exécution des conseils. Le conseil de priorité la plus élevée s'exécute en premier "sur le chemin d'entrée" (donc, étant donné deux conseils avant, celui qui a la priorité la plus élevée passe en premier). «À la sortie» d'un point de jointure, le conseil de priorité le plus élevé s'exécute en dernier (donc, étant donné deux conseils après, celui qui a la priorité la plus élevée sera en second).

Traduction simple: dans la même classe @Aspect, Spring AOP suit les mêmes règles de priorité qu'AspectJ pour déterminer l'ordre d'exécution des conseils.

Creusez plus profondément, quelles sont les règles de priorité pour AspectJ?

J'ai trouvé la documentation d'AspectJ:

https://www.eclipse.org/aspectj/doc/next/progguide/semantics-advice.html

Ne marchez pas sur la fosse!  L'ordre d'exécution des aspects Spring AOP est un grand trou

 

À un point de jonction particulier, les conseils sont classés par ordre de priorité.

Un élément de conseil autour contrôle si le conseil de priorité inférieure sera exécuté en appelant procède. L'appel à continuer exécutera l'avis avec la priorité suivante, ou le calcul sous le point de jointure s'il n'y a pas d'autres conseils.

Un élément d'avis préalable peut empêcher l'exécution d'un avis de priorité inférieure en lançant une exception. Si elle retourne normalement, cependant, alors l'avis de la priorité suivante, ou le calcul sous la pinte de jointure s'il n'y a pas d'autres conseils, s'exécutera.

Exécuter après avoir renvoyé un avis exécutera l'avis de priorité suivante, ou le calcul sous le point de jointure s'il n'y a pas d'autres conseils. Ensuite, si ce calcul est retourné normalement, le corps de l'avis s'exécutera.

L'exécution après avoir lancé un conseil exécutera l'avis de la priorité suivante, ou le calcul sous le point de jointure s'il n'y a pas d'autres conseils. Ensuite, si ce calcul a levé une exception d'un type approprié, le corps de l'avis s'exécutera.

Exécuter après l'avis exécutera l'avis de la priorité suivante, ou le calcul sous le point de jointure s'il n'y a pas d'autres conseils. Ensuite, le corps du conseil fonctionnera.

Tout le monde va reparler, oh trop longtemps pour regarder! En bref, les règles d'Aspectj sont comme indiqué dans le diagramme de séquence que nous pouvons trouver sur Internet ci-dessus, qui est toujours l'ancienne séquence.

Vérification du code

J'ai supprimé la logique métier du code et vérifié uniquement l'ordre d'exécution de ces conseils:

package com.bj58.xfbusiness.cloudstore.system.aop;

import com.bj58.xfbusiness.cloudstore.utils.IPAddressUtil;
import com.bj58.xfbusiness.cloudstore.utils.TraceIdUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 日志切面
 */
@Aspect
@Component
public class WebLogAspect {

    private final static Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);

    /** 以 controller 包下定义的所有请求为切入点 */
    @Pointcut("execution(public * com.xx.xxx.xxx.controller..*.*(..))")
    public void webLog() {}

    /**
     * 在切点之前织入
     * @param joinPoint
     * @throws Throwable
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        LOGGER.info("-------------doBefore-------------");
    }

    @AfterReturning("webLog()")
    public void afterReturning() {
        LOGGER.info("-------------afterReturning-------------");
    }
    @AfterThrowing("webLog()")
    public void afterThrowing() {
        LOGGER.info("-------------afterThrowing-------------");
    }

    /**
     * 在切点之后织入
     * @throws Throwable
     */
    @After("webLog()")
    public void doAfter() throws Throwable {
        LOGGER.info("-------------doAfter-------------");
    }

    /**
     * 环绕
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        LOGGER.info("-------------doAround before proceed-------------");
        Object result = proceedingJoinPoint.proceed();
        LOGGER.info("-------------doAround after proceed-------------");
        return result;
    }

Nous avons changé la version en 2.2.5.RELEASE, le résultat est montré dans la figure:

Ne marchez pas sur la fosse!  L'ordre d'exécution des aspects Spring AOP est un grand trou

 

Nous avons changé la version en 2.3.4.RELEASE, et le résultat est montré dans la figure:

Ne marchez pas sur la fosse!  L'ordre d'exécution des aspects Spring AOP est un grand trou

 

en conclusion

Après avoir consulté les documents ci-dessus, la conclusion que je peux donner est :

À partir de Spring 5.2.7, Spring AOP n'exécute plus les conseils strictement selon les règles définies par AspectJ, mais exécute les conseils en fonction de leur type de haut en bas: @Around, @Before, @After, @AfterReturning, @AfterThrowing.

La recherche et la réflexion cette fois-ci sont très hâtives. Si la conclusion est fausse, n'hésitez pas à la corriger. Vous êtes également invité à l'essayer vous-même. Après tout, la seule norme pour tester la vérité en laboratoire n'est pas fondée!

Je suppose que tu aimes

Origine blog.csdn.net/weixin_45132238/article/details/112705647
conseillé
Classement