异常是开发过程中必须要处理的一个问题,应用系统不可能不遇到异常。如何处理好系统的异常,有多种方式,也需要更高的水平。因为它不是会用那么简单,需要先做好异常的设计,再进行编码测试,改进等。
因为想把Java系统异常的东西写在一篇文章里,所以内容可能条理不是很清晰,望见凉。
一、关于Java异常的知识
1.1 Java异常体系
关于Java异常的一些基础知识,不做描述了,做过一段时间的开发大概就应该知道。这里给出Java异常错误的体系图,如下:
其中,Exception包含运行时异常(RuntimeException,也叫非检查异常)和检查异常(也叫非运行时异常)两类。运行时异常是代码运行过程中发生的异常,比如NullPointerException等;检查运行是代码编写好后,在编译阶段语法不通过而需要处理的异常,是不运行就需要处理的异常。
1.2 try catch内部实现
异常,是扰乱的程序的指令的正常运行的执行期间发生的一个事件。
当发生中发生错误时,该方法会创建一个对象并将期交给运行时系统。该对象称为异常对象,包含了有关错误的信息——错误发生时的类型和程序状态。创建异常对象并将其交给运行时系统称为抛出异常。
在方法抛出异常后,运行时系统会尝试查找要处理它的内容。处理异常的可能“somethings”的集合是已经被调用以获取发生错误的方法的有序方法列表。方法列表称为调用堆栈,如下图。
运行时系统在调用的堆栈中搜索包含可处理异常的代码块的方法。这段代码称为异常处理程序。搜索从发生错误的方法开始,并按照调用方法的相反顺序继续通过调用堆栈,找到适当的处理程序后,运行时系统会将异常传递给处理程序。如果抛出的异常对象的类型与处理程序的类型匹配,则认为异常处理程序是合适的。
选择的异常处理程序可以捕获异常。如果运行时系统穷举搜索调用堆栈上的所有方法仍然没有找到适当的异常处理程序,如下图所示,则运行时系统(以及程序)终止。
使用异常来管理错误与传统的错误管理技术相比具有一些优势。
二、系统异常设计
2.1 异常设计的基本要求
在系统的异常设计时,要考虑多方面的因素。但有基本的要求:
- 能够将异常信息很好的展示,抛出给用户时,友好的提示;
- 抛出异常后,能够让技术人员快速的定位到问题,异常信息简洁而易懂;
- 能够区别是调用不正确而导致的异常(比如一些常量写错了而导致解析出现异常)还是内部本身的异常。
2.2 异常设计的分类
系统的异常设计,也要考虑到有业务方面的异常提示,数据库方面的异常,系统内部的异常,第三方调用的异常等多方面的异常。能够对这些异常进行分类归纳,并正确的抛出,友好的提示。这样抛出异常后,可以明确的是哪方面的异常,比较明确。
如果一个很大的系统,抛出异常时,若只是给出异常提示,那我感觉我会崩溃的。不能很容易清楚哪个模块或功能出现的异常,提示也不友说。如果能在异常提示信息中,明确指出某个系统某个模块某个功能出现的异常,会不会舒服很多?
至于内部异常,是可以恢复的异常,一般记录日志或打印异常的相关信息,告知用户出现的异常信息。
2.3 不同系统之间异常的兼容
当不同系统之间有调用,那异常信息如何处理呢?当然可以捕捉到异常,但将异常转化为标准的接口返回格式,这样是可行的。但也要求异常的处理有足够的兼容性。如果好的话,直接转化就可以了,而不需要再进行中间的处理。
2.4 其它问题的考虑
比较个性化的异常。很多情况下,较好的异常设计可以满足要求。但如果出现一些特殊的情况就有些麻烦了。比如在某些特殊情况下,有个性化的要求。比如某个模块的异常与其它模块的异常不太一样,是比较特殊的一类异常。那怎么处理呢?
安全的异常处理。如果系统的接入有PC端、移动端等多种设备,且还有其它的不同情况,那是不是很多异常都能检测到并拦截呢?有些是系统层面的异常,比如系统出现HTTP 500问题时,还有请求时,那怎么给出对应的提示呢,而不是让用户直接知道系统出现了问题。
2.5 异常设计的总结
综上内容,系统的异常处理细说起来,需要考虑的方面还是挺多的。个人以为异常设计时,可以从大方面着手,比如进行分类归纳,然后好确定小目标。
- 根据情况确定分类,这样会清晰容定位,不同模块、内外部异常都做清晰的分类;
- 尽可能保证异常能够被全面拦截,比如出现500,那也在页面或接口中返回其它"友好"的提示,可以不直接说系统出大问题了;
- 确定异常的格式,比如以JSON格式展示,包含异常代码、异常信息等,要有良好的兼容性;
- 定义常见异常,包含异常代码、异常信息等,这样做有利于统一异常,而不至于同样的异常在这里是这样提示,在另一个地方就是另一个提示了;比如通过枚举定义常见的异常;而且异常代码最好跟一些普通的错误代码统一或者兼容;
- 抽象封装基本常用的异常处理或处理方法;
- 异常以日志等形式记录下来,最好附带全面清晰的异常信息,比如在异常日志文件里写入模块、类名甚至方法名、入参、出参等,这样有利于快速排查解决异常;
- 做好细节处理的规范,比如需要抛出的异常就需要抛出,而不是只打印信息,异常捕获和记录的边界;
- 定义异常的开发规范等规范性文章和制度,约束开发人员的行为;毕竟设计的再好,最后还是开发人员进行编码工作,把好最后一关,才能达到很好的效果。
三、系统的异常处理实现——@ControllerAdvice
3.1 @ControllerAdvice简介
在Spring3.2中,新增了@ControllerAdvice注解,作为特化@Component,允许通过类路径扫描自动检测实现类。通常用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到@RequestMapping、@PostMapping、@GetMapping注解中,统一处理异常。参考@ControllerAdvice。
在4.3版本中,带来了@RestControllerAdvice。它是一个更加使得的注解,@ControllerAdvice加上@ResponseBody。参考@RestControllerAdvice。
下面是一个示例,使用的Spring版本是5.1.2.RELEASE。
自定义ControllerAdvice:在对应的方法上加入@InitBinder、@ModelAttribute、@ExceptionHandler注解。
@ControllerAdvice
public class SysControllerAdvice {
// 应用到所有的@RequestMapping注解方法,在其执行前初始化数据绑定器
@InitBinder
public void initBinder(WebDataBinder binder) {
System.out.println("------------initBinder---------------");
// 统一日期处理
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
// 添加检验
binder.addValidators(sysValidator());
}
// 检验,这里只是为了举例,实际上没有检验的具体的内容
private Validator sysValidator() {
System.out.println("------------sysValidator---------------");
return null;
}
// 设置属性,属性值绑定到Model中,使全局@RequestMapping可以获得该值
@ModelAttribute
public void addAttributes(Model model) {
System.out.println("------------addAttributes---------------");
model.addAttribute("author", "Tom");
}
// 全局异常的捕捉处理
@ResponseBody
@ExceptionHandler(value = Exception.class)
public Map errorHandler(Exception e) {
System.out.println("------------errorHandler---------------");
Map map = new HashMap();
map.put("code", 100);
map.put("msg", e.getMessage());
return map;
}
// 拦截自定义的异常
@ResponseBody
@ExceptionHandler(value = SysException.class)
public Map myErrorHandler(SysException e) {
System.out.println("------------myErrorHandler---------------");
Map map = new HashMap();
map.put("code", e.getCode());
map.put("msg", e.getMsg());
return map;
}
// 自定义异常处理
@ResponseBody
@ExceptionHandler(value = Exception.class)
public ModelAndView handlerException(SysException e) {
System.out.println("------------handlerException---------------");
ModelAndView mv = new ModelAndView();
Map map = new HashMap();
map.put("code", 100);
map.put("msg", e.getMessage());
mv.addObject(map);
mv.setViewName("index");
return mv;
}
@ResponseBody
@ExceptionHandler(value = Exception.class)
public Response handlerException() {
System.out.println("------------handlerException---------------");
Response response = new Response();
response.setData(null);
response.setCount(0);
return response;
}
}
响应
public class Response {
// 响应代码
private String code;
// 响应提示信息
private String msg;
// 响应返回的数据
private Object data;
// 响应的数据的条数
private int count;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
自定义异常
public class SysException extends RuntimeException {
// 异常类型:业务类/系统类等
private String exceptionType;
// 异常代码
private String code;
// 异常信息
private String msg;
public SysException(String exceptionType, String code, String msg) {
this.exceptionType = exceptionType;
this.code = code;
this.msg = msg;
}
public String getExceptionType() {
return exceptionType;
}
public void setExceptionType(String exceptionType) {
this.exceptionType = exceptionType;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
用于测试的Controller
@Controller
@RequestMapping("/system")
public class IndexController {
// 直接抛出异常
@RequestMapping("/index")
public String index() throws Exception {
// 返回后,对应JSON格式是{"msg":"错误 错误","code":"101"}
throw new SysException("BUSINESS", "101", "错误");
}
// 显示页面
@RequestMapping("/index")
public ModelAndView business() throws Exception {
ModelAndView mv = new ModelAndView();
// 获取设置的属性
mv.addObject("author");
return mv;
}
}
配置(如果不起作用,把注释的一行配置放开)
<context:component-scan base-package="com" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<!--<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" />-->
</context:component-scan>
错误页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>错误页面</title>
</head>
<body>
<h1>${exceptionType}</h1>
<h1>${code}</h1>
<h1>${msg}</h1>
</body>
</html>
四、系统的异常处理实现——Spring AOP
下面是AOP使用统一异常处理的核心代码,其它的SysException类,参见上面的代码。
@Aspect
@Component
public class SysExceptionAspect {
private static final Logger log = LoggerFactory.getLogger(SysExceptionAspect.class);
@Pointcut("execution(public * com.controller.*(..))")
private void sysPointcut() {}
@Before("sysPointcut()")
public void before(JoinPoint joinPoint) {}
@Around("sysPointcut()")
public void around(ProceedingJoinPoint proceedingJoinPoint) {}
@AfterReturning(pointcut = "sysPointcut()", returning = "object")
public void afterReturning(Object object) {}
@AfterThrowing(pointcut = "sysPointcut()", throwing = "cause")
public void doException(JoinPoint joinPoint, Throwable cause) {
if (null != cause) {
throw new SysException("", "100", cause.getMessage());
}
}
}