背景
在 spring boot 項(xiàng)目中欺嗤,使用@RestController / @RequestMapping / @GetMapping / @PostMapping
等注解提供api的功能,但是每個(gè)Mapping
返回的類型各不相同,有的是void艇纺,有的是基礎(chǔ)類型如strping /integer,有的是dto。
在前后端分離的項(xiàng)目中毁欣,返回格式不統(tǒng)一躲叼,使得前端處理返回結(jié)果也不能統(tǒng)一,會(huì)導(dǎo)致寫很多代碼垄提。
原始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方法統(tǒng)一返回類型ApiResult
新建統(tǒng)一返回類
@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");
}
缺點(diǎn)
每個(gè)方法統(tǒng)一返回ApiResult類型铡俐,但是有一個(gè)缺點(diǎn),就是需要程序員自身關(guān)注這件事情妥粟,如果忘記返回了审丘,會(huì)影響使用。
方式二勾给,使用攔截器
spring mvc 提供了一個(gè)接口ResponseBodyAdvice
, 用來攔截響請求響應(yīng)滩报,可以通過自定義攔截器完成統(tǒng)一結(jié)果返回
定義攔截器
/**
* 通過結(jié)果返回?cái)r截器,只攔截 @RestController 標(biāo)識的類
*/
@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;
}
}
測試代碼不用修改脓钾,運(yùn)行測試,發(fā)現(xiàn)測試是通過桩警,說明通過攔截器可训,可以統(tǒng)一返回類型,并且不需要強(qiáng)制Controller方法返回ApiResult類型
過濾器中指定方法不使用ApiResult
定義注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DontWrapResult {
}
在Controller方法或類上捶枢,添加注解@DontWrapResult
, 擴(kuò)展 controller 方法
@PostMapping("helloNoWrap")
@DontWrapResult
public HelloDto helloNoWrap(@RequestBody HelloCmd cmd){
HelloDto result = new HelloDto();
result.setResult("hello,"+ cmd.getName());
return result;
}
修改攔截器沉噩,是的@DontWrapResult
注解的方法或類直接返回結(jié)果
@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");
}
異常
統(tǒng)一返回類型后,全局異常也要包裝到類型ApiResult
定義友好的業(yè)務(wù)異常類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;
}
}
修改攔截器柱蟀,進(jìn)行異常攔截
@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 添加 除法運(yùn)算
@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 添加 加法運(yùn)算
@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");
}
總結(jié)
在spring boot項(xiàng)目中川蒙,讓controller返回統(tǒng)一結(jié)果有兩種實(shí)現(xiàn)方式:
- 方法代碼寫死返回類型,弊端是沒有有效的檢測機(jī)制长已,如果方法沒有返回畜眨,會(huì)影響使用一致性
- 繼承
ResponseBodyAdvice<Object>
接口自定義攔截器,不強(qiáng)制要求方法返回統(tǒng)一類型术瓮,并且針對個(gè)性化要求康聂,比如DontWrapResult
和異常攔截,都可以很好的支持