文章目录
代码已上传gitee:https://gitee.com/shang_jun_shu/springboot-exception-jsr303.git
其他系列博客
java异常处理(二)—从字节码层面看throw关键字及try…catch…finally的实现
java异常处理(三)—Springboot全局异常处理(@ControllerAdvice和ErrorController)
java异常处理(四)—还在用if判断校验参数?试试SpringBoot全局异常+JSR303校验吧!
一、Java异常基本概念
1.1、异常是什么
程序执行过程发生了不正常的情况,这种情况叫做异常
java提供一套发现并处理异常的机制,使程序员更容易发现错误
异常信息是由JVM打印在控制台
1.2异常以什么方式存在
类的方式,每一个异常类都可以创建对象
对应现实生活
火灾(异常类):
- 2008年8月8日,小明加着火了(异常对象)
- 2008年8月9日,小方加着火了(异常对象)
- 2008年8月10日,小红加着火了(异常对象)
1.3异常继承结构图
所有错误执行执行的结果就是终止程序,不管错误还是异常皆可抛出,因为都是Throwable类的子类
1.4异常分类
从上图中可以看出,异常主要分为两类
- 编译时异常:该异常类直接继承了Exception,程序员在编写程序时要处理这种异常,要不然编译不通过
- 运行时异常:该异常类继承了RuntimeException,程序员可以不用处理这种异常,如果不处理会直接交给jvm处理,打印堆栈信息
两者区别:
- 编译时异常发生概率高,要进行预处理
- 运行时异常发生概率低,没有必要预处理
举例子:
如上图所示,
method1抛出了IOException,该异常直接继承了Exception,该异常为编译时异常,所以编辑器工具会报出红线,提示程序员进行处理。
而method2抛出了RuntimeException,为运行时异常,此时程序员不需要进行处理。
二、处理异常方式
在java中处理异常一共有两种方式
-
1.在方法声明位置,使用throws关键字,抛给上一级
谁调用我,就抛给谁,上一级也有这两种处理方式,如果接着向上抛,抛给main方法,main方法抛给jvm,就会终止程序
-
2.使用try…catch…finally进行异常处理,此时处理完的异常就不用抛出去了,当然如果catch或者finally语块中包含throw关键字,也是抛出异常,这个后面讨论。
总结如下图
三、java异常在程序中的几个表现
下面按照导图顺序说下在实际开发中对异常的处理方式以及适用情况
3.1自动抛出异常
public class ExceptionTest1 {
public static void main(String[] args) throws Exception {
method1(0);
}
//不管异常
public static void method1(int b){
int a= 6;
//除数为0发生异常
int c=a/b;
System.out.println(c);
}
}
//输出结果
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.thinkcoder.ExceptionTest1.method1(ExceptionTest1.java:14)
at com.thinkcoder.ExceptionTest1.main(ExceptionTest1.java:8)
Process finished with exit code 1
从输出结果可以看出可以得出两点
1.该异常交给了jvm去处理,最后调用printStrace方法打印出堆栈日志
2.异常发生在主线程(main方法)直接终止程序,后面的代码不执行
但是一般情况下,不希望这种情况发生,因为用户的数据导致整个程序不能用,现在使用springboot后会终止线程,但是需要给用户友好提示,所以实际生产情况下要避免这种情况发生,因此要处理异常
3.2catch语块处理异常
catch语块处理异常分为三种情况
只打印日志
return方法结束
throw抛出异常
3.2.1只打印日志
对于只打印日志的情况就是,即使异常发生,后续代码也会执行即程序也会正常执行,那么什么情况下适用只打印日志呢?
1.该方法代码不是核心功能
比如统计用户点击量,如果此代码出现异常导致后面代码不能执行,核心功能不能用,那就得不偿失了,此时就可以只打印日志
public Object run() {
log.info("用户点击统计模块开始过滤");
RequestContext ctx = RequestContext.getCurrentContext();
String requestURI = ctx.getRequest().getRequestURI();
try{
//是否在uri在模块中
if(AppUserHitEnum.compareUri(requestURI)){
DataModuleHits hits = new DataModuleHits();
AppUserHitEnum userHit = AppUserHitEnum.getUserHit(requestURI);
hits.setModuleCode(userHit.getModuleCode());
hits.setModuleName(userHit.getModuleName());
log.info("用户请求的模块,{},在统计列表中",userHit.getModuleName());
this.dataAppUserDistClient.add(hits);
}else{
log.info("App用户请求的uri不在统计列表中"+requestURI);
}
}catch (Exception e){
log.error(e.getMessage());
}finally {
return null;
}
}
如上代码所示,即使try块中的代码出现异常,也不会导致程序结束运行
2.不使用try块中变量值,但是像这种情况很少见
3.2.2return语句结束方法
在catch块中使用return语句表明方法有能力处理异常,一般分为两种情况
一种方法是没有返回值即void,可以直接结束用户请求
另一种方法是有返回值的,此时return出去的是一个默认值
总结来说,return在实际生产中不常用,因为此时用户并不知道异常存在,以为自己的数据没有问题,而导致数据错误。
3.2.3throw抛出异常
使用throw抛出异常即可以终止程序,又可以提示用户发生异常,所以在实际编程中使用throw自定义异常,一般是运行时异常,因为编译已经通过了,将异常抛出去
public UserToken getUserToken(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader(JwtConstans.CONTEXT_TOKEN);
UserToken userInfo=new UserToken();
try{
userInfo = (UserToken)redisUtil.get(token);
}catch (Exception e){
//抛出去没有权限的异常
throw new UnAuthException(401,"token获得用户为空");
}
return userInfo;
}
//自定义异常
public class UnAuthException extends RuntimeException {
private Integer code;
public UnAuthException(Integer code,String message) {
super(message);
this.code = code;
}
}
在上述的例子中在redis中取出token对应的用户信息可能会发生异常,比如redis环境突然变了,redis突然down了等等,所以加了catch语块处理,而实际编程中一般是使用throw加自定义异常,代替return的做法
3.3 throw关键字与throws关键字
先说下两者的区别,严格意义来说,这两者不是同一性质的东西,因为throw是抛出异常,而throws是处理异常的方式。有那么一点共同点是都是抛出异常
接下来要补充下对throw关键字的认识
-
1.throw关键字可以用在代码可能出现异常的任何地方
-
2.throw关键字是throw的Exception的一个实例,throw new RuntimeException是可以不处理的,而throw new Exception是需要处理异常的
下面举例子说明一下
throw 编译时异常
可以看出来throw编译时异常需要让程序员处理异常,要用到处理异常的两种方式,throws出去或者try…catch
throw 运行时异常如果是运行时异常则不是必须让程序员处理
四、方法执行过程
从上面可以看出来,异常是在方法执行中产生的,要总结异常处理的流程,首先要先了解下方法在JVM中是怎样执行的,如下图所示
虚拟机栈是描述java方法的执行过程的内存模型,它在当前栈帧(Stack Frame)中存储了局部变量表、操作数栈,动态链接,方法出口等信息。
同时栈帧用来存储部分运行时数据及其数据结构,处理动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch Exception).
栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈和出栈。
注意:无论方法是正常运行完成还是异常完成(抛出了在方法内未被捕获的异常),都视为方法运行结束
当一个方法执行完毕,Java虚拟机会从调用栈中弹出该方法的栈结构,然后继续处理前一个方法。
五、异常处理流程
下面以比较熟悉的Controller、Service、Mapper举个例子,说明一下方法调用及其异常处理的流程,如下图所示:
下面用文字描述下
如果在执行方法的过程中抛出异常,则Java虚拟机必须找到能捕获该异常的catch代码块。它首先查看当前方法是否存在这样的catch代码块,如果存在,那么就执行该catch代码块;否则,Java虚拟机会从调用栈中弹出该方法的栈结构,继续到前一个方法中查找合适的catch代码块。在回溯过程中,如果Java虚拟机在某个方法中找到了处理该异常的代码块,则该方法的栈结构将成为栈顶元素,程序流程将转到该方法的异常处理代码部分继续执行。
当Java虚拟机追溯到调用栈的底部的方法时,如果仍然没有找到处理该异常的代码块,按以下步骤处理。
(1)调用异常对象的printStackTrace()方法,打印来自方法调用栈的异常信息。
(2)如果该线程不是主线程,那么终止这个线程,其他线程继续正常运行。如果该线程是主线程(即方法调用栈的底部为main()方法),那么整个应用程序被终止。
六、总结精华
-
1.异常分为编译时异常和运行时异常,编译时异常必须程序员处理,运行时异常不一定
-
2.异常情况和throw都会终止程序进行,并抛出异常
创作不易,觉得有帮助的,来个三连吧