带有 aspectj 和 Spring AOP 的 Springboot

问题描述 投票:0回答:1

我正在尝试让一个 springboot (2.6.2) 项目与 AspectJ 和 Spring AOP 一起工作。

我有以下示例类:

@Entity
public class Item {
  @Id @Getter private String uuid = UUID.randomUUID().toString();

  private String name;

  @Verify.Access
  public String getName() {
    return name;
  }
}
public @interface Verify {
 @Target({ElementType.METHOD})
  @Retention(RetentionPolicy.RUNTIME)
  @interface Access {}
}
@Aspect
@Slf4j
public class MyAspect {

  @Before("@annotation(Verify.Access)")
  public void beforeAnnotation(JoinPoint joinPoint) {
    log.error("BEFORE ANNOTATION");
  }
}
@Aspect
@Service
public class OtherAspect {
  @Autowired private MyUtility myUtility;

  @Around("@annotation(SystemCall)")
  public Object run(@NonNull final ProceedingJoinPoint join) throws Throwable {
    return myUtility.getInfo();
  }
}
@Service
@Data
public class MyUtility {
  Object info;
}

我的 pom.xml 文件定义了以下插件:

<plugin>
   <groupId>com.nickwongdev</groupId>
   <artifactId>aspectj-maven-plugin</artifactId>
   <version>1.12.6</version>
   <configuration>
      <source>${java.version}</source>
      <target>${java.version}</target>
      <proc>none</proc>
      <complianceLevel>${java.version}</complianceLevel>
      <showWeaveInfo>true</showWeaveInfo>
      <forceAjcCompile>true</forceAjcCompile>
      <sources/>
         <weaveDirectories>
            <weaveDirectory>${project.build.directory}/classes</weaveDirectory>
         </weaveDirectories>
   </configuration>
   <executions>
      <execution>
         <goals>
            <goal>compile</goal>
         </goals>
      </execution>
   </executions>
   <dependencies>
      <dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjtools</artifactId>
         <version>${aspectj.version}</version>
      </dependency>
   </dependencies>
</plugin>

<plugin>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok-maven-plugin</artifactId>
   <version>1.18.20.0</version>
   <executions>
      <execution>
         <phase>generate-sources</phase>
         <goals>
            <goal>delombok</goal>
         </goals>
      </execution>
   </executions>
   <configuration>
      <addOutputDirectory>false</addOutputDirectory>
      <sourceDirectory>src/main/java</sourceDirectory>
      <encoding>UTF-8</encoding>
   </configuration>
</plugin>

我还定义了一个src/main/resources/org/aspectj/aop.xml:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
   <weaver>
      <include within="mypackage..*" />
      <include within="org.springframework.boot..*" />
   </weaver>

   <aspects>
      <aspect name="mypackage.MyAspect" />
   </aspects>
</aspectj>

编译似乎没问题,我看到了建议连接点的信息消息。 但是,在 OtherAspect 中,自动装配的 MyUtility 没有设置。

据我所知,我希望 Spring 将 OtherAspect 识别为 MyUtility 中的组件和自动装配,但我得到的是 NullPointerException。

有什么想法吗? 谢谢!

spring spring-boot aspectj spring-aop
1个回答
1
投票

好的,我有一点时间准备了 MCVE,这实际上是您的工作。我做了以下假设:

  1. 你需要原生的 AspectJ,因为你想编织一个不是 Spring bean 的目标类。
  2. 您想使用编译时,而不是加载时编织。因此,您将使用 AspectJ Maven 插件。
  3. 您想使用 Spring 依赖注入将 Spring bean 连接到原生 AspectJ 方面,如 Spring 手册 中所述,即在 Spring 中为原生方面使用
    aspectOf
    工厂方法。
  4. 你绝对坚持结合 Lombok 和本地 AspectJ,即使它们是 开箱即用的不兼容。即,您需要 Maven 中的解决方法,二进制编织(例如,如果 Lombok 仅用于您的非方面类)或“delombok”构建步骤(例如,如果您的方面也使用 Lombok,不幸的是他们使用
     @Slf4j
    MyAspect
    .
  5. 中的 Lombok 注释

我在你的设置中改变了什么:

  • 我删除了对 Spring Data JPA 的依赖以使事情变得更容易,因为我懒得设置一个虚拟的内存数据库。它与此处的解决方案无关。即,我还在课堂
    @Entity
    .
    中注释掉了
    @Id
    Item
  • 注释
  • 您已经配置了一个“delombok”构建步骤,我想坚持使用它,因为它似乎是您的偏好。因此,您的示例代码仅在使用
    ${project.build.directory}/generated-sources/delombok
    作为源目录时使用 AspectJ Maven 进行编译。您使用
    <weaveDirectory>
    的想法行不通,因为带有 Lombok 注释的方面不会以这种方式编译,因为它指的是 Lombok 生成的静态
    log
    字段。
  • 我从本机 AspectJ 方面删除了
    @Service
    注释,因为这会导致在连接应用程序时出现问题。相反,我在
    @Bean
    中添加了一个
    OtherAspect
    工厂方法,这样我们就可以在那里使用
    @Autowired MyUtility myUtility
    了。在同一方面,我也从
    @annotation(SystemCall)
    (由于您的示例中缺少代码)切换到
    @annotation(Verify.Access)
    以便进行测试。
  • 我删除了多余的 aop.xml 文件。
  • 我添加了一点 Spring Boot 驱动程序应用程序。
  • 我从不再维护的
    com.nickwongdev
    AspectJ Maven 插件切换到当前的
    dev.aspectj
    插件,它具有更多功能并且也支持 Java 17+。

整个应用程序如下所示:

<?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>org.example</groupId>
  <artifactId>SO_AJ_SpringAutowireBeanNativeAspect_74661663</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <aspectj.version>1.9.9.1</aspectj.version>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>dev.aspectj</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
        <version>1.13.1</version>
        <configuration>
          <complianceLevel>${maven.compiler.target}</complianceLevel>
          <proc>none</proc>
          <showWeaveInfo>true</showWeaveInfo>
          <forceAjcCompile>true</forceAjcCompile>
          <sources>
            <source>
              <basedir>${project.build.directory}/generated-sources/delombok</basedir>
            </source>
          </sources>
          <!--
          <weaveDirectories>
            <weaveDirectory>${project.build.directory}/classes</weaveDirectory>
          </weaveDirectories>
          -->
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
            </goals>
          </execution>
        </executions>
        <dependencies>
          <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>${aspectj.version}</version>
          </dependency>
        </dependencies>
      </plugin>
      <plugin>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok-maven-plugin</artifactId>
        <version>1.18.20.0</version>
        <executions>
          <execution>
            <phase>generate-sources</phase>
            <goals>
              <goal>delombok</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <addOutputDirectory>false</addOutputDirectory>
          <sourceDirectory>src/main/java</sourceDirectory>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
      <version>2.6.2</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>2.6.2</version>
      <scope>compile</scope>
    </dependency>
    <!--
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
      <version>2.6.2</version>
      <scope>compile</scope>
    </dependency>
    -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.24</version>
    </dependency>
  </dependencies>

</project>
package org.example;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public @interface Verify {
  @Target({ ElementType.METHOD })
  @Retention(RetentionPolicy.RUNTIME)
  @interface Access {}
}
package org.example;

import lombok.Data;
import org.springframework.stereotype.Service;

@Service
@Data
public class MyUtility {
  Object info;
}
package org.example;

import lombok.Getter;

//import javax.persistence.Entity;
//import javax.persistence.Id;
import java.util.UUID;

//@Entity
public class Item {
//  @Id
  @Getter
  private String uuid = UUID.randomUUID().toString();

  private String name;

  public Item(String name) {
    this.name = name;
  }

  @Verify.Access
  public String getName() {
    return name;
  }
}
package org.example;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
@Slf4j
public class MyAspect {
  @Before("@annotation(Verify.Access)")
  public void beforeAnnotation(JoinPoint joinPoint) {
    log.error("BEFORE ANNOTATION");
  }
}
package org.example;

import lombok.NonNull;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;

@Aspect
public class OtherAspect {
  @Autowired
  private MyUtility myUtility;

//  @Around("@annotation(SystemCall)")
  @Around("@annotation(Verify.Access)")
  public Object run(@NonNull final ProceedingJoinPoint join) throws Throwable {
    return myUtility.getInfo();
//    return join.proceed();
  }
}
package org.example;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.Aspects;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@SpringBootApplication
@Configuration
@Slf4j
public class Main {
  @Bean
  public OtherAspect otherAspect() {
    return Aspects.aspectOf(OtherAspect.class);
  }

  public static void main(String[] args) {
    try (ConfigurableApplicationContext appContext = SpringApplication.run(Main.class, args)) {
      doStuff(appContext);
    }
  }

  private static void doStuff(ConfigurableApplicationContext appContext) {
    MyUtility myUtility = appContext.getBean(MyUtility.class);
    myUtility.setInfo("my info");
    Item item = new Item("my name");
    log.info(item.getName());
  }
}

如果运行 Spring Boot 应用程序,您将在控制台上看到以下内容(已删除时间戳):

ERROR 20680 --- [           main] org.example.MyAspect                     : BEFORE ANNOTATION
 INFO 20680 --- [           main] org.example.Main                         : my info

如您所见,两个方面都起作用,第一个记录错误,另一个将返回值从“我的名字”更改为“我的信息”。

“delombok”变体的优点是在同一个 Maven 模块中,您可以将方面编织到 Lombok 生成的源代码中。缺点是,由于非常不寻常的自定义配置,在您的 IDE 中您可能无法编译从 Maven 导入的项目。在 IntelliJ IDEA 中,我不得不将构建委托给 Maven,但源代码编辑器仍然显示波浪线。

作为替代方案,您可以使用 Lombok 编译(不是“delombok”)创建一个模块,并使用二进制编织创建第二个模块,以便将方面编织到 Lombok 增强的类文件中,如 here 所述。不过,如果没有 Lombok,一切都会容易得多。第三种选择是使用 Lombok 和为 Spring Boot 配置的本机 AspectJ 加载时织入进行编译,而不是在构建时使用编译时或二进制织入。我无法在这里详细解释和展示每个变体,这已经是一个很长的答案了。

© www.soinside.com 2019 - 2024. All rights reserved.