目录
本文源码地址:https://github.com/nieandsun/NRSC-STUDY
1 统一数据返回格式
在实际项目开发中为了与前端更好的交互,我们一般都会固定参数的返回格式,如下图:
为了达到这一目的我比较喜欢的一种做法是定义三个类来完成这件事:
代码很简单,这里贴一下:
- ResultVO — http请求返回的最外层对象
package com.nrsc.elegant.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class ResultVO<T> implements Serializable {
private static final long serialVersionUID = 3009132526980391812L;
/** 错误码. */
private Integer code;
/** 提示信息. */
private String msg;
/** 具体的内容. */
private T data;
}
- ResultEnum — 封装返回状态码和提示信息的枚举类
package com.nrsc.elegant.enums;
import lombok.Getter;
@Getter
public enum ResultEnum {
SUCCESS(0, "成功"),
FAILURE(-1,"系统异常"),
PARAM_ERROR(300, "参数不正确"),
RESULT_NOT_EXIST(301, "查询结果不存在");
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
- ResultVOUtil — 将返回数据处理成ResultVO的工具类
package com.nrsc.elegant.util;
import com.nrsc.elegant.vo.ResultVO;
public class ResultVOUtil {
public static ResultVO success(Object object) {
ResultVO resultVO = new ResultVO();
resultVO.setData(object);
resultVO.setCode(ResultEnum.SUCCESS.getCode());
resultVO.setMsg(ResultEnum.SUCCESS.getMessage());
return resultVO;
}
public static ResultVO success() {
return success(null);
}
public static ResultVO error(Integer code, String msg) {
ResultVO resultVO = new ResultVO();
resultVO.setCode(code);
resultVO.setMsg(msg);
return resultVO;
}
public static ResultVO error(ResultEnum resultEnum) {
ResultVO resultVO = new ResultVO();
resultVO.setCode(resultEnum.getCode());
resultVO.setMsg(resultEnum.getMessage());
return resultVO;
}
}
2 全局异常处理概述
在真实项目中我们都会经常与异常进行打交道,合理的对异常进行归类,并按照类别进行统一处理会让你的代码变得更加优雅
。
本文对于异常处理的基本逻辑为:
(1)对请求参数校验异常单独进行全局统一处理
(2)对自定义的异常单独进行全局统一处理
(3)对项目中未知或不好归类的异常单独进行全局统一处理
2.1 请求参数校验异常全局处理
在后端开发中对请求参数的校验主要可以分为三类:
- 对URL中请求参数的校验
- form表单请求 — 且请求参数用实体类来接收时,对各个参数的校验
- application/json请求 — 且请求参数用实体类来接收时,对各个参数的校验
当校验失败时,springboot默认情况下会根据这三种情况抛出三种异常:
- ConstraintViolationException(javax.validation.ConstraintViolationException)
- BindException(org.springframework.validation.BindException)
- MethodArgumentNotValidException(org.springframework.web.bind.MethodArgumentNotValidException;)
我们可以对这些异常进行全局统一处理,代码如下:
package com.nrsc.elegant.handle;
import com.nrsc.elegant.enums.ResultEnum;
import com.nrsc.elegant.exception.ElegantException;
import com.nrsc.elegant.util.ResultVOUtil;
import com.nrsc.elegant.vo.ResultVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author : Sun Chuan
* @date : 2019/10/18 22:26
* Description:全局统一异常处理类
* 该类的处理逻辑:
* (1)对请求参数异常和我们自定义的异常分别做单独处理
* (2)对可能出现的未知异常进行单独处理
*/
@Slf4j
@ControllerAdvice
public class ExceptionHandle {
/***
* 可能出现的未知异常
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultVO handle(Exception e) {
log.error("【系统异常】{}", e);
return ResultVOUtil.error(ResultEnum.FAILURE);
}
/***
* 参数异常 -- ConstraintViolationException()
* 用于处理类似http://localhost:8080/user/getUser?age=30&name=yoyo请求中age和name的校验引发的异常
* @param e
* @return
*/
@ExceptionHandler(value = {ConstraintViolationException.class})
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResultVO urlParametersExceptionHandle(ConstraintViolationException e) {
log.error("【请求参数异常】:{}", e);
//收集所有错误信息
List<String> errorMsg = e.getConstraintViolations()
.stream().map(s -> s.getMessage()).collect(Collectors.toList());
return ResultVOUtil.error(ResultEnum.PARAM_ERROR.getCode(), errorMsg.toString());
}
/***
* 参数异常 --- MethodArgumentNotValidException和BindException
* MethodArgumentNotValidException --- 用于处理请求参数为实体类时校验引发的异常 --- Content-Type为application/json
* BindException --- 用于处理请求参数为实体类时校验引发的异常 --- Content-Type为application/x-www-form-urlencoded
* @param e
* @return
*/
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResultVO bodyExceptionHandle(Exception e) {
log.error("【请求参数异常】:{}", e);
BindingResult bindingResult = null;
if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
bindingResult = ex.getBindingResult();
} else {
BindException ex = (BindException) e;
ex.getBindingResult();
}
if (bindingResult != null) {
//收集所有错误信息
List<String> errorMsg = bindingResult.getFieldErrors().stream()
.map(s -> s.getDefaultMessage()).collect(Collectors.toList());
return ResultVOUtil.error(ResultEnum.PARAM_ERROR.getCode(), errorMsg.toString());
}
return ResultVOUtil.error(ResultEnum.PARAM_ERROR);
}
/***
* 自定义异常 --- 自定义异常一般不要设置为ERROR级别,因为我们用自定义的异常主要是为了辅助我们处理业务逻辑,
* --- 它们其实并不能被真正当作异常来看待
* @param e
* @return
*/
@ResponseStatus(HttpStatus.IM_USED)
@ExceptionHandler(value = ElegantException.class)
@ResponseBody
public ResultVO elegantExceptionHandle(ElegantException e) {
log.warn("【自定义异常】", e);
return ResultVOUtil.error(e.getCode(), e.getMessage());
}
}
2.2 自定义异常全局处理
实际项目中,为了方便的将订单不存在、购物车为空、订单更新失败等结果返回给前端,同时又要保持住统一的数据返回格式,我们经常会利用自定义异常+全局异常处理的方式
来解决。
下面是自定义异常的代码,全局处理该异常的方法参见2中的代码。
package com.nrsc.elegant.exception;
import com.nrsc.elegant.enums.ResultEnum;
import lombok.Getter;
/**
* @author : Sun Chuan
* @date : 2019/10/18 22:21
* Description:自定义异常
*/
@Getter
public class ElegantException extends RuntimeException {
private Integer code;
public ElegantException(ResultEnum resultEnum) {
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
public ElegantException(Integer code, String message) {
super(message);
this.code = code;
}
}
3 测试代码
写了一些测试代码,也贴在这里吧,有兴趣的可以拿着测试一下:
package com.nrsc.elegant.controller;
import com.nrsc.elegant.enums.ResultEnum;
import com.nrsc.elegant.exception.ElegantException;
import com.nrsc.elegant.pojo.UserInfo;
import com.nrsc.elegant.util.ResultVOUtil;
import com.nrsc.elegant.vo.ResultVO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("/user")
@Validated //校验URL中的请求参数时,该注解需要放在这里
public class UserInfoController {
/***
* ConstraintViolationException异常统一处理测试
* @param name
* @param age
* @return
*/
@GetMapping("/getUser")
public ResultVO<UserInfo> getUser(@NotNull(message = "name不能为空") String name,
@Min(value = 18, message = "未满18岁")
@NotNull(message = "age不能为空") Integer age) {
UserInfo userInfo = new UserInfo();
userInfo.setAge(age);
userInfo.setName(name);
return ResultVOUtil.success(userInfo);
}
/***
* MethodArgumentNotValidException异常统一处理测试
* Content-Type为application/json ---> 实体类前必须要加一个@RequestBody注解才能获得到前端传来的参数
* @param userInfo
* @return
*/
@PostMapping("/saveUser")
public ResultVO<UserInfo> saveUser(@RequestBody @Validated UserInfo userInfo) {
userInfo.setId(111L);
return ResultVOUtil.success(userInfo);
}
/***
* BindException异常统一处理测试
* Content-Type为application/x-www-form-urlencoded ---> 即表单请求,此时不能用@RequestBody注解
* @param userInfo
* @return
*/
@PostMapping("/saveUserInfo")
public ResultVO<UserInfo> saveUserInfo(@RequestBody @Validated UserInfo userInfo) {
userInfo.setId(111L);
return ResultVOUtil.success(userInfo);
}
/***
* ElegantException --- 自定义异常、未知异常和URL---统一处理测试
* @param id
* @return
*/
@GetMapping("/getUserById/{id}")
public ResultVO<UserInfo> getUser(@PathVariable @Max(value = 100, message = "id应小于100") Long id) {
UserInfo userInfo = new UserInfo();
//自定义异常
if (id < 0) {
throw new ElegantException(ResultEnum.PARAM_ERROR);
}
//未知异常
else if (id == 0) {
throw new RuntimeException("xxoo");
}
userInfo.setId(id);
userInfo.setAge(18);
userInfo.setName("yoyo");
return ResultVOUtil.success(userInfo);
}
}