关于异常处理的学习思考。
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;
@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()); }
@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的数据为
前端收到返回的异常数据为
{ "code": 10001, "message": "参数为空", "request": "POST/api/user" }
|
对删除用户接口进行测试,前端收到异常信息为
{ "code": 99999, "message": "服务器异常", "request": "DELETE /api/user" }
|
好像基本达到了想要的效果。狗头
对应的后端也会打印异常信息。
其实也可以将异常信息封装返回到前端,不过感觉没必要,反正还是后端看。
参考
SpringBoot全局异常处理方案