¿Estás realmente seguro del orden de ejecución de Spring AOP?

¿Estás realmente seguro del orden de ejecución de Spring AOP?

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. Esto no es así cuando se trata de proyectos de construcción para empresas.

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 compañía 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 solo actualice 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, como un aspecto soportado por Spring's AOP, que cumple perfectamente con los requisitos de registro.

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

¿Estás realmente seguro del orden de ejecución de Spring AOP?

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í:

¿Estás realmente seguro del orden de ejecución de Spring AOP?

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:

circunstancias normales

¿Estás realmente seguro del orden de ejecución de Spring AOP?

situación anormal

¿Estás realmente seguro del orden de ejecución de Spring AOP?

Múltiples aspectos

¿Estás realmente seguro del orden de ejecución de Spring AOP?

Entonces @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 es el problema (o el cambio de orden) 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 simplemente "formulario", 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

¿Estás realmente seguro del orden de ejecución de Spring AOP?

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 en función de 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). "A la salida" 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

¿Estás realmente seguro del orden de ejecución de Spring AOP?

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.

Todo el mundo tiene que volver a decir, ¡vaya, es demasiado largo para leer! En resumen, las reglas de Aspectj son las que se muestran en el diagrama de secuencia que podemos encontrar en Internet. Sigue siendo la secuencia antigua.

Verificación de código

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

/** 
 * 日志切面 
 */ 
@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, y el resultado se muestra en la figura:

¿Estás realmente seguro del orden de ejecución de Spring AOP?

Cambiamos la versión a 2.3.4.RELEASE, y los resultados se muestran en la figura:

¿Estás realmente seguro del orden de ejecución de Spring AOP?

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, corríjame activamente. También puede intentarlo usted mismo. Después de todo, ¡el único estándar para las pruebas de laboratorio de la verdad!

Supongo que te gusta

Origin blog.csdn.net/qwe123147369/article/details/109129164
Recomendado
Clasificación