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

@Data
public class ReturnResult {

    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 {

    @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 response = restTemplate.getForEntity(url, ReturnResult.class);

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

        ReturnResult returnResult = MAPPER.readValue(
                MAPPER.writeValueAsString(response.getBody()), new TypeReference>() {});
        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 response = restTemplate.getForEntity(url, ReturnResult.class);

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

        ReturnResult returnResult = MAPPER.readValue(
                MAPPER.writeValueAsString(response.getBody()), new TypeReference>() {});
        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")));
    }

}

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

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