您当前的位置: 首页 > 

大别山码将

暂无认证

  • 3浏览

    0关注

    126博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

统一响应结构体 | 大别山码将

大别山码将 发布时间:2022-01-04 17:16:57 ,浏览量:3

在做前后端分离的项目时,后端通常都会拆分成多个独立的微服务,这时候就会涉及每个服务返回给前端的数据格式问题了 在项目中,应该对响应给前端的 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 层也不需要在手动记录错误日志了

关注
打赏
1664364263
查看更多评论
立即登录/注册

微信扫码登录

0.0661s