对于需要统一对所有的http响应增加响应头(比如增加响应签名,响应时间,响应序号等)以及响应数据进行统一处理(比如数据包装,数据转换等)的场景,可以通过ResponseBodyAdvice进行实现。
ResponseBodyAdvice就是spring框架中预留的钩子,它作用在Controller方法执行完成之后,http响应体写回客户端之前,这个时候我们就能方便的织入一些自己的业务逻辑处理了。
1. 定义ResponseBodyAdvice实现类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice {
protected static Logger logger = LoggerFactory.getLogger(MyResponseBodyAdvice.class);
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
Class targetClass = returnType.getMethod().getDeclaringClass();
logger.info("supports execute methodParameter={} targetClass={} class={}", returnType, targetClass, converterType);
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
logger.info("beforeBodyWrite data={}", body);
// response.addHeader("Pragma", "no-cache");
if(body instanceof String)
{
body = "this is modified body!";
}
// 这里计算签名值,并设置到请求头
response.getHeaders().set("x-abc", "header-value");
return body;
}
}
2. 接口说明
查看ResponseBodyAdvice接口的源码中,提供了两个方法:supports和beforeBodyWrite。supports方法用于判断beforeBodyWrite方法的执行与否,返回值为布尔类型,返回true即执行beforeBodyWrite方法。
-
MethodParameter以及HttpMessageConverter类型判断是否需要改写http响应体
-
beforeBodyWrite方法提供了用于修改http响应体的能力
3. 特别说明
对于通过@ResponseBody和ResponseEntity方式返回响应给调用者的情况,filter和interceptor对响应体和响应头的修改,没有效果,原因在于,请求返回给前端以后,才会执行filter和interceptor的后处理,所以此时,只能用于响应体信息的读取和记录。
interceptor中记录请求信息。
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
MyResponseWrapper myResponseWrapper = null;
if(response instanceof MyResponseWrapper)
{
myResponseWrapper = (MyResponseWrapper)response;
byte[] body = myResponseWrapper.getBody();
String strBody = new String(body);
logger.info("body is:" + strBody);
}
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
filter中记录响应体的方法:
public class AddResponseHeaderFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
response.addHeader("X-Frame-Options", "DENY");
response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
MyResponseWrapper myResponseWrapper = new MyResponseWrapper((HttpServletResponse)response);
filterChain.doFilter(request, myResponseWrapper);
byte[] body = myResponseWrapper.getBody();
String strBody = new String(body);
System.out.println("strBody:" + strBody);
}
}
对于返回前端用于页面显示的情况下,是可以用filter和interceptor的方式来改写响应体和响应头的,原因是controller执行完成以后,执行过滤器的后处理,然后再进行页面渲染,最终返回给前端用于显示。
对于记录响应信息时,还需要对响应做一次封装,避免在读取信息后,后续读取信息时出现错误。以下是对响应的封装类。
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
public class MyResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
private HttpServletResponse response;
public MyResponseWrapper(HttpServletResponse response) {
super(response);
this.response = response;
}
public byte[] getBody() {
return byteArrayOutputStream.toByteArray();
}
@Override
public ServletOutputStream getOutputStream() {
return new ServletOutputStreamWrapper(this.byteArrayOutputStream , this.response);
}
@Override
public PrintWriter getWriter() throws IOException {
return new PrintWriter(new OutputStreamWriter(this.byteArrayOutputStream , this.response.getCharacterEncoding()));
}
private static class ServletOutputStreamWrapper extends ServletOutputStream {
private ByteArrayOutputStream outputStream;
private HttpServletResponse response;
public ServletOutputStreamWrapper(ByteArrayOutputStream outputStream, HttpServletResponse response)
{
this.outputStream = outputStream;
this.response = response;
}
public ByteArrayOutputStream getOutputStream() {
return outputStream;
}
public void setOutputStream(ByteArrayOutputStream outputStream) {
this.outputStream = outputStream;
}
public HttpServletResponse getResponse() {
return response;
}
public void setResponse(HttpServletResponse response) {
this.response = response;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener listener) {
}
@Override
public void write(int b) throws IOException {
this.outputStream.write(b);
}
@Override
public void flush() throws IOException {
if (! this.response.isCommitted()) {
byte[] body = this.outputStream.toByteArray();
ServletOutputStream outputStream = this.response.getOutputStream();
outputStream.write(body);
outputStream.flush();
}
}
}
}
所以,具体采用哪种方式,还需要看具体的场景,如果是返回接口调用结果,就使用ResponseBodyAdvice,如果是返回页面显示,就可以使用filter或interceptor。