Search code examples
javaspringtypesspring-aop

Spring AOP @Around advice return type


I am learning Spring AOP and I know that in @Around advice we use Object return type because the return value of the target method can be of any type. But my question is when the return value is downcasted to actual return type? Does Proxy downcasts it before sending it to main method(where the target method was called)?

In Main -

String result = account.getAccountHolderName();

In aspect class -

@Around("execution(* getAccountHolderName())")
public Object myAroundAdvice(ProceedingJoinPoint joinPoint)  
{
   Object result = joinPoint.proceed();

   return result;
}

I want to know when the result is downcasted to String class after returning from the advice as an Object


Solution

  • There is no casting within the Spring AOP core classes themselves, because they only pass along Objects when delegating method calls to AOP proxies. The user is responsible to return the correct type from the @Around advice. But of course, there is a final cast, which you can clearly see if you e.g. make the advice return something other than the actual return type of the intercepted method. Then you will see an exception like:

    Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class de.scrum_master.spring.q59783423.HoldResponse (java.lang.String is in module java.base of loader 'bootstrap'; de.scrum_master.spring.q59783423.HoldResponse is in unnamed module of loader 'app')
        at de.scrum_master.spring.q59783423.HoldPaymentOrchestrationService$$EnhancerBySpringCGLIB$$13706881.execute(<generated>)
        at de.scrum_master.spring.q59783423.MyApplication.doStuff(MyApplication.java:46)
        at de.scrum_master.spring.q59783423.MyApplication.main(MyApplication.java:22)
    

    In this example, I made my around advice falsely return a String instead of a HoldResponse. You can see that the ClassCastException occurs within the dynamic proxy generated by Spring, in this case a CGLIB proxy (could also be a JDK proxy, depending on the circumstances). So there is actually a cast happening, otherwise there would not be a class cast exception.

    When dumping the proxy's byte code using the little agent I described in this answer, you will see something like:

      public final doExecute(Lde/scrum_master/spring/q59783423/HoldRequest;)Lde/scrum_master/spring/q59783423/HoldResponse; throws de/scrum_master/spring/q59783423/PaymentServiceException 
        ALOAD 0
        GETFIELD de/scrum_master/spring/q59783423/HoldPaymentOrchestrationService$$EnhancerBySpringCGLIB$$13706881.CGLIB$CALLBACK_0 : Lorg/springframework/cglib/proxy/MethodInterceptor;
        DUP
        IFNONNULL L0
        POP
        ALOAD 0
        INVOKESTATIC de/scrum_master/spring/q59783423/HoldPaymentOrchestrationService$$EnhancerBySpringCGLIB$$13706881.CGLIB$BIND_CALLBACKS (Ljava/lang/Object;)V
        ALOAD 0
        GETFIELD de/scrum_master/spring/q59783423/HoldPaymentOrchestrationService$$EnhancerBySpringCGLIB$$13706881.CGLIB$CALLBACK_0 : Lorg/springframework/cglib/proxy/MethodInterceptor;
       L0
        DUP
        IFNULL L1
        ALOAD 0
        GETSTATIC de/scrum_master/spring/q59783423/HoldPaymentOrchestrationService$$EnhancerBySpringCGLIB$$13706881.CGLIB$doExecute$0$Method : Ljava/lang/reflect/Method;
        ICONST_1
        ANEWARRAY java/lang/Object
        DUP
        ICONST_0
        ALOAD 1
        AASTORE
        GETSTATIC de/scrum_master/spring/q59783423/HoldPaymentOrchestrationService$$EnhancerBySpringCGLIB$$13706881.CGLIB$doExecute$0$Proxy : Lorg/springframework/cglib/proxy/MethodProxy;
        INVOKEINTERFACE org/springframework/cglib/proxy/MethodInterceptor.intercept (Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;Lorg/springframework/cglib/proxy/MethodProxy;)Ljava/lang/Object; (itf)
        CHECKCAST de/scrum_master/spring/q59783423/HoldResponse
        ARETURN
       L1
        ALOAD 0
        ALOAD 1
        INVOKESPECIAL de/scrum_master/spring/q59783423/HoldPaymentOrchestrationService.doExecute (Lde/scrum_master/spring/q59783423/HoldRequest;)Lde/scrum_master/spring/q59783423/HoldResponse;
        ARETURN
        MAXSTACK = 7
        MAXLOCALS = 2
    

    Please especially note

        INVOKEINTERFACE org/springframework/cglib/proxy/MethodInterceptor.intercept (Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;Lorg/springframework/cglib/proxy/MethodProxy;)Ljava/lang/Object; (itf)
        CHECKCAST de/scrum_master/spring/q59783423/HoldResponse
    

    I.e., the proxy calls org.springframework.cglib.proxy.MethodInterceptor#intercept and then casts the result to the intercepted method's return type. There you have your Spring AOP magic.

    Interesting question, actually. 👍


    BTW, if you wish to make your around advice more specific, you may give it a return type other than Object. But then you need to cast the result of proceed() before returning it. In Spring AOP, you are responsible for declaring a return type matching the intercepted method(s). In native AspectJ, declaring a specific return type would automatically narrow down matching to joinpoints compatible with the return type, which is not the case in Spring AOP.