关于异常处理的学习思考。

1.Spring Boot的异常处理机制

SpringBoot默认的异常处理主要是由BasicErrorController类来进行处理的。

处理分为了两类,一类是浏览器请求一个不存在的页面时发生异常,SpringBoot会默认响应一个Whitelabel Error Page的html文档内容。这是由于一般情况下浏览器默认发送的请求头中是Accept: text/html。

还有一类是在请求后服务端处理发生异常,返回包含异常信息的Json字符串,如:

{
"timestamp": "2020-08-04T06:11:45.209+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/index.html"
}

Spring Boot 默认提供了程序出错的结果映射路径/error。在BasicErrorController中进行映射处理。

@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {

BasicErrorController中是通过判断请求头中的Accept的内容是否为text/html来区分请求是来自客户端浏览器(浏览器通常默认自动发送请求头内容Accept:text/html)还是客户端接口的调用,以此来决定返回页面视图还是 JSON 消息内容。

@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}

不过现在都是前后端分离式开发了,更多的是返回Json字符串的形式。

2.全局异常处理方案

Spring Boot提供的ErrorController是一种全局性的处理机制。但是在开发中我们会遇到许多的自定义异常,并且需要将一些报错提示信息反馈给前端,比如自定义的状态码和自定义的报错提示信息。之前也试着用枚举类Enum写了一个抛异常信息的工具类,总的来说还是不够好用,也比较死板。

现在采用@ControllerAdvice注解和@ExceptionHandler注解实现对指定异常的特殊处理。

@ControllerAdvice 是一个Controller增强器,可对controller中被 @RequestMapping注解的方法加一些逻辑处理。

@ExceptionHandler 注解一般是用来自定义异常。 可以认为它是一个异常拦截器(处理器)。

需要注意的是@ControllerAdvice是应用到所有的@RequestMapping中,只有进入Controller层的错误才会由@ControllerAdvice处理,拦截器抛出的错误以及访问错误地址的情况@ControllerAdvice处理不了,由SpringBoot默认的异常处理机制处理。

为了配置异常信息方便,采用了配置文件的形式。首先在resourses下创建/config/exception-codes-demo.properties文件

为了和常见的异常状态码区别开,采用自定义状态码。类似

sell.codes[1]=SUCCESS
#参数错误
sell.codes[10001]=参数为空
sell.codes[10002]=参数不全
sell.codes[10003]=参数类型错误
sell.codes[10004]=参数无效
#用户错误
sell.codes[20001]=永久不存在
sell.codes[20002]=用户未登录
sell.codes[20003]=用户名或者密码错误
sell.codes[20004]=用户账户已被禁用
sell.codes[20005]=用户已经存在
#业务错误
sell.codes[30001]=系统业务出现问题
#系统错误
sell.codes[40001]=系统内部错误
#数据错误
sell.codes[50001]=数据未找到
sell.codes[50002]=数据有误
sell.codes[50003]=数据已存在
#接口错误
sell.codes[60001]=系统业务出现问题
sell.codes[60002]=系统外部接口调用异常
sell.codes[60003]=接口禁止访问
sell.codes[60004]=接口地址无效
sell.codes[60005]=接口请求超时
sell.codes[60006]=接口负载过高

#权限错误
sell.codes[70001]=没有访问权限

与配置文件绑定的处理类

@Component
@Getter
@Setter
@PropertySource(value = "classpath:config/exception-codes-demo.properties",encoding = "utf-8")
@ConfigurationProperties(prefix = "sell")
public class ExceptionCodeConfiguration {

private Map<Integer,String> codes = new HashMap<>();

public String getMessage(int code){
return codes.get(code);
}
}

根据状态码获取相应的异常信息。

因为返回前端的统一为Json数据格式,先定义一下Json数据格式以及实体类。

@Data
@AllArgsConstructor
public class UnifyResponse {
private Integer code;//状态码
private String message;//异常信息
private String request;//请求接口
}

自定义业务异常类,用来处理发生的业务异常

@Data
@AllArgsConstructor
public class DemoException extends RuntimeException {
private Integer code;
}

自定义全局异常处理通知类

@ControllerAdvice
public class GlobalExceptionAdvice {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionAdvice.class);
@Autowired
ExceptionCodeConfiguration codeConfiguration;

/**
* 处理自定义业务异常
*
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = DemoException.class)
@ResponseBody
public UnifyResponse demoExceptionHandler(HttpServletRequest req,DemoException e){
logger.error(("运行时业务异常!原因是:"+e));
return new UnifyResponse(e.getCode(),codeConfiguration.getMessage(e.getCode()),
req.getMethod() + req.getRequestURI());
}

/**
* 处理其他异常
*
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public UnifyResponse exceptionHandler(HttpServletRequest req,Exception e){
logger.error("未知异常!原因是:"+ e );
return new UnifyResponse(99999,"服务器异常",req.getMethod() + " " + req.getRequestURI());
}
}

3. 测试用例

简单的创建一个用户的实体类用于测试

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/** 编号 */
private Integer id;
/** 姓名 */
private String name;
/** 年龄 */
private Integer age;
}

用户控制类

@Controller
@ResponseBody
@RequestMapping("/api")
public class UserRestController {

@PostMapping("/user")
public Boolean insert(@RequestBody User user){
System.out.println("开始新增!");
if(user.getName() == null){
throw new DemoException(10001);
}
return true;
}

@PutMapping("/user")
public boolean update(@RequestBody User user) {
System.out.println("开始更新...");
//这里故意造成一个空指针的异常,并且不进行处理
String str=null;
str.equals("111");
return true;
}

@DeleteMapping("/user")
public boolean delete(@RequestBody User user) {
System.out.println("开始删除...");
//这里故意造成一个异常,并且不进行处理
Integer.parseInt("abc123");
return true;
}

@GetMapping("/user")
public List<User> findByUser(User user) {
System.out.println("开始查询...");
List<User> userList =new ArrayList<>();
User user2= new User();
user2.setId(1);
user2.setName("x2yu");
user2.setAge(18);
userList.add(user2);
return userList;
}
}

用Post工具进行测试

对新增用户的接口进行测试,前端Post的数据为

{
"id": 1,
"age": 18
}

前端收到返回的异常数据为

{
"code": 10001,
"message": "参数为空",
"request": "POST/api/user"
}

对删除用户接口进行测试,前端收到异常信息为

{
"code": 99999,
"message": "服务器异常",
"request": "DELETE /api/user"
}

好像基本达到了想要的效果。狗头

对应的后端也会打印异常信息。

image-20200805232149495

其实也可以将异常信息封装返回到前端,不过感觉没必要,反正还是后端看。

参考

SpringBoot全局异常处理方案