Skip to content

Commit 7301b58

Browse files
committed
Improve info on use of @controller's with aop proxying
Before this change, issues surrounding the use of @controller's in combination with AOP proxying, resulted in an IllegalArgumentException when trying to invoke the controller method. This change detects such cases proactively and reports them with a clear recommendation to use class-based proxying when it comes to @controller's. This is the most optimcal approach for controllers in many respects, also allows @MVC annotations to remain on the class. The documentation has also been updated to have a specific section on @controller's and AOP proxying providing the same advice. Issue:SPR-11281
1 parent 12598f8 commit 7301b58

File tree

3 files changed

+55
-34
lines changed

3 files changed

+55
-34
lines changed

spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -183,11 +183,11 @@ private Object resolveProvidedArgument(MethodParameter parameter, Object... prov
183183
private Object invoke(Object... args) throws Exception {
184184
ReflectionUtils.makeAccessible(this.getBridgedMethod());
185185
try {
186+
assertTargetBean(getBridgedMethod(), getBean(), args);
186187
return getBridgedMethod().invoke(getBean(), args);
187188
}
188189
catch (IllegalArgumentException e) {
189-
String msg = getInvocationErrorMessage(e.getMessage(), args);
190-
throw new IllegalArgumentException(msg, e);
190+
throw new IllegalArgumentException(getInvocationErrorMessage(e.getMessage(), args), e);
191191
}
192192
catch (InvocationTargetException e) {
193193
// Unwrap for HandlerExceptionResolvers ...
@@ -208,6 +208,25 @@ else if (targetException instanceof Exception) {
208208
}
209209
}
210210

211+
/**
212+
* Assert that the target bean class is an instance of the class where the given
213+
* method is declared. In some cases the actual controller instance at request-
214+
* processing time may be a JDK dynamic proxy (lazy initialization, prototype
215+
* beans, and others). {@code @Controller}'s that require proxying should prefer
216+
* class-based proxy mechanisms.
217+
*/
218+
private void assertTargetBean(Method method, Object targetBean, Object[] args) {
219+
Class<?> methodDeclaringClass = method.getDeclaringClass();
220+
Class<?> targetBeanClass = targetBean.getClass();
221+
if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
222+
String message = "The mapped controller method class '" + methodDeclaringClass.getName() +
223+
"' is not an instance of the actual controller bean instance '" +
224+
targetBeanClass.getName() + "'. If the controller requires proxying " +
225+
"(e.g. due to @Transactional), please use class-based proxying.";
226+
throw new IllegalArgumentException(getInvocationErrorMessage(message, args));
227+
}
228+
}
229+
211230
private String getInvocationErrorMessage(String message, Object[] resolvedArgs) {
212231
StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message));
213232
sb.append("Resolved arguments: \n");

spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -211,11 +211,11 @@ private Object resolveProvidedArgument(MethodParameter parameter, Object... prov
211211
private Object invoke(Object... args) throws Exception {
212212
ReflectionUtils.makeAccessible(this.getBridgedMethod());
213213
try {
214+
assertTargetBean(getBridgedMethod(), getBean(), args);
214215
return getBridgedMethod().invoke(getBean(), args);
215216
}
216217
catch (IllegalArgumentException e) {
217-
String msg = getInvocationErrorMessage(e.getMessage(), args);
218-
throw new IllegalArgumentException(msg, e);
218+
throw new IllegalArgumentException(getInvocationErrorMessage(e.getMessage(), args), e);
219219
}
220220
catch (InvocationTargetException e) {
221221
// Unwrap for HandlerExceptionResolvers ...
@@ -236,6 +236,25 @@ else if (targetException instanceof Exception) {
236236
}
237237
}
238238

239+
/**
240+
* Assert that the target bean class is an instance of the class where the given
241+
* method is declared. In some cases the actual controller instance at request-
242+
* processing time may be a JDK dynamic proxy (lazy initialization, prototype
243+
* beans, and others). {@code @Controller}'s that require proxying should prefer
244+
* class-based proxy mechanisms.
245+
*/
246+
private void assertTargetBean(Method method, Object targetBean, Object[] args) {
247+
Class<?> methodDeclaringClass = method.getDeclaringClass();
248+
Class<?> targetBeanClass = targetBean.getClass();
249+
if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
250+
String message = "The mapped controller method class '" + methodDeclaringClass.getName() +
251+
"' is not an instance of the actual controller bean instance '" +
252+
targetBeanClass.getName() + "'. If the controller requires proxying " +
253+
"(e.g. due to @Transactional), please use class-based proxying.";
254+
throw new IllegalArgumentException(getInvocationErrorMessage(message, args));
255+
}
256+
}
257+
239258
private String getInvocationErrorMessage(String message, Object[] resolvedArgs) {
240259
StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message));
241260
sb.append("Resolved arguments: \n");

src/asciidoc/index.adoc

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28381,7 +28381,7 @@ snippet:
2838128381

2838228382

2838328383
[[mvc-ann-requestmapping]]
28384-
==== Mapping Requests With Using @RequestMapping
28384+
==== Mapping Requests With @RequestMapping
2838528385

2838628386
You use the `@RequestMapping` annotation to map URLs such as `/appointments` onto an
2838728387
entire class or a particular handler method. Typically the class-level annotation maps a
@@ -28472,26 +28472,17 @@ application shows a multi-action controller using `@RequestMapping`:
2847228472
}
2847328473
----
2847428474

28475-
.@RequestMapping On Interface Methods
28476-
[TIP]
28477-
====
28478-
28479-
A common pitfall when working with annotated controller classes happens when applying
28480-
functionality that requires creating a proxy for the controller object (e.g.
28481-
`@Transactional` methods). Usually you will introduce an interface for the controller in
28482-
order to use JDK dynamic proxies. To make this work you must move the `@RequestMapping`
28483-
annotations, as well as any other type and method-level annotations (e.g.
28484-
`@ModelAttribute`, `@InitBinder`) to the interface as well as the mapping mechanism can
28485-
only "see" the interface exposed by the proxy. Alternatively, you could activate
28486-
`proxy-target-class="true"` in the configuration for the functionality applied to the
28487-
controller (in our transaction scenario in `<tx:annotation-driven />`). Doing so
28488-
indicates that CGLIB-based subclass proxies should be used instead of interface-based
28489-
JDK proxies. For more information on various proxying mechanisms see <<aop-proxying>>.
28490-
28491-
Note however that method argument annotations, e.g. `@RequestParam`, must be present in
28492-
the method signatures of the controller class.
28493-
====
28475+
[[mvc-ann-requestmapping-proxying]]
28476+
===== `@Controller`'s and AOP Proxying
2849428477

28478+
In some cases a controller may need to be decorated with an AOP proxy at runtime.
28479+
One example is if you choose to have `@Transactional` annotations directly on the
28480+
controller. When this is the case, for controllers specifically, we recommend
28481+
using class-based proxying. This is typically the default choice with controllers.
28482+
However if a controller must implement an interface that is not a Spring Context
28483+
callback (e.g. `InitializingBean`, `*Aware`, etc), you may need to explicitly
28484+
configure class-based proxying. For example with `<tx:annotation-driven />`,
28485+
change to `<tx:annotation-driven proxy-target-class="true" />`.
2849528486

2849628487
[[mvc-ann-requestmapping-31-vs-30]]
2849728488
===== New Support Classes for @RequestMapping methods in Spring MVC 3.1
@@ -29465,14 +29456,6 @@ attribute name:
2946529456
}
2946629457
----
2946729458

29468-
[NOTE]
29469-
====
29470-
When using controller interfaces (e.g., for AOP proxying), make sure to consistently put
29471-
__all__ your mapping annotations - such as `@RequestMapping` and `@SessionAttributes` -
29472-
on the controller __interface__ rather than on the implementation class.
29473-
====
29474-
29475-
2947629459
[[mvc-ann-redirect-attributes]]
2947729460
===== Specifying redirect and flash attributes
2947829461
By default all model attributes are considered to be exposed as URI template variables

0 commit comments

Comments
 (0)