10分钟阅读时长
在 Java 中,Exception 被单独列为了一章,足以证明其独立性与重要性。所以什么是异常?
Java 的基本理念是“结构不佳的代码不能运行”。
对于程序 Error 而言,恢复系统 Error 十分重要,而
Exception 作为一种对 Error 改善的机制
,能够极大的提高代码健壮性
。对于 Exception , 在编译期间发现它自然是最好的时机,因为此时我们可以立即更改代码,提高健壮性来解决问题。
但是, 有些错误,属于运行时错误
RuntimeException
, 只有在程序编译完成后,在 JVM 中运行时,才能发现这种隐患。那么此时就需要有一种方式,将错误的信息传递出来,让接收者可以收到信息,以便定位问题,解决问题。
Exception 就是这样一种错误报告机制
。
基本异常
对于异常来说,根据异常情形(exceptional condition)可以分为两种情况:
- 普通问题:在当前环境下能得到足够的信息,总能处理这个错误。也就是你可以通过修改代码让这个问题不会再发生。
- 异常问题: 我们的代码在当前环境(代码块)由于缺少某些信息数据,导致程序不能再继续执行。也就是代码会在特定情形下发生错误。
此时,对于异常问题,你所能做的事情就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。
当抛出异常后,有几件事会随之发生。
首先,同 Java 中其他对象的创建一样,我们会使用关键字 new
在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。
举一个抛出异常的简单例子:
if( obj == null ){
throw new NullPointerException();
}
它意味着,当前代码可能出现为 obj 为空的情况,出现这种情况时,由于执行了 “throw 抛出” 这种操作,因此可以在别的地方(稍后会介绍)来接收并处理错误信息。
于是乎,在当前环境(代码块),我们不必再为 obj 可能为空 这一情况所担心,它会在其他地方被处理。
异常的参数
与使用 Java 中的其他对象一样,我们总是用关键字 new
在堆上创建异常对象,这也伴随着存储空间的分配和构造器的调用。
所有标准异常类都有两个构造器:一个是无参构造器;另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器:
throw new NullPointerException("t = null");
异常处理程序
前面提到了,抛出的异常会在其他“地方”被处理,这个其他地方叫做 “异常处理程序”. 而 try{}catch(e){}
中的 catch块
就是异常处理程序
try {
// 可能产生异常的代码
int x = 1;
int y = x/0;
} catch(Type1 id1) {
// 异常处理程序:专用于处理类型Type1的异常
onMyException1();
} catch(Type2 id2) {
// 异常处理程序:专用于处理类型Type2的异常
onMyException2();
} catch(Type3 id3) {
// 异常处理程序:专用于处理类型Type3的异常
onMyException3();
} finally {
// 总会执行
customExceptionHandler();
}
实践:spring 中的异常
在 spring 框架中,spring 已经对异常进行了抽象封装,便于我们去管理创建异常处理程序。你总不想看到代码里面,这里一块 try…catch ,那里一块 try…catch 对吧。
spring 提供了多种处理异常的解决方案,你只需要记住 @ControllerAdvice 这一种即可。
- 通过
@ControllerAdvice
标记的类GlobalExceptionHandler
,被称之为为 全局异常处理程序。看看上面,catch 块 可以有多个,他们分别用于处理不同的异常. - 因此,我们可以创建多个异常处理程序在
customErrorHandler(), onMyException11(),onMyException2()
来专门处理不同类型的异常。其中 customErrorHandler 被用来返回统一格式的数据。
接下来给出一个通用的示例:
@ControllerAdvice
public class GlobalExceptionHandler {
@Autowired
private ObjectMapper objectMapper;
/**
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {}
/**
* 把值绑定到Model中,使再controller中可以通过 @RequestMapping可以获取到该值
* @param model
*/
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("author", "ifredom");
}
/**
* 所有异常都返回统一的json数据
* @param CustomException
*/
@ResponseBody
@ExceptionHandler(value = CustomException.class)
public Map<String,Object> customErrorHandler(CustomException ex) {
Map<String,Object> map = new HashMap<>(16);
map.put("code", ex.getCode());
map.put("msg", ex.getMessage());
return map;
}
@ExceptionHandler(value = {MyCustomException1.class})
public void onMyException1(){
// 处理 MyCustomException1 这种异常
}
@ExceptionHandler(value = {MyCustomException2.class})
public void onMyException2(){
// 处理 MyCustomException2 这种异常
}
/**
* 处理 MethodArgumentNotValidException, BindException 这两种异常
* 解释:利用 jackson 对请求参数进行校验
*/
@ExceptionHandler(value= {MethodArgumentNotValidException.class , BindException.class})
public String onException(Exception e) throws JsonProcessingException {
BindingResult bindingResult = null;
if (e instanceof MethodArgumentNotValidException) {
bindingResult = ((MethodArgumentNotValidException)e).getBindingResult();
} else if (e instanceof BindException) {
bindingResult = ((BindException)e).getBindingResult();
}
Map<String,String> errorMap = new HashMap<>(16);
assert bindingResult != null;
bindingResult.getFieldErrors().forEach((fieldError)->
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage())
);
return objectMapper.writeValueAsString(errorMap);
}
}
// 自定义一种异常类型
public class CustomException extends RuntimeException{
private static final long serialVersionUID = 1L;
private Integer code;
private String message;
public CustomException(String message){
this.message = message;
}
public CustomException(String message, Integer code){
this.message = message;
this.code = code;
}
public CustomException(String message, Throwable e){
super(message, e);
this.message = message;
}
public MyCustomException(String message, int code, Throwable e) {
super(message, e);
this.message = message;
this.code = code;
}
@Override
public String getMessage(){
return message;
}
public Integer getCode(){
return code;
}
}
// 自定义一种异常类型
@ResponseStatus(value = HttpStatus.UNAUTHORIZED)
class MyCustomException1 extends RuntimeException{
private String message;
public MyCustomException() {}
public MyCustomException(String message) {
this.message = message;
}
}
// 自定义一种异常类型
@ResponseStatus(value = HttpStatus.FORBIDDEN)
class MyCustomException2 extends RuntimeException{
private String message;
public MyCustomException() {}
public MyCustomException(String message) {
this.message = message;
}
}
现在已经完成异常的定义,接下来启动应用访问 localhost:8080/user
, 你会发现异常被拦截住了。
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 19 Oct 2022 06:25:33 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"msg": "here",
"code": 500
}
@RestController
public class UserController {
@GetMapping("/user")
public void registerUser( @ModelAttribute("author") String author){
System.out.println(author);
throw new MyCustomException("hehhe");
}
}
有时。如果不需要返回 json 数据,而要渲染某个页面模板返回给浏览器。那么可以利用这个类 ModelAndView
;
@ExceptionHandler(value = MyException.class)
public ModelAndView myErrorHandler(MyException ex) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("code", ex.getCode());
modelAndView.addObject("msg", ex.getMsg());
return modelAndView;
}
补充:如果全部异常处理返回 json,那么可以使用 @RestControllerAdvice 代替 @ControllerAdvice ,这样在方法上就可以不需要添加 @ResponseBody。
拓展阅读
------ 如果文章对你有用,感谢右上角 >>>点赞 | 收藏 <<<