I'm having a difficult time determining the minimal reproduction of this problem, but I can describe the behavior in detail and am available for debugging:

I have an MVC controller written in Groovy that uses traits. This produces a class that implements several interfaces (including the trait itself and some Groovy synthetics), but none of these interfaces is directly relevant to MVC. One of them defines an @Autowired(required = false) setter that is not in use in this case:

@RestController
@RequestMapping(BASE_URL)
class UsersController implements HasIdGenerator {
  public static final String BASE_URL = '/users'

  @PostMapping
  ResponseEntity register(@RequestBody User newUser) {
    // logic
  }
}

This controller is registered properly with RequestMappingHandlerMapping. However, when I add an annotation to register that triggers Spring AOP (specifically, in this case, a @PreAuthorize meta-annotation), the controller disappears from registration.

I've tracked down the immediate problem to RequestMappingHandlerMapping#isHandler(Class). When the Spring Security AOP is active, the argument passed to this method is com.sun.proxy.$Proxy123, and then AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) returns false because the annotation scanner ends up checking the proxy interface and its superinterfaces but not the actual controller class where the controller methods are declared. The result is that all methods that aren't part of an interface API disappear from Spring's visibility of the bean and thence from registration.

So far the logic underlying this failure case appears sound, but I can't figure out why it doesn't usually break; that is, controllers work all the time with Spring Security annotations. I'm suspecting that it has something to do with the decisions of AOP application strategies, but this is getting farther into the infrastructure than I have intuition for. Removing the Groovy trait (and thus the interfaces involved, but not the GroovyObject interface), results in a CGLIB proxy properly exposing all of the controller methods.

Update: On further debugging, it appears that ProxyProcessorSupport explicitly lists groovy.lang.GroovyObject as "not a 'real' interface" for purposes of determining whether a class "looks JDK-proxyable", and it's the addition of the extra interface by the trait that leads #evaluateProxyInterfaces to decide to use a JDK proxy.

The end result appears to be that adding a nontrivial interface to any Spring MVC controller will silently disable its controller methods by means of hiding them from the annotation scan. It seems to me that either classes with @Controller (including a meta) should always be forced to CGLIB, or #isHandler should unwrap proxies (but since this would, I think, break the actual advice itself, that's probably not practical).

Comment From: mdeinum

When using AOP and a controller implementing an interface you must use class-based proxies. That is also documented in the reference guide.

And for Spring Security to use class-based proxies you need to specify that on @EnableGlobalMethodSecurity by setting the proxyTargetClass attribute to true.

The same applies to @EnableAsync (see #26535).

All in all enabling class-based proxying application-wide for all the different AOP parts is a bit of a pain that needs improvement.

Comment From: chrylis

I understand the mechanical reason why this behavior happens, but it's still remarkably surprising (I've been using Spring since before JavaConfig and still got bitten; it showed up on a regression).

  1. The note in the docs doesn't provide the context that would have anchored the requirement in my mental model (in contrast to, for example, the self-call problem). Would you be interested if I provided an expanded section, and if so, what's the best way to contribute that?

  2. Specifically because this is such a counterintuitive requirement and happens with a well-known configuration, would it be acceptable to add a list of annotations (e.g., @Controller) that, if present on a bean being wrapped with a JDK proxy, would cause DefaultAopProxyFactory to log a message warning that "@Controllers need class-based proxies"?

(Note: I think that what may have surprised me is that the proxy infrastructure explicitly disregards certain interfaces by name when making its strategy selection, and so it's not that the controller class went from no-interface to has-interface, but that the new interface was "reasonable".)

Comment From: StephenOTT

Hit this issue working with groovy beans being compiled at runtime. would be nice to have a few refs/notes in annotation's java doc for the annotations that require the class proxying. feels pretty rate that someone would end up on https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestmapping-proxying when working with the typical annotations such as @Controller

Comment From: snicoll

That's the reason why Spring Boot enables cglib proxies out-of-the-box. I don't see that default to change short term, but we certainly welcome any update to the reference guide you'd find useful. Feel free to raise a PR with your proposal and we can take it from here.