Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mockito mock falsely initialized as CGLIB proxy with AspectJ aspect #33113

Closed
andrethiel1und1 opened this issue Jun 28, 2024 · 12 comments
Closed
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: backported An issue that has been backported to maintenance branches status: feedback-provided Feedback has been provided type: regression A bug that is also a regression
Milestone

Comments

@andrethiel1und1
Copy link

Affects: Framework 6.1.10 (most likely also 6.1.9)

Disclaimer: I have a hard time digging into this issue, because most of the concepts here are new to me. I might use the terminology wrong or be way off with my assessment.

We experienced a change with Framework 6.1.8 where AspectJ related Mockito tests started failing, most likely related to #32970
However, instead of being executed twice there were now 3 calls for one method.
Besides the actual delegate and the cglib proxy (as described in the issue) there was another call from a Mockito mock instance with a cglib proxy - at least I think that is what the debugger indicates: a call from MyService$MockitoMock$<somerandomstuff>$$SpringCGLIB$$0

Since we only manage the spring-boot version directly, we tested the backported fix (I think it is this one: 628b050) using framework version 6.1.10 (spring-boot update to 3.2.7).
The tests still fail because there are still two calls. The CTW cglib proxy call has been fixed, but there is still the Mockito mock related one.
At least that is what it looks like to me.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 28, 2024
@jhoeller
Copy link
Contributor

jhoeller commented Jun 28, 2024

Thanks for raising this, I can imagine a side effect in a Mockito scenario indeed.

Am I understanding correctly that you got an AspectJ aspect configured as a bean and applied with compile-time weaving, and the Spring AOP auto-proxying accidentally picked it up for building a CGLIB proxy - and that latter part got fixed by #32970?

So what remains is an accidental extra call for a Mockito mock that's set up as a Spring bean as well? Is there still a CGLIB proxy being built for that Mockito mock where you do not intend to have a CGLIB proxy as all?

I assume that Mockito mock is of the same type as your regular target bean and therefore matched by auto-proxying as well, and not excluded by #32970 either since the Mockito mock class itself is not compiled by ajc.

What is your test actually trying to assert there? Is it expecting any interactions between the aspect and the Mockito mock at all?

@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label Jun 28, 2024
@jhoeller
Copy link
Contributor

jhoeller commented Jun 28, 2024

Is your Mockito mock effectively extending the class that the aspect actually targets? Is its superclass compiled by ajc and we are not detecting that yet? Any other indication why it is not passing the compiledByAjc(targetClass) check in AspectJExpressionPointcut:286 which is meant to find out about pre-weaved classes that we need to skip for proxying?

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Jun 28, 2024
@jhoeller
Copy link
Contributor

Trying various Mockito scenarios, your MyService$MockitoMock class name above clearly indicates a subclass generated by Mockito (in case of extra interfaces or extra settings - otherwise Mockito just transforms the existing class), with the original MyService class as its superclass. Assuming this is registered as a bean and that the original MyService class is compile-time weaved, it looks we should simply check for ajc markers in the superclass as well in such a scenario.

I've tried that with a patch in AspectJExpressionPointcut.compiledByAjc where it recurses through the given class hierarchy now - which seems to work fine for me so far. @andrethiel1und1 if this seems to match your scenario, I'll commit the patch so that you could try a 6.1.11 snapshot with it. If there is anything else to consider for your scenario, let me know.

@jhoeller jhoeller self-assigned this Jun 28, 2024
@jhoeller jhoeller changed the title AspectJ behavior changed, might Mockito mock falsely be initialized as cglib proxy? Mockito mock falsely initialized as CGLIB proxy with AspectJ aspect Jun 28, 2024
@jhoeller jhoeller added type: regression A bug that is also a regression and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jun 28, 2024
@jhoeller jhoeller added this to the 6.1.11 milestone Jun 28, 2024
@andrethiel1und1
Copy link
Author

Thanks for raising this, I can imagine a side effect in a Mockito scenario indeed.

Am I understanding correctly that you got an AspectJ aspect configured as a bean and applied with compile-time weaving, and the Spring AOP auto-proxying accidentally picked it up for building a CGLIB proxy - and that latter part got fixed by #32970?

The project configuration is all over the place, so I have a hard time evaluating this. I might outline what I find in more detail later.
An a quick initial response, I can outline how the tests work.

We definitely have compile time weaving set up in the project. That much I know.

What is your test actually trying to assert there? Is it expecting any interactions between the aspect and the Mockito mock at all?

There is a LoggingAspect where I set a breakpoint in the log Method to see where it is called from in the stack:

@Aspect
@Slf4j
public class LoggingAspect {

    ...

    private String log(...) {
        return myService.log(le);
    }

    ...

This is a rough outline of a test that fails:

        InOrder inOrder = inOrder(myServiceMock, myAdminServiceMock);
        myAdminService.cancelSomething(getInfo(), SOME_ID);
        inOrder.verify(myServiceMock).log(isCancellation(...));

Here the inOrder.verify() call indirectly expects only one call to log and with 6.1.8 there were 3 calls to log instead of one, now there are 2 calls:

 // I think the Delegate classes are Camunda Steps.

 3.2.6 (6.1.8)
 MyAdminServiceAspectDelegate$$SpringCGLIB$$0 -> cancelSomething
 MyAdminServiceAspectDelegate -> cancelSomething
 MyAdminService$MockitoMock$w7AKM2b0$$SpringCGLIB$$0 -> cancelSomething
 
 3.2.7 (6.1.10)
 MyAdminServiceAspectDelegate -> cancelSomething
 MyAdminService$MockitoMock$xYX7lgEd$$SpringCGLIB$$0 -> cancelSomething

I think up to 6.1.8 there was only one call from MyAdminServiceAspectDelegate, none from a Mockito mock.

So I think what you are writing is basically correct!?

So what remains is an accidental extra call for a Mockito mock that's set up as a Spring bean as well? Is there still a CGLIB proxy being built for that Mockito mock where you do not intend to have a CGLIB proxy as all?

I think this is the case and debugging confirmed it - not sure if I understand the nuances here, though.

I assume that Mockito mock is of the same type as your regular target bean and therefore matched by auto-proxying as well, and not excluded by #32970 either since the Mockito mock class itself is not compiled by ajc.

This is where I have a hard time wrapping my head around the configuration. I'm still looking for the code that defines how the Mockito mock instance is defined. But that seems to make sense. If LTW for example has changed in 6.1.8 and is not covered by the fix, something like that might explain things.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jun 28, 2024
@andrethiel1und1
Copy link
Author

Trying various Mockito scenarios, your MyService$MockitoMock class name above clearly indicates a subclass generated by Mockito (in case of extra interfaces or extra settings - otherwise Mockito just transforms the existing class), with the original MyService class as its superclass. Assuming this is registered as a bean and that the original MyService class is compile-time weaved, it looks we should simply check for ajc markers in the superclass as well in such a scenario.

I've tried that with a patch in AspectJExpressionPointcut.compiledByAjc where it recurses through the given class hierarchy now - which seems to work fine for me so far. @andrethiel1und1 if this seems to match your scenario, I'll commit the patch so that you could try a 6.1.11 snapshot with it. If there is anything else to consider for your scenario, let me know.

Purely intuitively speaking I think checking for ajc markers in the superclass is worth a shot and nothing else comes to mind immediately that needs to be taken into consideration. It is worth a shot, I think.

@jhoeller
Copy link
Contributor

jhoeller commented Jun 28, 2024

Alright, thanks for the feedback! I've pushed the superclass check to 6.1.x, this will be available in the upcoming 6.1.11-SNAPSHOT from https://repo.spring.io/snapshot in a couple of minutes. Please give it a try and let me know whether it makes any difference...

@andrethiel1und1
Copy link
Author

Alright, thanks for the feedback! I've pushed the superclass check to 6.1.x, this will be available in the upcoming 6.1.11-SNAPSHOT from https://repo.spring.io/snapshot in a couple of minutes. Please give it a try and let me know whether it makes any difference...

Thank you for looking into this! I have tested it, but it didn't seem to make a difference.

I imported the 6.1.11-SNAPSHOT version of the spring-framework-bom to override the spring-boot 3.2.7 bom versions and checked the Maven tree to verify that the expected spring libraries have been resolved properly.
Basically I see the same behavior as before.

@jhoeller
Copy link
Contributor

jhoeller commented Jun 28, 2024

Hmm, could you try to step through AspectJExpressionPointcut.matches(Class) line 286 and into the compiledByAjc method from there - when it is being called with the MyService$MockitoMock$... class? For some reason, compiledByAjc seems to return false there when it should really identify it as pre-weaved (now even with a superclass check).

Another possible explanation is that your Mockito mock is somehow running with the pre-compiled AspectJ aspect in the application context but not with compile-time weaving applied to the corresponding application classes. We would have simply ignored the ajc-compiled aspect in that case before, whereas we are actually considering the aspect for proxying now if the target classes are not weaved. Your test setup might have accidentally relied on this before. In order to arrive at consistent behavior in that case, you'd have to run against the ajc-compiled application classes even in such a test scenario - or to remove the aspect bean from your test application context if it is not meant to be used at all.

@jhoeller jhoeller reopened this Jun 28, 2024
@andrethiel1und1
Copy link
Author

Hmm, could you try to step through AspectJExpressionPointcut.matches(Class) line 286 and into the compiledByAjc method from there - when it is being called with the MyService$MockitoMock$... class? For some reason, compiledByAjc seems to return false there when it should really identify it as pre-weaved (now even with a superclass check).

Another possible explanation is that your Mockito mock is somehow running with the pre-compiled AspectJ aspect in the application context but not with compile-time weaving applied to the corresponding application classes. We would have simply ignored the ajc-compiled aspect in that case before, whereas we are actually considering the aspect for proxying now if the target classes are not weaved. Your test setup might have accidentally relied on this before. In order to arrive at consistent behavior in that case, you'd have to run against the ajc-compiled application classes even in such a test scenario - or to remove the aspect bean from your test application context if it is not meant to be used at all.

I tried to step through using the debugger.
I added a breakpoint condition at 286: targetClass.toString().contains("AdminService$MockitoMock")

There are no Fields that start with ajc$ and the parent is simply java.lang.Object. The field this.aspectCompiledByAjc is set to true.
I suppose that is what we would see in the "another explanation" case?

I might have to look into the suggested changes with a colleague next week. With my current understanding of the configuration I wouldn't know where to start changing things ^^. Thanks you for the pointers and the time you put into this.

@jhoeller
Copy link
Contributor

jhoeller commented Jun 28, 2024

That makes sense, AdminService is an interface then? The same Mockito class naming pattern applies there as well. In that case, the base class would indeed be java.lang.Object. So I suppose this interface-based mock object completely replaces the regular AdminService implementation in your system, with only that regular implementation class being ajc-compiled. As a consequence, the AdminService mock is not weaved in your test setup, and the aspect is therefore applied via proxying.

Now I'm wondering why the aspect is present in your test configuration to begin with... when it is effectively not expected to apply to the mocked AdminService. Maybe it is there for some other non-mocked service class that it is expected to apply to, possibly even a separate instance of your regular AdminService implementation class? If so, the easiest way out of this would be to adapt the test assertions, expecting the aspect to apply to the mocked AdminService as well. Or otherwise, if possible, to completely remove the aspect bean from the test configuration so that it does not apply to any variant of your test services.

@jhoeller
Copy link
Contributor

jhoeller commented Jul 3, 2024

At this point, the superclass check seems worth fixing and backporting in any case since it covers superclass-derived mocks where the superclass has the aspect behavior weaved in already.

For scenarios where the target bean does not have the aspect behavior yet, it arguably is correct to apply it via CGLIB then. The general goal is to consistently apply aspects but prevent double execution of aspects against the same target bean, and as far as I understand the scenario above, that is the case here. The test assertions should expect the aspect behavior to be applied to the Mockito mock as well: for every matching target object, but just once per target object.

@jhoeller
Copy link
Contributor

jhoeller commented Jul 3, 2024

Closing this issue based on the reasoning above, to be reopened if any further steps on our side are being identified before the release next week.

@jhoeller jhoeller closed this as completed Jul 3, 2024
jhoeller added a commit that referenced this issue Jul 3, 2024
jhoeller added a commit that referenced this issue Jul 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: backported An issue that has been backported to maintenance branches status: feedback-provided Feedback has been provided type: regression A bug that is also a regression
Projects
None yet
Development

No branches or pull requests

5 participants