在一个优秀的项目中一定少不了对程序流程良好的异常捕获与日志打印,通过二者结合可实现异常程序的快速定位,本片文章将详细介绍如何优雅的实现异常捕获与日志打印输出。
话不多说,下面我们就直奔主题开始介绍相关知识吧。
一、异常捕获
1. 处理方式
程序异常是开发时不可避免的,很多时候我们需要针对不同异常进行不同的处理,最常用的就是try{} catch(){}
语句,这里主要说明一下两种常见异常处理的异同。
堆栈打印
当用 printStackTrace()
处理异常时,当程序出现异常将会在控制台打印异常信息,然后继续执行之后的代码。
java
public void Exception1Demo() {
try {
Integer.parseInt("abc");
} catch (Exception e) {
e.printStackTrace()
}
// 打印正常输出
System.out.println("This is will show.");
}
异常抛出
通过 throw new xxxException()
则会将异常信息根据调用层级逐层向上抛出,程序将在异常处中断,不会继续执行后续代码。
java
public void Exception2Demo() {
try {
Integer.parseInt("abc");
} catch (Exception e) {
throw new IllegalArgumentException();
}
// 打印不会被输出
System.out.println("This is will not show.");
}
2. 捕获示例
在上一点中介绍了两种捕获异常的处理方式,那在实际的程序开发中应该如何进行选择呢?
最常见的一种规范即底层异常永远向上抛出,由最顶层统一处理。如下示例中 demo()
调用了 task()
方法,相对而言 task()
更为底层因此其捕获异常时通过 throw
关键字向上抛出,而 demo()
为最顶层则可以通过 printStackTrace()
打印异常堆栈,当然也可以选择继续抛出由系统处理异常。
java
@Test
public void demo() {
try {
task();
} catch (Exception e) {
e.printStackTrace();
}
}
private void task() {
try {
Integer.parseInt("abc");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
但如果将上述示例的 task()
方法中捕获异常替换为 e.printStackTrace();
则 demo()
在调用 task()
时将无法捕获到程序异常,从而导致代码的执行顺序可能将与我们设想的有所偏差。
当然还有一种情景是需要执行批量操作,但是我们又想每一批之间可以互不影响,此时底层模块也可以选择不向上抛出异常。
稍微修改一下上述的 task()
方法为如下,这里通过 continue
跳过,替换为 e.printStackTrace()
效果一致。
java
private void task() {
for(int i = 0; i < 5; i++) {
try {
Integer.parseInt("abc");
} catch (Exception e) {
// 不抛出异常,继续下一循环
continue;
}
}
}
3. 自定义异常
自定义异常类十分简单,只需继承 RuntimeException
,并编写相应的构造方法即可,使用方法同上。
java
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);
}
}
4. 断言处理
在 JDK 1.4
中引入断言语法更简洁的实现异常抛出,在 try catch
中是无法预判异常环节从而用其实现捕获,断言则更多的用于条件判断。
即通过断言用于判断是否满足先决条件,若否则在该处抛出异常,若是则正常执行后续代码,下面通过示例说明。
assert
通过 assert
可实现更便捷的数据合法性验证,其基本语法如下,当表达式 <expression>
返回值为 false
时将抛出一个异常,通过 <message>
定义异常信息提示。
java
assert <expression> : <message>;
下面通过一个具体示例演示效果,两个示例的作用效果相同,除了抛出的异常类型不同。
java
public void AssertDemo() {
int y = -1;
assert y > 0 : "The value of y is lower then zero";
}
public void AssertDemo() {
int y = -1;
if(y < 0) {
throw new RuntimeException("The value of y is lower then zero");
}
}
Assert
更简洁的语法规则,效果同上,当捕获到异常后将中断程序,不会继续执行后续内容。
java
public void Assert2Demo() {
String msg = "";
// 打印异常,效果等同 printStackTrace()
Assert.hasLength(msg, "不允许为空");
System.out.println("1111");
}
二、日志监控
在开发时如果需要查看某一处代码信息时我们可以直接使用 println()
进行打印输出,但生成环境下控制台信息显然变得没有意义,此时我们就需要通过日志进行信息打印。
1. 日志打印
Java
提供原生日志工具类,导入包即可使用。
java
import java.util.logging.Logger;
public void LogDemo() {
Logger logger = Logger.getGlobal();
logger.info("start process...");
logger.warning("memory is running out...");
logger.fine("ignored.");
logger.severe("process will be terminated...");
}
2. Log4j框架
除了自带的日志框架,Log4j
是当下较为流行的日志插件,在项目工程中引入下列依赖,其中 slf4j
指的是日志规范。
xml
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.7</version>
</dependency>
其提供了一下两种初始化方式,区别在于使用 getClass()
定义的实例对象其子类仍可使用。
java
// 只能当前类可用
Logger logger = LoggerFactory.getLogger(LogTest.class);
// 当前类与其子类都可用
Logger logger = LoggerFactory.getLogger(getClass());
Slf4j
规范针对不同级别的日志提供不同的接口方法如:info()
、 warn()
、 debug()
、 error()
, 基本使用示例如下:
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest{
Logger logger = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
logger.info("info ...");
logger.warn("warn ...");
logger.debug("debug ...");
logger.error("error ...");
}
}
三、Logbak配置
1. 项目配置
在工程的 application.yml
添加 logging.config
用于指定日志配置文件路径。
yaml
# 日志配置文件
logging:
config: classpath:logback-spring.xml
2. 日志级别
这里单独介绍一下 logger
标签的作用,它可以用于控制指定包路径下的日志是否输出。
如配置了 <logger name="xyz.ibudai.slf4j.error" level="ERROR"/>
则 xyz.ibudai.slf4j.error
包路径下的 info, debug, warn
将不会出现在配置的输出日志文件中。
logger
标签中不同的level
配置输出的信息如下:
- INFO :输出
info, warn, error
级别日志。- DEBUG :输出
info, debug, warn, error
级别日志。- WARN :只输出
error
级别日志。- ERROR :只输出
error
级别日志。
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 指定包路径的日志输出级别, 过滤低级别日志 -->
<logger name="xyz.ibudai.slf4j.info" level="INFO"/>
<logger name="xyz.ibudai.slf4j.debug" level="DEBUG"/>
<logger name="xyz.ibudai.slf4j.warn" level="WARN"/>
<logger name="xyz.ibudai.slf4j.error" level="ERROR"/>
</configuration>
3. 配置格式
在 resources
目录下新建 logback-spring.xml
配置文件,常见标签参考下表。
标签 | 作用 |
---|---|
property | 定义全局变量,可通过 ${} 表达式获取。 |
appender | 搭配 appender-ref 可为不同级别日志设置文件输出配置。 |
logger | 用于控制指定包下文件的日志输出。 |
如下配置示例中即分别为 INFO, DEBUG, ERROR
三种日志级别配置了日志内容文件输出,具体作用参考备注信息。
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 设置日志存储路径 -->
<property name="LOG_HOME" value="./logs"/>
<!-- 指定基础的日志输出级别 -->
<root level="INFO">
<!-- appender 将会添加到这个 logger -->
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO"/>
<appender-ref ref="DEBUG"/>
<appender-ref ref="ERROR"/>
</root>
<!-- 控制台日志输出 -->
<!-- 设置彩色输出: -Dlog4j.skipJansi=false -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 设置输出格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!-- 格式化输出 -->
<!-- (1) %d: 表示日期 -->
<!-- (2) %thread: 表示线程名 -->
<!-- (3) %-5level: 日志级别, 从左显示 4 个字符宽度 -->
<!-- (4) %msg: 表示日志消息 -->
<!-- (5) %n: 表示换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-4level) %cyan(%logger{50}:%L) - %msg%n</pattern>
<!-- 设置编码 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 按照 INFO 每天生成日志文件 -->
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志名, 指定最新的文件名, 其他文件名使用 FileNamePattern -->
<file>${LOG_HOME}/info.log</file>
<!-- 文件滚动模式 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志输出的文件名 -->
<!-- (1) %i: 表示序号, 当日文件多份时用于区分 -->
<!-- (1) gz: 文件类型, 开启文件压缩 -->
<FileNamePattern>${LOG_HOME}/bak/info.log.%d{yyyy-MM-dd}.%i.log.gz</FileNamePattern>
<!-- 日志文件保留天数 -->
<MaxHistory>7</MaxHistory>
<!-- 按大小分割同一天的 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>128MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志级别过滤, 过滤低级别日志 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<!-- 日志内容输出格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 按照 DEBUG 每天生成日志文件 -->
<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_HOME}/debug.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/bak/info.%d{yyyy-MM-dd}.%i.log.gz</FileNamePattern>
<MaxHistory>7</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>128MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 按照 ERROR 每天生成日志文件 -->
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/bak/error.log.%d{yyyy-MM-dd}.%i.log.gz</FileNamePattern>
<MaxHistory>7</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>128MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- MyBatis log configure -->
<logger name="com.apache.ibatis" level="INFO"/>
<logger name="java.sql.Connection" level="ERROR"/>
<logger name="java.sql.Statement" level="ERROR"/>
<logger name="java.sql.PreparedStatement" level="ERROR"/>
</configuration>