diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index ea798a66824..4dcedff0018 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -55,6 +55,8 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe private final ContentNegotiationManager contentNegotiationManager; + private final ResponseBodyInterceptorChain interceptorChain; + protected AbstractMessageConverterMethodProcessor(List> messageConverters) { this(messageConverters, null); @@ -62,9 +64,15 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe protected AbstractMessageConverterMethodProcessor(List> messageConverters, ContentNegotiationManager manager) { + this(messageConverters, manager, null); + } + + protected AbstractMessageConverterMethodProcessor(List> messageConverters, + ContentNegotiationManager manager, List responseBodyInterceptors) { super(messageConverters); this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager()); + this.interceptorChain = new ResponseBodyInterceptorChain(responseBodyInterceptors); } @@ -152,6 +160,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } } if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { + returnValue = this.interceptorChain.invoke(returnValue, selectedMediaType, + (Class>) messageConverter.getClass(), + returnType, inputMessage, outputMessage); ((HttpMessageConverter) messageConverter).write(returnValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index e1c3775b94d..33b06258bfe 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -79,6 +79,9 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); + private final List responseBodyInterceptors = new ArrayList(); + + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap, ExceptionHandlerMethodResolver>(64); @@ -106,6 +109,19 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce 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 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 class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce @Override public void afterPropertiesSet() { + + // Do this first, it may add ResponseBody interceptors + initExceptionHandlerAdviceCache(); + if (this.argumentResolvers == null) { List resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); @@ -241,7 +261,30 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce List 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 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 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce 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 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce return handlers; } - private void initExceptionHandlerAdviceCache() { - if (getApplicationContext() == null) { - return; - } - if (logger.isDebugEnabled()) { - logger.debug("Looking for exception mappings: " + getApplicationContext()); - } - - List 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. */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index c7e787f4e7e..5177f48935d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -57,12 +57,16 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro public HttpEntityMethodProcessor(List> messageConverters, ContentNegotiationManager contentNegotiationManager) { - super(messageConverters, contentNegotiationManager); } + public HttpEntityMethodProcessor(List> messageConverters, + ContentNegotiationManager contentNegotiationManager, List responseBodyInterceptors) { + super(messageConverters, contentNegotiationManager, responseBodyInterceptors); + } - @Override + + @Override public boolean supportsParameter(MethodParameter parameter) { return HttpEntity.class.equals(parameter.getParameterType()); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 4f8ee6599ec..aaddbd21fc7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -132,8 +132,11 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter private List> messageConverters; + private List responseBodyInterceptors = new ArrayList(); + private WebBindingInitializer webBindingInitializer; + private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync"); private Long asyncRequestTimeout; @@ -306,6 +309,14 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter 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 @@ -315,14 +326,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter this.messageConverters = messageConverters; } - /** - * 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; - } - /** * Return the configured message body converters. */ @@ -330,6 +333,19 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter return messageConverters; } + /** + * 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 responseBodyInterceptors) { + this.responseBodyInterceptors.clear(); + if (responseBodyInterceptors != null) { + this.responseBodyInterceptors.addAll(responseBodyInterceptors); + } + } + /** * Provide a WebBindingInitializer with "global" initialization to apply * to every DataBinder instance. @@ -481,6 +497,10 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter @Override public void afterPropertiesSet() { + + // Do this first, it may add ResponseBody interceptors + initControllerAdviceCache(); + if (this.argumentResolvers == null) { List resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); @@ -493,7 +513,35 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter List 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 beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); + Collections.sort(beans, new OrderComparator()); + + for (ControllerAdviceBean bean : beans) { + Set attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS); + if (!attrMethods.isEmpty()) { + this.modelAttributeAdviceCache.put(bean, attrMethods); + logger.info("Detected @ModelAttribute methods in " + bean); + } + Set 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 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter 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 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter // 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 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter return handlers; } - private void initControllerAdviceCache() { - if (getApplicationContext() == null) { - return; - } - if (logger.isDebugEnabled()) { - logger.debug("Looking for controller advice: " + getApplicationContext()); - } - - List beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); - Collections.sort(beans, new OrderComparator()); - - for (ControllerAdviceBean bean : beans) { - Set attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS); - if (!attrMethods.isEmpty()) { - this.modelAttributeAdviceCache.put(bean, attrMethods); - logger.info("Detected @ModelAttribute methods in " + bean); - } - Set 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 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index bc226326e9e..752e8436a42 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -69,10 +69,15 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter public RequestResponseBodyMethodProcessor(List> messageConverters, ContentNegotiationManager contentNegotiationManager) { - super(messageConverters, contentNegotiationManager); } + public RequestResponseBodyMethodProcessor(List> messageConverters, + ContentNegotiationManager contentNegotiationManager, List responseBodyInterceptors) { + super(messageConverters, contentNegotiationManager, responseBodyInterceptors); + } + + @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptor.java new file mode 100644 index 00000000000..55a266953b3 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptor.java @@ -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 the type supported by the message converter + * + * @return the body that was passed in or a modified, possibly new instance + */ + T beforeBodyWrite(T body, MediaType contentType, Class> converterType, + MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChain.java new file mode 100644 index 00000000000..f74dd75d566 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChain.java @@ -0,0 +1,75 @@ +/* + * 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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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; +import org.springframework.util.Assert; +import org.springframework.web.method.ControllerAdviceBean; + +import java.util.List; + +/** + * Invokes a a list of ResponseBodyInterceptor's. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +class ResponseBodyInterceptorChain { + + private static Log logger = LogFactory.getLog(ResponseBodyInterceptorChain.class); + + private final List interceptors; + + + public ResponseBodyInterceptorChain(List interceptors) { + this.interceptors = interceptors; + } + + + public T invoke(T body, MediaType contentType, Class> converterType, + MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { + + if (this.interceptors != null) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking ResponseBody interceptor chain for body=" + body); + } + for (Object interceptor : this.interceptors) { + if (interceptor instanceof ControllerAdviceBean) { + ControllerAdviceBean adviceBean = (ControllerAdviceBean) interceptor; + if (!adviceBean.isApplicableToBeanType(returnType.getContainingClass())) { + continue; + } + interceptor = adviceBean.resolveBean(); + } + Assert.state(interceptor instanceof ResponseBodyInterceptor); + body = ((ResponseBodyInterceptor) interceptor).beforeBodyWrite( + body, contentType, converterType, returnType, request, response); + } + if (logger.isDebugEnabled()) { + logger.debug("After interceptor chain body=" + body); + } + } + return body; + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChainTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChainTests.java new file mode 100644 index 00000000000..90f6b0885fe --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChainTests.java @@ -0,0 +1,26 @@ +/* + * 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; + +/** + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class ResponseBodyInterceptorChainTests { + + +}