Browse Source

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
pull/493/merge
Rossen Stoyanchev 11 years ago
parent
commit
96b18c8dc2
  1. 11
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java
  2. 71
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java
  3. 8
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
  4. 95
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java
  5. 7
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java
  6. 52
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptor.java
  7. 75
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChain.java
  8. 26
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyInterceptorChainTests.java

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

@ -55,6 +55,8 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe @@ -55,6 +55,8 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
private final ContentNegotiationManager contentNegotiationManager;
private final ResponseBodyInterceptorChain interceptorChain;
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
this(messageConverters, null);
@ -62,9 +64,15 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe @@ -62,9 +64,15 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
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 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe @@ -152,6 +160,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
}
}
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 [" +

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

@ -79,6 +79,9 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce @@ -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 class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce @@ -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<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 class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce @@ -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<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
@ -241,7 +261,30 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce @@ -241,7 +261,30 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
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 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce @@ -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 @@ -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<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.
*/

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

@ -57,12 +57,16 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro @@ -57,12 +57,16 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
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());
}

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

@ -132,8 +132,11 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter @@ -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 class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter @@ -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
@ -316,18 +327,23 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter @@ -316,18 +327,23 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
}
/**
* 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 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter @@ -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<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
@ -493,7 +513,35 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter @@ -493,7 +513,35 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
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 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter @@ -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 @@ -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 @@ -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<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

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

@ -69,10 +69,15 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter @@ -69,10 +69,15 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
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);

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

@ -0,0 +1,52 @@ @@ -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);
}

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

@ -0,0 +1,75 @@ @@ -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<Object> interceptors;
public ResponseBodyInterceptorChain(List<Object> interceptors) {
this.interceptors = interceptors;
}
public <T> T invoke(T body, MediaType contentType, Class<HttpMessageConverter<T>> 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;
}
}

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

@ -0,0 +1,26 @@ @@ -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 {
}
Loading…
Cancel
Save