Controller 瘦身计划:利用 ResponseBodyAdvice 实现结果(Result)自动包装

10 阅读2分钟

前言

“如何封装一个生产级 Result 实体” ,我们规范了响应格式。但随之而来的是 Controller 中大量重复的编码:


@GetMapping("/users")
public Result<List<User>> getUsers() {
    return Result.ok(userService.getUsers());
}

@GetMapping("/user/{id}")
public Result<User> getUserById(@PathVariable Long id) {
    return Result.ok(userService.getUserByid(id));
}

这种写法在每个方法中都需要手动声明返回值类型并构建 Result 对象,产生了大量冗余代码。为了让 Controller 只关注业务数据,我们可以利用 ResponseBodyAdvice 自动完成结果的包装,干掉这些“不好看”的模板代码。

ResponseBodyAdvice

ResponseBodyAdvice 本质上是 Spring MVC 提供的拦截增强机制。它能在 ResponseBody 写入响应流之前,对返回结果进行“二次加工”。

我将演示如何通过实现该接口,实现接口返回值的自动包装.

IgnoreResponseWrapper

在使用 ResponseBodyAdvice 之前,我们定义一个 IgnoreResponseWrapper 注解,这个注解的作用是告诉 ResponseBodyAdvice 只要被 IgnoreResponseWrapper 注解的方法或者类,都不需要”加工返回结果了“


@Documented
@Target({ElementType.METHOD, ElementType.TYPE}) // 可直接用于接口类或者方法上
@Retention(RetentionPolicy.RUNTIME) // 声明周期:运行时有效,可用于反射
public @interface IgnoreResponseWrapper {
}

ResponseBodyAdvice 接口

ResponseBodyAdvice 接口中有两个方法,分别是:

  • boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

    • 用于指示当前方法是否需要“加工”,及调用下面的beforeBodyWrite 方法处理返回值。
  • @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);

    • 处理返回值的核心逻辑

ResponseBodyAdvice 实现

@AllArgsConstructor
@RestControllerAdvice
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {

    private final ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 如果方法或类上标记了 @IgnoreResponseWrapper,则不进行包装
        if (returnType.getDeclaringClass().isAnnotationPresent(IgnoreResponseWrapper.class) ||
                returnType.hasMethodAnnotation(IgnoreResponseWrapper.class)) {
            return false;
        }
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        // 如果已经是 Result 类型,直接返回
        if (body instanceof Result) {
            return body;
        }

        // 如果是 String 类型,需要手动序列化,否则会报 ClassCastException
        if (body instanceof String) {
            try {
                response.getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
                return objectMapper.writeValueAsString(Result.success(body));
            } catch (Exception e) {
                throw new HttpMessageNotWritableException("接口响应处理序列化 String 响应失败", e);
            }
        }

        // 统一包装为 Result
        return Result.success(body);
    }
}


现在我们就可以简化 Controller 中返回数据的格式了!

@GetMapping("/users")
public List<User> getUsers() {
    return userService.getUsers();
}

@GetMapping("/user/{id}")
public User getUserById(@PathVariable Long id) {
    return userService.getUserById(id);
}

当然,如果某些 Controller 中就需要显示使用 Result,就可以使用 IgnoreResponseWrapper 注解了

@PutMapping("/user/{id}")
public Result<Long> updaeUserById(@PathVariable Long id) {
    return userService.updaeUserById(id);
}