统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此),使用一个状态码、状态信息就能清楚地了解接口调用情况。
public interface IResult {
Integer getCode();
String getMessage();
}
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
private Integer code;
private String message;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;
private String message;
private T data;
public static Result success(T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
}
public static Result success(String message, T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
}
public static Result> failed() {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
}
public static Result> failed(String message) {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
}
public static Result> failed(IResult errorResult) {
return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
}
public static Result instance(Integer code, String message, T data) {
Result result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
return result;
}
}
统一返回结构后,在 Controller 中就可以使用了,但是每一个 Controller 都写这么一段最终封装的逻辑,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构。
Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求。
ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。那这样就可以把统一包装的工作放到这个类里面。
public interface ResponseBodyAdvice {
boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType);
@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}
supports:判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要
beforeBodyWrite:对 response 进行具体的处理。
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}
经过这样改造,既能实现对 Controller 返回的数据进行统一包装,又不需要对原有代码进行大量的改动。
处理 cannot be cast to java.lang.String 问题
如果直接使用 ResponseBodyAdvice,对于一般的类型都没有问题,当处理字符串类型时,会抛出 xxx.包装类 cannot be cast to java.lang.String 的类型转换的异常。
在 ResponseBodyAdvice 实现类中 debug 发现,只有 String 类型的 selectedConverterType 参数值是 org.springframework.http.converter.StringHttpMessageConverter,而其他数据类型的值是 org.springframework.http.converter.json.MappingJackson2HttpMessageConverter。
现在问题已经较为清晰了,因为我们需要返回一个 Result 对象。
所以使用 MappingJackson2HttpMessageConverter 是可以正常转换的。
而使用 StringHttpMessageConverter 字符串转换器会导致类型转换失败。
现在处理这个问题有两种方式:
①在 beforeBodyWrite 方法处进行判断,如果返回值是 String 类型就对 Result 对象手动进行转换成 JSON 字符串,另外方便前端使用,最好在 @RequestMapping 中指定 ContentType。
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
...
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
if (body instanceof String) {
try {
return this.objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return Result.success(body);
}
...
}
@GetMapping(value = "/returnString", produces = "application/json; charset=UTF-8")
public String returnString() {
return "success";
}
②修改 HttpMessageConverter 实例集合中 MappingJackson2HttpMessageConverter 的顺序。因为发生上述问题的根源所在是集合中 StringHttpMessageConverter 的顺序先于 MappingJackson2HttpMessageConverter 的,调整顺序后即可从根源上解决这个问题。
网上有不少做法是直接在集合中第一位添加 MappingJackson2HttpMessageConverter。
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}
诚然,这种方式可以解决问题,但其实问题的根源不是集合中缺少这一个转换器,而是转换器的顺序导致的,所以最合理的做法应该是调整 MappingJackson2HttpMessageConverter 在集合中的顺序。
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List> converters) {
for (int i = 0; i < converters.size(); i++) {
if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = (MappingJackson2HttpMessageConverter) converters.get(i);
converters.set(i, converters.get(0));
converters.set(0, mappingJackson2HttpMessageConverter);
break;
}
}
}
}
Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation ,spring validation 是对其的二次封装,常用于 SpringMVC 的参数自动校验,参数校验的代码就不需要再与业务逻辑代码进行耦合了。
@PathVariable 和 @RequestParam 参数校验
Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参。
对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解。
如果校验失败,会抛出 MethodArgumentNotValidException 异常。
@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
@Validated
public class TestController {
private TestService testService;
@GetMapping("/{num}")
public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) {
return num * num;
}
@GetMapping("/getByEmail")
public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) {
TestDTO testDTO = new TestDTO();
testDTO.setEmail(email);
return testDTO;
}
@Autowired
public void setTestService(TestService prettyTestService) {
this.testService = prettyTestService;
}
}
在 SpringMVC 中,有一个类是 RequestResponseBodyMethodProcessor ,这个类有两个作用(实际上可以从名字上得到一点启发)。
用于解析 @RequestBody 标注的参数
处理 @ResponseBody 标注方法的返回值
解析 @RequestBoyd 标注参数的方法是 resolveArgument。
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver