Search code examples
javaspring-bootjava-8aspectjspring-aop

Why is Spring initialising my Aspect twice?


I have a simple aspect that is doing some logic around one method. I am using Spring Boot with AspjectJ. For some reason, the constructor of the aspect is being invoked twice.

My aspect looks like this:

@Aspect
@Component
public class HandlerLoggingAspect {
  private static final Logger log = LoggerFactory.getLogger(HandlerLoggingAspect.class);

  public HandlerLoggingAspect() {
    log.info("Initialising HandlerLoggingAspect");
  }

  @Around("execution (* io.netty.handler.codec.ByteToMessageDecoder+.decode(*,*,*)) && args(ctx,byteBuf,outList)")
  public void interceptByteDecoding(ProceedingJoinPoint joinPoint, ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> outList) throws Throwable {
    setupMdcAroundJoinPoint(joinPoint, ctx);
  }

 //... rest of the code ...
}

The aspect runs fine around the methods I am expecting it to, but for some reason Spring Boot is initialising my aspect twice. The Intialising HandlerLoggingAspect message appears twice at start.

2016-09-25 18:36:26.041 [main] DEBUG  Running with Spring Boot v1.4.0.RELEASE, Spring v4.3.2.RELEASE
2016-09-25 18:36:26.041 [main] INFO   No active profile set, falling back to default profiles: default
2016-09-25 18:36:29.891 [main] INFO   Initialising HandlerLoggingAspect
2016-09-25 18:36:29.892 [main] INFO   Initialising HandlerLoggingAspect

If I remove the @Component from the Aspect, it is not initialised at all.

My main class looks like this:

@ComponentScan
@Configuration
@EnableAutoConfiguration
@EnableAspectJAutoProxy
@SpringBootApplication
public class Launcher implements CommandLineRunner {
  private static final Logger log = LoggerFactory.getLogger(Launcher.class);

  public static void main(String[] args) {
    SpringApplication.run(Launcher.class, args);
  }

  @Override
  public void run(String... strings) throws Exception {
     //... logic performed by the class ...
  }
}

In case it makes any difference, this is my plugin configuration in my pom.xml for the AspectJ compile time weaving.

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <maven-compiler-plugin.version>3.5.1</maven-compiler-plugin.version>
        <aspectj-maven-plugin.version>1.8</aspectj-maven-plugin.version>
        <org.aspectj.version>1.8.9</org.aspectj.version>

        <org.springframework.boot.version>1.4.0.RELEASE</org.springframework.boot.version>
    </properties>

...
       <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${maven-compiler-plugin.version}</version>
            <configuration>
                <source>${java.version}</source>
                <target>${java.version}</target>
                <proc>none</proc>
            </configuration>
        </plugin>

        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>aspectj-maven-plugin</artifactId>
            <version>${aspectj-maven-plugin.version}</version>
            <configuration>
                <complianceLevel>${java.version}</complianceLevel>
                <source>${java.version}</source>
                <target>${java.version}</target>
                <showWeaveInfo/>
                <forceAjcCompile>true</forceAjcCompile>
                <sources/>
                <weaveDirectories>
                    <weaveDirectory>${project.build.directory}/classes</weaveDirectory>
                </weaveDirectories>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>       <!-- use this goal to weave all your main classes -->
                        <goal>test-compile</goal>  <!-- use this goal to weave all your test classes -->
                    </goals>
                </execution>
            </executions>
            <dependencies>
                <dependency>
                    <groupId>org.aspectj</groupId>
                    <artifactId>aspectjtools</artifactId>
                    <version>${org.aspectj.version}</version>
                </dependency>
            </dependencies>
        </plugin>

When I analysed the rest of the logs, it seems that only one of the instances is actually intercepting the pointcuts. So at least that is good.

What could be the reason for this double initialisation?

** FURTHER INFO **

It seems that in the Spring Documentation, there is something mentioned about Aspects being initialised twice.

http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/aop.html#aop-proxying

However it says the following (I am using Spring 4.3.2):

As of Spring 4.0, the constructor of your proxied object will NOT be called twice anymore since the CGLIB proxy instance will be created via Objenesis. Only if your JVM does not allow for constructor bypassing, you might see double invocations and corresponding debug log entries from Spring’s AOP support.

There is also written the following, and since I am using the @EnableAspectJAutoProxy annotation this should apply too, so I know I am using CGLIB proxy:

To be clear: using proxy-target-class="true" on <tx:annotation-driven/>, <aop:aspectj-autoproxy/> or <aop:config/> elements will force the use of CGLIB proxies for all three of them.

I am using Java 1.8. Is there any known incompatibility between the combination of components I am using that is preventing the constructor bypassing mentioned above?


Solution

  • Its been a while since I had asked this question, and recently encountered the issue again. After some digging around, found the right combination of configurations.

    First of all, its important that the aspectj-maven-plugin has both the aspectjrt and aspectjtools dependencies. For some reason, in my case I had aspectjrt missing (probably copied from some code online) and it was not generating the the aspects properly even though it gave no error. Make sure they are exactly the same version:

         <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>aspectj-maven-plugin</artifactId>
                    <version>1.15.0</version>
                    <configuration>
                        <complianceLevel>21</complianceLevel>
                        <source>21</source>
                        <target>21</target>
                        <showWeaveInfo>true</showWeaveInfo>
                        <verbose>true</verbose>
                        <Xlint>ignore</Xlint>
                        <encoding>UTF-8 </encoding>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <!-- use this goal to weave all your main classes -->
                                <goal>compile</goal>
                                <!-- use this goal to weave all your test classes -->
                                <goal>test-compile</goal>
                            </goals>
                        </execution>
                    </executions>
                    <dependencies>
                        <dependency>
                            <groupId>org.aspectj</groupId>
                            <artifactId>aspectjrt</artifactId>
                            <version>${org.aspectj.version}</version>
                        </dependency>
                        <dependency>
                            <groupId>org.aspectj</groupId>
                            <artifactId>aspectjtools</artifactId>
                            <version>${org.aspectj.version}</version>
                        </dependency>
                    </dependencies>
                </plugin>
    

    Also, make sure you have the right dependencies in your pom.xml file:

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjrt</artifactId>
                <version>${org.aspectj.version}</version>
            </dependency>
    

    Secondly, do not make the Aspects fully Spring Managed beans. Instead just annotate them with @Aspect and @Configurable. This is important, because if you annotate the aspect with @Component or one of the similar annotations, it will get initialised twice.

    For this to work then you need to enable Spring to auto-wire dependencies into your aspect by adding the @EnableSpringConfigured to where you initialise your Spring Boot application (where you have the @SpringBootApplication annotation).

    Finally, initialize your aspect by adding @Bean configuration somewhere where you initialise the rest of your bean config, using the Aspects.aspectOf() factory method.

    For example:

        @Bean
        public HandlerLoggingAspect handlerLoggingAspect() {
            return Aspects.aspectOf(HandlerLoggingAspect.class);
        }
    

    I decided to create a separate AspectConfig class annotated with @Configuration to initialise all aspects from there.

    Make sure your aspect has a no-args constructor, and make any dependencies you want to initialise using the @Autowired annotation on the setter methods of your dependency. (Do not use field injection).

    This solved my issue in having the aspect initialised only once, with Spring dependency injection working, and Compile Time weaving which was something I wanted because I didn't want special class loaders and runtime machinery of that sort.

    Tested on Spring Boot 2.7.18 and Java 21.

    Hope this is useful for anyone encountering this issue.