利用好异常捕获,再也不担心问题无从下手

在一个优秀的项目中一定少不了对程序流程良好的异常捕获与日志打印,通过二者结合可实现异常程序的快速定位,本片文章将详细介绍如何优雅的实现异常捕获与日志打印输出。

话不多说,下面我们就直奔主题开始介绍相关知识吧。

一、异常捕获

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>
相关推荐
m0_748254881 分钟前
Spring Boot实现多数据源连接和切换
spring boot·后端·oracle
吴冰_hogan7 分钟前
Java虚拟机(JVM)的类加载器与双亲委派机制
java·开发语言·jvm
程序员shen1616118 分钟前
注意⚠️:矩阵系统源码开发/SaaS矩阵系统开源/抖音矩阵开发优势和方向
java·大数据·数据库·python·php
青春男大31 分钟前
java队列--数据结构
java·开发语言·数据结构·学习·eclipse
yzhSWJ1 小时前
mybatisplu设置自动填充
java·spring·tomcat
Java雪荷1 小时前
基于 Vant UI + Redisson BitSet 实现签到日历
java·redis·vue
18号房客1 小时前
云原生后端开发(一)
后端·云原生
白宇横流学长1 小时前
基于Javaee的影视创作论坛的设计与实现【源码+文档+部署讲解】
java·java-ee
南宫生1 小时前
力扣-数据结构-4【算法学习day.75】
java·数据结构·学习·算法·leetcode
TANGLONG2222 小时前
【初阶数据结构与算法】八大排序算法之归并排序与非比较排序(计数排序)
java·数据结构·c++·算法·面试·蓝桥杯·排序算法