¡No pises el hoyo! El orden de ejecución de los aspectos de Spring AOP es un gran agujero

Prefacio

La compañía ha lanzado un nuevo proyecto en los últimos dos meses y el cronograma del proyecto ha expirado, pero aún es necesario aprender. No, cuando configuré un proyecto para la empresa, pisé un pozo Spring AOP.

Aspectos destacados de este artículo:

  • Descripción del problema
  • Orden de ejecución de Spring AOP
  • Explore la verdad sobre el orden incorrecto
  • Verificación de código
  • En conclusión

Descripción del problema

El nuevo proyecto de la empresa necesita construir un nuevo servicio HTTP separado de adelante hacia atrás. Elegí el conocido SpringBoot Web para construir rápidamente un sistema utilizable.

Lu Xun dijo, no se limite a actualizar la versión estable . No creo en este mal. He usado Spring durante tanto tiempo, así que ¿cómo no voy a apresurarme? Sin mencionar que se introdujo directamente la última versión de SprinBoot 2.3.4.RELEASE y se empezó a construir el proyecto.

Al principio, la introducción de la mayoría de los componentes se realizó sin problemas. Pensé que estaba a punto de hacerse, pero no esperaba encontrarme con la sección de registro .

Como servicio de interfaz, para facilitar la consulta del estado de la llamada de la interfaz y localizar problemas, generalmente se imprime el registro de solicitudes y se admite el AOP de Spring como un aspecto, que satisface perfectamente las necesidades de registro.

En el proyecto anterior, el efecto de ejecutar el registro de registro de aspecto correcto es el siguiente:

¡No pises el hoyo!  El orden de ejecución de los aspectos de Spring AOP es un gran agujero

 

Puede ver que una llamada al método en la figura generará la URL de solicitud, los parámetros de entrada y salida, y la IP de solicitud, etc., para verse bien, se agregó una línea divisoria.

Puse esta clase de implementación en un nuevo proyecto, pero la implementación se ve así:

¡No pises el hoyo!  El orden de ejecución de los aspectos de Spring AOP es un gran agujero

 

Me froté los ojos y eché un vistazo más de cerca al código antiguo copiado. La versión simplificada es la siguiente:

/**
 * 在切点之前织入
 * @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;
}

El código siente que no hay ningún problema en absoluto, hay un error en la nueva versión de SpringBoot.

Obviamente, los frameworks maduros no cometerán errores en esta dirección general ¿Podría ser que la nueva versión de SpringBoot invierta el orden de @After y @Around?

De hecho, las cosas no son tan sencillas.

Orden de ejecución de Spring AOP

Primero revisemos la secuencia de ejecución de Spring AOP.

Buscamos información sobre la orden de ejecución de SpringAop en Internet. La mayoría de las veces, encontrará las siguientes respuestas:

situación normal

¡No pises el hoyo!  El orden de ejecución de los aspectos de Spring AOP es un gran agujero

 

situación anormal

¡No pises el hoyo!  El orden de ejecución de los aspectos de Spring AOP es un gran agujero

 

Múltiples aspectos

¡No pises el hoyo!  El orden de ejecución de los aspectos de Spring AOP es un gran agujero

 

Por lo tanto, @Around debería estar antes de @After, pero en la versión de SprinBoot 2.3.4.RELEASE, @Around se implementa realmente después de @After.

Cuando intenté volver a la versión 2.2.5.RELEASE, la orden de ejecución volvió a @Around -> @ After

Explore la verdad sobre el orden incorrecto

Ahora que sabe que el problema (o el cambio de secuencia) es causado por la actualización de la versión de SpringBoot, veamos qué biblioteca ha cambiado el orden de ejecución de AOP. Después de todo, SpringBoot es solo una "forma", y el núcleo real es Spring.

Abrimos el archivo pom.xml, usamos el complemento para verificar la versión de spring-aop y encontramos que el AOP usado por SpringBoot 2.3.4.RELEASE es spring-aop-5.2.9.RELEASE.

Y 2.2.5.RELEASE corresponde a spring-aop-5.2.4.RELEASE

Así que fui al sitio web oficial para buscar la documentación. Debo decir que debido a que Spring es demasiado grande, la documentación en el sitio web oficial ha llegado al punto de ser engorroso, pero finalmente la encontré:

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

¡No pises el hoyo!  El orden de ejecución de los aspectos de Spring AOP es un gran agujero

 

A partir de Spring Framework 5.2.7, a los métodos de asesoramiento definidos en la misma clase @Aspect que deben ejecutarse en el mismo punto de unión se les asigna prioridad según su tipo de asesoramiento en el siguiente orden, de mayor a menor precedencia: @Around, @Before , @Después, @AfterReturning, @AfterThrowing.

Permítanme traducir brevemente los puntos clave:

A partir de Spring 5.2.7, en la misma clase @Aspect, el método de notificación se ejecutará de acuerdo con su tipo de prioridad alta a baja: @Around, @Before, @After, @AfterReturning, @AfterThrowing.

De esta manera, la comparación no es obvia. Volvamos a la versión anterior, que es spring-aop-5.2.4.RELEASE correspondiente a 2.2.5.RELEASE. El documento en ese momento estaba escrito así:

¿Qué sucede cuando varios consejos quieren ejecutarse en el mismo punto de unión? Spring AOP sigue las mismas reglas de precedencia que AspectJ para determinar el orden de ejecución de los avisos. El consejo de mayor precedencia se ejecuta primero "en el camino de entrada" (por lo tanto, dados dos consejos anteriores, el que tiene mayor precedencia se ejecuta primero). "Al salir" de un punto de unión, el consejo de mayor precedencia se ejecuta en último lugar (por lo tanto, dados dos consejos posteriores, el que tenga la mayor precedencia se ejecutará en segundo lugar).

Traducción simple: en la misma clase @Aspect, Spring AOP sigue las mismas reglas de prioridad que AspectJ para determinar el orden de ejecución de los consejos.

Profundice, ¿cuáles son las reglas de prioridad para AspectJ?

Encontré la documentación de AspectJ:

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

¡No pises el hoyo!  El orden de ejecución de los aspectos de Spring AOP es un gran agujero

 

En un punto de unión en particular, los avisos se ordenan por precedencia.

Un consejo general controla si los avisos de menor precedencia se ejecutarán llamando a Proceder. La llamada para continuar ejecutará el aviso con la siguiente prioridad, o el cálculo bajo el punto de unión si no hay más consejos.

Un consejo anterior puede evitar que se ejecuten los consejos de menor precedencia lanzando una excepción. Sin embargo, si regresa normalmente, se ejecutará el aviso de la siguiente precedencia, o el cálculo bajo la pinta de unión si no hay más avisos.

Ejecutar después de devolver un consejo ejecutará el consejo de la siguiente precedencia, o el cálculo bajo el punto de unión si no hay más consejos. Luego, si ese cálculo regresó normalmente, se ejecutará el cuerpo del consejo.

Ejecutar después de lanzar un consejo ejecutará el consejo de la siguiente precedencia, o el cálculo bajo el punto de unión si no hay más consejos. Luego, si ese cálculo arrojó una excepción de un tipo apropiado, se ejecutará el cuerpo del consejo.

Al ejecutar el consejo, se ejecutará el consejo de la siguiente precedencia o el cálculo bajo el punto de unión si no hay más consejos. Entonces se publicará el cuerpo del consejo.

Todos van a hablar de nuevo, ¡oh, demasiado tiempo para mirar! En resumen, las reglas de Aspectj son las que se muestran en el diagrama de secuencia que podemos encontrar en Internet arriba, que sigue siendo la secuencia anterior.

Verificación de código

Eliminé la lógica empresarial del código y solo verifiqué el orden de ejecución de estos consejos:

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

Cambiamos la versión a 2.2.5.RELEASE, el resultado se muestra en la figura:

¡No pises el hoyo!  El orden de ejecución de los aspectos de Spring AOP es un gran agujero

 

Cambiamos la versión a 2.3.4.RELEASE, y el resultado se muestra en la figura:

¡No pises el hoyo!  El orden de ejecución de los aspectos de Spring AOP es un gran agujero

 

En conclusión

Después de consultar los documentos anteriores, la conclusión que puedo dar es :

A partir de Spring 5.2.7, Spring AOP ya no ejecuta los consejos estrictamente de acuerdo con las reglas definidas por AspectJ, sino que ejecuta los consejos de acuerdo con su tipo de mayor a menor: @Around, @Before, @After, @AfterReturning, @AfterThrowing.

La investigación y el pensamiento esta vez son muy apresurados. Si la conclusión es incorrecta, no dude en corregirla. También puede intentarlo usted mismo. Después de todo, ¡el único estándar para probar la verdad en el laboratorio es infundado!

Supongo que te gusta

Origin blog.csdn.net/weixin_45132238/article/details/112705647
Recomendado
Clasificación