异常处理 练习 #practice #exception-handling #cx-root #try-catch #abap-objects
关键词
答案
异常类根
CX_ROOT,所有异常类的基类
异常分类
CX_STATIC_CHECK / CX_DYNAMIC_CHECK / CX_NO_CHECK
抛出异常
RAISE EXCEPTION TYPE cx_class 或 NEW
捕获异常
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.
异常链形成过程 :
内层 100 / 0 抛出 CX_SY_ZERO_DIVIDE
内层 CATCH 捕获它,存储在 lo_inner_ex 中
抛出新的 ZCX_CALCULATION_ERROR,通过 PREVIOUS 参数将 lo_inner_ex 链接到新异常
外层 CATCH 捕获 ZCX_CALCULATION_ERROR
通过 previous 属性可以追溯到原始的 CX_SY_ZERO_DIVIDE
最佳实践 :
始终使用 PREVIOUS 参数链接原始异常,保留完整的错误上下文
异常链允许在不丢失原始信息的情况下转换异常类型
外层可以使用循环遍历整个异常链,找到根本原因
日志系统应记录完整的异常链,而非仅最外层异常
题目9 - 异常处理综合编程 [应用] 请编写一个银行转账功能的完整代码,要求:
定义自定义异常类 ZCX_TRANSFER_ERROR(继承 CX_STATIC_CHECK)
实现转账方法,包含余额检查和金额有效性检查
在调用处使用 TRY-CATCH 捕获并处理异常
使用异常链保留原始错误信息
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.
修复原则总结 :
捕获具体异常类型,避免笼统的 CX_ROOT
子类异常 CATCH 在前,父类在后
永远不要空处理器——至少记录日志
使用 CLEANUP 释放资源
必要时重新抛出异常或转换异常类型
关键词
答案
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
最佳实践
捕获具体类型,不空处理,记录日志