Maven + AspectJ LTW [错误] [AppClassloader] 和 Joinpoint 目标为空

Maven + AspectJ LTW [ERROR] [AppClassloader] and Joinpoint target null

我正在尝试使用 LoadTime-Weaving 使用 Maven 和 AspectJ 制作一个简单的应用程序。
我有一个方面,它以注释为目标并计算方法的执行时间是否比预期的长。但是当它调用 getSLAAnnotation() 来获取连接点方法时,它会抛出 NullPointerException。不过,它确实 return 连接点签名。我相信这可能与我得到的 Maven 输出有关。 \

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[ERROR] [AppClassLoader@18b4aac2] info AspectJ Weaver Version 1.9.7 built on Thursday Jun 24, 2021 at 16:14:45 PDT
[ERROR] [AppClassLoader@18b4aac2] info register classloader sun.misc.Launcher$AppClassLoader@18b4aac2
[ERROR] [AppClassLoader@18b4aac2] info using configuration /D:/RFS/prueba-aspectj/target/classes/META-INF/aop-ajc.xml
[ERROR] [AppClassLoader@18b4aac2] info using configuration file:/C:/Users/MT27745023/.m2/repository/io/qameta/allure/allure-testng/2.17.2/allure-testng-2.17.2.jar!/META-INF/aop-ajc.xml
[ERROR] [AppClassLoader@18b4aac2] info register aspect aspectPackage.SLAAspect
[ERROR] [AppClassLoader@18b4aac2] info register aspect io.qameta.allure.aspects.StepsAspects
[ERROR] [AppClassLoader@18b4aac2] info register aspect io.qameta.allure.aspects.AttachmentsAspects
[INFO] Running TestSuite
[ERROR] [AppClassLoader@18b4aac2] info processing reweavable type page.StepTestPage: page\StepTestPage.java
[ERROR] [AppClassLoader@18b4aac2] info successfully verified type aspectPackage.SLAAspect exists.  Originates from aspectPackage\SLAAspect.java
[ERROR] [AppClassLoader@18b4aac2] weaveinfo Join point 'method-execution(void page.StepTestPage.pruebaStepAspect())' in Type 'page.StepTestPage' (StepTestPage.java:11) advised by before advice from 'aspectPackage.SLAAspect' (SLAAspect.java)
[ERROR] [AppClassLoader@18b4aac2] weaveinfo Join point 'method-execution(void page.StepTestPage.pruebaStepAspect())' in Type 'page.StepTestPage' (StepTestPage.java:11) advised by after advice from 'aspectPackage.SLAAspect' (SLAAspect.java)
[ERROR] [AppClassLoader@18b4aac2] weaveinfo Join point 'method-execution(void page.StepTestPage.pruebaStepAspect())' in Type 'page.StepTestPage' (StepTestPage.java:11) advised by before advice from 'io.qameta.allure.aspects.StepsAspects' (StepsAspects.java)
[ERROR] [AppClassLoader@18b4aac2] weaveinfo Join point 'method-execution(void page.StepTestPage.pruebaStepAspect())' in Type 'page.StepTestPage' (StepTestPage.java:11) advised by afterThrowing advice from 'io.qameta.allure.aspects.StepsAspects' (StepsAspects.java)
[ERROR] [AppClassLoader@18b4aac2] weaveinfo Join point 'method-execution(void page.StepTestPage.pruebaStepAspect())' in Type 'page.StepTestPage' (StepTestPage.java:11) advised by afterReturning advice from 'io.qameta.allure.aspects.StepsAspects' (StepsAspects.java)
[ERROR] [AppClassLoader@18b4aac2] info processing reweavable type aspectPackage.SLAAspect: aspectPackage\SLAAspect.java
[ERROR] [AppClassLoader@18b4aac2] info successfully verified type aspectPackage.SLAAspect exists.  Originates from aspectPackage\SLAAspect.java

看点:

package aspectPackage;

import java.lang.reflect.Method;

import env.EnvironmentConsumer;
import io.github.cdimascio.dotenv.Dotenv;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import java.time.LocalTime;
import java.time.temporal.Temporal;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

@Aspect
public class SLAAspect{
    private DateTimeFormatter ISODateTimeFormatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
    private DateTimeFormatter DurationFormatter = java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss");
    ZonedDateTime dtFechaInicial;
    SLA ca;

    @Before("execution(* *(..)) && @annotation(aspectPackage.SLA)")
    public void beforeTiempos(JoinPoint jp) {
        System.out.println("BEFORE SLAASPECT");
        ca = getSLAAnnotation(jp);
        String fechaI = ZonedDateTime.now().format(ISODateTimeFormatter);
        dtFechaInicial = ZonedDateTime.parse(fechaI, ISODateTimeFormatter);
    }

    @After("execution(* *(..)) && @annotation(aspectPackage.SLA)")
    public void afterLogger(JoinPoint jp) throws Exception{
        String fechaF = ZonedDateTime.now().format(ISODateTimeFormatter);
        System.out.println("AFTER SLAASPECT");
        ZonedDateTime dtFechaFinal = ZonedDateTime.parse(fechaF,ISODateTimeFormatter);

        Duration tiempoTotal = calcularTiempoEntreFechas(dtFechaInicial,dtFechaFinal);
        System.out.println(ca.toString());
        System.out.println(ca.tiempoEsperado());

        Duration sla = Duration.ofSeconds(ca.tiempoEsperado());

        int resultSla = calcularSla (tiempoTotal, sla);
        String resultadoFormateado = LocalTime.MIDNIGHT.plus(tiempoTotal).format(DurationFormatter);
        Dotenv settings = null;
        Duration timeout = null;
        try{
            settings = EnvironmentConsumer.getInstance("settings");
            timeout = Duration.ofSeconds(Long.parseLong(settings.get("Timeout")));
        }catch(Exception e){
            System.out.println("\n\n\n\nERROR CON EL ENVIRONMENT\n\n\n\n");
            throw e;
        }

        if(resultSla != 1 && tiempoTotal.compareTo(timeout) != 0){
            throw new Exception("SLA Exception: Se excedió del tiempo esperado, el step duró: " + resultadoFormateado + " segundos");
        }else{
            throw new Exception("Step timeout");
        }
    }

    private Duration calcularTiempoEntreFechas(Temporal fechaInicial, Temporal fechaFinal) {
        // Calcula el tiempo final de ejecucion
        return Duration.between(fechaInicial, fechaFinal);
    }

    private int calcularSla(Duration tiempoT, Duration tiempoE) {
        // Calcula si tarda mas del tiempo esperados

        // System.out.println(tiempoE.compareTo(tiempoT));

        return tiempoE.compareTo(tiempoT);
    }

    private SLA getSLAAnnotation(JoinPoint joinPoint) {
        SLA ca = null;
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Object target = joinPoint.getTarget();
            Method method = target.getClass().
                    getMethod(signature.getMethod().getName(), signature.getMethod().getParameterTypes());

            if (method.isAnnotationPresent(SLA.class)){
                ca = method.getAnnotation(SLA.class);
            }else if (joinPoint.getTarget().getClass().isAnnotationPresent(SLA.class)){
                ca = joinPoint.getTarget().getClass().getAnnotation(SLA.class);
            }
            return ca;
        }catch(Exception e) {
            return ca;
        }
    }
}

这是pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>ar.com.samsistemas</groupId>
    <artifactId>aspectSLADemo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <aspectj.version>1.9.7</aspectj.version>
        <allure-testng.version>2.17.2</allure-testng.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M5</version>
                <configuration>
                    <argLine>
                         -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
                    </argLine>
                    <suiteXmlFiles>
                        <suiteXmlFile>./testng.xml</suiteXmlFile>
                    </suiteXmlFiles>
                    <useSystemClassLoader>true</useSystemClassLoader>
                    <forkMode>always</forkMode>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>7.4.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${aspectj.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aspectj.version}</version>
        </dependency>
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-testng</artifactId>
            <version>${allure-testng.version}</version>
        </dependency>
        <dependency>
            <groupId>ar.com.samsistemas</groupId>
            <artifactId>web-utils</artifactId>
            <version>0.0.1m</version>
        </dependency>

        <!-- Environment properties consumer from personal utils-->
        <dependency>
            <groupId>ar.com.samsistemas</groupId>
            <artifactId>environment-utils</artifactId>
            <version>0.0.1b</version>
        </dependency>
    </dependencies>
</project>

和aop.xml

<aspectj>
    <aspects>
        <aspect name="aspectPackage.SLAAspect"/>
        <weaver options="-verbose -showWeaveInfo">
            <include within="*"/>
        </weaver>
    </aspects>
</aspectj>

前言

抱歉,这将是一个冗长的回答,因为您自己的方面代码有太多错误或问题,我忍不住建议如何修复它。但我们会一步一步地做到这一点。

如何提出好的问题

您的 POM 中的两个依赖项未公开可用,其他如 io.github.cdimascio:dotenv-java 完全缺失,或者您依赖它们作为 non-public 的传递依赖项,这是一个 Maven anti-pattern。我在本地修复了这个问题,创建了缺少的 classes,比如 SLA 注释和 EnvironmentConsumer,添加了一个 .env 文件,其中包含一个 Timeout 变量和一个 TestNG 测试。现在我可以编译 运行 项目了。所有这一切都是你的工作。因为你是新手,我的早茶需要拼图,所以这次我为你做了。这是你的免费拍摄。下次,请你自己做。谢谢你。

顺便说一句,您还忘记了 post 您的 NullPointerException 包括堆栈跟踪。

Surefire 错误地记录了 [ERROR] 条消息

至于 Maven Surefire 将 AspectJ weaver info 消息记录为 [ERROR],您可以忽略它。可能 Surefire 认为它们是错误,因为在测试开始 运行ning 之前它不期望任何日志输出。我之前和 Surefire 的维护者讨论过这个问题,他们并不真正了解 Java 代理,但这是另一天的话题。

关于getSLAAnnotation(Pointcut)

我能说的是,在简单的情况下,即被拦截的方法直接用@SLA(tiempoEsperado = 2)之类的注释,该方法不会抛出任何错误。即使注释方法来自超级 class,它也能按预期工作。这样的方法是不必要的,正如我将在这个长答案的末尾解释的那样,因为 AspectJ 有一种更优雅的方式来从拦截的方法或 class 中获取注释。但让我们保留它以备后用。

超时逻辑错误

在测试你的方面时,我看到总是抛出 java.lang.Exception: Step timeout,这是一个错误。你必须改变错误的逻辑

    if (resultSla != 1 && tiempoTotal.compareTo(timeout) != 0) {
      throw new Exception("SLA Exception: Se excedió del tiempo esperado, el step duró: " + resultadoFormateado + " segundos");
    }
    else {
      throw new Exception("Step timeout");
    }

正确

    if (resultSla < 0)
      throw new Exception("Step timeout");
    if (timeout.compareTo(tiempoTotal) < 0)
      throw new Exception("SLA Exception: Se excedió del tiempo esperado, el step duró: " + resultadoFormateado + " segundos");

这与 AOP 完全无关,只是一个 Java 错误。

不必要地使用 DurationZonedDateTime

此外,我不明白您为什么在这种情况下使用 DurationZonedDateTime。这是缓慢且不必要的,因为您没有处理来自不同时区的时间。同样丑陋的是,您首先将持续时间转换为字符串,只是稍后再将它们解析回 zoned date-times 。为什么要把一件简单的事情搞得这么复杂?

您还应该只读取一次设置文件,而不是每次触发方面建议时都读取,并将设置设置为静态。

这个简化怎么样(请自己把我的英文错误信息翻译成西班牙文)?

  private static Dotenv settings = EnvironmentConsumer.getInstance("settings");
  private static long timeout = Long.parseLong(settings.get("Timeout"));

  private long dtFechaInicial;
  private SLA sla;

  @Before("execution(* *(..)) && @annotation(aspectPackage.SLA)")
  public void beforeTiempos(JoinPoint jp) {
    sla = getSLAAnnotation(jp);
    dtFechaInicial = currentTimeMillis();
  }

  @After("execution(* *(..)) && @annotation(aspectPackage.SLA)")
  public void afterLogger(JoinPoint jp) throws Exception {
    long tiempoTotal = currentTimeMillis() - dtFechaInicial;
    if (tiempoTotal > sla.tiempoEsperado() * 1000)
      throw new Exception(String.format(
        "Step timeout, method SLA of %d s exceeded, actual was %.2f s", 
        sla.tiempoEsperado(), tiempoTotal / 1000.0
      ));
    if (tiempoTotal > timeout * 1000)
      throw new Exception(String.format(
        "Step timeout, global SLA of %d s exceeded, actual was %.2f s",
        timeout, tiempoTotal / 1000.0
      ));
  }

Thread-unsafe实例字段的使用

但现在还有一个问题。请注意,通过实例字段在通知前后传输状态不是 thread-safe。想象一下,切面被多个线程并行调用,这在业务应用程序中是完全正常的。您要么需要使用 ThreadLocal 字段,要么只需将 before/after 通知对替换为一个环绕通知:

  private static Dotenv settings = EnvironmentConsumer.getInstance("settings");
  private static long timeout = Long.parseLong(settings.get("Timeout"));

  @Around("execution(* *(..)) && @annotation(aspectPackage.SLA)")
  public Object aroundTiempos(ProceedingJoinPoint jp) throws Throwable {
    SLA sla = getSLAAnnotation(jp);
    long dtFechaInicial = currentTimeMillis();
    Object result = jp.proceed();
    long tiempoTotal = currentTimeMillis() - dtFechaInicial;
    if (tiempoTotal > sla.tiempoEsperado() * 1000)
      throw new Exception(String.format(
        "Step timeout, method SLA of %d s exceeded, actual was %.2f s",
        sla.tiempoEsperado(), tiempoTotal / 1000.0
      ));
    if (tiempoTotal > timeout * 1000)
      throw new Exception(String.format(
        "Step timeout, global SLA of %d s exceeded, actual was %.2f s",
        timeout, tiempoTotal / 1000.0
      ));
    return result;
  }

隐藏应用程序异常的超时异常

请注意,现在仅当目标方法本身未抛出任何异常时才会报告超时。我认为在这种情况下,常规错误应该优先于超时。在您的原始解决方案中,即使目标方法抛出异常,您也会在 @After 建议中抛出超时异常,从而隐藏原始异常。这使得应用程序调试变得困难甚至不可能。所以我改了一下,让原来的异常通过。

如何正确获取 @SLA 注释

您可以摆脱 getSLAAnnotation(JoinPoint) 方法。只需将注释绑定到建议方法,使用适当的方面语法,添加一个 SLA 方法参数并在 @annotation 切入点内使用它的名称而不是完全限定的 class 名称。现在,所有优化后您的完整外观如下所示:

package aspectPackage;

import env.EnvironmentConsumer;
import io.github.cdimascio.dotenv.Dotenv;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import static java.lang.System.currentTimeMillis;

@Aspect
public class SLAAspect {
  private static Dotenv settings = EnvironmentConsumer.getInstance("settings");
  private static long timeout = Long.parseLong(settings.get("Timeout"));

  @Around("execution(* *(..)) && @annotation(sla)")
  public Object aroundTiempos(ProceedingJoinPoint jp, SLA sla) throws Throwable {
    long dtFechaInicial = currentTimeMillis();
    Object result = jp.proceed();
    long tiempoTotal = currentTimeMillis() - dtFechaInicial;
    if (tiempoTotal > sla.tiempoEsperado() * 1000)
      throw new Exception(String.format(
        "Step timeout, method SLA of %d s exceeded, actual was %.2f s",
        sla.tiempoEsperado(), tiempoTotal / 1000.0
      ));
    if (tiempoTotal > timeout * 1000)
      throw new Exception(String.format(
        "Step timeout, global SLA of %d s exceeded, actual was %.2f s",
        timeout, tiempoTotal / 1000.0
      ));
    return result;
  }
}

这比您的原始版本更短、更易读且更易于维护。在此过程中,我们还消除了几个错误。

GitHub 示例项目

我在这里解释的所有内容都在我的 example project on GitHub. I also added a TestNG test 验证方面行为中。