Skip to content

Commit

Permalink
Add ResponseBodyInterceptor
Browse files Browse the repository at this point in the history
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
rstoyanchev committed May 19, 2014
1 parent f73a8ba commit 96b18c8
Showing 8 changed files with 284 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -55,16 +55,24 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe

private final ContentNegotiationManager contentNegotiationManager;

private final ResponseBodyInterceptorChain interceptorChain;


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

protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager manager) {
this(messageConverters, manager, null);
}

protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager manager, List<Object> responseBodyInterceptors) {

super(messageConverters);
this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
this.interceptorChain = new ResponseBodyInterceptorChain(responseBodyInterceptors);
}


@@ -152,6 +160,9 @@ else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICAT
}
}
if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
returnValue = this.interceptorChain.invoke(returnValue, selectedMediaType,
(Class<HttpMessageConverter<T>>) messageConverter.getClass(),
returnType, inputMessage, outputMessage);
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +
Original file line number Diff line number Diff line change
@@ -79,6 +79,9 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce

private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();

private final List<Object> responseBodyInterceptors = new ArrayList<Object>();


private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>(64);

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

/**
* Add one or more interceptors to be invoked after the execution of a controller
* method annotated with {@code @ResponseBody} or returning {@code ResponseEntity}
* but before the body is written to the response with the selected
* {@code HttpMessageConverter}.
*/
public void setResponseBodyInterceptors(List<ResponseBodyInterceptor> responseBodyInterceptors) {
this.responseBodyInterceptors.clear();
if (responseBodyInterceptors != null) {
this.responseBodyInterceptors.addAll(responseBodyInterceptors);
}
}

/**
* Provide resolvers for custom argument types. Custom resolvers are ordered
* after built-in ones. To override the built-in support for argument
@@ -233,6 +249,10 @@ public ApplicationContext getApplicationContext() {

@Override
public void afterPropertiesSet() {

// Do this first, it may add ResponseBody interceptors
initExceptionHandlerAdviceCache();

if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
@@ -241,7 +261,30 @@ public void afterPropertiesSet() {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
initExceptionHandlerAdviceCache();
}

private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Looking for exception mappings: " + getApplicationContext());
}

List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
Collections.sort(beans, new OrderComparator());

for (ControllerAdviceBean bean : beans) {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(bean.getBeanType());
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(bean, resolver);
logger.info("Detected @ExceptionHandler methods in " + bean);
}
if (ResponseBodyInterceptor.class.isAssignableFrom(bean.getBeanType())) {
this.responseBodyInterceptors.add(bean);
logger.info("Detected ResponseBodyInterceptor implementation in " + bean);
}
}
}

/**
@@ -274,11 +317,13 @@ protected List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers()
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
handlers.add(new HttpEntityMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyInterceptors));

// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
handlers.add(new RequestResponseBodyMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyInterceptors));

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

private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Looking for exception mappings: " + getApplicationContext());
}

List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
Collections.sort(beans, new OrderComparator());

for (ControllerAdviceBean bean : beans) {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(bean.getBeanType());
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(bean, resolver);
logger.info("Detected @ExceptionHandler methods in " + bean);
}
}
}

/**
* Find an {@code @ExceptionHandler} method and invoke it to handle the raised exception.
*/
Original file line number Diff line number Diff line change
@@ -57,12 +57,16 @@ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters

public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager) {

super(messageConverters, contentNegotiationManager);
}

public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyInterceptors) {
super(messageConverters, contentNegotiationManager, responseBodyInterceptors);
}

@Override

@Override
public boolean supportsParameter(MethodParameter parameter) {
return HttpEntity.class.equals(parameter.getParameterType());
}
Original file line number Diff line number Diff line change
@@ -132,8 +132,11 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter

private List<HttpMessageConverter<?>> messageConverters;

private List<Object> responseBodyInterceptors = new ArrayList<Object>();

private WebBindingInitializer webBindingInitializer;


private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync");

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

/**
* Set the {@link ContentNegotiationManager} to use to determine requested media types.
* If not set, the default constructor is used.
*/
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
this.contentNegotiationManager = contentNegotiationManager;
}

/**
* Provide the converters to use in argument resolvers and return value
* handlers that support reading and/or writing to the body of the
@@ -316,18 +327,23 @@ public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters
}

/**
* Set the {@link ContentNegotiationManager} to use to determine requested media types.
* If not set, the default constructor is used.
* Return the configured message body converters.
*/
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
this.contentNegotiationManager = contentNegotiationManager;
public List<HttpMessageConverter<?>> getMessageConverters() {
return messageConverters;
}

/**
* Return the configured message body converters.
* Add one or more interceptors to be invoked after the execution of a controller
* method annotated with {@code @ResponseBody} or returning {@code ResponseEntity}
* but before the body is written to the response with the selected
* {@code HttpMessageConverter}.
*/
public List<HttpMessageConverter<?>> getMessageConverters() {
return messageConverters;
public void setResponseBodyInterceptors(List<ResponseBodyInterceptor> responseBodyInterceptors) {
this.responseBodyInterceptors.clear();
if (responseBodyInterceptors != null) {
this.responseBodyInterceptors.addAll(responseBodyInterceptors);
}
}

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

@Override
public void afterPropertiesSet() {

// Do this first, it may add ResponseBody interceptors
initControllerAdviceCache();

if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
@@ -493,7 +513,35 @@ public void afterPropertiesSet() {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
initControllerAdviceCache();
}

private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
if (logger.isInfoEnabled()) {
logger.info("Looking for @ControllerAdvice: " + getApplicationContext());
}

List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
Collections.sort(beans, new OrderComparator());

for (ControllerAdviceBean bean : beans) {
Set<Method> attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(bean, attrMethods);
logger.info("Detected @ModelAttribute methods in " + bean);
}
Set<Method> binderMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(bean, binderMethods);
logger.info("Detected @InitBinder methods in " + bean);
}
if (ResponseBodyInterceptor.class.isAssignableFrom(bean.getBeanType())) {
this.responseBodyInterceptors.add(bean);
logger.info("Detected ResponseBodyInterceptor implementation in " + bean);
}
}
}

/**
@@ -583,7 +631,8 @@ private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
handlers.add(new HttpEntityMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyInterceptors));
handlers.add(new HttpHeadersReturnValueHandler());
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
@@ -592,7 +641,8 @@ private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {

// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager));
handlers.add(new RequestResponseBodyMethodProcessor(
getMessageConverters(), this.contentNegotiationManager, this.responseBodyInterceptors));

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

private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Looking for controller advice: " + getApplicationContext());
}

List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
Collections.sort(beans, new OrderComparator());

for (ControllerAdviceBean bean : beans) {
Set<Method> attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(bean, attrMethods);
logger.info("Detected @ModelAttribute methods in " + bean);
}
Set<Method> binderMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(bean, binderMethods);
logger.info("Detected @InitBinder methods in " + bean);
}
}
}

/**
* Always return {@code true} since any method argument and return value
* type will be processed in some way. A method argument not recognized
Original file line number Diff line number Diff line change
@@ -69,10 +69,15 @@ public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageC

public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager) {

super(messageConverters, contentNegotiationManager);
}

public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyInterceptors) {
super(messageConverters, contentNegotiationManager, responseBodyInterceptors);
}


@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.servlet.mvc.method.annotation;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;

/**
* Allows customizing the response after the execution of an {@code @ResponseBody}
* or an {@code ResponseEntity} controller method but before the body is written
* with an {@code HttpMessageConverter}.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public interface ResponseBodyInterceptor {

/**
* Invoked after an {@code HttpMessageConverter} is selected and just before
* its write method is invoked.
*
* @param body the body to be written
* @param contentType the selected content type
* @param converterType the selected converter that will write the body
* @param returnType the return type of the controller method
* @param request the current request
* @param response the current response
* @param <T> the type supported by the message converter
*
* @return the body that was passed in or a modified, possibly new instance
*/
<T> T beforeBodyWrite(T body, MediaType contentType, Class<HttpMessageConverter<T>> converterType,
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response);

}
Loading
Oops, something went wrong.

0 comments on commit 96b18c8

Please sign in to comment.