在做前后端分离的项目时,后端通常都会拆分成多个独立的微服务,这时候就会涉及每个服务返回给前端的数据格式问题了 在项目中,应该对响应给前端的 JSON 数据的格式进行统一的规定,在很多响应结构中,一般会包含 code、message、data 三个属性。
code 代表状态码,这个不是 HTTP 的响应状态码,而是后端系统的业务状态码,代表了后端响应给前端的业务状态。
message 代表了后端给前端的响应信息,如果发生异常,或者系统错误,会将错误信息存储在 message 中,响应给前端。
data 代表了后端响应给前端的数据,如果请求获取了数据,会将数据放入 data 域中。
一般返回的数据格式会包括4个部分: 第一部分:请求处理是否成功, 第二部分:服务处理结果编码 code, 第三部分:编码对应的文本信息 message, 第四部分:返回值 date
{
"result": true,
"code": 1000,
"message": "SUCCESS",
"data": {
"lantian": 17,
"qingfen": 16,
"baiyun": 18
}
}
对于异常处理情况,我们也需要统一成上面的格式。如果我们在controller中通过try catch来处理异常的话,会出现一个问题就是每个函数里都加一个Try catch,代码会变的很乱。下面我们就通过spring boot的注解来省略掉controller中的try-catch 帮助我们来封装异常信息并返回给前端,这样用户也不会得到一些奇奇怪怪的错误提示。
@PostMapping(value = "")
AppResponse add(@Validated(Add.class) @RequestBody Dog dog, Errors errors){
AppResponse resp = new AppResponse();
try {z
BSUtil.controllerValidate(errors);
Dog newDog = dogService.save(dog);
resp.setData(newDog);
}catch (BusinessException e){
LOGGER.error(e.getMessage(), e);
resp.setFail(e.getMessage());
}catch (Exception e){
LOGGER.error(e.getMessage(), e);
resp.setFail("操作失败!");
}
return resp;
}
{
"result": false,
"code": 3000,
"message": "THIS IS AN UNKNOW EXCEPTION",
"data": null
}
使用@RestControllerAdvice+ @ExceptionHandler 进行全局的 Controller 层异常处理,只要设计得当,就再也不用在 Controller 层进行 try-catch 了!同时也不用在controller层手动记录错误日志了!
统一响应结构体我的做法是这样子的: 1.定义返回结构体和异常
/**
* implements Serializable 序列化是指把对象转换为字节序列的过程,我们称之为对象的序列化,就是把内存中的这些对象变成一连串的字节(bytes)描述的过程。
* 那么什么情况下需要序列化呢?大概有这样两类比较常见的场景:
* 1)、需要把内存中的对象状态数据保存到一个文件或者数据库中的时候,这个场景是比较常见的,例如我们利用mybatis框架编写持久层insert对象数据到数据库中时;
* 2)、网络通信时需要用套接字在网络中传送对象时,如我们使用RPC协议进行网络通信时;
*/
@NoArgsConstructor
@Slf4j
@Data
public class ResponseData implements Serializable {
//1.自定义返回值结构信息
/**
* 错误码
* @mock 1
*/
private int code;
/**
* 错误信息
* @mock Success
*/
private String msg;
/**
* 返回时间戳
* @mock 1637220171871
*/
private Long timestamp;
/**
* 返回数据
*/
private T data;
//2.定义返回值和对应code的信息(code的信息在I18nConstants)
public ResponseData(int code, String msg, T data) {
this.code = code;
this.msg = msg;
//currentTimeMillis()以毫秒为单位返回当前时间
this.timestamp = System.currentTimeMillis();
this.data = data;
}
//3.自定义异常
public static ResponseData ok() {
return ok(null);
}
public static ResponseData ok(T data) {
return new ResponseData(I18nConstants.OK, I18nConstants.OK_MESSAGE, data);
}
public static ResponseData failed() {
return failed(null);
}
public static ResponseData failed(T data) {
return new ResponseData(I18nConstants.FAIL, I18nConstants.FAIL_MESSAGE, data);
}
public static ResponseData failed(int code, String message) {
return failed(code, message, null);
}
public static ResponseData failed(int code, String message, T data) {
return new ResponseData(code, message, data);
}
}
2.定义code信息
/**
* 给 ResponseData用
*/
public interface I18nConstants {
/**
* 成功返回码
*/
int OK = 200;
/**
* 异常返回码
*/
int FAIL = 500;
/**
* controller异常
*/
int CTRL_ERROR = 501;
/**
* 参数异常返回码
*/
int ARGUMENT_ERROR = 503;
/**
* 成功返回信息
*/
String OK_MESSAGE = "成功";
/**
* 异常返回信息
*/
String FAIL_MESSAGE = "服务异常";
/**
* 参数异常返回信息
*/
String ARGUMENT_ERROR_MESSAGE = "请求参数异常";
}
测试
/**
* 内部错误国际化管理(内部接口)
*/
/**
* @RequiredArgsConstructor: 会生成一个包含常量,和标识了NotNull的变量的构造方法。生成的构造方法是私有的private。
* @slf4j是一个日志标准,使用它可以完美的桥接到具体的日志框架。slf4j提供了日志接口、获取具体日志对象的方法
*/
@RestController
@RequestMapping("/inner/errorTranslate")
@Slf4j
@RequiredArgsConstructor
public class InnerErrorTranslateController {
private final ErrorTranslateService errorTranslateService;
/**
* 查询错误国际化列表
*/
@GetMapping("/selectList")
public ResponseData selectList(ErrorTranslateQuery query) {
return ResponseData.ok(errorTranslateService.selectList(query));
}
}
测试结果(OK)
{
"code": 200,
"msg": "成功",
"timestamp": 1641280641936,
"data": [
{
"code": "cn",
"name": "中文",
"gmtCreated": 1640293629000
},
{
"code": "en",
"name": "英文",
"gmtCreated": 1640293640000
}
]
}
一般我们都进行全局异常处理,免得每次使用都要写一遍ResponseData请求体对象
3.全局异常处理 3.1定义异常类
/**
* 基本异常类
*/
public class BizException extends RuntimeException {
/**
* 错误码
* 异常返回码 FAIL:500
*/
@Getter
private int code = I18nConstants.FAIL;
public BizException(String message) {
super(message);
}
public BizException(int code, String message) {
super(message);
this.code = code;
}
public BizException(String message, Throwable t) {
super(message, t);
}
public BizException(int code, String message, Throwable t) {
super(message, t);
this.code = code;
}
}
3.2全局异常处理
/**
* 全局异常处理
*/
/**
* 依赖于 ResponseData
* @RestControllerAdvice 注解自动扫描整个项目,捕获全局异常
*/
@RestControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class ResultHandlerResolver implements ResponseBodyAdvice {
/**
* 未捕获异常处理
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public ResponseData handleGlobalException(Exception e) {
log.error("全局异常信息", e);
return ResponseData.failed(I18nConstants.FAIL_MESSAGE);
}
/**
* 业务异常
*
* @param e
* @return
*/
@ExceptionHandler(BizException.class)
public ResponseData handleBusinessException(BizException e) {
log.error("业务异常", e);
return ResponseData.failed(e.getCode(), e.getMessage());
}
/**
* Controller上层相关异常
*
* @param e
* @return
*/
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
MissingServletRequestPartException.class,
AsyncRequestTimeoutException.class
})
public ResponseData handleServletException(Exception e) {
log.error("Controller上层异常", e);
return ResponseData.failed(I18nConstants.CTRL_ERROR, e.getMessage());
}
/**
* 参数绑定异常
*
* @param e
* @return
*/
@ExceptionHandler(value = BindException.class)
public ResponseData handleBindException(BindException e) {
log.error("参数绑定校验异常", e);
return wrapperBindingResult(e.getBindingResult());
}
/**
* 参数绑定异常
*
* @param e
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseData handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("参数绑定校验异常", e);
return wrapperBindingResult(e.getBindingResult());
}
/**
* 参数绑定异常
*
* @param e
* @return
*/
@ExceptionHandler(value = ConstraintViolationException.class)
public ResponseData handleConstraintViolationException(ConstraintViolationException e) {
log.error("参数绑定校验异常", e);
// stringBuilder拼接
StringBuilder stringBuilder = new StringBuilder();
// 循环处理参数绑定校验异常信息
for (ConstraintViolation constraintViolation : e.getConstraintViolations()) {
stringBuilder.append(constraintViolation.getMessage()).append(",");
}
// 错误信息
String errorMessage = stringBuilder.toString();
// 去除最后的逗号
if (errorMessage.length() > 1) {
errorMessage = StringUtils.removeEnd(errorMessage, ",");
}
return ResponseData.failed(I18nConstants.ARGUMENT_ERROR, errorMessage);
}
/**
* 包装绑定异常结果
*
* @param bindingResult
* @return
*/
private ResponseData wrapperBindingResult(BindingResult bindingResult) {
StringBuilder msg = new StringBuilder();
for (ObjectError error : bindingResult.getAllErrors()) {
msg.append(", ");
msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage());
}
return ResponseData.failed(I18nConstants.ARGUMENT_ERROR, msg.substring(2));
}
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object obj, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if(obj instanceof ResponseData || obj instanceof File) {
return obj;
}
return ResponseData.ok(obj);
}
}
有了全局异常处理后,我们就可以专注写自己的业务代码了
/**
* 内部错误国际化管理(内部接口)
*/
/**
* @RequiredArgsConstructor: 会生成一个包含常量,和标识了NotNull的变量的构造方法。生成的构造方法是私有的private。
* @slf4j是一个日志标准,使用它可以完美的桥接到具体的日志框架。slf4j提供了日志接口、获取具体日志对象的方法
*/
@RestController
@RequestMapping("/inner/errorTranslate")
@Slf4j
@RequiredArgsConstructor
public class InnerErrorTranslateController {
private final ErrorTranslateService errorTranslateService;
/**
* 查询错误国际化列表
*/
@GetMapping("/selectList")
public List selectList(ErrorTranslateQuery query) {
return errorTranslateService.selectList(query);
}
}
测试结果(FAIL)
{
"code": 500,
"msg": "语言编码已存在",
"timestamp": 1641283519114,
"data": null
}
可以看到,所有的 Controller 层的异常的日志记录,都是在这个 ResultHandlerResolver 中进行记录。也就是说,Controller 层也不需要在手动记录错误日志了