背景
在 spring boot 项目中,使用@RestController / @RequestMapping / @GetMapping / @PostMapping
等注解提供api的功能,但是每个Mapping
返回的类型各不相同,有的是void,有的是基础类型如strping /integer,有的是dto。
在前后端分离的项目中,返回格式不统一,使得前端处理返回结果也不能统一,会导致写很多代码。
原始controller
例子的代码如下
t org.springframework.web.bind.annotation.RestController;
@RestController()
@RequestMapping
public class NoResultWarpperController {
@PostMapping("hello")
public HelloDto hello(@RequestBody HelloCmd name){
HelloDto result = new HelloDto();
result.setResult("hello,"+name);
return result;
}
@Data
public class HelloCmd{
private String name;
}
@Data
public class HelloDto{
private String result;
}
}
测试代码如下
@SpringBootTest
@AutoConfigureMockMvc
public class NoResultWarpperControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testHello() throws Exception{
ObjectMapper map = new ObjectMapper();
NoResultWarpperController.HelloCmd cmd = new NoResultWarpperController.HelloCmd();
cmd.setName("zhangsan");
String body = map.writeValueAsString(cmd);
MvcResult mvcResult = mockMvc.perform(
MockMvcRequestBuilders.post("/hello")
.contentType(MediaType.APPLICATION_JSON)
.content(body)
).andReturn();
assertThat(mvcResult.getResponse().getStatus()).isEqualTo(200);
NoResultWarpperController.HelloDto dto = map.readValue(mvcResult.getResponse().getContentAsString(), NoResultWarpperController.HelloDto.class);
assertThat(dto).isNotNull();
assertThat(dto.getResult()).isEqualTo("hello,zhangsan");
}
}
方式一,Controller方法统一返回类型ApiResult
新建统一返回类
@Data
public class ApiResult<T> {
private T result;
private boolean success;
private String errorCode;
private String errorMessage;
private String errorDetail;
}
修改上面例子的Controller, 方法返回ApiResult
@RestController()
@RequestMapping
public class NoResultWarpperController {
@PostMapping("hello")
public ApiResult<HelloDto> hello(@RequestBody HelloCmd cmd){
HelloDto result = new HelloDto();
result.setResult("hello,"+ cmd.getName());
return new ApiResult<>(result);
}
}
测试代码
@Test
public void testHello() throws Exception{
ObjectMapper map = new ObjectMapper();
NoResultWarpperController.HelloCmd cmd = new NoResultWarpperController.HelloCmd();
cmd.setName("zhangsan");
String body = map.writeValueAsString(cmd);
MvcResult mvcResult = mockMvc.perform(
MockMvcRequestBuilders.post("/hello")
.contentType(MediaType.APPLICATION_JSON)
.content(body)
).andReturn();
assertThat(mvcResult.getResponse().getStatus()).isEqualTo(200);
ApiResult<NoResultWarpperController.HelloDto> dto = map.readValue(mvcResult.getResponse().getContentAsString(),
new TypeReference<ApiResult<NoResultWarpperController.HelloDto>>(){});
assertThat(dto).isNotNull();
assertThat(dto.isSuccess()).isTrue();
assertThat(dto.getResult().getResult()).isEqualTo("hello,zhangsan");
}
缺点
每个方法统一返回ApiResult类型,但是有一个缺点,就是需要程序员自身关注这件事情,如果忘记返回了,会影响使用。
方式二,使用拦截器
spring mvc 提供了一个接口ResponseBodyAdvice
, 用来拦截响请求响应,可以通过自定义拦截器完成统一结果返回
定义拦截器
/**
* 通过结果返回拦截器,只拦截 @RestController 标识的类
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@RestControllerAdvice(annotations = RestController.class)
public class RequestResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
ObjectMapper mapper = new ObjectMapper();
if (body instanceof ApiResult){
return body;
}
// 包装 string 类型
if(body instanceof String){
return mapper.writeValueAsString(new ApiResult<>(body));
}
return new ApiResult<>(body);
}
}
修改方法一的方法,去掉返回类型ApiResult
@RestController()
@RequestMapping
public class NoResultWarpperController {
@PostMapping("hello")
public HelloDto hello(@RequestBody HelloCmd cmd){
HelloDto result = new HelloDto();
result.setResult("hello,"+ cmd.getName());
return result;
}
}
测试代码不用修改,运行测试,发现测试是通过,说明通过拦截器,可以统一返回类型,并且不需要强制Controller方法返回ApiResult类型
过滤器中指定方法不使用ApiResult
定义注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DontWrapResult {
}
在Controller方法或类上,添加注解@DontWrapResult
, 扩展 controller 方法
@PostMapping("helloNoWrap")
@DontWrapResult
public HelloDto helloNoWrap(@RequestBody HelloCmd cmd){
HelloDto result = new HelloDto();
result.setResult("hello,"+ cmd.getName());
return result;
}
修改拦截器,是的@DontWrapResult
注解的方法或类直接返回结果
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (methodParameter.hasMethodAnnotation(DontWrapResult.class)){
return body;
}
if (AnnotationUtils.findAnnotation(methodParameter.getDeclaringClass(),DontWrapResult.class)!=null){
return body;
}
ObjectMapper mapper = new ObjectMapper();
if (body instanceof ApiResult){
return body;
}
// 包装 string 类型
if(body instanceof String){
return mapper.writeValueAsString(new ApiResult<>(body));
}
return new ApiResult<>(body);
}
添加测试代码
@Test
public void testHelloNoWrap() throws Exception{
ObjectMapper map = new ObjectMapper();
NoResultWarpperController.HelloCmd cmd = new NoResultWarpperController.HelloCmd();
cmd.setName("liubei");
String body = map.writeValueAsString(cmd);
MvcResult mvcResult = mockMvc.perform(
MockMvcRequestBuilders.post("/helloNoWrap")
.contentType(MediaType.APPLICATION_JSON)
.content(body)
).andReturn();
assertThat(mvcResult.getResponse().getStatus()).isEqualTo(200);
NoResultWarpperController.HelloDto dto = map.readValue(mvcResult.getResponse().getContentAsString(),
NoResultWarpperController.HelloDto.class);
assertThat(dto).isNotNull();
assertThat(dto.getResult()).isEqualTo("hello,liubei");
}
异常
统一返回类型后,全局异常也要包装到类型ApiResult
定义友好的业务异常类UserFriendlyException
public class UserFriendlyException extends Exception{
private int code;
public int errorCode(){
return code;
}
public UserFriendlyException(){}
public UserFriendlyException(String msg){
super(msg);
}
public UserFriendlyException(int code, String msg){
this(msg);
this.code= code;
}
}
修改拦截器,进行异常拦截
@ExceptionHandler(Exception.class)
@ResponseBody
public ApiResult<Object> exceptionHandler(
HttpServletRequest request,
HttpServletResponse serverHttpResponse, Exception e) {
serverHttpResponse.setStatus(500);
return error(500, e);
}
private ApiResult<Object> error(int code,Exception ex){
ApiResult<Object> result = new ApiResult<>();
if (ex instanceof UserFriendlyException){
result.setErrorCode(((UserFriendlyException) ex).errorCode());
}
else{
result.setErrorCode(code);
}
result.setSuccess(false);
result.setErrorMessage(ex.getMessage());
result.setResult(null);
return result;
}
普通异常测试
Controller 添加 除法运算
@GetMapping("div")
public Double div(){
throw new RuntimeException("b is zero");
}
测试
@Test
public void testDiv() throws Exception {
ObjectMapper map = new ObjectMapper();
MvcResult mvcResult = mockMvc.perform(
MockMvcRequestBuilders.get("/div")
).andReturn();
assertThat(mvcResult.getResponse().getStatus()).isEqualTo(500);
assertThat(mvcResult.getResponse().getContentAsString()).isNotNull();
ApiResult<Object> errorInfo = map.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<ApiResult<Object>>(){});
assertThat(errorInfo).isNotNull();
assertThat(errorInfo.getErrorCode()).isEqualTo(500);
assertThat(errorInfo.getErrorMessage()).isEqualTo("b is zero");
}
友好异常
Controller 添加 加法运算
@GetMapping("add")
public void add() throws UserFriendlyException {
throw new UserFriendlyException(10000, "no method");
}
测试代码
@Test
public void testAdd() throws Exception {
ObjectMapper map = new ObjectMapper();
MvcResult mvcResult = mockMvc.perform(
MockMvcRequestBuilders.get("/add")
).andReturn();
assertThat(mvcResult.getResponse().getStatus()).isEqualTo(500);
assertThat(mvcResult.getResponse().getContentAsString()).isNotNull();
ApiResult<Object> errorInfo = map.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<ApiResult<Object>>(){});
assertThat(errorInfo).isNotNull();
assertThat(errorInfo.getErrorCode()).isEqualTo(10000);
assertThat(errorInfo.getErrorMessage()).isEqualTo("no method");
}
总结
在spring boot项目中,让controller返回统一结果有两种实现方式:
- 方法代码写死返回类型,弊端是没有有效的检测机制,如果方法没有返回,会影响使用一致性
- 继承
ResponseBodyAdvice<Object>
接口自定义拦截器,不强制要求方法返回统一类型,并且针对个性化要求,比如DontWrapResult
和异常拦截,都可以很好的支持