这是「Java 从基础到实战」系列的第 4 篇。异常处理写得好不好,直接决定线上问题排查是 5 分钟还是 5 小时。本文用「错误写法 → 正确写法」的对照重构方式,讲清异常分类、try-with-resources 和自定义业务异常,所有代码在 JDK 17 下可直接运行。
前言
很多人写异常处理只有一招:try { ... } catch (Exception e) { e.printStackTrace(); }。这种写法会吞掉关键信息、掩盖真正的错误。本文通过重构一段「读取配置文件」的烂代码,一步步演进到生产级写法。
环境准备
| 软件 |
版本 |
| JDK |
17(Eclipse Temurin) |
| IntelliJ IDEA |
Community 社区版 |
在 java-basics 项目中新建包 exception,本文所有类放在这个包下。
步骤 1:三分钟搞懂异常体系
Throwable ├── Error // JVM 级错误(OOM 等),程序无法处理,不要捕获 └── Exception ├── 受检异常(checked) // IOException、SQLException:编译器强制处理 └── RuntimeException // NPE、越界、参数非法:编译器不强制处理
|
一句话选型:调用方有能力恢复的用受检异常;属于编程错误或业务校验失败的用运行时异常。实际工程中,业务异常几乎都继承 RuntimeException(后面步骤 4 会实现)。
步骤 2:重构第一轮 —— 别再手动 close 资源
需求:读取文件第一行内容。先看典型的错误示范 ❌:
package exception;
import java.io.BufferedReader; import java.io.FileReader;
public class ReadFileBad { public static String readFirstLine(String path) { BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(path)); return reader.readLine(); } catch (Exception e) { e.printStackTrace(); return null; } finally { try { if (reader != null) reader.close(); } catch (Exception ignored) {} } } }
|
问题:手动 close 啰嗦且容易漏、catch (Exception) 一把抓、返回 null 把问题传染给调用方。
正确写法:JDK 7+ 的 try-with-resources ✅。新建 ReadFileGood.java:
package exception;
import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path;
public class ReadFileGood { public static String readFirstLine(String path) throws IOException { try (BufferedReader reader = Files.newBufferedReader(Path.of(path))) { return reader.readLine(); } }
public static void main(String[] args) throws IOException { System.out.println(readFirstLine("config.txt")); } }
|
预期输出:
只要类实现了 AutoCloseable 接口(IO 流、JDBC 连接、Lock 等都实现了),就能放进 try 括号自动关闭。从今天起,所有资源操作一律 try-with-resources。
步骤 3:重构第二轮 —— 捕获的三条纪律
try (var reader = Files.newBufferedReader(Path.of("config.txt"))) { return reader.readLine(); } catch (NoSuchFileException e) { return "app.name=default"; }
public static String load(String path) throws IOException { ... }
throw new ConfigException("加载配置失败: " + path, e);
|
⚠️ 最恶劣的写法是 catch (Exception e) {} 空 catch——异常被静默吞掉,线上出问题连日志都没有。IDEA 中它会被灰色高亮警告,看到就消灭。
步骤 4:自定义业务异常 —— 实战标配
真实需求:用户服务中,「用户不存在」「余额不足」这类业务失败需要携带错误码返回给前端。新建 BizException.java:
package exception;
public class BizException extends RuntimeException { private final int code;
public BizException(int code, String message) { super(message); this.code = code; }
public BizException(int code, String message, Throwable cause) { super(message, cause); this.code = code; }
public int getCode() { return code; } }
|
新建 UserService.java 模拟使用:
package exception;
import java.util.Map;
public class UserService { private static final Map<Long, String> DB = Map.of(1L, "张三");
public String getUser(long id) { String name = DB.get(id); if (name == null) { throw new BizException(40401, "用户不存在, id=" + id); } return name; }
public static void main(String[] args) { UserService service = new UserService(); System.out.println(service.getUser(1L));
try { service.getUser(999L); } catch (BizException e) { System.out.println("code=" + e.getCode() + ", msg=" + e.getMessage()); } } }
|
预期输出:
张三 code=40401, msg=用户不存在, id=999
|
这套「BizException + 错误码」模式会在 Spring Boot 篇升级为 @RestControllerAdvice 全局异常处理,模型完全一样。
常见坑
坑 1:finally 中 return,吞掉异常
错误示范 ❌:
try { return Integer.parseInt("abc"); } finally { return -1; }
|
正确写法:finally 只做清理,永远不要写 return ✅。
坑 2:先打日志再抛出,导致一个错误打两遍日志
错误示范 ❌:
catch (IOException e) { log.error("读取失败", e); throw new BizException(500, "读取失败", e); }
|
正确写法:要么记录、要么抛出,二选一。抛出的异常最终由全局处理器统一记录 ✅。
坑 3:用异常控制正常业务流程
错误示范 ❌:
try { return Integer.parseInt(input) > 0; } catch (NumberFormatException e) { return false; }
|
原因:异常构造时要抓取整个调用堆栈,比普通判断慢百倍以上,且语义混乱。
正确写法:能预判的条件用判断处理 ✅:
return input != null && input.matches("\\d+") && Integer.parseInt(input) > 0;
|
小结
- ✅ 受检异常 vs 运行时异常:业务异常选
RuntimeException
- ✅ 资源关闭一律 try-with-resources
- ✅ 捕获三纪律:按需捕获、处理不了就 throws、转译保留 cause
- ✅ 手写
BizException + 错误码,为 Spring Boot 全局异常处理打底
下一篇回到日常编码效率:《Java 8 Stream 流式编程实战:告别 for 循环的 20 个例子》,全是可直接复制的片段,敬请期待。
上一篇:《一文吃透 Java 泛型:从 List<T> 到通配符 ? extends》
本系列完整目录见博客「技术博文」分类。
💬 评论