Why Does @Around Run Twice in Spring AOP and How to Fix It?
This article explains the execution order of Spring AOP annotations, demonstrates why an @Around advice may invoke a method twice when ProceedingJoinPoint.proceed() is called multiple times, and provides corrected code to ensure proper single execution.
Interview series article – continuously updated.
In Spring AOP programming, common annotations include @Before, @After, @Around, and @AfterReturning.
First, define the pointcuts in the aspect class:
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
ThreadLocal<Long> startTime = new ThreadLocal<>();
// * means any return type, any class, any method, any parameters
@Pointcut("execution(public * com.lmx.blog.controller.*.*(..))")
@Order(2)
public void pointCut() {};
@Pointcut("@annotation(com.lmx.blog.annotation.RedisCache)")
@Order(1) // lower number = higher priority
public void annotationPoint() {};
@Before(value = "annotationPoint() || pointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("Method execution before...before");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
logger.info("<=====================================================");
logger.info("Request source: =>" + request.getRemoteAddr());
logger.info("Request URL: " + request.getRequestURL().toString());
logger.info("Request method: " + request.getMethod());
logger.info("Handler method: " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("Request args: " + Arrays.toString(joinPoint.getArgs()));
logger.info("------------------------------------------------------");
startTime.set(System.currentTimeMillis());
}
// Define a pointcut that also matches a method argument
@Around("pointCut() && args(arg)")
public Response around(ProceedingJoinPoint pjp, String arg) throws Throwable {
System.out.println("name:" + arg);
System.out.println("Method around start...around");
String result = null;
try {
result = pjp.proceed().toString() + "aop String";
System.out.println(result);
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("Method around end...around");
return (Response) pjp.proceed();
}
@After("within(com.lmx.blog.controller.*Controller)")
public void after() {
System.out.println("Method after...after.");
}
@AfterReturning(pointcut = "pointCut()", returning = "rst")
public void afterRunning(Response rst) {
if (startTime.get() == null) {
startTime.set(System.currentTimeMillis());
}
System.out.println("Method afterRunning...");
logger.info("Time taken (ms): " + (System.currentTimeMillis() - startTime.get()));
logger.info("Return data: {}", rst);
logger.info("==========================================>");
}
@AfterThrowing("within(com.lmx.blog.controller.*Controller)")
public void afterThrowing() {
System.out.println("Exception after...afterThrowing");
}
}The difference between @Before, @After and @Around can be looked up online; essentially, @Around can implement the functionality of both @Before and @After in a single method.
First test a controller method that retrieves a database record:
@RequestMapping("/achieve")
public Response achieve() {
System.out.println("Method execution-----------");
return Response.ok(articleDetailService.getPrimaryKeyById(1L));
}Console logs show that only @Before and @After advices are triggered because the method does not match the @Around pointcut (it has no parameters).
Next, test a method with a parameter and a custom @RedisCache annotation:
@RedisCache(type = Response.class)
@RequestMapping("/sendEmail")
public Response sendEmailToAuthor(String content) {
System.out.println("Test execution count");
return Response.ok(true);
}Logs reveal that this method matches the @Around pointcut, but the method body is executed twice. The root cause is that ProceedingJoinPoint.proceed() is called twice inside the @Around advice.
Execution order for a matched @Around advice is: @Around (enter advice) @Before Proceed to the target method via
ProceedingJoinPoint.proceed() @AfterReturn to the remaining part of the @Around advice @AfterReturning (or @AfterThrowing if an exception occurs)
Because proceed() was invoked twice, the whole advice chain ran twice.
Corrected @Around implementation uses a single proceed() call:
@Around("pointCut() && args(arg)")
public Response around(ProceedingJoinPoint pjp, String arg) throws Throwable {
System.out.println("name:" + arg);
System.out.println("Method around start...around");
String result = null;
Object object = pjp.proceed();
try {
result = object.toString() + "aop String";
System.out.println(result);
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("Method around end...around");
return (Response) object;
}After this change, the logs show a single execution of the target method and the advice chain.
The reason the custom annotation still triggers @Around is that the method resides in the com.lmx.blog.controller package, satisfying the pointcut expression. If the method were placed in a different controller (e.g., TestController), it would not match the current pointcut.
Two ways to limit the scope of advices:
Use a custom annotation as a pointcut:
@Pointcut("@annotation(com.lmx.blog.annotation.RedisCache)")
@Order(1)
public void annotationPoint() {}Apply @RedisCache to the target methods.
Specify a concrete controller or method:
@Pointcut("execution(public * com.lmx.blog.controller.UserController.*(..))")
@Order(2)
public void pointCut() {}Summary of execution order:
If a method matches the pointcut but not the @Around rule: @Before → @After → @AfterReturning (or @AfterThrowing)
If a method matches both the pointcut and the @Around rule: @Around → @Before → (target method) → @After → @Around (remaining code) → @AfterReturning (or @AfterThrowing)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
