SpringMVC-ResponseBodyAdvice

前言

ResponseBodyAdvice接口可以在将handler方法的返回值写入response前对返回值进行处理,例如将返回值封装成一个与客户端约定好的对象以便于客户端处理响应数据。本篇文章将学习如果使用ResponseBodyAdvice以及其实现原理。

SpringBoot版本:2.4.1

正文

一. ResponseBodyAdvice的使用

假如已经存在一个Controller,如下所示。

@RestController
public class LoginController {

    private static final String DATE_STRING = "20200620";

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");

    private final Student student;

    public LoginController() {
        student = new Student();
        student.setName("Lee");
        student.setAge(20);
        student.setSex("male");
        try {
            student.setDate(dateFormat.parse(DATE_STRING));
        } catch (ParseException e) {
            System.out.println(e.getMessage());
        }
    }

    @RequestMapping(value = "/api/v1/student/name", method = RequestMethod.GET)
    public ResponseEntity<Object> getStudentByName(@RequestParam(name = "name") String name) {
        if (student.getName().equals(name)) {
            return new ResponseEntity<>(student, HttpStatus.OK);
        } else {
            return new ResponseEntity<>(String.format("get student failed by name: %s", name), HttpStatus.BAD_REQUEST);
        }
    }

    @RequestMapping(value = "/api/v1/student/age", method = RequestMethod.GET)
    public Student getStudentByAge(@RequestParam(name = "age") int age) {
        if (student.getAge() == age) {
            return student;
        } else {
            return null;
        }
    }

}

@Data
public class Student {

    private String name;
    private int age;
    private String sex;
    private Date date;

}

上述Controller中有两个方法,并且返回值分别为ResponseEntity<Object>Student。此时客户端收到响应之后,针对响应体的处理变得十分不方便,如果增加更多的方法,并且返回值都不相同,那么客户端将需要根据不同的请求来特定的处理响应体。因此为了方便客户端处理响应数据,服务器端专门创建了一个返回结果类ReturnResult,并且规定服务器端的所有handler方法执行后往response中写入的响应体都必须为ReturnResult。在这种情况下,使用ResponseBodyAdvice可以在不修改已有业务代码的情况下轻松实现上述需求。假设自定义的返回结果类ReturnResult如下所示。

@Data
public class ReturnResult<T> {

    private int statusCode;
    private T body;

    public ReturnResult() {}

    public ReturnResult(T body) {
        this.body = body;
    }

}

ReturnResultbody就是原本需要写入response的响应内容,现在整个ReturnResult为需要写入response的响应内容,相当于ReturnResulthandler方法的返回值进行了一层封装。

现在创建一个ReturnResultAdvice类并实现ResponseBodyAdvice接口,如下所示。

@ControllerAdvice
public class ReturnResultAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(@Nullable MethodParameter returnType, @Nullable Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, @Nullable MethodParameter returnType,
                                  @Nullable MediaType selectedContentType, @Nullable Class selectedConverterType,
                                  @Nullable ServerHttpRequest request, @Nullable ServerHttpResponse response) {
        if (body == null) {
            return null;
        }
        if (body instanceof ReturnResult) {
            return body;
        }
        return new ReturnResult<>(body);
    }

}

ReturnResultAdvicebeforeBodyWrite()方法会在handler方法返回值写入response前被调用。

下面在单元测试中模拟客户端发起请求。

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles
class LoginControllerTest {

    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void givenName_whenGetStudentByNameAndStudentConvertedToReturnResultByResponseBodyAdvice_thenGetStudentSuccess() throws Exception {
        String name = "Lee";
        String url = "/api/v1/student/name?name=" + name;

        ResponseEntity<ReturnResult> response = restTemplate.getForEntity(url, ReturnResult.class);

        assertThat(response.getBody() != null, is(true));

        ReturnResult<Student> returnResult = MAPPER.readValue(
                MAPPER.writeValueAsString(response.getBody()), new TypeReference<ReturnResult<Student>>() {});
        Student student = returnResult.getBody();
        assertThat(student != null, is(true));
        assertThat(student.getName(), is("Lee"));
        assertThat(student.getAge(), is(20));
        assertThat(student.getSex(), is("male"));
        assertThat(student.getDate(), is(DATE_FORMAT.parse("20200620")));
    }

    @Test
    void givenAge_whenGetStudentByAgeAndStudentConvertedToReturnResultByResponseBodyAdvice_thenGetStudentSuccess() throws Exception {
        int age = 20;
        String url = "/api/v1/student/age?age=" + age;

        ResponseEntity<ReturnResult> response = restTemplate.getForEntity(url, ReturnResult.class);

        assertThat(response.getBody() != null, is(true));

        ReturnResult<Student> returnResult = MAPPER.readValue(
                MAPPER.writeValueAsString(response.getBody()), new TypeReference<ReturnResult<Student>>() {});
        Student student = returnResult.getBody();
        assertThat(student != null, is(true));
        assertThat(student.getName(), is("Lee"));
        assertThat(student.getAge(), is(20));
        assertThat(student.getSex(), is("male"));
        assertThat(student.getDate(), is(DATE_FORMAT.parse("20200620")));
    }

}

运行测试程序,断言全部通过。

最后对整个例子进行两点说明。

  • 虽然LoginControllergetStudentByName()方法的返回值类型为ResponseEntity<Object>,但是实际往response写的响应体内容为ResponseEntity中的body,例子中这个body对应的就是创建ResponseEntity对象时传入的student。因此ReturnResultAdvice处理getStudentByName()方法的返回值时处理的实际上还是Student对象。至于原理将在第二小节进行说明;
  • 在单元测试程序中,MAPPER.readValue(MAPPER.writeValueAsString(...), ...)这样的用法是因为使用restTemplateresponse中将响应内容反序列化为带有泛型参数的对象时会将对象中的泛型的内容反序列化为LinkHashMap,因此借助ObjectMapperTypeReference来直接获取到Student对象。

小节:由@ControllerAdvice注解修饰并实现ResponseBodyAdvice接口的类所实现的beforeBodyWrite()方法会在handler方法返回值写入response前被调用,并且handler方法返回值会作为入参传入beforeBodyWrite(),从而可以在返回值写入response前对返回值进行一些定制操作,例如对返回值进行一层封装。

二. ResponseBodyAdvice的原理

首先说明一下为什么第一小节中LoginControllergetStudentByName()方法的返回值类型为ResponseEntity<Object>,但是实际往response写的响应体内容为ResponseEntity中的body。首先所有ResponseBodyAdvice接口的调用是发生在AbstractMessageConverterMethodProcessorwriteWithMessageConverters()中,这个方法的声明如下所示。

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
            ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException

其中value就是需要写入响应体的值,同时也是ResponseBodyAdvice要处理的值。然后如果handler方法的返回值是非ResponseEntity对象且handler方法由@ResponseBody注解修饰,那么writeWithMessageConverters()的调用发生在RequestResponseBodyMethodProcessor#handleReturnValue()中;如果handler方法的返回值是ResponseEntity对象,那么writeWithMessageConverters()的调用发生在HttpEntityMethodProcessor#handleReturnValue()中,分别看一下在这两个方法中调用writeWithMessageConverters()时传入的参数,就可以解释之前的疑问了。

RequestResponseBodyMethodProcessor#handleReturnValue()

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    ...

    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

HttpEntityMethodProcessor#handleReturnValue()

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

    ......
    
    HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue;

    ......

    writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage);

    ......
}

现在开始对ResponseBodyAdvice的原理进行分析。已知所有ResponseBodyAdvice接口的调用是发生在AbstractMessageConverterMethodProcessorwriteWithMessageConverters()中,其部分源码如下所示。

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    ......

    if (selectedMediaType != null) {
        selectedMediaType = selectedMediaType.removeQualityValue();
        for (HttpMessageConverter<?> converter : this.messageConverters) {
            GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
                    (GenericHttpMessageConverter<?>) converter : null);
            if (genericConverter != null ?
                    ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
                    converter.canWrite(valueType, selectedMediaType)) {
                // ResponseBodyAdvice的调用发生在这里
                body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
                        (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
                        inputMessage, outputMessage);
                if (body != null) {
                    Object theBody = body;
                    LogFormatUtils.traceDebug(logger, traceOn ->
                            "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
                    addContentDispositionHeader(inputMessage, outputMessage);
                    if (genericConverter != null) {
                        genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                    }
                    else {
                        ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                    }
                }
                else {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Nothing to write: null body");
                    }
                }
                return;
            }
        }
    }

    ......
}

AbstractMessageConverterMethodProcessorgetAdvice()方法会返回其在构造函数中加载好的RequestResponseBodyAdviceChain对象,下面看一下RequestResponseBodyAdviceChainbeforeBodyWrite()方法。

public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType,
        Class<? extends HttpMessageConverter<?>> converterType,
        ServerHttpRequest request, ServerHttpResponse response) {

    return processBody(body, returnType, contentType, converterType, request, response);
}

private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,
        Class<? extends HttpMessageConverter<?>> converterType,
        ServerHttpRequest request, ServerHttpResponse response) {
    // 从加载好的ResponseBodyAdvice中获取适用于当前handler的ResponseBodyAdvice
    for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
        if (advice.supports(returnType, converterType)) {
            // 执行ResponseBodyAdvice的beforeBodyWrite()方法以处理handler方法返回值
            body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
                    contentType, converterType, request, response);
        }
    }
    return body;
}

private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
    // 获取ResponseBodyAdvice集合
    List<Object> availableAdvice = getAdvice(adviceType);
    if (CollectionUtils.isEmpty(availableAdvice)) {
        return Collections.emptyList();
    }
    List<A> result = new ArrayList<>(availableAdvice.size());
    for (Object advice : availableAdvice) {
        // 判断ResponseBodyAdvice是否由@ControllerAdvice注解修饰
        if (advice instanceof ControllerAdviceBean) {
            ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
            // 判断ResponseBodyAdvice是否适用于当前handler
            if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
                continue;
            }
            advice = adviceBean.resolveBean();
        }
        if (adviceType.isAssignableFrom(advice.getClass())) {
            result.add((A) advice);
        }
    }
    return result;
}

RequestResponseBodyAdviceChain中,beforeBodyWrite()方法调用了processBody()方法,processBody()方法会遍历所有加载好并且适用于当前handlerResponseBodyAdvice并执行,至此,所有由@ControllerAdvice注解修饰的ResponseBodyAdvice接口会在这里执行。

小节:由@ControllerAdvice注解修饰的ResponseBodyAdvice接口会被SpringMVC框架加载到RequestResponseBodyMethodProcessorHttpEntityMethodProcessor这两个返回值处理器中,当这两个返回值处理器将返回值写入response前,适用于当前handler的ResponseBodyAdvice接口会被调用,从而可以完成对返回值的定制化改造。

三. ResponseBodyAdvice的加载

由第二小节可知,正是因为RequestResponseBodyMethodProcessorHttpEntityMethodProcessor这两个返回值处理器会将由@ControllerAdvice注解修饰的ResponseBodyAdvice接口加载,才能够实现将返回值写入response前调用这些ResponseBodyAdvice接口对返回值进行一些操作。那么本小节将对ResponseBodyAdvice接口的加载进行学习。

首先给出结论:ResponseBodyAdvice的加载发生在RequestMappingHandlerAdapterafterPropertiesSet()方法中。

已知,RequestMappingHandlerAdapter实现了InitializingBean接口,因此RequestMappingHandlerAdapter实现了afterPropertiesSet()方法。该方法实现如下。

public void afterPropertiesSet() {
    // 加载ControllerAdviceBean相关内容(同时就会将由@ControllerAdvice注解修饰的ResponseBodyAdvice接口加载)
    initControllerAdviceCache();

    if (this.argumentResolvers == null) {
        List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
        this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    if (this.initBinderArgumentResolvers == null) {
        List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
        this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    if (this.returnValueHandlers == null) {
        // 获取返回值处理器,在这里就会完成RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor的初始化,初始化的同时就会完成ResponseBodyAdvice接口的加载
        List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
        this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
    }
}

上述实现中,initControllerAdviceCache()会加载ControllerAdviceBean相关内容到RequestMappingHandlerAdapter中,这其中就包含由@ControllerAdvice注解修饰的ResponseBodyAdvice接口。然后在getDefaultReturnValueHandlers()方法中会创建返回值处理器,在创建RequestResponseBodyMethodProcessorHttpEntityMethodProcessor时会使用加载好的ResponseBodyAdvice接口完成这两个返回值处理器的初始化。上述两个方法的部分源码如下所示。

initControllerAdviceCache()

private void initControllerAdviceCache() {
    if (getApplicationContext() == null) {
        return;
    }

    // 获取由@ControllerAdvice注解修饰的bean
    List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());

    List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

    for (ControllerAdviceBean adviceBean : adviceBeans) {
        Class<?> beanType = adviceBean.getBeanType();
        if (beanType == null) {
            throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
        }
        Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
        if (!attrMethods.isEmpty()) {
            this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
        }
        Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
        if (!binderMethods.isEmpty()) {
            this.initBinderAdviceCache.put(adviceBean, binderMethods);
        }
        // 如果ControllerAdviceBean实现了ResponseBodyAdvice接口,那么这个ControllerAdviceBean需要加载到requestResponseBodyAdvice中
        if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
            requestResponseBodyAdviceBeans.add(adviceBean);
        }
    }

    if (!requestResponseBodyAdviceBeans.isEmpty()) {
        this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
    }

    ......
}

getDefaultReturnValueHandlers()

private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
    List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);

    ...

    // 创建并加载HttpEntityMethodProcessor
    handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
            this.contentNegotiationManager, this.requestResponseBodyAdvice));

    ...
    
    // 创建并加载RequestResponseBodyMethodProcessor
    handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
            this.contentNegotiationManager, this.requestResponseBodyAdvice));

    ...

    return handlers;
}

根据getDefaultReturnValueHandlers()方法可知,在创建HttpEntityMethodProcessor或者RequestResponseBodyMethodProcessor时,会将RequestMappingHandlerAdapter加载好的ResponseBodyAdvice传入构造函数,并且,无论是HttpEntityMethodProcessor还是RequestResponseBodyMethodProcessor,其构造函数最终都会调用到父类AbstractMessageConverterMethodArgumentResolver的构造函数,并在其中初始化一个RequestResponseBodyAdviceChain以完成ResponseBodyAdvice的加载。构造函数源码如下所示。

HttpEntityMethodProcessor#HttpEntityMethodProcessor()

public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
        @Nullable ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {

    super(converters, manager, requestResponseBodyAdvice);
}

AbstractMessageConverterMethodProcessor#AbstractMessageConverterMethodProcessor()

protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
        @Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice) {

    super(converters, requestResponseBodyAdvice);

    this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
    this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
    this.safeExtensions.addAll(SAFE_EXTENSIONS);
}

AbstractMessageConverterMethodArgumentResolver#AbstractMessageConverterMethodArgumentResolver()

public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters,
        @Nullable List<Object> requestResponseBodyAdvice) {

    Assert.notEmpty(converters, "'messageConverters' must not be empty");
    this.messageConverters = converters;
    this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters);
    this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);
}

小节:RequestMappingHandlerAdapter会在其实现的afterPropertiesSet()方法中加载由@ControllerAdvice注解修饰的ResponseBodyAdvice接口,然后会创建并加载返回值处理器,在创建RequestResponseBodyMethodProcessorHttpEntityMethodProcessor这两个返回值处理器时会传入加载好的ResponseBodyAdvice,从而完成了ResponseBodyAdvice的加载。

总结

如果需要使用ResponseBodyAdvice来对handler方法的返回值做处理,则需要创建一个类并实现ResponseBodyAdvice接口,同时该类还需要被@ControllerAdvice注解修饰,这样的一个ResponseBodyAdvice接口会被RequestMappingHandlerAdapter加载,以及在初始化RequestResponseBodyMethodProcessorHttpEntityMethodProcessor这两个返回值处理器时被加载,最终通过这两个返回值处理器将返回值写入response前,加载好的ResponseBodyAdvice接口的beforeBodyWrite()方法会被返回值处理器调用,完成对返回值的定制化处理。

41 声望
23 粉丝
0 条评论
推荐阅读
数据库连接池-Druid数据库连接池源码解析
本文将对Druid数据库连接池的源码进行分析和学习,以了解Druid数据库连接池的工作原理。Druid数据库连接池的基本逻辑几乎全部在DruidDataSource类中,所以本文主要是围绕DruidDataSource的各项功能展开论述。

半夏之沫阅读 993

Spring Security + JWT
Spring Security默认是基于session进行用户认证的,用户通过登录请求完成认证之后,认证信息在服务器端保存在session中,之后的请求发送上来后SecurityContextPersistenceFilter过滤器从session中获取认证信息、...

4阅读 1.4k

我服了!SpringBoot升级后这服务我一个星期都没跑起来!(上)
最近由于各方面的原因在准备升级 Spring Cloud 和 Spring Boot,经过一系列前置的调研和分析,决定把Spring Boot 相关版本从 2.1.6 升级到 2.7.5,Spring Cloud 相关版本从 Greenwich.SR1 升级为 2021.0.4。

艾小仙2阅读 800

从源码层面深度剖析Spring循环依赖
作者:郭艳红以下举例皆针对单例模式讨论图解参考 [链接]1、Spring 如何创建Bean?对于单例Bean来说,在Spring容器整个生命周期内,有且只有一个对象。Spring 在创建 Bean 过程中,使用到了三级缓存,即 Default...

京东云开发者3阅读 565

封面图
汽车行业场景化营销新方向:基于 WebGL 的网上虚拟车展
车展作为车市的风向标,代表着汽车发展的趋势,也是厂商展示自己、推广自己的舞台。WebGL 作为一种新兴的技术,为 Web 端提供了交互式三维动画新体验,汽车之家的网上车展就是两者结合之后的一种新的产品形态。

之家技术阅读 4.8k

封面图
Mybatis缓存机制
Mybatis内置了强大的事务性查询缓存机制,正确使用Mybatis的缓存机制可以有效提高应用的性能。因为一般情况下我们应用的大部分性能消耗都和数据库查询有关,如果能够有效命中缓存、适当避免或减少与数据库的交互...

2阅读 943

前端脚手架开发入门
脚手架是一个通用开发工具,之前自己写一下原生web工程时总是需要重复搭建开发环境、重复写几乎相同的配置文件。为了避免每次的重复工作,统一开发环境、规范,于是想到自己写一个脚手架用用,顺便记录一下。

coderLeo1阅读 431

封面图
41 声望
23 粉丝
宣传栏