本篇博文目录:
前言1.Java的异常(1) Java的异常体系结构(2) Java异常捕获规定
2.捕获异常(1) 异常传递(2) 异常捕获
3.抛出异常(1) 抛出异常(2) 保留原有异常(3) 被屏蔽异常
4.自定义异常5.空指针异常问题(1) 空指针异常(NPE)(2) 处理空指针异常(3) 定位NullPointerException
6.全局异常7.使用断言8.日志(1) 内置日志logging(2) Commons Logging和Log4j(3) SLF4J和Logback
前言
本篇博文主要对廖雪峰大神的博客Java异常部分的知识进行学习:https://www.liaoxuefeng.com/wiki/1252599548343744/1255943543190176
1.Java的异常
(1) Java的异常体系结构
Java内置了一套异常处理机制,总是使用异常来表示错误,在 Java 中,通过 Throwable 及其子类来描述各种不同类型的异常,Java异常体系结构图如下:
从继承关系可知:Throwable是异常体系的根,它继承自Object。Throwable有两个体系:Error和Exception,Error表示严重的错误,程序对此一般无能为力,例如:
OutOfMemoryError:内存耗尽NoClassDefFoundError:无法加载某个ClassStackOverflowError:栈溢出
而Exception则是运行时的错误,它可以被捕获并处理,Exception又分为两大类:
运行时异常(RuntimeException以及它的子类)非运行时异常(包括IOException、ReflectiveOperationException等等)
(2) Java异常捕获规定
Java关于异常是否被捕获的规定:
必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。
某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
NumberFormatException:数值类型的格式错误FileNotFoundException:未找到文件SocketException:读取网络失败
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
NullPointerException:对某个null的对象调用方法或字段IndexOutOfBoundsException:数组索引越界
2.捕获异常
(1) 异常传递
在最底层的代码如果有异常我们可以不做捕获,而是将异常抛出交给上一层进行处理,直到程序的最顶层再对异常做捕获。
例子:
java.io.UnsupportedEncodingException异常就是一个必须进行处理的异常,编译器在未编译时就已经报错了。
运行效果:
通过getBytes(String charsetName)的源码可知java.io.UnsupportedEncodingException异常是由getBytes()方法抛出( 抛出的意思就是在本层不做处理,交给上一层进行处理 ),当然在这里我们也可以不做处理,继续将异常交给上一层, 这里的上一层为main方法
本层不做处理继续交给上一层进行处理:
这时候toGBK(String s) 就没有报错了,但错误出现在了main方法中的 toGBK(“中文”)。
main方法中捕获toGBK(“中文”)抛出的异常:
调用printStackTrace()方法打印异常栈,这是一个简单有用的快速打印异常的方法。
main方法中不捕获toGBK(“中文”)抛出的异常:
其实我们在mian方法中也可以不进行捕获,而是进行抛出,但是这种方式异常在程序内部我们无法获取,并且发生异常后后面的代码不会执行,所以这种方式非常的不好,尽量避免。
(2) 异常捕获
单个catch语句
捕获异常使用try…catch语句,把可能发生异常的代码放到try {…}中,然后使用catch捕获对应的Exception及其子类:
例子:
运行效果:
成功捕获异常,并做了处理,执行了System.out.println(Arrays.toString(bs));
如果不做处理,将异常抛出: 运行效果:
异常抛出,没有捕获异常,不执行System.out.println(Arrays.toString(bs));
多个catch语句
可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。
例子:
注意catch中的异常顺序非常重要,应该把最小的子类放在前面,把父类放在后面,不然子类的异常永远也不会捕获到: finally语句
无论是否有异常发生,如果我们都希望执行一些语句,例如清理工作等,我们可以把代码放在finall语句中来做善后工作,因为finally语句最后执行。
例子: 运行效果:
注意:finall需要放在 try()catch{} 后面
3.抛出异常
(1) 抛出异常
当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常,抛出异常只需要二步,第一步创建异常对象,第二步使用throw语句抛出。
常常这二步都是合起来使用,如下图Integer的parseInt(String s, int radix)方法的源码也是这样处理的:
(2) 保留原有异常
当一个异常捕获到上一个异常时,如果抛出一个新的异常,对于上一个异常信息就会丢失:
运行效果:
引起异常的原因是由str = null 触发的NullPointerException异常,但是控制台输出的异常定位却是process1中的IllegalArgumentException异常,原始异常消失了。
为了能追踪到完整的异常栈,在构造异常的时候,把原始的Exception实例传进去,新的Exception就可以持有原始Exception信息:
运行效果( 原始异常任然存在 ):
(3) 被屏蔽异常
当finally语句抛出异常时,catch语句的异常将会被屏蔽,没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)。
例子:
运行效果( throw new RuntimeException(e)异常并没有被抛出 ):
我们可以通过Throwable.addSuppressed()方法保存catch中的异常信息,具体操作如下:
运行效果:
备注:上面的方式虽然可以保留cach中的异常,但是尽量不要在finally语句抛出异常。
4.自定义异常
在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。 一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。 BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生:
public class BaseException extends RuntimeException {
}
其他业务类型的异常就可以从BaseException派生:
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
...
自定义的BaseException应该提供多个构造方法:
public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}
下面的代码来源于github的开源项目mall中的自定义异常ApiException:
为什么自定义异常继承RuntimeException而不是其他异常呢?
在前文中说过运行时异常不需要强制捕获,所以继承RuntimeException可以避免强制try catch。
5.空指针异常问题
(1) 空指针异常(NPE)
NullPointerException即空指针异常,俗称NPE,是一个比较常见的运行时异常。如果一个对象为null,调用其方法或访问其字段就会产生NullPointerException,这个异常通常是由JVM抛出的,例如:
例子:
运行效果:
(2) 处理空指针异常
如果遇到NullPointerException,我们应该如何处理?首先,必须明确,NullPointerException是一种代码逻辑错误,遇到NullPointerException,遵循原则是早暴露,早修复,严禁使用catch来隐藏这种编码错误:
错误示范:
// 错误示例: 捕获NullPointerException
try {
transferMoney(from, to, amount);
} catch (NullPointerException e) {
}
好的编码习惯可以极大地降低NullPointerException的产生,例如:
成员变量在定义时初始化:
public class Person {
private String name = "";
}
使用空字符串 " " 而不是默认的null可避免很多NullPointerException,编写业务逻辑时,用空字符串 " " 表示未填写比null安全得多。返回空字符串""、空数组而不是null:
public String[] readLinesFromFile(String file) {
if (getFileSize(file) == 0) {
// 返回空数组而不是null:
return new String[0];
}
...
}
如果调用方一定要根据null判断,比如返回null表示文件不存在,那么考虑返回 Optional
public Optional
if (!fileExist(file)) {
return Optional.empty();
}
...
}
这样调用方必须通过Optional.isPresent()判断是否有结果。
(3) 定位NullPointerException
通过打印变量,判断是否为null进行定位
如果产生了NullPointerException,例如,调用a.b.c.x()时产生了NullPointerException,原因可能是:
a是null;a.b是null;a.b.c是null;
例子:
public class test {
public static void main(String[] args) {
Person p = new Person();
System.out.println(p.address.city.toLowerCase()); //空指针异常
System.out.println(p.name[0].toLowerCase()); //空指针异常
}
}
class Person {
String[] name = new String[2];
Address address = new Address();
}
class Address {
String city;
String street;
String zipcode;
}
如何判断呢:
p.address.city.toLowerCase()发生空指针异常,打印p.address.city的值判断是否为null
运行效果(定位到了,引发空指针的原因是p.address.city =null):
对p.address.city = “重庆”;进行初始化,p.address.city.toLowerCase()的空指针问题得到解决
但是还是存在空指针异常,是由p.name[0].toLowerCase()引发的异常:
同样的方式打印p.name[0]的值看是否为null:
运行效果(定位到了,引发空指针的原因是p.name[0] =null):
解决方法对 p.name[0] = “张三”;进行初始化操作,如下:
再次运行( 问题解决 ):
通过JVM定位空指针异常
从Java14开始,如果产生了NullPointerException,JVM可以给出详细的信息告诉我们null对象到底是谁。
public class Main {
public static void main(String[] args) {
Person p = new Person();
System.out.println(p.address.city.toLowerCase());
}
}
class Person {
String[] name = new String[2];
Address address = new Address();
}
class Address {
String city;
String street;
String zipcode;
}
可以在NullPointerException的详细信息中看到类似… because ".address.city"is null,意思是city字段为null,这样我们就能快速定位问题所在。这种增强的NullPointerException详细信息是Java 14新增的功能,但默认是关闭的,我们可以给JVM添加一个-XX:+ShowCodeDetailsInExceptionMessages参数启用它:
java -XX:+ShowCodeDetailsInExceptionMessages Main.java
备注:上面的方式未作验证!
6.全局异常
在开发过程中,不管是Dao、Servie、Controller,层都有可能发生异常,对于异常处理,通常是try-catch或者直接throw,这会让try-catch的代码在代码中任意出现,系统的代码耦合度高,代码不美观,统一异常处理(全局异常)可以美化代码,简化业务逻辑,异常标准化,用户友好返回,下面主要通过开源项目mall学习一下全局异常处理方式。
在这个项目中主要写了三个异常类,一个自定义的APi异常,一个断言异常和全局异常处理。 自定义ApiException异常( 这里的自定义异常和前面提到的自定义创建方式基本一致 ):
断言异常:
全局异常处理:
@ControllerAdvice 注解表示开启全局异常处理,使用该注解表示开启了全局异常的捕获。@ExceptionHandler注解表示定义捕获异常的类型,即可对这些捕获的异常进行统一的处理。@ResponseBody注解表示将java对象转为json格式的数据返回到前端。
上文中的 @ExceptionHandler(value = ApiException.class)表示对ApiException.class引发的异常做处理, @ExceptionHandler(value = MethodArgumentNotValidException.class)表示对MethodArgumentNotValidException.class引发的异常做处理, @ExceptionHandler(value = BindException.class)表示对BindException.class引发的异常做处理。 其中ApiException是自定义的Api异常,后面二种异常你可以通过这篇博文进行学习:BindException、ConstraintViolationException、MethodArgumentNotValidException入参验证异常分析和全局异常处理解决方法
7.使用断言
断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。
断言是一种调试方式,断言失败会抛出AssertionError,只能在开发和测试阶段启用断言;对可恢复的错误不能使用断言,而应该抛出异常;断言很少被使用,更好的方法是编写单元测试。
例子:
使用assert语句时,还可以添加一个可选的断言消息在判断语句后面加上 : "断言消息"
因为JVM默认关闭断言指令,即遇到assert语句就自动忽略了,不执行,要执行assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)参数启用断言,还可以有选择地对特定地类启用断言,命令行参数是:-ea:com.itranswarp.sample.Main,表示只对com.itranswarp.sample.Main这个类启用断言,或者对特定地包启用断言,命令行参数是:-ea:com.itranswarp.sample…(注意结尾有3个.),表示对com.itranswarp.sample这个包启动断言。
设置步骤如下:
运行效果:
8.日志
在写程序的时候,有时我们常常使用 System.out.println(x); 语句来打印一些中间变量,观察程序执行情况但是这种方式比较不好的一点就是每次测试完后需要手动删除不然上线的时候会影响程序执行效率,其实Java标准库内置了日志包 java.util.logging 给我们提供了相关的日志手段来解决这个问题,目前主流的二种方式分别为 Commons Logging和Log4j 和 SLF4J 和 Logback。
(1) 内置日志logging
Java标准库内置了日志包java.util.logging,我们可以直接使用, 并且JDK的Logging定义了7个日志级别(java.util.logging.Level),从严重到普通如下(默认日志级别为INFO),相对应的根据日志级别提供了相对应的方法供我们使用:
SEVERE(最高值)WARNINGINFO (默认)CONFIGFINEFINERFINEST(最低值)
java.util.logging包里的结构如下:
下面主要是用Logger对象输出特定的日志消息:
例子:
运行效果:
输出1,2,4日志级别的内容而没有输出3日志级别的内容,这是因为默认日志级别为INFO,所以INFO级别以下的日志方法是不输出的,需要进行相应的操作。
操作如下:
找到Java的JRE文件夹lib目录下的 logging.properties
默认级别为INFO:
修改.level=ALL显示所有日志级别:
除了设置上面7种级别外,还有一个级别 OFF,可用来关闭日志记录,使用级别 ALL 启用所有消息的日志记录。
修改java.util.logging.ConsoleHandler.level的级别为ALL,显示所有级别的信息
在项目的启动JVM的参数中设置日志配置文件的位置:
设置VM的参数: -Djava.util.logging.config.file=D:\java\jre1.8\lib\logging.properties
再次运行:
备注:Java内置的日志logging使用起来并不是一件简单的事情,操作非常的不方便,该方式使用并不广泛。
(2) Commons Logging和Log4j
Commons Logging的使用
和Java标准库提供的日志不同,Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
操作如下:
导入Commons Logging的依赖:
Maven项目方式中导入依赖:
Gradle 项目方式导入依赖:
implementation("commons-logging:commons-logging:1.2")
原生Java程序导入jar包
如果是原生Java程序需要下载commons-logging-1.2.jar的jar,下载地址:https://commons.apache.org/proper/commons-logging/download_logging.cgi
这里我采用Maven方式导入依赖:
接下来开始使用Commons Logging,使用只需要和两个类打交道,并且只有两步: 第一步,通过LogFactory获取Log类的实例; 第二步,使用Log实例的方法打日志,实例代码如下:
例子:
注意上面的log设置为静态常量是因为在静态方法中,并且getLog里的参数为xxx.class这种方式( 无法设置为getclass(),因为静态变量中无法使用this关键字 ),在一些实例方法中我们常常使用非静态变量,并且在getLog里的参数设置为getClass(),这样在子类中也可以使用日志方法,如下:
运行效果:
Commons Logging定义了6个日志级别:
FATAL (最高)ERRORWARNINGINFO (默认)DEBUGTRACE (最低)
例子:
Commons Logging是一种日志接口,如果没有找到Log4j,Commons Logging还是会使用JDK Logging
运行效果:
修改Log的日志级别在JDK Logging中怎么操作的在这里就可以怎么操作。
设置Vm的参数,设置日志级别为ALL:
再次运行:
此外,Commons Logging的日志方法,例如info(),除了标准的info(String)外,还提供了一个非常有用的重载方法:info(String, Throwable),这使得记录异常更加简单:
try {
...
} catch (Exception e) {
log.error("got exception!", e);
}
Log4j的使用
Log4j是一种非常流行的日志框架,是一个组件化设计的日志系统,它的架构大致如下:
当我们使用Log4j输出一条日志时,Log4j自动通过不同的Appender把同一条日志输出到不同的目的地。例如:
console:输出到屏幕;file:输出到文件;socket:通过网络输出到远程计算机;jdbc:输出到数据库
在输出日志的过程中,通过Filter来过滤哪些log需要被输出,哪些log不需要被输出。例如,仅输出ERROR级别的日志。 最后,通过Layout来格式化日志信息,例如,自动添加日期、时间、方法名称等信息。 上述结构虽然复杂,但我们在实际使用的时候,并不需要关心Log4j的API,而是通过配置文件来配置它。 以XML配置为例,使用Log4j的时候,我们把一个log4j2.xml的文件放到classpath下就可以让Log4j读取配置文件并按照我们的配置来输出日志。下面是一个配置文件的例子:
关于classpath路径问题你可以通过这篇博文了解:https://blog.csdn.net/qq_33393542/article/details/80322141,在项目中我们只需要把配置文件放在resources中即可。
操作:
Maven导入相关依赖:
导入的依赖情况如下(不要忘了commons-logging):
备注:Jar包方式导入,点击下载jar包https://logging.apache.org/log4j/2.x/download.html
编写log4j的配置文件,文件名为log4j2.xml:
虽然配置Log4j比较繁琐,但一旦配置完成,使用起来就非常方便。对上面的配置文件,凡是INFO级别的日志,会自动输出到屏幕,而ERROR级别的日志,不但会输出到屏幕,还会同时输出到文件。并且,一旦日志文件达到指定大小(1MB),Log4j就会自动切割新的日志文件,并最多保留10份。
还是前面的代码:
运行效果:
日志的文件确实生成了,如下: log4j和log4j2有什么区别
log4j是Apache的一个开源项目,log4j2和log4j是同一个作者,只不过log4j2是重新架构的一款日志组件,他抛弃了之前log4j的不足,以及吸取了优秀的logback的设计重新推出的一款新组件。log4j2的社区活跃很频繁而且更新的也很快,更加详细的信息请参照这篇博文:Log4j和Log4j2的区别。
(3) SLF4J和Logback
其实SLF4J类似于Commons Logging,也是一个日志接口,而Logback类似于Log4j,是一个日志的实现。为什么有了Commons Logging和Log4j,又会蹦出来SLF4J和Logback?这是因为Java有着非常悠久的开源历史,不但OpenJDK本身是开源的,而且我们用到的第三方库,几乎全部都是开源的。开源生态丰富的一个特定就是,同一个功能,可以找到若干种互相竞争的开源库。因为对Commons Logging的接口不满意,有人就搞了SLF4J。因为对Log4j的性能不满意,有人就搞了Logback。
SLF4J和Logback与Commons Logging和Log4j的不同
我们先来看看SLF4J对Commons Logging的接口有何改进。在Commons Logging中,我们要打印日志,有时候得这么写:
int score = 99;
p.setScore(score);
log.info("Set score " + score + " for Person " + p.getName() + " ok.");
拼字符串是一个非常麻烦的事情,所以SLF4J的日志接口改进成这样了:
int score = 99;
p.setScore(score);
logger.info("Set score {} for Person {} ok.", score, p.getName());
我们靠猜也能猜出来,SLF4J的日志接口传入的是一个带占位符的字符串,用后面的变量自动替换占位符,所以看起来更加自然。
SLF4J和Logback的使用
SLF4J和Logback的使用和Commons Logging和Log4j差不多。
SLF4J的接口实际上和Commons Logging几乎一模一样:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Main {
final Logger logger = LoggerFactory.getLogger(getClass());
}
对比一下Commons Logging和SLF4J的接口:
Commons LoggingSLF4Jorg.apache.commons.logging.Logorg.slf4j.Loggerorg.apache.commons.logging.LogFactoryorg.slf4j.LoggerFactory
不同之处就是Log变成了Logger,LogFactory变成了LoggerFactory。
操作: Maven项目中导入相关依赖:
编辑logback.xml配置文件放入到classpath,这里我放在resources文件下:
测试代码:
运行效果:
相关链接
发表评论