《学会 SpringMVC 系列 · 写入拦截器 ResponseBodyAdvice》-LMLPHP

写在前面的话

前几篇博文,大致了解了SpringMVC请求流程中的参数与返回值的源码分析,后续的几篇博文,会将流程中涉及的若干关键环节单独拿出来讲解,并结合实战中的运用,帮助领略SpringMVC带来的定制和扩展能力。
本篇文章先介绍一下 ResponseBodyAdvice 相关内容。

相关博文
《学会 SpringMVC 系列 · 基础篇》
《学会 SpringMVC 系列 · 剖析篇(上)》
《学会 SpringMVC 系列 · 剖析入参处理》
《学会 SpringMVC 系列 · 剖析出参处理》
《学会 SpringMVC 系列 · 返回值处理器》
《学会 SpringMVC 系列 · 消息转换器 MessageConverters》
《学会 SpringMVC 系列 · 写入拦截器 ResponseBodyAdvice》
《学会 SpringMVC 系列 · 剖析初始化》
《程序猿入职必会(1) · 搭建拥有数据交互的 SpringBoot 》


ResponseBodyAdvice

技术说明

0、ResponseBodyAdvice 是 Spring Framework 的 Web 模块中的一个接口,它允许你在将响应体写入 HTTP 响应之前拦截和修改它。它提供了一种全局定制响应处理逻辑的方式,适用于 Spring MVC 或 Spring WebFlux 应用程序。
1、ResponseBodyAdvice 可以在注解 @ResponseBody 将返回值处理成相应格式之前操作返回值,实现这个接口即可完成相应操作,可用于对response 数据的一些统一封装或者加密等操作。
2、ResponseBodyAdvice 接口和 RequestBodyAdvice 接口类似,RequestBodyAdvice 是请求到Controller 之前拦截,做相应的处理操作,而ResponseBodyAdvice 是对Controller返回的{@code @ResponseBody}or a {@code ResponseEntity} 后,{@code HttpMessageConverter} 类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。
3、实现 ResponseBodyAdvice 接口,需要重写其 supports 和 beforeBodyWrite 方法。
1)supports方法:判断是否要执行beforeBodyWrite方法,true为执行,false不执行。通过该方法可以选择哪些类或那些方法的response要进行处理,其他的不进行处理。
2)beforeBodyWrite方法:对response方法进行具体操作处理。

public interface ResponseBodyAdvice<T> {
    /**
     * 1、选择是否执行 beforeBodyWrite 方法,返回 true 执行,false 不执行
     * 2、通过 supports 方法,可以选择对哪些类或方法的 Response 进行处理
     * @param returnType:返回类型
     * @param converterType:转换器
     * @return :返回 true 则下面的 beforeBodyWrite  执行,否则不执行
     */
	boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
 
    /**
     * 对 Response 处理的具体执行方法
     * @param body:响应对象(response)中的响应体
     * @param returnType:控制器方法的返回类型
     * @param selectedContentType:通过内容协商选择的内容类型
     * @param selectedConverterType:选择写入响应的转换器类型
     * @param request:当前请求
     * @param response:当前响应
     * @return :返回传入的主体或修改过的(可能是新的)主体
     */
	@Nullable
	T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}

基础示例

@ControllerAdvice
public class CustomResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 根据返回类型和转换器类型检查是否应用此建议
        // 你可以在这里放置任何条件
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class selectedConverterType, ServerHttpRequest request,
                                  ServerHttpResponse response) {
        // 在将响应体写入输出流之前修改它
        // 你可以在这里检查或修改 'body' 对象
        return body;
    }
}

总结:ResponseBodyAdvice 接口允许在执行 @ResponseBody 或 ResponseEntity 控制器方法之后,但在使用 HttpMessageConverter 写入响应体之前自定义响应,进行功能增强。通常用于加密,签名,统一数据格式等。


知识拓展

【知识扩展1:与 HandlerMethodReturnValueHandler 区别】
sbdemo4 项目的返回值包装,使用自定义HandlerMethodReturnValueHandler实现,那两个都能实现,有什么区别呢?先参考一下下方GPT的回答。

HandlerMethodReturnValueHandler 和 ResponseBodyAdvice 都是 Spring MVC 中用于处理控制器方法返回值
的扩展点,但它们的功能和使用方式有所不同。

HandlerMethodReturnValueHandler:
功能:HandlerMethodReturnValueHandler 用于处理控制器方法的返回值,并决定如何将返回值转换为响应。它负责控制器方法返回值的处理过程,例如将返回值转换为特定的响应格式(JSON、XML等)、对返回值进行处理、将返回值写入响应等。
使用方式:你可以通过实现 HandlerMethodReturnValueHandler 接口来定义自定义的返回值处理器,并将其注册到 Spring MVC 的配置中。你可以通过配置 WebMvcConfigurer 的 addReturnValueHandlers 方法来注册自定义的返回值处理器。

ResponseBodyAdvice:
功能:ResponseBodyAdvice 用于在将响应返回给客户端之前对响应进行自定义处理。它允许你在响应体写入之前对响应进行修改、加密、压缩等操作。它不负责决定如何将控制器方法的返回值转换为响应,而是在返回值已经被转换为响应体后进行处理。
使用方式:你可以通过实现 ResponseBodyAdvice 接口来定义全局性的响应体处理逻辑,并将其注册到 Spring MVC 的配置中。你可以通过 @ControllerAdvice 或 @RestControllerAdvice 注解来标记全局的响应体处理器。

总的来说,HandlerMethodReturnValueHandler 更加底层,负责控制器方法返回值的处理过程;
而 ResponseBodyAdvice 更加高层,用于在将响应返回给客户端之前对响应进行全局性的自定义处理。
通常情况下,你可以根据具体的需求选择合适的扩展点。

因为 SpringBoot 默认的 ResponseBody 的处理程序就是 HandlerMethodReturnValueHandler(具体是RequestResponseBodyMethodProcessor),所以我们自定义的通常 HandlerMethod 正常无法生效,非要使用HandlerMethod,那么只能替换掉默认的(放到第一个),如果只是想对Controller的所有返回值进行封装,产生上面的效果,使用ResponseBodyAdvice会更加简单一些,总之,改动不会那么大,尽量不要影响框架默认的实现。

【知识扩展2:关于返回值处理总结】
首先想到拦截器,HandlerInterceptor,其 postHandle 方法通常用于在控制器方法执行之后、视图渲染之前执行一些自定义逻辑,它并不直接提供获取返回值的功能。因为在 postHandle 方法被调用时,控制器方法的返回值已经被用于生成响应,拦截器只能对请求和响应进行处理,无法直接获取返回值。
要想处理返回值,可以有如下做法:
1、使用ResponseBodyAdvice:ResponseBodyAdvice 是一个用于全局性响应体处理的接口,在控制器方法返回值转换为响应体之前被调用,你可以在 beforeBodyWrite 方法中获取控制器方法的返回值。处理返回的数据在传递给HttpMessageConverter之前。
2、使用AOP切面:通过定义一个切面,在切面的方法中获取控制器方法的返回值,并进行相应的处理。通过切面拦截控制器方法的执行,你可以在方法执行完毕后获取返回值。
3、在拦截器的 postHandle 方法中,将返回值存储到请求属性中,然后在其他地方从请求属性中获取。这样虽然是间接的方式,但也可以实现获取返回值的目的。

public class MyInterceptor extends HandlerInterceptorAdapter {

    private ThreadLocal<Object> returnValueThreadLocal = new ThreadLocal<>();

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 在处理器执行后调用,但在视图渲染前调用
        System.out.println("Post-handle method is called");
        
        // 在这里获取控制器方法的返回值,并存储到ThreadLocal变量中
        Object returnValue = modelAndView != null ? modelAndView.getModel().get("handlerReturnValue") : null;
        returnValueThreadLocal.set(returnValue);
    }

    // 提供一个公共方法,供其他组件调用获取返回值
    public Object getControllerReturnValue() {
        return returnValueThreadLocal.get();
    }
}

【知识扩展3:@ControllerAdvice】
ResponseBodyAdvice 自定义使用过程中,加上了@ControllerAdvice注解,有什么用?
顾名思义,@ControllerAdvice就是@Controller 的增强版。@ControllerAdvice主要用来处理全局数据,一般搭配@ExceptionHandler、@ModelAttribute以及@InitBinder使用。
这里不展开介绍,框架的 GlobalExceptionHandler 全局异常处理就是用了这个方式。
1、在使用ResponseBodyAdvice时,通常需要将其标注为@ControllerAdvice注解的类,该注解用于定义全局的控制器增强,可以对所有的Controller进行统一的处理,当我们在@ControllerAdvice注解的类中实现ResponseBodyAdvice接口时,就可以对所有Controller方法返回的响应体进行统一处理;
2、经测试,不添加@ControllerAdvice注解,或仅使用@Component注解,功能都是无效的;

【知识扩展4:@ControllerAdvice 和 @RestControllerAdvice 的区别】
@ControllerAdvice 和 @RestControllerAdvice 都是 Spring 中用于定义全局异常处理、数据绑定、模型数据处理的注解。两者的主要区别在于它们如何处理控制器的返回值。
@ControllerAdvice 是一个更通用的注解,用于为控制器提供全局的异常处理、数据绑定等功能。它适用于处理所有类型的控制器,包括返回视图名称的控制器。使用这个注解时,返回的对象通常是一个视图名称,或者是包含视图名称和模型数据的 ModelAndView 对象。

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public String handleException(Exception ex) {
        return "error"; // 返回视图名称
    }
}

@RestControllerAdvice 是 @ControllerAdvice 的一个特化版本,专门用于 RESTful Web 服务。它与 @ControllerAdvice 的区别在于,它隐式地在类的每个方法上添加了 @ResponseBody 注解。因此,返回的对象会自动序列化为 JSON 或 XML 等格式,写入响应体。

@RestControllerAdvice
public class GlobalRestExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ErrorResponse handleException(Exception ex) {
        return new ErrorResponse("Error occurred", ex.getMessage()); // 返回 JSON 对象
    }
}

@ControllerAdvice@RestControllerAdvice 用于实现 ResponseBodyAdvice 时,两者的行为是相同的,都会在响应体写入前提供一个拦截点,无论返回类型是 ViewModelAndView 还是 JSON、XML 等。


源码知识回顾

本篇为 SpringMVC 源码分析系列文章,正片开始前,先总结回顾一下全流程。

【一次请求的主链路节点】
DispatcherServlet#doDispatch(入口方法)
DispatcherServlet#getHandler(根据path找到对应的HandlerExecutionChain
DispatcherServlet#getHandlerAdapter(根据handle找到对应的HandlerAdapter
HandlerExecutionChain#applyPreHandle(触发拦截器的前置逻辑)
AbstractHandlerMethodAdapter#handle(核心逻辑)
HandlerExecutionChain#applyPostHandle(触发拦截器的后置逻辑)

【核心handle方法的主链路节点】
RequestMappingHandlerAdapter#handleInternal(入口方法)
RequestMappingHandlerAdapter#invokeHandlerMethod(入口方法2)
ServletInvocableHandlerMethod#invokeAndHandle(入口方法3)
InvocableHandlerMethod#invokeForRequest(参数和实际执行的所在,3.1)
InvocableHandlerMethod#getMethodArgumentValues(参数处理,3.1.1)
InvocableHandlerMethod#doInvoke(实际执行,3.1.2)
HandlerMethodReturnValueHandlerComposite#handleReturnValue(返回处理,3.2)
《学会 SpringMVC 系列 · 写入拦截器 ResponseBodyAdvice》-LMLPHP

【针对 @RequestBody 和 @ResponseBody 场景】
《学会 SpringMVC 系列 · 写入拦截器 ResponseBodyAdvice》-LMLPHP


总结陈词

本篇博文继请求源码分析后,继续介绍了ResponseBodyAdvice的用法,无独有偶,请求阶段也有对应的RequestBodyAdvice,而且读取前和读取后,都可以自定义扩展,欲知后事如何,请听下回分解。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

《学会 SpringMVC 系列 · 写入拦截器 ResponseBodyAdvice》-LMLPHP

08-04 19:47