Search code examples
springspring-bootaspectjspring-aop

Springboot with both aspectj and Spring AOP


I am trying to get a springboot (2.6.2) project to work with both AspectJ and Spring AOP.

I have the following sample classes:

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

My pom.xml file has the following plugins defined:

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

I have also defined a 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>

It seems to compile okay and I see the info messages that the join points are being advised. However, in the OtherAspect the autowired MyUtility is not getting set.

From what I could find I would expect Spring to recognize OtherAspect as a Component and Autowire in MyUtility but instead I get a NullPointerException.

Any thoughts? Thanks!


Solution

  • OK, I had a little bit of time and prepared the MCVE which actually would have been your job to provide. I made the following assumptions:

    1. You need native AspectJ, because you want to weave a target class which is not a Spring bean.
    2. You want to use compile-time, not load-time weaaving. Therefore, you would use AspectJ Maven Plugin.
    3. You want to use Spring dependency injection for wiring Spring beans into native AspectJ aspects, as described in the Spring manual, i.e. using an aspectOf factory method for the native aspect in Spring.
    4. You absolutely insist on combining Lombok and native AspectJ, even though they are incompatible out of the box. I.e., you need a workaround in Maven, either binary weaving (e.g. if Lombok is only used for your non-aspect classes) or a "delombok" build step (e.g. if your aspects also use Lombok, which unfortunately they do, using the @Slf4j Lombok annotation in MyAspect.

    What I changed in your setup:

    • I removed the dependency on Spring Data JPA to make things easier, because I was too lazy to set up a dummy in-memory database. It is not relevant for the solution here. I.e., I also commented out the @Entity and @Id annotations in class Item.
    • You already configured a "delombok" build step, which I wanted to stick with, because it seems to be your preference. Hence, your sample code only compiles with AspectJ Maven when using ${project.build.directory}/generated-sources/delombok as the source directory. Your idea to use a <weaveDirectory> does not work, because the aspect with the Lombok annotation does not compile that way, as it refers to the Lombok-generated static log field.
    • I removed the @Service annotation from the native AspectJ aspect, because that would lead to problems when wiring the application. Instead, I added a @Bean factory method to OtherAspect, so we can use @Autowired MyUtility myUtility there. In the same aspect, I also switched from @annotation(SystemCall) (due to missing code in your example) to @annotation(Verify.Access) in order to have something to test against.
    • I removed the superfluous aop.xml file.
    • I added a little Spring Boot driver application.
    • I switched from the no longer maintained com.nickwongdev AspectJ Maven plugin to the current dev.aspectj plugin which has more features and supports Java 17+, too.

    The whole application looks like this:

    <?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());
      }
    }
    

    If you run the Spring Boot application, you will see the following on the console (timestamps removed):

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

    As you can see, both aspects kick in, the first one logging an ERROR and the other one changing the return value from "my name" to "my info".

    The advantage of the "delombok" variant is that within the same Maven module, you can weave aspects into the Lombok-generated source code. The disadvantage is, that in your IDE you might not be able to compile the project imported from Maven because of the very unusual custom configuration. In IntelliJ IDEA, I had to delegate the build to Maven, but still the source code editor shows squiggly lines.

    As an alternative, you could create one module with Lombok compilation (no "delombok") and a second module using binary weaving in order to weave aspects into the Lombok-enhanced class files, as described here. It would all be much easier without Lombok, though. The third alternative is compilation with Lombok and native AspectJ load-time weaving configured for Spring Boot instead of compile-time or binary weaving during build time. I cannot explain and show every variant in detail here, it is a long answer already.