文章目录
背景
我们通常接触的项目大多已经是前后端分离的项目,前后端的数据交互往往封装成统一的格式,形如以下:
{
"code" : 200,
"message" : "操作成功",
"data" : null
}
正常请求时通过data提供返回数据对象,请求失败时通过code和message提供错误提示。
目标
体验如何使用springboot
框架为Java
后端web
项目提供统一的响应对象。同时体验异常替代错误返回的用法。
准备工作
基础知识
springmvc
的处理流程:请求映射、响应映射、异常处理视图@ControllerAdvice
、@RestController
、@ExceptionHandler
;RequestBodyAdvice
、ResponseBodyAdvice
ResponseEntityExceptionHandler
、BasicErrorController
统一响应
- 定义统一的响应体
- 使用统一响应
- 直接返回对象
- 使用
ResponseBodyAdvice
拦截封装 Aop
拦截封装(类似b
,拦截@ResponseBody
+@RequestMapping
)
定义统一响应
Response<T>
推荐参考《Java开发手册(黄山版)》中前后端规约和异常日志部分。
按实际项目需要,通常定义code
、message
、data
如下:
@Data
public class Response<T> {
/**
* 响应码
*/
private String code;
/**
* 响应信息
*/
private String message;
/**
* 响应数据
*/
private T data;
private Response(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
private static <T> Response<T> of(BizError bizError, String message, T data) {
return new Response<>(bizError.getCode(), message, data);
}
public static <T> Response<T> fail(BizError bizError, String message) {
return of(bizError, message, null);
}
public static <T> Response<T> fail(BizError bizError) {
return fail(bizError, bizError.getMessage());
}
public static <T> Response<T> success(T data) {
return of(BizErrorEnum.SUCCESS, BizErrorEnum.SUCCESS.getMessage(), data);
}
}
为了方便使用,我们定义了通用的success
和fail
静态方法。下面来写一个web
版的hello world
吧~
使用统一响应
不建议使用
JsonObject
来作为对象/入参/出参用,本质是一个Map
,阅读成本贼高。我们有些老项目为了Json
而Json
是一种不恰当的使用姿势,返回的数据格式应该通过MediaType
识别并自动转换,比较棒的是Spring
提供了诸多的HttpMessageConverter
帮我们做了这些事情。所以业务开发上面向对象就好~
使用我们封装的Response
对象有2种方式:
controller
层直接返回该对象包装的业务数据,如Response.success("hello,world")
- controller层返回业务数据,通过
ResponseBodyAdvice
或者Aop
封装成Response
对象返回
Response.success
package com.gitee.theskyone.bird.web;
@RestController
public class HelloWorldController {
@GetMapping("/hello/{world}")
public Response<String> hello(@PathVariable String world) {
String helloWorld = "hello " + world + " ~";
// 第1种方式: 使用Response.success(helloWorld)返回
return Response.success(helloWorld);
}
@GetMapping(value = "/hello/kitty")
public String helloKitty() {
// 第2种方式: 直接返回业务对象。但是对象是个String!
return "hello kitty~";
}
@GetMapping(value = "/hello")
public Hello hello() {
// 第2种方式: 直接返回业务对象
return new Hello("nice kitty");
}
}
ResponseBodyAdvice
不建议大家真的使用~
- 理解成本高:小伙伴强迫症
- 易踩坑:比如返回
String
的时候,更适合纯后端分离的项目- 接口文档工具不友好:像
swagger
在controller
层解析,不会关心到controllerAdvice
对于第2种直接返回业务对象的方式,我们使用ResponseBodyAdvice
来封装返回的业务对象为Response
。注意,ResponseBodyAdvice
需要结合@ControllerAdvice
或@RestControllerAdvice
使用。
package com.gitee.theskyone.bird.common.web.advice;
/**
* @see RequestMappingHandlerAdapter#initControllerAdviceCache() ResponseBodyAdvice 结合 ControllerAdvice使用
*/
@RestControllerAdvice
public class ResponseBody2ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 只处理AbstractJackson2HttpMessageConverter类型,注意是使用默认的jackson
return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType)
&& (!returnType.getMethod().getReturnType().isAssignableFrom(Response.class)
// 忽略ResponseEntity
&& !returnType.getMethod().getReturnType().isAssignableFrom(ResponseEntity.class));
}
@Override
@Nullable
public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType, Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest request, ServerHttpResponse response) {
return Response.success(body);
}
}
测试
测试一下!
package com.gitee.theskyone.bird.web;
@SpringBootTest
@AutoConfigureMockMvc
class HelloWorldControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void helloKitty() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/hello/kitty"))
.andDo(MockMvcResultHandlers.print())
// assert 响应字符串包含 Response.code字段
.andExpect(MockMvcResultMatchers.content()
.string(StringContains.containsString("code")));
// 翻车了? string类型默认返回text/plain,使用StringHttpMessageConverter序列化,不会走我们定义的ResponseBodyAdvice
}
@Test
void helloWorld() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/hello/world"))
.andDo(MockMvcResultHandlers.print())
// assert 响应字符串包含 Response.code字段
.andExpect(MockMvcResultMatchers.content()
.string(StringContains.containsString("code")));
}
@Test
void hello() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/hello"))
.andDo(MockMvcResultHandlers.print())
// assert 响应字符串包含 Response.code字段
.andExpect(MockMvcResultMatchers.content()
.string(StringContains.containsString("code")));
}
}
// 项目地址 https://gitee.com/theskyone/new-bird.git
// 这里helloKitty()会测试失败,因为使用String返回不会被拦截封装成Response
到这里,我们一个正常的成功请求都能以我们定义的Response
对象返回。那么如果发生了异常呢?
异常处理
异常处理主要做2件事:异常拦截和处理。
- 什么时候(在哪儿)拦截异常?
- 异常发生时
- 异常传递时(内部,外部)
- 如何拦截异常?
- 异常处理:
catch or not
- 记录日志:请求响应上下文
- 全局处理
全局异常处理
@ExceptionHandler
我们使用@RestControllerAdvice
结合@ExceptionHandler
,并继承ResponseEntityExceptionHandler
支持web类的错误处理。
package com.gitee.theskyone.bird.common.web.advice;
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({BizException.class})
public ResponseEntity<Response<?>> handleBizException(BizException bizException) {
return ResponseEntity
.badRequest() // 自定义httpStatus
.body(Response.fail(bizException.getBizError(), bizException.getMessage()));
}
@ExceptionHandler({Exception.class})
public ResponseEntity<Response<?>> handleBizException(Exception exception) {
return handleBizException(new BizException(exception, BizErrorEnum.A0000));
}
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
// web类错误封装成Response返回
return super.handleExceptionInternal(ex, Response.fail(BizErrorEnum.A0000, ex.getMessage()), headers, status, request);
}
}
这里定义的比较简单实用,拦截BizException
是我们定义的通用业务异常,拦截Exception
作为兜底保证所有的异常都被拦截处理,重写handleExceptionInternal
则是用Response封装了默认的web类错误响应信息。
BizException
package com.gitee.theskyone.bird.common.error;
public class BizException extends RuntimeException {
private BizError bizError;
public BizException(String message, BizError bizError) {
super(message);
this.bizError = bizError;
}
public BizException(Throwable cause, BizError bizError) {
super(cause);
this.bizError = bizError;
}
public BizError getBizError() {
return bizError;
}
}
测试
测试一下!
package com.gitee.theskyone.bird.web;
@SpringBootTest
@AutoConfigureMockMvc
class HelloWorldControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void postHello() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/hello"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.content()
.string(StringContains.containsString("code")));
}
}
// HttpRequestMethodNotSupportedException
// {"code":"A0000","message":"Request method 'POST' not supported","data":null}
基本到这里关于全局异常处理的部分也就差不多了,我们将异常也封装成默认的Response
对象进行返回。那么一个有趣的实用技巧就跃然纸上了:使用异常替代错误处理。
异常替代错误处理
比较以下2种coding
姿势,你会喜欢用哪一种呢?
package com.gitee.theskyone.bird.web;
/**
* @author theskyzero
* @date 2022-04-04
*/
@RestController
public class HelloWorldController {
@GetMapping("/fail/hello1/{world}")
public Response<Hello> failHello1(@PathVariable String world) {
if (!"world".equals(world)) {
return Response.fail(BizErrorEnum.A0000, "你说啥我看不懂");
}
return Response.success(new Hello(world));
}
@GetMapping("/fail/hello2/{world}")
public Response<Hello> failHello2(@PathVariable String world) {
Assert.isTrue("world".equals(world), "你说啥我也看不懂");
return Response.success(new Hello(world));
}
}
后者可以直接通过使用Assert
或者throw Exception
的方式来拒绝处理,比Response.fail
更加灵活且清晰直观。比如在底层一些逻辑的校验失败能用Response
封装出去吗?不符合依赖倒置原则同时读起来也很费力~而全局结合异常拦截处理我们可以大胆的使用异常拒绝不合理的请求!至于产生异常会不会引起的一点点性能损耗,忽略就好啦!
ErrorController
最后还有一个小知识点,比如我们访问的路径不对,默认返回的是一个"空白页"。这个错误默认被转发到"/error"
视图,而被BasicErrorController
处理到。当然我们也可以自定义,虽然不是很必要。
package com.gitee.theskyone.bird.common.web.advice;
@RestController
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomErrorController extends AbstractErrorController {
public CustomErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes);
}
@RequestMapping
public ResponseEntity<Response<?>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(Response.fail(BizErrorEnum.A0000, status.getReasonPhrase()), status);
}
Map<String, Object> body = getErrorAttributes(request, ErrorAttributeOptions.defaults());
return new ResponseEntity<>(Response.fail(BizErrorEnum.A0000, String.valueOf(body.get("error"))), status);
}
}