Skip to content

Commit 96b18c8

Browse files
committed
Add ResponseBodyInterceptor
This change introduces a new ResponseBodyInterceptor interface that can be used to modify the response after @responsebody or ResponseEntity methods but before the body is actually written to the response with the selected HttpMessageConverter. The RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver each have a property to configure such interceptors. In addition both RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver detect if any @ControllerAdvice bean implements ResponseBodyInterceptor and use it accordingly. Issue: SPR-10859
1 parent f73a8ba commit 96b18c8

File tree

8 files changed

+284
-61
lines changed

8 files changed

+284
-61
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,24 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
5555

5656
private final ContentNegotiationManager contentNegotiationManager;
5757

58+
private final ResponseBodyInterceptorChain interceptorChain;
59+
5860

5961
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
6062
this(messageConverters, null);
6163
}
6264

6365
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
6466
ContentNegotiationManager manager) {
67+
this(messageConverters, manager, null);
68+
}
69+
70+
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
71+
ContentNegotiationManager manager, List<Object> responseBodyInterceptors) {
6572

6673
super(messageConverters);
6774
this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
75+
this.interceptorChain = new ResponseBodyInterceptorChain(responseBodyInterceptors);
6876
}
6977

7078

@@ -152,6 +160,9 @@ else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICAT
152160
}
153161
}
154162
if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
163+
returnValue = this.interceptorChain.invoke(returnValue, selectedMediaType,
164+
(Class<HttpMessageConverter<T>>) messageConverter.getClass(),
165+
returnType, inputMessage, outputMessage);
155166
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
156167
if (logger.isDebugEnabled()) {
157168
logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +

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

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
7979

8080
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
8181

82+
private final List<Object> responseBodyInterceptors = new ArrayList<Object>();
83+
84+
8285
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
8386
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>(64);
8487

@@ -106,6 +109,19 @@ public ExceptionHandlerExceptionResolver() {
106109
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
107110
}
108111

112+
/**
113+
* Add one or more interceptors to be invoked after the execution of a controller
114+
* method annotated with {@code @ResponseBody} or returning {@code ResponseEntity}
115+
* but before the body is written to the response with the selected
116+
* {@code HttpMessageConverter}.
117+
*/
118+
public void setResponseBodyInterceptors(List<ResponseBodyInterceptor> responseBodyInterceptors) {
119+
this.responseBodyInterceptors.clear();
120+
if (responseBodyInterceptors != null) {
121+
this.responseBodyInterceptors.addAll(responseBodyInterceptors);
122+
}
123+
}
124+
109125
/**
110126
* Provide resolvers for custom argument types. Custom resolvers are ordered
111127
* after built-in ones. To override the built-in support for argument
@@ -233,6 +249,10 @@ public ApplicationContext getApplicationContext() {
233249

234250
@Override
235251
public void afterPropertiesSet() {
252+
253+
// Do this first, it may add ResponseBody interceptors
254+
initExceptionHandlerAdviceCache();
255+
236256
if (this.argumentResolvers == null) {
237257
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
238258
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
@@ -241,7 +261,30 @@ public void afterPropertiesSet() {
241261
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
242262
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
243263
}
244-
initExceptionHandlerAdviceCache();
264+
}
265+
266+
private void initExceptionHandlerAdviceCache() {
267+
if (getApplicationContext() == null) {
268+
return;
269+
}
270+
if (logger.isDebugEnabled()) {
271+
logger.debug("Looking for exception mappings: " + getApplicationContext());
272+
}
273+
274+
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
275+
Collections.sort(beans, new OrderComparator());
276+
277+
for (ControllerAdviceBean bean : beans) {
278+
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(bean.getBeanType());
279+
if (resolver.hasExceptionMappings()) {
280+
this.exceptionHandlerAdviceCache.put(bean, resolver);
281+
logger.info("Detected @ExceptionHandler methods in " + bean);
282+
}
283+
if (ResponseBodyInterceptor.class.isAssignableFrom(bean.getBeanType())) {
284+
this.responseBodyInterceptors.add(bean);
285+
logger.info("Detected ResponseBodyInterceptor implementation in " + bean);
286+
}
287+
}
245288
}
246289

247290
/**
@@ -274,11 +317,13 @@ protected List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers()
274317
handlers.add(new ModelAndViewMethodReturnValueHandler());
275318
handlers.add(new ModelMethodProcessor());
276319
handlers.add(new ViewMethodReturnValueHandler());
277-
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
320+
handlers.add(new HttpEntityMethodProcessor(
321+
getMessageConverters(), this.contentNegotiationManager, this.responseBodyInterceptors));
278322

279323
// Annotation-based return value types
280324
handlers.add(new ModelAttributeMethodProcessor(false));
281-
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
325+
handlers.add(new RequestResponseBodyMethodProcessor(
326+
getMessageConverters(), this.contentNegotiationManager, this.responseBodyInterceptors));
282327

283328
// Multi-purpose return value types
284329
handlers.add(new ViewNameMethodReturnValueHandler());
@@ -295,26 +340,6 @@ protected List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers()
295340
return handlers;
296341
}
297342

298-
private void initExceptionHandlerAdviceCache() {
299-
if (getApplicationContext() == null) {
300-
return;
301-
}
302-
if (logger.isDebugEnabled()) {
303-
logger.debug("Looking for exception mappings: " + getApplicationContext());
304-
}
305-
306-
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
307-
Collections.sort(beans, new OrderComparator());
308-
309-
for (ControllerAdviceBean bean : beans) {
310-
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(bean.getBeanType());
311-
if (resolver.hasExceptionMappings()) {
312-
this.exceptionHandlerAdviceCache.put(bean, resolver);
313-
logger.info("Detected @ExceptionHandler methods in " + bean);
314-
}
315-
}
316-
}
317-
318343
/**
319344
* Find an {@code @ExceptionHandler} method and invoke it to handle the raised exception.
320345
*/

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,16 @@ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters
5757

5858
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
5959
ContentNegotiationManager contentNegotiationManager) {
60-
6160
super(messageConverters, contentNegotiationManager);
6261
}
6362

63+
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
64+
ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyInterceptors) {
65+
super(messageConverters, contentNegotiationManager, responseBodyInterceptors);
66+
}
6467

65-
@Override
68+
69+
@Override
6670
public boolean supportsParameter(MethodParameter parameter) {
6771
return HttpEntity.class.equals(parameter.getParameterType());
6872
}

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

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,11 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
132132

133133
private List<HttpMessageConverter<?>> messageConverters;
134134

135+
private List<Object> responseBodyInterceptors = new ArrayList<Object>();
136+
135137
private WebBindingInitializer webBindingInitializer;
136138

139+
137140
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync");
138141

139142
private Long asyncRequestTimeout;
@@ -306,6 +309,14 @@ public List<ModelAndViewResolver> getModelAndViewResolvers() {
306309
return modelAndViewResolvers;
307310
}
308311

312+
/**
313+
* Set the {@link ContentNegotiationManager} to use to determine requested media types.
314+
* If not set, the default constructor is used.
315+
*/
316+
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
317+
this.contentNegotiationManager = contentNegotiationManager;
318+
}
319+
309320
/**
310321
* Provide the converters to use in argument resolvers and return value
311322
* handlers that support reading and/or writing to the body of the
@@ -316,18 +327,23 @@ public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters
316327
}
317328

318329
/**
319-
* Set the {@link ContentNegotiationManager} to use to determine requested media types.
320-
* If not set, the default constructor is used.
330+
* Return the configured message body converters.
321331
*/
322-
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
323-
this.contentNegotiationManager = contentNegotiationManager;
332+
public List<HttpMessageConverter<?>> getMessageConverters() {
333+
return messageConverters;
324334
}
325335

326336
/**
327-
* Return the configured message body converters.
337+
* Add one or more interceptors to be invoked after the execution of a controller
338+
* method annotated with {@code @ResponseBody} or returning {@code ResponseEntity}
339+
* but before the body is written to the response with the selected
340+
* {@code HttpMessageConverter}.
328341
*/
329-
public List<HttpMessageConverter<?>> getMessageConverters() {
330-
return messageConverters;
342+
public void setResponseBodyInterceptors(List<ResponseBodyInterceptor> responseBodyInterceptors) {
343+
this.responseBodyInterceptors.clear();
344+
if (responseBodyInterceptors != null) {
345+
this.responseBodyInterceptors.addAll(responseBodyInterceptors);
346+
}
331347
}
332348

333349
/**
@@ -481,6 +497,10 @@ protected ConfigurableBeanFactory getBeanFactory() {
481497

482498
@Override
483499
public void afterPropertiesSet() {
500+
501+
// Do this first, it may add ResponseBody interceptors
502+
initControllerAdviceCache();
503+
484504
if (this.argumentResolvers == null) {
485505
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
486506
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
@@ -493,7 +513,35 @@ public void afterPropertiesSet() {
493513
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
494514
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
495515
}
496-
initControllerAdviceCache();
516+
}
517+
518+
private void initControllerAdviceCache() {
519+
if (getApplicationContext() == null) {
520+
return;
521+
}
522+
if (logger.isInfoEnabled()) {
523+
logger.info("Looking for @ControllerAdvice: " + getApplicationContext());
524+
}
525+
526+
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
527+
Collections.sort(beans, new OrderComparator());
528+
529+
for (ControllerAdviceBean bean : beans) {
530+
Set<Method> attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
531+
if (!attrMethods.isEmpty()) {
532+
this.modelAttributeAdviceCache.put(bean, attrMethods);
533+
logger.info("Detected @ModelAttribute methods in " + bean);
534+
}
535+
Set<Method> binderMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS);
536+
if (!binderMethods.isEmpty()) {
537+
this.initBinderAdviceCache.put(bean, binderMethods);
538+
logger.info("Detected @InitBinder methods in " + bean);
539+
}
540+
if (ResponseBodyInterceptor.class.isAssignableFrom(bean.getBeanType())) {
541+
this.responseBodyInterceptors.add(bean);
542+
logger.info("Detected ResponseBodyInterceptor implementation in " + bean);
543+
}
544+
}
497545
}
498546

499547
/**
@@ -583,7 +631,8 @@ private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
583631
handlers.add(new ModelAndViewMethodReturnValueHandler());
584632
handlers.add(new ModelMethodProcessor());
585633
handlers.add(new ViewMethodReturnValueHandler());
586-
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
634+
handlers.add(new HttpEntityMethodProcessor(
635+
getMessageConverters(), this.contentNegotiationManager, this.responseBodyInterceptors));
587636
handlers.add(new HttpHeadersReturnValueHandler());
588637
handlers.add(new CallableMethodReturnValueHandler());
589638
handlers.add(new DeferredResultMethodReturnValueHandler());
@@ -592,7 +641,8 @@ private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
592641

593642
// Annotation-based return value types
594643
handlers.add(new ModelAttributeMethodProcessor(false));
595-
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
644+
handlers.add(new RequestResponseBodyMethodProcessor(
645+
getMessageConverters(), this.contentNegotiationManager, this.responseBodyInterceptors));
596646

597647
// Multi-purpose return value types
598648
handlers.add(new ViewNameMethodReturnValueHandler());
@@ -614,31 +664,6 @@ private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
614664
return handlers;
615665
}
616666

617-
private void initControllerAdviceCache() {
618-
if (getApplicationContext() == null) {
619-
return;
620-
}
621-
if (logger.isDebugEnabled()) {
622-
logger.debug("Looking for controller advice: " + getApplicationContext());
623-
}
624-
625-
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
626-
Collections.sort(beans, new OrderComparator());
627-
628-
for (ControllerAdviceBean bean : beans) {
629-
Set<Method> attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
630-
if (!attrMethods.isEmpty()) {
631-
this.modelAttributeAdviceCache.put(bean, attrMethods);
632-
logger.info("Detected @ModelAttribute methods in " + bean);
633-
}
634-
Set<Method> binderMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS);
635-
if (!binderMethods.isEmpty()) {
636-
this.initBinderAdviceCache.put(bean, binderMethods);
637-
logger.info("Detected @InitBinder methods in " + bean);
638-
}
639-
}
640-
}
641-
642667
/**
643668
* Always return {@code true} since any method argument and return value
644669
* type will be processed in some way. A method argument not recognized

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,15 @@ public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageC
6969

7070
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
7171
ContentNegotiationManager contentNegotiationManager) {
72-
7372
super(messageConverters, contentNegotiationManager);
7473
}
7574

75+
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
76+
ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyInterceptors) {
77+
super(messageConverters, contentNegotiationManager, responseBodyInterceptors);
78+
}
79+
80+
7681
@Override
7782
public boolean supportsParameter(MethodParameter parameter) {
7883
return parameter.hasParameterAnnotation(RequestBody.class);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.servlet.mvc.method.annotation;
18+
19+
import org.springframework.core.MethodParameter;
20+
import org.springframework.http.MediaType;
21+
import org.springframework.http.converter.HttpMessageConverter;
22+
import org.springframework.http.server.ServerHttpRequest;
23+
import org.springframework.http.server.ServerHttpResponse;
24+
25+
/**
26+
* Allows customizing the response after the execution of an {@code @ResponseBody}
27+
* or an {@code ResponseEntity} controller method but before the body is written
28+
* with an {@code HttpMessageConverter}.
29+
*
30+
* @author Rossen Stoyanchev
31+
* @since 4.1
32+
*/
33+
public interface ResponseBodyInterceptor {
34+
35+
/**
36+
* Invoked after an {@code HttpMessageConverter} is selected and just before
37+
* its write method is invoked.
38+
*
39+
* @param body the body to be written
40+
* @param contentType the selected content type
41+
* @param converterType the selected converter that will write the body
42+
* @param returnType the return type of the controller method
43+
* @param request the current request
44+
* @param response the current response
45+
* @param <T> the type supported by the message converter
46+
*
47+
* @return the body that was passed in or a modified, possibly new instance
48+
*/
49+
<T> T beforeBodyWrite(T body, MediaType contentType, Class<HttpMessageConverter<T>> converterType,
50+
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response);
51+
52+
}

0 commit comments

Comments
 (0)