Skip to content

Commit f28a5d0

Browse files
committed
Proper exception for controller method return types that do not work with MvcUriComponentsBuilder (e.g. final classes)
Includes direct use of ControllerMethodInvocationInterceptor for return type Object, avoiding the attempt to generate an Object subclass. Issue: SPR-16710
1 parent a6885c7 commit f28a5d0

File tree

2 files changed

+93
-60
lines changed

2 files changed

+93
-60
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,8 @@ public static UriComponentsBuilder fromMethodName(UriComponentsBuilder builder,
253253
* controller.getAddressesForCountry("US")
254254
* builder = MvcUriComponentsBuilder.fromMethodCall(controller);
255255
* </pre>
256-
*
257256
* <p><strong>Note:</strong> This method extracts values from "Forwarded"
258257
* and "X-Forwarded-*" headers if found. See class-level docs.
259-
*
260258
* @param info either the value returned from a "mock" controller
261259
* invocation or the "mock" controller itself after an invocation
262260
* @return a UriComponents instance
@@ -610,7 +608,11 @@ public static <T> T controller(Class<T> controllerType) {
610608

611609
@SuppressWarnings("unchecked")
612610
private static <T> T initProxy(Class<?> type, ControllerMethodInvocationInterceptor interceptor) {
613-
if (type.isInterface()) {
611+
if (type == Object.class) {
612+
return (T) interceptor;
613+
}
614+
615+
else if (type.isInterface()) {
614616
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
615617
factory.addInterface(type);
616618
factory.addInterface(MethodInvocationInfo.class);
@@ -709,8 +711,18 @@ public UriComponentsBuilder withMethod(Class<?> controllerType, Method method, O
709711
}
710712

711713

714+
public interface MethodInvocationInfo {
715+
716+
Class<?> getControllerType();
717+
718+
Method getControllerMethod();
719+
720+
Object[] getArgumentValues();
721+
}
722+
723+
712724
private static class ControllerMethodInvocationInterceptor
713-
implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor {
725+
implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor, MethodInvocationInfo {
714726

715727
private final Class<?> controllerType;
716728

@@ -727,23 +739,29 @@ private static class ControllerMethodInvocationInterceptor
727739
@Override
728740
@Nullable
729741
public Object intercept(Object obj, Method method, Object[] args, @Nullable MethodProxy proxy) {
730-
if (method.getName().equals("getControllerMethod")) {
742+
if (method.getName().equals("getControllerType")) {
743+
return this.controllerType;
744+
}
745+
else if (method.getName().equals("getControllerMethod")) {
731746
return this.controllerMethod;
732747
}
733748
else if (method.getName().equals("getArgumentValues")) {
734749
return this.argumentValues;
735750
}
736-
else if (method.getName().equals("getControllerType")) {
737-
return this.controllerType;
738-
}
739751
else if (ReflectionUtils.isObjectMethod(method)) {
740752
return ReflectionUtils.invokeMethod(method, obj, args);
741753
}
742754
else {
743755
this.controllerMethod = method;
744756
this.argumentValues = args;
745757
Class<?> returnType = method.getReturnType();
746-
return (void.class == returnType ? null : returnType.cast(initProxy(returnType, this)));
758+
try {
759+
return (returnType == void.class ? null : returnType.cast(initProxy(returnType, this)));
760+
}
761+
catch (Throwable ex) {
762+
throw new IllegalStateException(
763+
"Failed to create proxy for controller method return type: " + method, ex);
764+
}
747765
}
748766
}
749767

@@ -752,16 +770,23 @@ else if (ReflectionUtils.isObjectMethod(method)) {
752770
public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable {
753771
return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null);
754772
}
755-
}
756773

774+
@Override
775+
public Class<?> getControllerType() {
776+
return this.controllerType;
777+
}
757778

758-
public interface MethodInvocationInfo {
759-
760-
Method getControllerMethod();
761-
762-
Object[] getArgumentValues();
779+
@Override
780+
public Method getControllerMethod() {
781+
Assert.state(this.controllerMethod != null, "Not initialized yet");
782+
return this.controllerMethod;
783+
}
763784

764-
Class<?> getControllerType();
785+
@Override
786+
public Object[] getArgumentValues() {
787+
Assert.state(this.argumentValues != null, "Not initialized yet");
788+
return this.argumentValues;
789+
}
765790
}
766791

767792

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2018 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.
@@ -40,6 +40,7 @@
4040
import org.springframework.mock.web.test.MockServletContext;
4141
import org.springframework.stereotype.Controller;
4242
import org.springframework.util.MultiValueMap;
43+
import org.springframework.web.bind.annotation.GetMapping;
4344
import org.springframework.web.bind.annotation.PathVariable;
4445
import org.springframework.web.bind.annotation.RequestBody;
4546
import org.springframework.web.bind.annotation.RequestMapping;
@@ -54,17 +55,9 @@
5455
import org.springframework.web.util.UriComponents;
5556
import org.springframework.web.util.UriComponentsBuilder;
5657

57-
import static org.hamcrest.Matchers.contains;
58-
import static org.hamcrest.Matchers.containsInAnyOrder;
59-
import static org.hamcrest.Matchers.endsWith;
60-
import static org.hamcrest.Matchers.is;
61-
import static org.hamcrest.Matchers.startsWith;
62-
import static org.junit.Assert.assertEquals;
63-
import static org.junit.Assert.assertThat;
64-
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.fromController;
65-
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.fromMethodCall;
66-
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.fromMethodName;
67-
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on;
58+
import static org.hamcrest.Matchers.*;
59+
import static org.junit.Assert.*;
60+
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.*;
6861

6962
/**
7063
* Unit tests for {@link org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder}.
@@ -142,15 +135,15 @@ public void testFromControllerWithCustomBaseUrlViaInstance() {
142135
}
143136

144137
@Test
145-
public void testFromMethodNamePathVariable() throws Exception {
146-
UriComponents uriComponents = fromMethodName(
147-
ControllerWithMethods.class, "methodWithPathVariable", new Object[]{"1"}).build();
138+
public void testFromMethodNamePathVariable() {
139+
UriComponents uriComponents = fromMethodName(ControllerWithMethods.class,
140+
"methodWithPathVariable", "1").build();
148141

149142
assertThat(uriComponents.toUriString(), is("http://localhost/something/1/foo"));
150143
}
151144

152145
@Test
153-
public void testFromMethodNameTypeLevelPathVariable() throws Exception {
146+
public void testFromMethodNameTypeLevelPathVariable() {
154147
this.request.setContextPath("/myapp");
155148
UriComponents uriComponents = fromMethodName(
156149
PersonsAddressesController.class, "getAddressesForCountry", "DE").buildAndExpand("1");
@@ -159,7 +152,7 @@ public void testFromMethodNameTypeLevelPathVariable() throws Exception {
159152
}
160153

161154
@Test
162-
public void testFromMethodNameTwoPathVariables() throws Exception {
155+
public void testFromMethodNameTwoPathVariables() {
163156
DateTime now = DateTime.now();
164157
UriComponents uriComponents = fromMethodName(
165158
ControllerWithMethods.class, "methodWithTwoPathVariables", 1, now).build();
@@ -168,7 +161,7 @@ public void testFromMethodNameTwoPathVariables() throws Exception {
168161
}
169162

170163
@Test
171-
public void testFromMethodNameWithPathVarAndRequestParam() throws Exception {
164+
public void testFromMethodNameWithPathVarAndRequestParam() {
172165
UriComponents uriComponents = fromMethodName(
173166
ControllerWithMethods.class, "methodForNextPage", "1", 10, 5).build();
174167

@@ -178,59 +171,56 @@ public void testFromMethodNameWithPathVarAndRequestParam() throws Exception {
178171
assertThat(queryParams.get("offset"), contains("10"));
179172
}
180173

181-
// SPR-12977
182-
183-
@Test
184-
public void fromMethodNameWithBridgedMethod() throws Exception {
174+
@Test // SPR-12977
175+
public void fromMethodNameWithBridgedMethod() {
185176
UriComponents uriComponents = fromMethodName(PersonCrudController.class, "get", (long) 42).build();
177+
186178
assertThat(uriComponents.toUriString(), is("http://localhost/42"));
187179
}
188180

189-
// SPR-11391
190-
191-
@Test
192-
public void testFromMethodNameTypeLevelPathVariableWithoutArgumentValue() throws Exception {
181+
@Test // SPR-11391
182+
public void testFromMethodNameTypeLevelPathVariableWithoutArgumentValue() {
193183
UriComponents uriComponents = fromMethodName(UserContactController.class, "showCreate", 123).build();
194184

195185
assertThat(uriComponents.getPath(), is("/user/123/contacts/create"));
196186
}
197187

198188
@Test
199-
public void testFromMethodNameNotMapped() throws Exception {
189+
public void testFromMethodNameNotMapped() {
200190
UriComponents uriComponents = fromMethodName(UnmappedController.class, "unmappedMethod").build();
201191

202192
assertThat(uriComponents.toUriString(), is("http://localhost/"));
203193
}
204194

205195
@Test
206-
public void testFromMethodNameWithCustomBaseUrlViaStaticCall() throws Exception {
196+
public void testFromMethodNameWithCustomBaseUrlViaStaticCall() {
207197
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://example.org:9090/base");
208198
UriComponents uriComponents = fromMethodName(builder, ControllerWithMethods.class,
209-
"methodWithPathVariable", new Object[] {"1"}).build();
199+
"methodWithPathVariable", "1").build();
210200

211201
assertEquals("http://example.org:9090/base/something/1/foo", uriComponents.toString());
212202
assertEquals("http://example.org:9090/base", builder.toUriString());
213203
}
214204

215205
@Test
216-
public void testFromMethodNameWithCustomBaseUrlViaInstance() throws Exception {
206+
public void testFromMethodNameWithCustomBaseUrlViaInstance() {
217207
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://example.org:9090/base");
218208
MvcUriComponentsBuilder mvcBuilder = MvcUriComponentsBuilder.relativeTo(builder);
219209
UriComponents uriComponents = mvcBuilder.withMethodName(ControllerWithMethods.class,
220-
"methodWithPathVariable", new Object[] {"1"}).build();
210+
"methodWithPathVariable", "1").build();
221211

222212
assertEquals("http://example.org:9090/base/something/1/foo", uriComponents.toString());
223213
assertEquals("http://example.org:9090/base", builder.toUriString());
224214
}
225215

226216
@Test
227-
public void testFromMethodNameWithMetaAnnotation() throws Exception {
217+
public void testFromMethodNameWithMetaAnnotation() {
228218
UriComponents uriComponents = fromMethodName(MetaAnnotationController.class, "handleInput").build();
229219
assertThat(uriComponents.toUriString(), is("http://localhost/input"));
230220
}
231221

232-
@Test // SPR-14405
233-
public void testFromMappingNameWithOptionalParam() throws Exception {
222+
@Test // SPR-14405
223+
public void testFromMappingNameWithOptionalParam() {
234224
UriComponents uriComponents = fromMethodName(ControllerWithMethods.class,
235225
"methodWithOptionalParam", new Object[] {null}).build();
236226

@@ -255,26 +245,26 @@ public void testFromMethodCallOnSubclass() {
255245

256246
@Test
257247
public void testFromMethodCallWithTypeLevelUriVars() {
258-
UriComponents uriComponents = fromMethodCall(on(
259-
PersonsAddressesController.class).getAddressesForCountry("DE")).buildAndExpand(15);
248+
UriComponents uriComponents = fromMethodCall(
249+
on(PersonsAddressesController.class).getAddressesForCountry("DE")).buildAndExpand(15);
260250

261251
assertThat(uriComponents.toUriString(), endsWith("/people/15/addresses/DE"));
262252
}
263253

264254

265255
@Test
266256
public void testFromMethodCallWithPathVar() {
267-
UriComponents uriComponents = fromMethodCall(on(
268-
ControllerWithMethods.class).methodWithPathVariable("1")).build();
257+
UriComponents uriComponents = fromMethodCall(
258+
on(ControllerWithMethods.class).methodWithPathVariable("1")).build();
269259

270260
assertThat(uriComponents.toUriString(), startsWith("http://localhost"));
271261
assertThat(uriComponents.toUriString(), endsWith("/something/1/foo"));
272262
}
273263

274264
@Test
275265
public void testFromMethodCallWithPathVarAndRequestParams() {
276-
UriComponents uriComponents = fromMethodCall(on(
277-
ControllerWithMethods.class).methodForNextPage("1", 10, 5)).build();
266+
UriComponents uriComponents = fromMethodCall(
267+
on(ControllerWithMethods.class).methodForNextPage("1", 10, 5)).build();
278268

279269
assertThat(uriComponents.getPath(), is("/something/1/foo"));
280270

@@ -285,8 +275,8 @@ public void testFromMethodCallWithPathVarAndRequestParams() {
285275

286276
@Test
287277
public void testFromMethodCallWithPathVarAndMultiValueRequestParams() {
288-
UriComponents uriComponents = fromMethodCall(on(
289-
ControllerWithMethods.class).methodWithMultiValueRequestParams("1", Arrays.asList(3, 7), 5)).build();
278+
UriComponents uriComponents = fromMethodCall(
279+
on(ControllerWithMethods.class).methodWithMultiValueRequestParams("1", Arrays.asList(3, 7), 5)).build();
290280

291281
assertThat(uriComponents.getPath(), is("/something/1/foo"));
292282

@@ -315,7 +305,7 @@ public void testFromMethodCallWithCustomBaseUrlViaInstance() {
315305
}
316306

317307
@Test
318-
public void testFromMappingName() throws Exception {
308+
public void testFromMappingName() {
319309
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
320310
context.setServletContext(new MockServletContext());
321311
context.register(WebConfig.class);
@@ -332,7 +322,7 @@ public void testFromMappingName() throws Exception {
332322
}
333323

334324
@Test
335-
public void testFromMappingNameWithCustomBaseUrl() throws Exception {
325+
public void testFromMappingNameWithCustomBaseUrl() {
336326
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
337327
context.setServletContext(new MockServletContext());
338328
context.register(WebConfig.class);
@@ -370,6 +360,13 @@ public void usesFirstHostOfXForwardedHost() {
370360
assertThat(uriComponents.toUriString(), startsWith("http://barfoo:8888"));
371361
}
372362

363+
@Test // SPR-16710
364+
public void withStringReturnType() {
365+
UriComponents uriComponents = MvcUriComponentsBuilder.fromMethodCall(
366+
on(BookingController.class).getBooking(21L)).buildAndExpand(42);
367+
assertEquals("http://localhost/hotels/42/bookings/21", uriComponents.encode().toUri().toString());
368+
}
369+
373370

374371
static class Person {
375372

@@ -516,4 +513,15 @@ public PersonsAddressesController controller() {
516513
}
517514
}
518515

516+
517+
@Controller
518+
@RequestMapping("/hotels/{hotel}")
519+
public class BookingController {
520+
521+
@GetMapping("/bookings/{booking}")
522+
public Object getBooking(@PathVariable Long booking) {
523+
return "url";
524+
}
525+
}
526+
519527
}

0 commit comments

Comments
 (0)