在Java开发的日常工作中,我们总会不可避免地遇到各种"意外状况"------空指针调用、文件找不到、数据库连接中断、请求参数非法等等。这些运行时的非正常情况,如果处理不当,轻则导致程序功能异常,重则直接造成程序崩溃,给业务带来不可预估的损失。
Java提供了一套完整的异常处理机制,将异常封装为对象,通过标准化流程实现异常的捕获、处理和传递。而在实际项目开发中,我们更多基于SpringBoot框架进行开发,异常处理的方式也从原生Java的局部捕获,升级为全局统一处理。
一、Java异常基础知识
1. 什么是异常?
Java中的异常(Exception) 是指程序运行期间 发生的非正常情况,它会中断程序的正常执行流程,导致程序无法按照预期完成任务。
异常与编译错误的核心区别:
| 对比项 | 异常 | 编译错误 |
|---|---|---|
| 出现阶段 | 程序运行时 | 程序编译阶段 |
| 产生原因 | 数据非法、资源缺失、逻辑漏洞等 | 语法错误、类/方法未找到、依赖缺失等 |
| 处理方式 | 运行时捕获、处理、降级 | 编译前修复错误,无法生成字节码 |
2. Java异常体系结构
Java异常体系的根类是java.lang.Throwable,所有异常和错误都继承自这个类,其核心分支分为两大块:Error(错误)和Exception(异常)。
Throwable(根类)
├─ Error(JVM级严重问题,无需手动处理)
│ ├─ VirtualMachineError(虚拟机错误)
│ │ ├─ OutOfMemoryError(OOM,内存溢出)
│ │ └─ StackOverflowError(栈溢出,递归过深)
│ └─ LinkageError(链接错误)
│ └─ NoClassDefFoundError(类未找到,编译存在运行缺失)
└─ Exception(程序级问题,可手动处理)
├─ 受检异常(Checked Exception,编译强制检查)
│ ├─ IOException(IO操作异常)
│ │ └─ FileNotFoundException(文件未找到异常)
│ └─ SQLException(数据库操作异常)
└─ 非受检异常(Unchecked Exception,运行时检查)
└─ RuntimeException(运行时异常根类)
├─ NullPointerException(NPE,空指针异常)
├─ ArrayIndexOutOfBoundsException(数组下标越界异常)
├─ ClassCastException(类型转换异常)
├─ ArithmeticException(算术异常,如除数为0)
└─ IllegalArgumentException(非法参数异常)
2.1 三大核心分类详解
- Error(错误)
- 定义:属于JVM级别的严重问题,超出了程序本身的处理能力,JVM无法恢复正常运行状态。
- 处理原则:无需捕获,也无法有效处理,开发人员能做的只是提前预防(如优化内存避免OOM、控制递归深度避免栈溢出)。
- 常见示例:
OutOfMemoryError(内存不足)、StackOverflowError(栈溢出)。
- 受检异常(Checked Exception)
- 定义:又称编译时异常,在编译阶段就会被编译器强制检查,不处理则无法通过编译。
- 产生原因:通常是程序外部因素导致的可预见异常,与程序逻辑无关(如文件不存在、数据库连接失败)。
- 处理原则:必须强制处理 ,二选一:①
try-catch捕获并处理;throws声明,将处理责任交给调用者。
- 常见示例:
FileNotFoundException、IOException、SQLException。
- 非受检异常(
Unchecked Exception)
- 定义:又称运行时异常(
RuntimeException及其子类),编译阶段编译器不做强制检查,程序可正常编译,运行时才可能触发。 - 产生原因:通常是程序内部逻辑错误导致的,开发人员可通过代码优化避免。
- 处理原则:不建议优先捕获处理 ,最优解是通过前置校验避免异常发生;若需提供友好提示,可选择
try-catch捕获。 - 常见示例:
NullPointerException、ArithmeticException、IllegalArgumentException。
3. 异常处理核心关键字(5个)
Java提供了5个核心关键字,实现异常的捕获、处理和主动抛出,是异常处理的基础。
| 关键字 | 作用 | 使用场景 |
|---|---|---|
try |
包裹可能抛出异常的代码块 | 标记需要监控异常的代码区域 |
catch |
捕获try块中抛出的指定类型异常 |
对捕获的异常进行针对性处理 |
finally |
无论是否发生异常,都会执行的代码块 | 释放资源(文件流、数据库连接等) |
throw |
方法内部主动抛出单个异常对象 | 业务校验失败时,主动触发异常 |
throws |
方法声明处,声明可能抛出的异常类型 | 不处理异常,将责任转移给调用者 |
核心组合说明
try-catch:最基础的异常处理组合,用于捕获并处理异常。try-catch-finally:捕获异常+释放资源,日常资源操作必备。throw+throws:主动抛出异常+声明异常,业务校验必备。- Java 7+ 新增特性:
多异常捕获(|分隔)、try-with-resources(自动关闭资源),简化异常处理代码。
二、Java异常日常使用示例
日常开发中,异常处理主要用于资源操作、业务校验、数据转换等场景。
示例1:try-catch-finally - 手动关闭文件流(经典资源操作)
适用于IO操作、数据库连接等需要手动释放资源的场景,
finally块保证资源无论是否异常都会被关闭。
java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
/**
* 日常示例:读取本地文件,手动关闭流避免资源泄露
*/
public class TryCatchFinallyDailyDemo {
public static void main(String[] args) {
// 声明资源变量,便于finally块中访问
BufferedReader br = null;
try {
// 初始化资源,可能抛出FileNotFoundException(受检异常)
br = new BufferedReader(new FileReader("daily_demo.txt"));
String line;
// 读取文件内容,可能抛出IOException(受检异常)
while ((line = br.readLine()) != null) {
System.out.println("文件内容:" + line);
}
} catch (IOException e) {
// 针对性处理IO异常,提供清晰提示
System.out.println("【日常异常】文件读取失败,请检查文件路径和权限");
e.printStackTrace();
} finally {
// 最终关闭资源,避免资源泄露(核心)
if (br != null) {
try {
// 关闭流可能抛出IOException,内部捕获不覆盖原始异常
br.close();
System.out.println("【资源释放】文件流已成功关闭");
} catch (IOException e) {
System.out.println("【日常异常】文件流关闭失败");
e.printStackTrace();
}
}
}
}
}
示例2:try-with-resources - 自动关闭资源(Java 7+ 简化版)
无需手动在
finally块中关闭资源,JVM自动关闭实现了AutoCloseable接口的资源,代码更简洁,日常IO操作首选。
java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
/**
* 日常示例:使用try-with-resources自动关闭文件流,简化代码
*/
public class TryWithResourcesDailyDemo {
public static void main(String[] args) {
// 资源声明在try括号中,JVM自动关闭,无需finally块
try (BufferedReader br = new BufferedReader(new FileReader("daily_demo.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println("文件内容:" + line);
}
} catch (IOException e) {
System.out.println("【日常异常】文件读取失败,请检查文件路径和权限");
e.printStackTrace();
}
}
}
示例3:throw+throws - 业务参数校验(主动抛出异常)
适用于用户注册、订单创建等业务场景,参数校验失败时主动抛出异常,提供清晰的业务错误提示。
java
import java.util.regex.Pattern;
/**
* 日常示例:用户注册业务,校验手机号和密码格式并主动抛出异常
*/
public class ThrowThrowsDailyDemo {
/**
* 用户注册方法
* @param phone 手机号
* @param password 密码
* @throws IllegalArgumentException 参数非法时抛出异常
*/
public static void userRegister(String phone, String password) throws IllegalArgumentException {
// 手机号校验(11位数字)
if (phone == null || !Pattern.matches("^1[3-9]\\d{9}$", phone)) {
// 主动抛出异常,附带关键参数,便于排查
throw new IllegalArgumentException("用户注册失败:手机号格式非法(当前:" + phone + ")");
}
// 密码校验(6-16位,包含字母和数字)
if (password == null || !Pattern.matches("^(?=.*[a-zA-Z])(?=.*\\d).{6,16}$", password)) {
throw new IllegalArgumentException("用户注册失败:密码需6-16位且包含字母和数字");
}
// 校验通过,执行注册逻辑
System.out.println("用户注册成功:手机号" + phone);
}
public static void main(String[] args) {
// 测试非法手机号
try {
userRegister("1380013800", "Abc123456");
} catch (IllegalArgumentException e) {
System.out.println("【捕获异常】" + e.getMessage());
}
// 测试合法参数
try {
userRegister("13800138000", "Abc123456");
} catch (IllegalArgumentException e) {
System.out.println("【捕获异常】" + e.getMessage());
}
}
}
示例4:自定义业务异常 - 标准化业务错误(项目必备)
Java内置异常无法满足业务场景的个性化需求(如错误码、业务模块标识),自定义异常可以实现业务异常的标准化,便于前后端协同。
java
/**
* 日常开发通用:自定义业务异常(继承RuntimeException,非受检异常,使用灵活)
*/
public class BusinessException extends RuntimeException {
// 业务错误码(前端可根据错误码做针对性处理)
private int errorCode;
// 构造方法1:仅错误信息(日常使用最多)
public BusinessException(String message) {
super(message);
}
// 构造方法2:错误信息+错误码(前后端分离必备)
public BusinessException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
// 构造方法3:错误信息+错误码+根因异常(保留异常链,便于排查)
public BusinessException(String message, int errorCode, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
// getter/setter
public int getErrorCode() {
return errorCode;
}
public void setErrorCode(int errorCode) {
this.errorCode = errorCode;
}
}
自定义异常使用场景(订单创建)
java
import java.math.BigDecimal;
/**
* 日常示例:订单创建业务,使用自定义异常
*/
public class CustomExceptionDailyDemo {
public static void createOrder(String productId, BigDecimal amount) {
// 商品ID校验
if (productId == null || productId.trim().isEmpty()) {
throw new BusinessException("订单创建失败:商品ID不能为空", 10001);
}
// 订单金额校验
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("订单创建失败:金额必须大于0", 10002);
}
// 执行订单创建逻辑
System.out.println("订单创建成功:商品ID=" + productId + ",金额=" + amount);
}
public static void main(String[] args) {
try {
createOrder("PROD_1001", new BigDecimal("-99.9"));
} catch (BusinessException e) {
System.out.println("【捕获业务异常】错误码:" + e.getErrorCode() + ",错误信息:" + e.getMessage());
}
}
}
示例5:多异常捕获 - 统一处理多种异常(批量数据处理)
当多种异常的处理逻辑一致时,使用|分隔多个异常类型,简化代码,避免冗余的catch块。
java
import java.util.Arrays;
import java.util.List;
/**
* 日常示例:批量转换数据类型,多异常统一捕获处理
*/
public class MultiExceptionCatchDailyDemo {
public static void main(String[] args) {
List<String> numStrList = Arrays.asList("100", "200a", null, "300");
for (String numStr : numStrList) {
try {
// 可能抛出:NullPointerException、NumberFormatException
int num = Integer.parseInt(numStr);
System.out.println("转换成功:" + numStr + " → " + num);
} catch (NullPointerException | NumberFormatException e) {
// 统一处理两种异常,简化代码
System.out.println("【日常异常】数据转换失败:" + numStr + ",异常类型:" + e.getClass().getSimpleName());
}
}
}
}
三、SpringBoot中的异常处理
在SpringBoot项目中,原生Java的
try-catch虽然可用,但对于全局统一的异常处理(如接口返回格式标准化、避免重复捕获),推荐使用全局异常处理器 (@RestControllerAdvice+@ExceptionHandler),搭配自定义异常和统一返回结果,形成完整的异常处理体系。
前置准备:SpringBoot项目环境搭建
- 核心依赖(
pom.xml):
xml
<!-- SpringBoot Web核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot 参数校验依赖(可选,用于参数校验异常演示) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok 简化代码(可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- 统一返回结果类(前后端分离必备,标准化异常返回格式):
java
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* SpringBoot项目:统一接口返回结果
* @param <T> 数据泛型
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL) // 忽略null值字段,简化返回结果
public class Result<T> {
// 响应码:200成功,400参数错误,500系统异常,其他业务异常
private int code;
// 响应消息
private String msg;
// 响应数据
private T data;
// 静态方法:成功返回(无数据)
public static <T> Result<T> success() {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
return result;
}
// 静态方法:成功返回(带数据)
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
return result;
}
// 静态方法:失败返回(带错误码和错误信息)
public static <T> Result<T> fail(int code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
}
实战1:自定义业务异常在SpringBoot中的使用(业务层)
SpringBoot中业务层抛出异常的方式与原生Java一致,无需额外修改,自定义异常可直接在Service层抛出。
java
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* SpringBoot实战:订单业务Service
*/
@Service
public class OrderService {
/**
* 创建订单(业务方法)
*/
public void createOrder(String productId, BigDecimal amount) {
// 商品ID校验
if (productId == null || productId.trim().isEmpty()) {
throw new BusinessException("订单创建失败:商品ID不能为空", 10001);
}
// 订单金额校验
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("订单创建失败:金额必须大于0", 10002);
}
// 模拟订单创建逻辑
System.out.println("订单创建成功:商品ID=" + productId + ",金额=" + amount);
}
}
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
/**
* SpringBoot实战:订单接口Controller
*/
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单接口
*/
@PostMapping("/create")
public Result<Void> createOrder(@RequestParam String productId, @RequestParam BigDecimal amount) {
// 业务层抛出异常,此处无需try-catch,交给全局异常处理器处理
orderService.createOrder(productId, amount);
return Result.success();
}
}
实战2:全局异常处理器(核心:@RestControllerAdvice + @ExceptionHandler)
全局异常处理器的核心作用是统一捕获项目中所有未处理的异常,标准化返回结果,避免将杂乱的异常堆栈返回给前端 ,同时减少重复的
try-catch代码。
java
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* SpringBoot实战:全局异常处理器
* 注解说明:
* 1. @RestControllerAdvice:全局捕获RestController的异常,返回JSON格式结果(@ControllerAdvice + @ResponseBody)
* 2. @ExceptionHandler:指定捕获的异常类型(优先级:具体异常 > 通用异常)
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 捕获自定义业务异常(BusinessException)- 优先级最高
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
// 打印业务异常日志(包含请求URL,便于后端排查)
System.out.println("【业务异常】请求URL:" + request.getRequestURI() + ",错误码:" + e.getErrorCode() + ",错误信息:" + e.getMessage());
// 返回统一失败结果(携带业务错误码和错误信息)
return Result.fail(e.getErrorCode(), e.getMessage());
}
/**
* 捕获请求参数绑定校验异常(MethodArgumentNotValidException)- 前端参数校验专用
*/
@ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValidException(org.springframework.web.bind.MethodArgumentNotValidException e, HttpServletRequest request) {
System.out.println("【参数校验异常】请求URL:" + request.getRequestURI());
// 提取具体的校验错误信息
StringBuilder errorMsg = new StringBuilder();
e.getBindingResult().getFieldErrors().forEach(fieldError -> {
errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(";");
});
// 返回参数校验失败结果(错误码400)
return Result.fail(400, errorMsg.toString().substring(0, errorMsg.length() - 1));
}
/**
* 捕获通用运行时异常(RuntimeException)- 处理未预期的运行时异常
*/
@ExceptionHandler(RuntimeException.class)
public Result<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
// 打印完整堆栈信息(便于排查系统问题)
System.out.println("【系统运行时异常】请求URL:" + request.getRequestURI());
e.printStackTrace();
// 返回统一失败结果(隐藏具体异常信息,避免泄露系统细节)
return Result.fail(500, "系统繁忙,请稍后再试");
}
/**
* 捕获所有异常(Exception)- 兜底,捕获未覆盖的所有异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e, HttpServletRequest request) {
System.out.println("【系统通用异常】请求URL:" + request.getRequestURI());
e.printStackTrace();
// 返回统一失败结果
return Result.fail(500, "系统异常,请联系管理员");
}
}
实战3:SpringBoot异常处理测试效果
启动SpringBoot项目,调用对应接口,可得到标准化的JSON返回结果,无需前端额外处理异常格式。
- 调用
/api/order/create,传入amount=-99.9(非法金额):
json
{
"code": 10002,
"msg": "订单创建失败:金额必须大于0"
}
- 调用接口出现未预期的NPE异常:
json
{
"code": 500,
"msg": "系统繁忙,请稍后再试"
}
- 参数校验异常(如空商品ID):
json
{
"code": 400,
"msg": "productId:商品ID不能为空"
}
实战4:SpringBoot异常处理最佳实践总结
- 统一异常返回格式 :使用
Result类标准化所有接口返回结果,包括异常场景,前后端协同更高效。 - 自定义业务异常优先 :项目中所有业务相关的异常,统一抛出
BusinessException,携带错误码和清晰提示。 - 全局异常处理器兜底 :使用
@RestControllerAdvice+@ExceptionHandler捕获所有未处理的异常,避免返回原始异常堆栈。 - 异常日志分级:业务异常打印简洁日志(包含关键参数),系统异常打印完整堆栈信息(便于排查根因)。
- 避免泄露系统细节:对外返回的系统异常信息,统一使用"系统繁忙,请稍后再试",避免泄露数据库表名、接口路径等敏感信息。
四、总结
- 基础知识 :Java异常体系根类是
Throwable,分为Error(无需处理)和Exception(可处理),核心关键字是try、catch、finally、throw、throws。 - 日常使用 :资源操作优先使用
try-with-resources自动关闭资源,业务校验使用throw主动抛出异常,项目中使用自定义异常标准化业务错误。 - SpringBoot实战 :核心是全局异常处理器(
@RestControllerAdvice+@ExceptionHandler),搭配自定义异常和统一返回结果,实现异常处理的优雅和高效。
异常处理的核心不是"捕获所有异常",而是"优雅地处理异常,快速地定位问题,平稳地实现降级"。