Java异常日志详解


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

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

一、异常处理

1. 捕获方式

程序异常是开发时不可避免的,很多时候我们需要针对不同异常进行不同的处理,最常用的就是 try{} catch(){} 语句,这里主要说明一下两种常见异常处理的异同。

(1) 堆栈打印

当用 printStackTrace() 处理异常时,在程序出现异常时将会在控制台或日志文件中输出异常信息,然后继续执行之后的代码。

public void exceptionDemo1() {
    try {
        Integer.parseInt("abc");
    } catch (Exception e) {
        e.printStackTrace()
    }

    // 打印正常输出
    System.out.println("This is will show.");
}
(2) 异常抛出

通过 throw new xxxException() 则会将异常信息根据调用层级逐层向上抛出,程序将在异常处中断,不会继续执行后续代码。

public void exceptionDemo2() {
    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() 打印异常堆栈,当然也可以选择继续抛出由系统处理异常。

@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() 效果一致。

private void task() {
    for(int i = 0; i < 5; i++) {
        try {
            Integer.parseInt("abc");
        } catch (Exception e) {
            // 不抛出异常,继续下一循环
            continue;
        }
    }
}

3. 自定义异常

自定义异常类十分简单,只需继承 RuntimeException ,并编写相应的构造方法即可,使用方法同上。

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);
    }
}

二、异常断言

JDK 1.4 中引入断言语法更简洁的实现异常抛出,在 try catch 中是无法预判异常环节从而用其实现捕获,断言则更多的用于条件判断。

即通过断言用于判断是否满足先决条件,若否则在该处抛出异常,若是则正常执行后续代码,下面通过示例说明。

1. assert

通过 assert 可实现更便捷的数据合法性验证,其基本语法如下,当表达式 <expression> 返回值为 false 时将抛出一个异常,通过 <message> 定义异常信息提示。

assert <expression> : <message>;

下面通过一个具体示例演示效果,两个示例的作用效果相同,除了抛出的异常类型不同。

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");
    }
}

2. Assert

更简洁的语法规则,效果同上,当捕获到异常后将中断程序,不会继续执行后续内容。

public void Assert2Demo() {
    String msg = "";

    // 打印异常,效果等同 printStackTrace()
    Assert.hasLength(msg, "不允许为空");

    System.out.println("1111");
}

三、日志监控

在开发时如果需要查看某一处代码信息时我们可以直接使用 println() 进行打印输出,但生成环境下控制台信息显然变得没有意义,此时我们就需要通过日志进行信息打印。

1. 日志打印

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 指的是日志规范。

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>2.0.7</version>
</dependency>

其提供了一下两种初始化方式,区别在于使用 getClass() 定义的实例对象其子类仍可使用。

// 只能当前类可用
Logger logger = LoggerFactory.getLogger(LogTest.class);
// 当前类与其子类都可用
Logger logger = LoggerFactory.getLogger(getClass());

Slf4j 规范针对不同级别的日志提供不同的接口方法如:info()warn()debug()error(), 基本使用示例如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogTest {
        
    private static final 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 用于指定日志配置文件路径。

# 日志配置文件
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 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. 开关配置

除了通过上述配置文件设置外,在 Spring 工程中可通过下述配置指定类的日志级别。

logging:
  level: 
    xyz.ibudai.MyTest: DEBUG

在配置上述后,在工程中即可通过 isDebugEnabled() 实现更便捷的日志调试。

package xyz.ibudai;

public class MyTest {

    private static final Logger logger = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {
        if (logger.isDebugEnabled()) {
            logger.info("info ...");
        }
    }
}

4. 配置格式

resources 目录下新建 logback-spring.xml 配置文件,常见标签参考下表。

标签 作用
property 定义全局变量,可通过 ${} 表达式获取。
appender 搭配 appender-ref 可为不同级别日志设置文件输出配置。
logger 用于控制指定包下文件的日志输出。

如下配置示例中即分别为 INFO, DEBUG, ERROR 三种日志级别配置了日志内容文件输出,具体作用参考备注信息。

<?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}  %highlight(%5level) --- [%16thread] %cyan(%-50logger{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}  %5level --- [%14thread] %-50logger{50} %4L : %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}  %5level --- [%14thread] %-50logger{50} %4L : %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}  %5level --- [%14thread] %-50logger{50} %4L : %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>

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录