After upgrading org.springframework.boot from version 3.1.6 to 3.1.12, the class function calls that are injected using @Autowired
or through constructor injection trigger the AOP twice. However, this issue does not occur when invoking functions within the same class, using objects instantiated with new, or utilizing static function calls across classes.
Logs:
2024-12-23 11:06:48,721 INFO MonitorMethodAspect L329 : thread_id=59 thread_name=http-nio-8080-exec-1 thread_priority=5 execution(String com.controller.KnowledgeBaseController.test()) test
2024-12-23 11:06:48,725 INFO MonitorMethodAspect L329 : thread_id=59 thread_name=http-nio-8080-exec-1 thread_priority=5 execution(String com.controller.KnowledgeBaseController.test()) test
Other logs with little different, the space between joint point function parameters.
2024-12-23 11:19:36,406 INFO MonitorMethodAspect L230 : thread_id=66 thread_name=http-nio-8080-exec-8 thread_priority=5 execution(Flux com.message.net.impl.MultiLlmClientImpl.callStreaming(ChatRequest, ResourceType, Message, Message, UUID)) FluxAutoConnectFuseable
2024-12-23 11:19:36,739 INFO MonitorMethodAspect L230 : thread_id=66 thread_name=http-nio-8080-exec-8 thread_priority=5 execution(Flux com.message.net.impl.MultiLlmClientImpl.callStreaming(ChatRequest,ResourceType,Message,Message,UUID)) FluxAutoConnectFuseable
To clarify, com.service.KnowledgeBaseService.test
method is confirmed to only exist once and is not duplicated within the codebase.
Additionally, org.springframework.boot
version 3.1.11 works fine, but this issue still exists through version 3.2.2 (the latest version my project can be upgraded to).
Is this a version-specific issue that will be resolved in a future release, or do I need to modify my implementation to address this?
AspectConfig:
@Configuration
@ComponentScan("com.monitor")
public class AspectConfig {
@Bean
public MonitorMethodAspect theAspect() {
MonitorMethodAspect aspect = Aspects.aspectOf(MonitorMethodAspect.class);
return aspect;
}
}
MonitorMethodAspect:
@Aspect
public class MonitorMethodAspect {
@Autowired MonitorService monitorService;
@AfterReturning(pointcut = "execution(* com.controller.KnowledgeBaseController.test(..))", returning = "result")
public void test(JoinPoint joinPoint, String result){
log.info(joinPoint+" "+result);
}
}
KnowledgeBaseController:
@RestController
public class KnowledgeBaseController {
public String test(){
return "test";
}
}
build.gradle:
plugins {
id 'java'
id "io.freefair.aspectj.post-compile-weaving" version "8.0.1"
id 'org.asciidoctor.convert' version '1.5.8'
id 'org.springframework.boot' version '3.1.12'
id 'io.spring.dependency-management' version '1.1.0'
id 'com.diffplug.spotless' version '6.17.0'
id 'jacoco'
id 'project-report'
id("com.google.osdetector") version "1.7.1"
}
spotless {
java {
importOrder()
removeUnusedImports()
googleJavaFormat()
formatAnnotations()
}
}
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
bootRun {
jvmArgs = [
"-Dspring.profiles.active=rd",
]
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.3'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation "com.google.guava:guava:32.0.1-jre"
runtimeOnly 'org.aspectj:aspectjrt:1.9.19'
runtimeOnly 'org.aspectj:aspectjweaver:1.9.19'
implementation 'org.apache.pdfbox:pdfbox:2.0.24'
implementation group: 'com.microsoft.azure', name: 'adal4j', version: '1.6.6'
implementation group: 'com.nimbusds', name: 'oauth2-oidc-sdk'
implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-spring', version: '4.33.0'
implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-provider-jdbc-template', version: '4.33.0'
if (osdetector.classifier == "osx-aarch_64") {
runtimeOnly('io.netty:netty-all:4.1.89.Final')
}
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'org.springframework.security:spring-security-jwt'
implementation 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.6.8'
implementation 'com.auth0:java-jwt:3.19.3'
implementation 'com.microsoft.graph:microsoft-graph:5.41.0'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.azure:azure-identity:1.10.4'
implementation 'org.springframework.security:spring-security-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'commons-beanutils:commons-beanutils:1.9.4'
implementation 'com.google.code.gson:gson:2.10'
implementation 'org.springframework.retry:spring-retry:2.0.2'
implementation 'com.knuddels:jtokkit:1.1.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
// test
testImplementation('org.junit.jupiter:junit-jupiter:5.9.3')
testImplementation ('org.springframework.boot:spring-boot-starter-test')
{
exclude group: 'org.mockito', module :'mockito-core'
}
testImplementation 'org.mockito:mockito-inline:5.2.0'
implementation 'io.opentelemetry:opentelemetry-api:1.26.0'
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
modules {
module("org.springframework.boot:spring-boot-starter-logging") {
replacedBy 'org.springframework.boot:spring-boot-starter-log4j2'
}
}
implementation platform('io.micrometer:micrometer-tracing-bom:1.0.4')
implementation 'io.micrometer:micrometer-tracing'
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.postgresql:postgresql'
implementation 'com.zaxxer:HikariCP:4.0.3'
testImplementation 'com.h2database:h2:2.1.214'
implementation 'org.liquibase:liquibase-core'
implementation 'org.apache.tinkerpop:gremlin-core:3.6.4'
implementation 'org.apache.tinkerpop:gremlin-driver:3.6.4'
implementation 'com.azure.spring:spring-cloud-azure-dependencies:5.3.0'
implementation 'com.azure.spring:spring-cloud-azure-starter-keyvault-secrets:5.3.0'
implementation 'org.springframework.data:spring-data-redis'
implementation 'redis.clients:jedis:4.4.3'
implementation 'io.protostuff:protostuff-core:1.7.4'
implementation 'io.protostuff:protostuff-runtime:1.7.4'
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.13'
implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1'
implementation 'io.projectreactor.addons:reactor-extra:3.5.1'
implementation 'com.opencsv:opencsv:3.7'
implementation 'com.azure:azure-storage-blob:12.23.0'
implementation 'commons-io:commons-io:2.16.0'
implementation 'commons-validator:commons-validator:1.7'
implementation 'org.apache.commons:commons-compress:1.24.0'
implementation 'org.apache.poi:poi:5.2.4'
implementation 'org.apache.poi:poi-ooxml:5.2.4'
}
ext {
set('snippetsDir', file("build/generated-snippets"))
}
asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
test {
useJUnitPlatform()
jacoco {
destinationFile = file("$buildDir/jacoco/jacocoTest.exec")
classDumpDir = file("$buildDir/jacoco/classpathdumps")
}
exclude '**/Api**'
}
jacoco {
toolVersion = "0.8.8"
reportsDir = file("$buildDir/reports/jacoco")
}
jacocoTestReport {
reports {
xml.enabled true
csv.enabled false
html.destination file("${buildDir}/jacocoHtml")
}
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, includes: [
], exclude: [
])
}))
}
}
That this ever worked for you in some Spring versions - I did not double-check that it really did - was pure luck, because you are making a mistake:
You use native AspectJ post-compile weaving via Freefair. Why you are doing that, I am not sure. Maybe, you want to enable aspects triggering for self-invocation within Spring components. Maybe, your real use case even needs to apply aspects to classes which are not Spring-managed at all.
On top of that, your Spring configuration explicitly instantiates the native aspect as a Spring @Bean
, probably to somehow trick Spring into supporting auto-wiring.
By doing these both things, your aspect now in a way exists twice: as a native AspectJ aspect and as a Spring AOP aspect or at least a Spring component. This is wrong.
The correct way to enable @Inject
or @Autowired
for non-Spring objects is described in the Spring manual, chapter "Using AspectJ to Dependency Inject Domain Objects with Spring": You use @Configurable
from spring-aspects
, which needs to be activated via @EnableSpringConfigured
and can be applied either by load-time weaving or by post-compile weaving. As you are using the latter via Freefair already anyway, we will stick with that.
package com.earny.test.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.aspectj.EnableSpringConfigured;
@Configuration
@ComponentScan("com.earny.test.monitor")
// Enable @Configurable aspect to inject Spring beans into non-managed objects
@EnableSpringConfigured
public class AspectConfig {}
package com.earny.test.monitor.aop;
import com.earny.test.monitor.annotation.MonitorOnce;
import com.earny.test.monitor.annotation.MonitorTwice;
import com.earny.test.monitor.service.MonitorService;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
@Aspect
@Log4j2
@Configurable // Inject Spring beans into non-managed objects
public class MonitorMethodAspect {
@Autowired MonitorService monitorService;
@Pointcut("@annotation(monitor)")
public void callMonitorOnce(MonitorOnce monitor) {}
@Pointcut("@annotation(monitor)")
public void callMonitorTwice(MonitorTwice monitor) {}
@AfterReturning(
value = "callMonitorOnce(monitor) && execution(* *(..)) && args(.., a, b)",
returning = "result")
public void afterReturn1(JoinPoint joinPoint, MonitorOnce monitor, int a, int b, String result) {
log.info("afterReturn1 joinPoint: " + joinPoint + " result:" + result);
if (monitorService == null)
throw new RuntimeException("@Autowired MonitorService is not working unexpectedly");
}
@AfterReturning(
value = "callMonitorTwice(monitor) && execution(* *(..)) && args(.., a, b)",
returning = "result")
public void afterReturn2(JoinPoint joinPoint, MonitorTwice monitor, int a, int b, String result) {
log.info("afterReturn2 joinPoint: " + joinPoint + " result:" + result);
if (monitorService == null)
throw new RuntimeException("@Autowired MonitorService is not working unexpectedly");
}
}
Why use @Configurable
here? Because a native AspectJ aspect is not Spring-managed and also should not be tricked into being so. But still, we can inject Spring components this way.
BTW, the runtime exception in case of monitorService == null
is just to prove that it works. You can remove those two pairs of lines from your aspect advice methods after you successfully tested it.
Last but not least, you also need this in your Gradle build configuration, telling Freefair to use spring-aspects
as an aspect library for post-compile weaving:
dependencies {
// ...
// Enable @Configurable aspect to inject Spring beans into non-managed objects
aspect 'org.springframework:spring-aspects'
}
Running the project like this, the log output should look like this:
-----------------------------------Test Start-----------------------------------
exec returnOnce function
afterReturn1 joinPoint: execution(String com.earny.test.controller.TestController.returnOnce(int, int)) result:returnOnce
exec returnTwice function
afterReturn2 joinPoint: execution(String com.earny.test.service.impl.TestServiceImpl.returnTwice(int, int)) result:returnTwice
-----------------------------------Test End-----------------------------------
For your convenience, I also created a GitHub pull request.