Spring Boot Mvc 統一返回結果
阿新 • • 發佈:2021-06-16
背景
在 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
和異常攔截,都可以很好的支援