一、异常核心语法
1.1 try-catch-finally
:异常捕获与处理结构
1)作用
-
用于捕获和处理程序运行过程中可能发生的异常
-
防止程序因异常中断,提高代码的鲁棒性(健壮性)
2)基本语法结构:
try {
// 可能抛出异常的代码块
} catch (ExceptionType1 e1) {
// 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
// 处理 ExceptionType2 类型的异常
} finally {
// 无论是否发生异常,都会执行(如关闭资源)
}
3)示例讲解:
public class Demo {
public static void main(String[] args) {
try {
int a = 10 / 0; // 运行时异常:ArithmeticException
} catch (ArithmeticException e) {
System.out.println("错误:除数不能为零!");
} finally {
System.out.println("程序结束,释放资源。");
}
}
}
运行结果:
错误:除数不能为零!
程序结束,释放资源。
4)多个 catch
:
try {
String s = null;
System.out.println(s.length());
} catch (NullPointerException e) {
System.out.println("空指针异常!");
} catch (Exception e) {
System.out.println("其他异常:" + e.getMessage());
}
建议先写具体异常,再写父类(Exception
),否则子类异常无法被捕获。
5)finally
详解
-
无论 try 块是否抛出异常,finally 总会执行
-
通常用于释放资源,如关闭文件、数据库连接
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// 读取文件
} catch (IOException e) {
System.out.println("读取失败");
} finally {
if (fis != null) {
try {
fis.close(); // 一定要关闭资源
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.2 throw
:手动抛出异常对象
1)作用
throw
用于在代码中主动抛出一个异常实例,可以抛出任何 Throwable
的子类。
2)语法格式:
throw new 异常类型("异常描述");
3)示例:
public void checkAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
}
-
程序执行到
throw
语句时会立即抛出异常并中断执行 -
如果这个异常没有在方法内被
try-catch
捕获,必须用throws
声明
1.3 throws
:方法声明异常
1)作用
-
用于方法签名中,声明该方法可能抛出哪些异常
-
告诉调用者:你要么用
try-catch
处理,要么继续throws
抛
2)语法格式:
返回类型 方法名(...) throws 异常类型1, 异常类型2 {
// 可能抛出异常的代码
}
3)示例:
public void readFile(String path) throws IOException {
FileReader fr = new FileReader(path); // FileReader 会抛 IOException
}
调用时:
try {
readFile("test.txt");
} catch (IOException e) {
System.out.println("文件读取失败:" + e.getMessage());
}
4)throw
vs throws
区别对比表:
特性 | throw | throws |
---|---|---|
用途 | 抛出异常对象 | 声明异常可能被抛出 |
位置 | 方法体内 | 方法声明处 |
后面跟的内容 | 异常对象(new) | 异常类(不带 new) |
示例 | throw new IOException("失败") | throws IOException |
1.4 综合例子:使用 throw
+ throws
+ try-catch
public class User {
public void login(String username) throws Exception {
if (username == null || username.isEmpty()) {
throw new Exception("用户名不能为空");
}
System.out.println("登录成功");
}
public static void main(String[] args) {
User user = new User();
try {
user.login(""); // 会抛出异常
} catch (Exception e) {
System.out.println("捕获到异常: " + e.getMessage());
} finally {
System.out.println("登录尝试结束");
}
}
}
输出:
捕获到异常: 用户名不能为空
登录尝试结束
1.5 实际用法
场景 | 应用 |
---|---|
开发中 | 用 try-catch 处理用户输入、文件读取等不确定行为 |
SDK 调试 | 通过日志堆栈 catch (Exception e) 观察调用流程 |
逆向分析中 | Hook 异常处理函数,绕过 throw 抛出的错误(例如:校验失败) |
安全测试中 | 利用错误提示、异常堆栈进行路径发现或代码注入入口分析 |
1.6 小结
try-catch-finally
│
├─ try:放入可能出错的代码
├─ catch:处理指定异常类型
├─ finally:一定执行,用于释放资源
│
throw:主动抛出异常对象
throws:方法声明可能抛出哪些异常
二、自定义异常类
2.1 自定义异常类的作用
在 Java 中,除了使用系统提供的异常(如 NullPointerException
, IOException
),我们还可以根据自己的业务逻辑需求定义新的异常类。
自定义异常的典型用途:
-
表示业务逻辑错误(例如:余额不足、权限异常)
-
抛出更清晰可读、可追踪的错误
-
与项目的模块/组件解耦,提高代码可维护性
-
在调试或逆向中,定位异常的抛出源
2.2 自定义异常类的本质
Java 中所有异常类,最终都继承自:
java.lang.Throwable
├── Error // 错误(虚拟机错误等)
└── Exception // 异常
├── RuntimeException(运行时异常)
└── 其他受检异常(IOException 等)
我们自定义的异常通常继承自:
1)Exception
(受检异常)
-
必须用
try-catch
或throws
处理
2)RuntimeException
(非受检异常)
-
编译器不强制捕获
-
更灵活,适合应用内部逻辑异常
2.3 自定义异常类的语法
1)继承 Exception
(受检异常)
public class MyCheckedException extends Exception {
public MyCheckedException() {
super();
}
public MyCheckedException(String message) {
super(message);
}
public MyCheckedException(String message, Throwable cause) {
super(message, cause);
}
}
2)继承 RuntimeException
(非受检异常)
public class MyRuntimeException extends RuntimeException {
public MyRuntimeException(String message) {
super(message);
}
}
2.4 使用自定义异常的例子
示例 1:余额不足异常
// 自定义异常类
public class InsufficientBalanceException extends Exception {
public InsufficientBalanceException(String message) {
super(message);
}
}
示例 2:在业务代码中使用
public class BankAccount {
private double balance = 100.0;
public void withdraw(double amount) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException("余额不足,取款失败!");
}
balance -= amount;
}
}
示例 3:调用者处理异常
public class Test {
public static void main(String[] args) {
BankAccount account = new BankAccount();
try {
account.withdraw(150.0); // 触发异常
} catch (InsufficientBalanceException e) {
System.out.println("异常捕获:" + e.getMessage());
}
}
}
2.5 规范建议(编写自定义异常)
建议项 | 内容 |
---|---|
类名 | 以 Exception 结尾(如 LoginFailedException ) |
构造方法 | 提供 String message 、Throwable cause 构造器 |
继承方式 | 业务类建议继承 Exception ,内部错误建议继承 RuntimeException |
包名 | 放在 com.xxx.exception 包下,统一管理 |
2.6 自定义异常在调试/逆向中的价值
在调试中:
-
通过日志或堆栈跟踪定位自定义异常的抛出点
-
比系统异常更具语义性,便于快速理解错误
在逆向中:
-
某些 SDK 或加密逻辑会用自定义异常抛出校验错误
-
通过 Frida/日志/trace 定位异常类名和抛出位置
-
分析异常触发条件,进而绕过或构造伪装数据
2.7 小结
自定义异常类
├─ 为什么要自定义?
├─ 继承 Exception / RuntimeException
├─ 如何定义:构造器 + 命名规范
├─ 如何使用:抛出 throw + 声明 throws
├─ 实际场景:业务逻辑错误、逆向定位
三、常见异常类型
3.1 NullPointerException
(空指针异常)
1)定义
空指针异常是指:访问了一个为 null
的引用对象的方法或字段时引发的异常。
java.lang.NullPointerException
2)常见触发场景
触发语句 | 说明 |
---|---|
obj.toString() | obj 为 null,调用方法抛异常 |
obj.field | 访问成员变量时,obj 为 null |
arr[0] | arr 是 null,访问数组元素抛异常 |
list.get(0) | list 是 null,而非空但索引越界时会抛 IndexOutOfBoundsException |
3)示例代码
public class Demo {
public static void main(String[] args) {
String s = null;
System.out.println(s.length()); // NullPointerException
}
}
4)调试方式
-
查看异常栈信息(Exception stack trace)
-
从堆栈中定位异常行号和方法名
-
使用 IDE 的断点或日志逐步排查 null 来源
栈追踪示例:
Exception in thread "main" java.lang.NullPointerException
at Demo.main(Demo.java:4)
5)如何防止空指针
方法 | 示例 |
---|---|
非空判断 | if (obj != null) |
Optional | Optional.ofNullable(obj).orElse(defaultValue) |
IDE 工具提醒 | IntelliJ IDEA 有 null 检测功能 |
Lombok 的 @NonNull 注解 | 编译时校验是否为 null |
3.2 ClassCastException
(类强制类型转换异常)
1)定义
该异常表示:试图将某个对象强制转换为不是其实际类型的类时引发的异常。
java.lang.ClassCastException
2)常见触发场景
Object obj = new Integer(5);
String str = (String) obj; // 报 ClassCastException
虽然 obj
是 Object
类型,但实际它是 Integer
,不能强制转成 String
。
3)示例代码
public class Demo {
public static void main(String[] args) {
Object obj = "Hello";
Integer num = (Integer) obj; // ClassCastException
}
}
输出异常信息:
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at Demo.main(Demo.java:4)
4)如何避免 ClassCastException
方法 | 示例 |
---|---|
使用 instanceof 判断 | if (obj instanceof Integer) |
使用泛型(推荐) | List<String> 比 List<Object> 更安全 |
明确接口和实现类边界 | 不随意强转接口实现 |
开发规范约束 | 明确参数传递和接收类型 |
3.3 调试角度分析
异常类型 | 常见错误栈信息 | 调试关键点 |
---|---|---|
NullPointerException | 指向具体行的空对象访问 | 追踪为 null 的变量,检查赋值 |
ClassCastException | 指出无法转换的两个类 | 看实际对象的类型(getClass() )和要转的类 |
3.4 在逆向/安全分析中的价值
1)辅助分析代码结构
-
App crash 日志中出现
ClassCastException
可能表示有 代码逻辑混淆或伪装 -
出现
NullPointerException
时,反编译代码可定位关键对象未初始化
2)模拟异常绕过检查
try {
if (!licenseValid) {
throw new NullPointerException("验证失败");
}
} catch (Exception e) {
// 验证失败后中断
return;
}
可以 Hook 掉这个 throw
,或者强改 licenseValid
为 true,绕过验证。
3.5 小结
异常 | 本质 | 触发时机 | 预防方法 |
---|---|---|---|
NullPointerException | 空引用访问 | 访问 null 的变量、方法或数组 | 非空检查、Optional |
ClassCastException | 类型错误 | 强转成非实际类型 | instanceof 判断、泛型 |
四、堆栈追踪分析
4.1 什么是堆栈追踪(Stack Trace)?
堆栈追踪是 Java 程序在运行中发生异常或错误时,JVM 自动打印的一系列方法调用栈信息,描述了从异常发生点逐层向上传递调用关系。
示例异常信息(Stack Trace):
Exception in thread "main" java.lang.NullPointerException
at com.example.MyClass.myMethod(MyClass.java:10)
at com.example.App.main(App.java:5)
4.2 Stack Trace 的结构组成
以典型的一条栈帧为例:
at com.example.MyClass.myMethod(MyClass.java:10)
部分 | 含义 |
---|---|
at | 表示当前调用栈的一帧 |
com.example.MyClass | 异常发生的类 |
myMethod | 异常发生的方法 |
(MyClass.java:10) | 源码文件及第几行发生了异常(10 行) |
4.3 堆栈追踪常见异常样式(举例对照)
1)NullPointerException(空指针)
Exception in thread "main" java.lang.NullPointerException
at com.demo.DemoClass.printName(DemoClass.java:15)
at com.demo.DemoClass.main(DemoClass.java:7)
分析:
-
异常发生在
DemoClass.java
的第 15 行 -
是从
main()
调用printName()
时触发的
2)ClassCastException(类强制转换)
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at com.test.CastTest.main(CastTest.java:8)
分析:
-
明确告诉你:实际是
String
,你试图转成Integer
-
报错发生在第 8 行
4.4 如何进行堆栈追踪分析?
步骤 1:从上到下阅读栈帧(第一条才是出错点)
-
第一条是异常抛出的具体位置
-
后面的每条是“谁调用了它”
步骤 2:对照源码或反编译代码,定位具体代码行
-
使用 IDE 跳转对应行(Ctrl + 单击)
-
或用
jadx
,JD-GUI
,Fernflower
反编译查看.class
步骤 3:结合异常类型,分析出错条件
-
是不是参数为 null?
-
是不是数据转换错误?
-
是不是调用了非法对象?
4.5 逆向调试场景中的应用
1)分析 App 崩溃日志
Caused by: java.lang.RuntimeException: 解密失败
at com.app.secure.SecurityManager.decrypt(SecurityManager.java:87)
at com.app.net.NetHandler.getUserInfo(NetHandler.java:122)
这说明:
-
decrypt()
方法出错,可能在使用 AES、RSA 时 key 异常 -
你可以重点 hook 这一段代码,或者跟进加密过程逻辑
2)分析 Web 安全漏洞、异常行为
java.lang.IllegalArgumentException: 参数不合法
at com.web.api.AuthHandler.checkToken(AuthHandler.java:52)
at com.web.api.UserController.getUser(UserController.java:19)
说明:
-
checkToken
方法校验失败,抛出异常 -
可用于判断接口是否有 token 依赖点,或可伪造点
3)结合 Frida 实现实时监控异常
可以使用 Frida 追踪所有异常抛出点:
Java.perform(function () {
var Exception = Java.use("java.lang.Exception");
Exception.$init.overload('java.lang.String').implementation = function (msg) {
console.log("Exception 被抛出: " + msg);
return this.$init(msg);
};
});
4.6 分析:Caused by
和 Suppressed
Caused by
有时异常是嵌套异常,会看到:
Exception in thread "main" java.lang.Exception: 顶层异常
Caused by: java.io.IOException: 文件不存在
at com.file.Reader.read(Reader.java:45)
-
Caused by
表示底层真正触发的异常 -
要分析最底层原因
Suppressed
当使用 try-with-resources 时,可能出现:
Suppressed: java.lang.Exception: 关闭资源失败
说明主异常之后还有资源释放过程中的异常。
4.7 小结(堆栈分析三步法)
堆栈分析三步法:
1. 定位第 1 行出错代码(准确行号)
2. 判断异常类型及触发原因
3. 向上追踪调用链,分析调用过程
4.8 实战建议
场景 | 技巧 |
---|---|
逆向异常分析 | 拿到 crash log,反编译对应类,查找触发条件 |
安全测试中断点排查 | 抓住抛异常的函数,设置 Frida Hook 或 JDWP 调试点 |
Web 渗透中判断逻辑 | 利用返回错误堆栈,看系统是怎么解析参数和验证身份 |