前言
到今天为止,相信大家开发 Web 项目应该都是前后端分离了吧?前后端分离中一般会使用 json 作为前后端的数据交换格式。json 中可以包含数值、字符串、json 对象、数组等等。
由于 json 可以转换为 JavaScript 对象,取对象的字段时需要保证对象不能为 null,因此前端同学通常期望后端接口返回对象或数组类型的字段时设置一个默认值。如果每个接口单独设置默认值将会异常繁琐,我们尝试来进行一个全局性的处理。
分析
提到全局设置默认字段值,首先想到的就是使用 AOP 拦截 Controller 方法的执行,方法执行后设置返回值字段的默认值。不过对于将 Controller 方法返回值作为 json 类型的响应体这种情况,还有更好的处理方式。
在上篇《Spring MVC @RequestBody @ResponseBody 序列化反序列化实现》中我们也提到过,@ResponseBody 注解是由 HttpMessageConverter 处理的,Spring MVC 使用 HttpMessageConverter 将方法返回值写入响应体前事实上是有预留给用户的回调的。
回调的接口如下。
public interface ResponseBodyAdvice<T> { // 是否支持给定的返回类型 boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType); // HttpMessageConverter 写响应前回调 @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); }
Spring 会从 ControllerAdviceBean 中查找这个接口的实现,并回调符合条件的接口方法。为了将一个 bean 设置为 ControllerAdviceBean,可以在类上直接添加 @ControllerAdvice 注解。
实现
因此,我们直接定义一个实现 ResponseBodyAdvice 接口的类作为 ControllerAdviceBean 在将 Controller 方法返回值写入响应体前设置字段的默认值就可以了。
这里还是拿前面文章中使用的 User 类做测试,该类定义如下。
@Data @Accessors(chain = true) public class User { private String username; private String password; private Address address; private List<String> interests; private Map<String, Object> extra; } @Data public class Address { private String province; private String city; }
注意我们的 User 类字段包括普通的 String 类型,自定义的类型、List 类型 及 Map 类型。
接口返回的通用格式使用 Result 类表示。
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("OK"); result.setData(data); return result; } }
我们定义一个测试接口如下。
@RestController public class UserController { @GetMapping("/login") public Result<User> login() { User user = new User(); Result<User> result = Result.ok(user); return result; } }
为了将 login 方法返回的 Result<User>
写入到响应体前设置默认字段值,我们可以定义 ResponseBodyAdvice 实现如下。
@ControllerAdvice public class ResponseBodyDefaultValueAdvice implements ResponseBodyAdvice<Result<?>> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { // 仅处理 Result 类型 return returnType.getParameterType() == Result.class; } @Override public Result<?> beforeBodyWrite(Result<?> body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { Object data = body.getData(); if (data == null) { data = BeanUtils.instantiateClass(ReflectionUtils.findField(body.getClass(), "data").getType()); } setDefault(data); return body; } private void setDefault(Object obj) { ReflectionUtils.doWithFields(obj.getClass(), field -> { ReflectionUtils.makeAccessible(field); Object val = ReflectionUtils.getField(field, obj); if (val != null) { return; } // 字段默认值 Object defaultValue = null; if (field.getType() == String.class) { defaultValue = ""; } else if (field.getType() == List.class) { defaultValue = Collections.emptyList(); } else if (field.getType() == Map.class) { defaultValue = Collections.emptyMap(); } else if (field.getType().getPackage().getName().startsWith("com.zzuhkp")) { defaultValue = BeanUtils.instantiateClass(field.getType()); setDefault(defaultValue); } // 设置字段默认值 ReflectionUtils.setField(field, obj, defaultValue); }); } }
这里我们只处理 Result 类型的 Controller 方法返回值,返回值写入响应前,如果字段值为 null 则进行处理,并将 String 类型的值设置为空字符串、List 类型的值设置为空列表、Map 类型的值设置为空 Map 值,以及递归处理了自定义的类型。
测试接口返回内容如下。
{ "code": 200, "message": "OK", "data": { "username": "", "password": "", "address": { "province": "", "city": "" }, "interests": [], "extra": {} } }
String、List、Map 以及自定义的 Address 类型都进行了处理,前端同学再也不会吐槽后端接口返回 null 了。