异常处理 练习

异常处理 练习

#practice #exception-handling #cx-root #try-catch #abap-objects

📌核心模式 (点击展开)
关键词 答案
异常类根 CX_ROOT,所有异常类的基类
异常分类 CX_STATIC_CHECK / CX_DYNAMIC_CHECK / CX_NO_CHECK
抛出异常 RAISE EXCEPTION TYPE cx_classNEW
捕获异常 TRY ... CATCH ... ENDTRY
异常传播 方法签名中声明 RAISING
获取文本 get_text( ) / get_longtext( )

题目1 - 异常类继承体系 [记忆]

请描述 ABAP 的异常类继承体系(CX_ROOT hierarchy),列出三大异常类别及其特点。

📌查看答案
CX_ROOT 继承体系
1
2
3
4
5
6
7
8
9
10
11
12
CX_ROOT
├── CX_STATIC_CHECK " 静态检查异常
│ ├── CX_SY_ARITHMETIC_ERROR
│ ├── CX_SY_ZERO_DIVIDE
│ └── (自定义异常类)
├── CX_DYNAMIC_CHECK " 动态检查异常
│ ├── CX_SY_REF_IS_INITIAL
│ ├── CX_SY_CAST_ERROR
│ └── ...
└── CX_NO_CHECK " 不可检查异常
├── CX_SY_PROGRAM_NOT_FOUND
└── (通常不自定义此类)

三大类别

类别 检查时机 RAISING 声明 适用场景
CX_STATIC_CHECK 编译时 必须声明 可预见的业务异常
CX_DYNAMIC_CHECK 运行时 可选声明 系统级运行时错误
CX_NO_CHECK 不检查 可选声明 严重错误、资源不足

关键区别

  • CX_STATIC_CHECK:如果方法可能抛出此类异常,必须在方法签名中用 RAISING 声明,否则编译错误
  • CX_DYNAMIC_CHECK:可以不在 RAISING 中声明,但调用者仍可 CATCH
  • CX_NO_CHECK:永远不需要在 RAISING 中声明,适合无法恢复的错误

题目2 - TRY/CATCH/ENDTRY 语法 [记忆]

写出 TRY/CATCH/ENDTRY 的完整语法结构,并说明一个 TRY 块中可以有多少个 CATCH 子句。CLEANUP 子句的作用是什么?

📌查看答案
完整语法结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
TRY.
" 可能抛出异常的代码
...

CATCH cx_static_check INTO DATA(lo_ex1).
" 处理 cx_static_check 及其子类异常
...

CATCH cx_sy_arithmetic_error cx_sy_cast_error INTO DATA(lo_ex2).
" 同时捕获多种异常
...

CATCH cx_root INTO DATA(lo_ex3).
" 捕获所有异常(必须放在最后)
...

CLEANUP.
" 无论是否发生异常,TRY 块结束后执行
" 用于释放资源、恢复状态
...

ENDTRY.

要点

  • 一个 TRY 块可以有 多个 CATCH 子句
  • CATCH 的顺序很重要:子类异常必须在父类异常之前
  • INTO 关键字将异常对象存储到引用变量中
  • CLEANUP 子句:在异常被外层 CATCH 捕获前执行,用于清理 TRY 块中已分配的资源(如关闭文件、释放锁等)
  • CLEANUP 只在 TRY 块中发生异常且异常向上传播时执行

题目3 - RAISE EXCEPTION 语法 [记忆]

写出 ABAP 中抛出异常的两种语法形式,并说明它们的区别和使用场景。

📌查看答案
两种抛出异常的语法

形式1:传统语法

1
2
3
RAISE EXCEPTION TYPE cx_sy_zero_divide
EXPORTING
textid = cx_sy_zero_divide=>division_by_zero.

形式2:内联创建(ABAP 7.40+)

1
2
RAISE EXCEPTION NEW cx_sy_zero_divide(
textid = cx_sy_zero_divide=>division_by_zero ).

区别

  • TYPE 关键字:先确定类型,再创建对象(传统语法)
  • NEW 关键字:直接创建对象实例并抛出(推荐的新语法,更简洁)

自定义消息抛出

1
2
3
RAISE EXCEPTION NEW zcx_business_error(
iv_message = '订单号不能为空'
iv_order = lv_order ).

在方法中抛出
方法签名必须用 RAISING 声明(对于 CX_STATIC_CHECK 类型):

1
2
3
4
METHODS: calculate
IMPORTING iv_value TYPE i
RETURNING VALUE(rv_result) TYPE i
RAISING cx_sy_arithmetic_error.

题目4 - 异常传播机制 [记忆]

什么是异常传播(Exception Propagation)?异常是如何沿着调用栈向上传播的?RAISING 子句在此过程中的作用是什么?

📌查看答案
异常传播机制
当方法内部抛出异常且该方法没有捕获(CATCH)该异常时,异常会自动沿调用链向上传播到调用者。

传播过程

1
2
3
4
5
方法 C (抛出异常)
↓ 未捕获,向上传播
方法 B (调用 C,未捕获)
↓ 未捕获,向上传播
方法 A (调用 B,TRY-CATCH 捕获)

RAISING 子句的作用

  • 文档作用:明确告知调用者该方法可能抛出哪些异常
  • 编译检查:对于 CX_STATIC_CHECK 类型,RAISING 是强制的,编译器会检查
  • 传播许可:声明了 RAISING 的异常才能从方法中传播出去
1
2
3
4
5
6
7
8
9
10
11
12
" 方法定义
METHODS: process_order
IMPORTING iv_order TYPE vbeln
RAISING zcx_order_not_found " 声明可能传播的异常
zcx_invalid_status.

" 方法实现中不捕获时,异常自动传播
METHOD process_order.
" 如果 find_order 抛出 zcx_order_not_found
" 此方法不捕获,则自动传播到调用者
DATA(lo_order) = find_order( iv_order ).
ENDMETHOD.

如果方法是 CX_STATIC_CHECK 类型异常且不在 RAISING 列表中,编译会报错。CX_NO_CHECK 类型无需声明即可传播。


题目5 - 获取异常信息 [记忆]

如何从异常对象中获取错误消息文本?get_text( )get_longtext( ) 的区别是什么?

📌查看答案
获取异常文本的方法
1
2
3
4
5
6
CATCH cx_root INTO DATA(lo_exception).
" 短文本
DATA(lv_short) = lo_exception->get_text( ).

" 长文本
DATA(lv_long) = lo_exception->get_longtext( ).

区别

方法 返回内容 长度
get_text( ) 异常的简短描述 通常一行
get_longtext( ) 详细的异常说明 多行,含占位符替换后的完整文本

获取异常链(Previous Exception)

1
2
3
4
5
6
7
8
CATCH cx_root INTO DATA(lo_ex).
" 当前异常文本
WRITE: / lo_ex->get_text( ).

" 获取原始异常(如果存在嵌套)
IF lo_ex->previous IS BOUND.
WRITE: / '原始原因:', lo_ex->previous->get_text( ).
ENDIF.

PREVIOUS 属性

  • 类型为 CX_ROOT 引用
  • 存储导致当前异常的前一个异常
  • 通过 RAISE EXCEPTION 的 PREVIOUS 参数传递
  • 形成异常链,用于追踪根本原因

题目6 - 自定义异常类 [记忆]

如何创建自定义异常类?自定义异常类通常继承自哪个基类?需要定义哪些内容?

📌查看答案
创建自定义异常类

步骤1:在 SE24 中创建

  • 类名:以 ZCX_ 开头(如 ZCX_BUSINESS_ERROR
  • 超类:通常为 CX_STATIC_CHECK(最常用的基类)
  • 勾选「Exception Class」复选框
  • 系统自动生成构造函数和文本管理

步骤2:定义属性和消息

1
2
3
4
5
6
" 属性
IV_MESSAGE TYPE STRING " 错误消息
IV_ORDER_ID TYPE VBELN " 相关订单号

" 文本符号(在 SE24 的 Texts 标签页中维护)
" 001 &ORDER_ID: &MESSAGE

步骤3:使用

1
2
3
4
5
6
7
8
9
10
" 抛出自定义异常
RAISE EXCEPTION NEW zcx_business_error(
iv_message = '订单状态不允许此操作'
iv_order_id = '12345' ).

" 捕获自定义异常
CATCH zcx_business_error INTO DATA(lo_err).
WRITE: / lo_err->get_text( ).
WRITE: / '订单:', lo_err->iv_order_id.
WRITE: / '消息:', lo_err->iv_message.

设计原则

  • 业务异常继承 CX_STATIC_CHECK(强制声明和处理)
  • 为不同错误场景创建不同的异常类
  • 在文本符号中定义可带占位符的消息模板
  • 添加有意义的属性帮助诊断问题

题目7 - 异常类选择与设计分析 [分析]

在以下场景中,应该选择哪种异常类别(CX_STATIC_CHECK / CX_DYNAMIC_CHECK / CX_NO_CHECK)?请分析原因。
场景 A:银行转账时余额不足
场景 B:除法运算中除数为零
场景 C:系统内存不足

📌查看答案
场景 A:余额不足 → CX_STATIC_CHECK

原因分析:

  • 余额不足是可预见的业务异常,调用者必须处理
  • 需要在编译时强制调用者处理(RAISING 声明)
  • 调用者应提供明确的错误提示(如”余额不足,请充值”)
  • 这类异常应该被精心设计和文档化
1
2
3
4
5
6
CLASS zcx_insufficient_balance DEFINITION
INHERITING FROM cx_static_check.
PUBLIC SECTION.
DATA: mv_current_balance TYPE wrbtr,
mv_required_amount TYPE wrbtr.
ENDCLASS.

场景 B:除数为零 → CX_DYNAMIC_CHECK

原因分析:

  • 这是运行时的计算错误,编译时无法确定
  • ABAP 系统已有 CX_SY_ZERO_DIVIDE(继承自 CX_DYNAMIC_CHECK)
  • 调用者可以选择捕获或不捕获
  • 通常在数学运算相关的上下文中才有意义处理

场景 C:内存不足 → CX_NO_CHECK

原因分析:

  • 这类错误极其严重且不可恢复
  • 任何方法都可能遇到,无法预知
  • 不需要强制声明(否则几乎所有方法都要声明)
  • 通常只能做最外层的捕获和日志记录,无法真正恢复
  • ABAP 系统已有 CX_SY_MALLOC_FAILED 等异常

设计原则总结

  • 可预见、必须处理 → CX_STATIC_CHECK
  • 运行时可能发生、可选处理 → CX_DYNAMIC_CHECK
  • 不可预见、无法恢复 → CX_NO_CHECK

题目8 - 异常嵌套与异常链分析 [分析]

在以下代码中,内层异常被捕获后又抛出了新的异常。分析异常链是如何形成的,以及如何通过异常链追踪根本原因。

📌查看答案
异常链分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
TRY.
" 外层操作
TRY.
" 内层操作:可能抛出 CX_SY_ZERO_DIVIDE
DATA(lv_result) = 100 / 0.
CATCH cx_sy_zero_divide INTO DATA(lo_inner_ex).
" 内层捕获,但抛出新的业务异常
RAISE EXCEPTION NEW zcx_calculation_error(
iv_message = '计算失败'
previous = lo_inner_ex ). " 关键:链接原始异常
ENDTRY.

CATCH zcx_calculation_error INTO DATA(lo_outer_ex).
WRITE: / '外层异常:', lo_outer_ex->get_text( ).
" 追踪异常链
DATA(lo_current) = lo_outer_ex.
WHILE lo_current IS BOUND.
WRITE: / '→', lo_current->get_text( ).
lo_current = lo_current->previous.
ENDWHILE.
ENDTRY.

异常链形成过程

  1. 内层 100 / 0 抛出 CX_SY_ZERO_DIVIDE
  2. 内层 CATCH 捕获它,存储在 lo_inner_ex
  3. 抛出新的 ZCX_CALCULATION_ERROR,通过 PREVIOUS 参数将 lo_inner_ex 链接到新异常
  4. 外层 CATCH 捕获 ZCX_CALCULATION_ERROR
  5. 通过 previous 属性可以追溯到原始的 CX_SY_ZERO_DIVIDE

最佳实践

  • 始终使用 PREVIOUS 参数链接原始异常,保留完整的错误上下文
  • 异常链允许在不丢失原始信息的情况下转换异常类型
  • 外层可以使用循环遍历整个异常链,找到根本原因
  • 日志系统应记录完整的异常链,而非仅最外层异常

题目9 - 异常处理综合编程 [应用]

请编写一个银行转账功能的完整代码,要求:

  1. 定义自定义异常类 ZCX_TRANSFER_ERROR(继承 CX_STATIC_CHECK)
  2. 实现转账方法,包含余额检查和金额有效性检查
  3. 在调用处使用 TRY-CATCH 捕获并处理异常
  4. 使用异常链保留原始错误信息
📌查看答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
" ---- 自定义异常类 ----
CLASS zcx_transfer_error DEFINITION
INHERITING FROM cx_static_check.
PUBLIC SECTION.
DATA: mv_from_account TYPE string,
mv_to_account TYPE string,
mv_amount TYPE wrbtr.
METHODS: constructor
IMPORTING
iv_from_account TYPE string OPTIONAL
iv_to_account TYPE string OPTIONAL
iv_amount TYPE wrbtr OPTIONAL
previous TYPE REF TO cx_root OPTIONAL.
ENDCLASS.

CLASS zcx_transfer_error IMPLEMENTATION.
METHOD constructor.
super->constructor( previous = previous ).
mv_from_account = iv_from_account.
mv_to_account = iv_to_account.
mv_amount = iv_amount.
ENDMETHOD.
ENDCLASS.

" ---- 银行账户类 ----
CLASS lcl_bank_account DEFINITION.
PUBLIC SECTION.
METHODS:
transfer
IMPORTING
io_to_account TYPE REF TO lcl_bank_account
iv_amount TYPE wrbtr
RAISING
zcx_transfer_error,
get_balance
RETURNING VALUE(rv_balance) TYPE wrbtr.
DATA: mv_balance TYPE wrbtr,
mv_account_id TYPE string.
ENDCLASS.

CLASS lcl_bank_account IMPLEMENTATION.
METHOD transfer.
" 金额有效性检查
IF iv_amount <= 0.
RAISE EXCEPTION NEW zcx_transfer_error(
iv_from_account = mv_account_id
iv_to_account = io_to_account->mv_account_id
iv_amount = iv_amount ).
ENDIF.

" 余额检查
IF mv_balance < iv_amount.
RAISE EXCEPTION NEW zcx_transfer_error(
iv_from_account = mv_account_id
iv_to_account = io_to_account->mv_account_id
iv_amount = iv_amount ).
ENDIF.

" 执行转账
mv_balance = mv_balance - iv_amount.
io_to_account->mv_balance = io_to_account->mv_balance + iv_amount.
ENDMETHOD.
METHOD get_balance.
rv_balance = mv_balance.
ENDMETHOD.
ENDCLASS.

" ---- 调用处 ----
START-OF-SELECTION.
DATA(lo_from) = NEW lcl_bank_account( ).
lo_from->mv_balance = 1000.
lo_from->mv_account_id = 'ACC001'.
DATA(lo_to) = NEW lcl_bank_account( ).
lo_to->mv_balance = 500.
lo_to->mv_account_id = 'ACC002'.

TRY.
lo_from->transfer(
io_to_account = lo_to
iv_amount = 2000 ).
CATCH zcx_transfer_error INTO DATA(lo_err).
WRITE: / '转账失败!',
/ '转出账户:', lo_err->mv_from_account,
/ '转入账户:', lo_err->mv_to_account,
/ '金额:', lo_err->mv_amount.
IF lo_err->previous IS BOUND.
WRITE: / '原因:', lo_err->previous->get_text( ).
ENDIF.
ENDTRY.

题目10 - 异常处理最佳实践 [应用]

以下代码存在多个异常处理的问题。请找出问题并修复,说明每个修复的理由。

1
2
3
4
5
TRY.
lo_object->process( ).
CATCH cx_root INTO DATA(lo_ex).
" 什么都不做
ENDTRY.
📌查看答案
问题分析

问题1:捕获范围过大
CATCH cx_root 捕获所有异常,可能隐藏真正的错误。

问题2:空处理器
捕获后不做任何处理(”吞噬异常”),是最差的做法。

问题3:缺少 CLEANUP
如果 process( ) 分配了资源,没有清理机制。

问题4:没有日志
异常信息完全丢失,无法诊断问题。

修复后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
TRY.
lo_object->process( ).

CATCH zcx_business_error INTO DATA(lo_biz_ex).
" 处理已知业务异常
MESSAGE lo_biz_ex->get_text( ) TYPE 'I' DISPLAY LIKE 'E'.
" 记录日志
WRITE: / '业务错误:', lo_biz_ex->get_text( ).

CATCH cx_sy_arithmetic_error INTO DATA(lo_math_ex).
" 处理计算异常
DATA(lv_msg) = |计算错误: { lo_math_ex->get_text( ) }|.
MESSAGE lv_msg TYPE 'E'.

CATCH cx_root INTO DATA(lo_other_ex).
" 处理未预期的异常(最后防线)
" 至少记录日志,不要忽略
WRITE: / '未预期错误:', lo_other_ex->get_text( ).
" 追踪异常链
DATA(lo_prev) = lo_other_ex->previous.
WHILE lo_prev IS BOUND.
WRITE: / '原因:', lo_prev->get_text( ).
lo_prev = lo_prev->previous.
ENDWHILE.
" 重新抛出或报告
RAISE EXCEPTION lo_other_ex. " 或使用 MESSAGE TYPE 'A'

CLEANUP.
" 清理分配的资源
" 例如:关闭文件、释放锁、重置状态
ENDTRY.

修复原则总结

  1. 捕获具体异常类型,避免笼统的 CX_ROOT
  2. 子类异常 CATCH 在前,父类在后
  3. 永远不要空处理器——至少记录日志
  4. 使用 CLEANUP 释放资源
  5. 必要时重新抛出异常或转换异常类型

📌模式总结 (点击展开)
关键词 答案
CX_ROOT 所有异常类的根,提供 get_text/previous
CX_STATIC_CHECK 编译时检查,RAISING 必须声明
CX_DYNAMIC_CHECK 运行时检查,RAISING 可选
CX_NO_CHECK 不检查,无需 RAISING
RAISE EXCEPTION 抛出异常,TYPE 或 NEW 语法
TRY/CATCH/ENDTRY 捕获异常,CATCH 顺序:子类在前
CLEANUP 异常传播前清理资源
RAISING 方法签名中声明可能传播的异常
PREVIOUS 异常链,链接原始异常
get_text( ) 获取异常短文本
get_longtext( ) 获取异常详细文本
自定义异常 ZCX_ 前缀,继承 CX_STATIC_CHECK
最佳实践 捕获具体类型,不空处理,记录日志