Mark的技术博客

SAP顾问 | 咨询专家 | IT与AI技术探索者

吴让宇(Mark)

吴让宇(Mark)

SAP顾问 | 咨询专家 | IT与AI技术探索者

吴让宇(Mark)的个人技术博客,专注于SAP系统实施、行业方案咨询、IT技术与AI技术的研究与分享。

SAP Fiori ABAP CDS OData Dify Docker AI LLM

最新文章

持久化对象与OO事务 (★★)

#abap-objects #persistent-object #oo-transaction

概览表

项目 关键点
持久化服务 将对象属性自动存入/读取数据库
持久化类 Class Builder 中创建,自动生成CRUD方法
Class Actor 管理持久化对象实例的代理类
OO事务 通过事务码调用类方法

持久化服务概述

ABAP程序中的对象通常是瞬态的 (transient) — 程序结束后消失。
持久化服务使对象可以永久存储到数据库中。

1
2
3
4
5
                    ┌─── 持久化服务 ───┐
│ │
程序 ←──→ 对象 ←──→│ 读写属性 │←──→ 数据库表
(内存) │ (自动SQL) │ (永久)
└───────────────────┘
💡本质
使用ABAP Objects技术模拟面向对象数据库管理系统(OODBMS),底层使用关系型数据库。

创建持久化类

步骤

  1. 设计类和对应的透明表
  2. 在 Class Builder 中创建持久化类(类型选择 “Persistent”)
  3. 定义持久化映射 — 属性到表字段的对应
  4. 系统自动生成:
    • Get/Set 方法
    • Class Actor (ZCA_)
    • 基础类 (ZCB_)

自动生成的类

命名 作用
持久化类 ZCL_<name> 定义属性和映射
Class Actor ZCA_<name> 管理实例,执行数据库访问
基础类 ZCB_<name> Actor 继承的基础方法

使用持久化对象

创建持久化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
" 获取 Actor 实例(单例)
DATA(lo_agent) = zca_vehicle=>agent.

" 创建新的持久化对象
TRY.
lo_agent->create_persistent(
EXPORTING
i_make = 'BMW'
i_model = 'X5'
RECEIVING
result = DATA(lo_veh) ).
CATCH cx_os_object_existing.
" 对象已存在
ENDTRY.

" 提交到数据库
COMMIT WORK.
📌COMMIT WORK
默认使用异步更新,必须执行 COMMIT WORK 才会真正保存到数据库。

读取持久化对象

1
2
3
4
5
6
7
8
9
10
" 读取单个对象
TRY.
DATA(lo_veh) = lo_agent->get_persistent( i_make = 'BMW' ).
CATCH cx_os_object_not_found.
" 未找到
ENDTRY.

" 使用 Query Service 读取多个
DATA(lt_results) = lo_agent->get_persistent_by_query(
i_query = cl_query_manager=>create_query( ... ) ).

修改和删除

1
2
3
4
5
6
7
" 修改 — 调用 set 方法
lo_veh->set_speed( 100 ).
COMMIT WORK.

" 删除
lo_agent->delete_persistent( lo_veh ).
COMMIT WORK.

OO事务 (OO Transaction)

概念

将事务码链接到类方法,用户可以通过菜单或命令字段直接调用。

创建OO事务

  1. SE93 → 创建事务码
  2. 选择 “Method of a Class (OO Transaction)”
  3. 输入类名和方法名
  4. 可选: 设置 OO Transaction Model 标志

OO事务的限制

  • 全局类不能包含屏幕定义
  • 构造器不能有 IMPORTING 参数
  • 不能处理 ABAP 列表显示
  • 解决方案: 使用局部程序类的方法

考试/测试模式

场景/关键词 答案
“持久化对象何时保存” COMMIT WORK 之后
“Class Actor 是什么” 管理持久化对象的单例代理类
“对象已存在异常” CX_OS_OBJECT_EXISTING
“未找到对象异常” CX_OS_OBJECT_NOT_FOUND
“OO事务能链接到什么” 全局类或局部类的公有方法
“OO事务的限制” 全局类不能有屏幕、构造器不能有参数

事件 (★★★)

#abap-objects #event #event-handler

概览表

项目 关键点
事件定义 EVENTS / CLASS-EVENTS,只有 EXPORTING 参数
触发事件 RAISE EVENT
处理器 FOR EVENT ... OF 方法定义
注册 SET HANDLER ... FOR

事件的概念

事件代表了与显式方法调用完全不同的编程模型

1
2
3
显式调用:                          事件控制:
调用者 ──知道──→ 被调用者 触发者 ──不知道──→ 处理者
(编译时确定) (运行时确定)

核心区别

特性 显式方法调用 事件控制调用
谁决定调用 调用者 触发者
谁定义协议 被调用者(方法签名) 触发者(事件参数)
编译时绑定 知道对方 不知道处理者
数量 一个调用者 可能有零到多个处理者
⚠️不要混淆
不要将OO事件与ABAP运行时事件(LOAD-OF-PROGRAM, START-OF-SELECTION)或后台处理/工作流控制混淆。

定义和触发事件

定义事件

1
2
3
4
5
6
7
8
9
CLASS lcl_vehicle DEFINITION.
PUBLIC SECTION.
" 实例事件
EVENTS: vehicle_created
EXPORTING VALUE(iv_make) TYPE string.

" 静态事件
CLASS-EVENTS: all_vehicles_cleared.
ENDCLASS.

规则:

  • 事件只能有 EXPORTING 参数,必须值传递 (VALUE(...))
  • 实例事件用 EVENTS,静态事件用 CLASS-EVENTS
  • 事件也可以在接口中定义

触发事件

1
2
3
4
5
METHOD constructor.
" ... 初始化 ...
RAISE EVENT vehicle_created
EXPORTING iv_make = mv_make.
ENDMETHOD.

规则:

  • 实例方法中可以触发实例事件和静态事件
  • 静态方法中只能触发静态事件
  • 触发事件后,所有注册的处理方法按不确定的顺序被调用

定义事件处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CLASS lcl_rental DEFINITION.
PUBLIC SECTION.
METHODS: on_vehicle_created
FOR EVENT vehicle_created OF lcl_vehicle
IMPORTING !iv_make
!sender. " 预定义参数 SENDER
ENDCLASS.

CLASS lcl_rental IMPLEMENTATION.
METHOD on_vehicle_created.
" sender 指向触发事件的对象
" iv_make 包含事件传递的数据
APPEND sender TO mt_vehicles.
ENDMETHOD.
ENDCLASS.

处理器规则:

  • FOR EVENT evt OF class — 指定事件和定义事件的类
  • 处理器参数 = 事件的 EXPORTING 参数 (用 IMPORTING 接收)
  • SENDER 是预定义隐式参数,类型为 REF TO 触发事件的类
  • 处理器也可以被显式调用 (CALL METHOD handler)

注册事件处理

1
2
3
4
5
6
7
8
9
10
11
12
" 注册特定实例的事件
SET HANDLER lo_rental->on_vehicle_created
FOR lo_vehicle.

" 注册所有实例的事件
SET HANDLER lo_rental->on_vehicle_created
FOR ALL INSTANCES.

" 取消注册
SET HANDLER lo_rental->on_vehicle_created
FOR lo_vehicle
ACTIVATION space.

注册方式对比

方式 语法 说明
特定实例 FOR ref 只响应指定对象的事件
所有实例 FOR ALL INSTANCES 响应所有对象的事件(包括尚未创建的)
静态事件 不需要 FOR 静态事件不需要指定对象
⚠️事件处理器调用顺序不确定
多个处理器注册到同一事件时,调用顺序不保证!不能依赖处理器的执行顺序。

事件处理流程

1
2
3
4
5
6
1. 定义事件 (在触发类中)
2. 定义处理器 (在处理类中)
3. 注册处理 (SET HANDLER ... FOR ...)
4. 触发事件 (RAISE EVENT)
5. 运行时调用已注册的处理器
6. 可选: 取消注册 (ACTIVATION space)

事件在接口中

1
2
3
4
5
6
7
8
9
10
11
INTERFACE lif_observable.
EVENTS: state_changed
EXPORTING VALUE(iv_state) TYPE string.
ENDINTERFACE.

" 实现接口的类自动拥有该事件
CLASS lcl_subject DEFINITION.
PUBLIC SECTION.
INTERFACES: lif_observable.
" 可以触发 lif_observable~state_changed
ENDCLASS.

考试/测试模式

场景/关键词 答案
“事件参数方向” 只有 EXPORTING,必须值传递
“处理器用什么接收” IMPORTING
“SENDER是什么” 指向触发事件的对象的预定义参数
“调用顺序保证吗” 不保证
“FOR ALL INSTANCES” 注册到所有实例(含未创建的)
“如何取消注册” SET HANDLER ... ACTIVATION space
“静态方法能触发实例事件吗” 不能,只能触发静态事件

运行时类型服务 RTTS (★★)

#abap-objects #rtts #rtti #rttc

概览表

项目 关键点
RTTI 运行时类型识别 — 查询类型属性
RTTC 运行时类型创建 — 动态创建类型
描述类层次 对应ABAP类型系统的类层次

RTTI (Run Time Type Identification)

RTTI 用于在运行时查询数据类型和对象类型的属性。

描述类层次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CL_ABAP_TYPEDESCR (根描述类,抽象)
│ DESCRIBE_BY_DATA( data ) → 获取数据类型描述
│ DESCRIBE_BY_OBJECT_REF( ref ) → 获取对象类型描述
│ DESCRIBE_BY_NAME( name ) → 通过名称获取描述
│ type_kind → 类型标识
│ absolute_name → 完整类型名

├── CL_ABAP_DATADESCR (数据类型)
│ ├── CL_ABAP_ELEMDESCR (基本类型)
│ ├── CL_ABAP_STRUCTDESCR (结构)
│ │ components → 组件表
│ ├── CL_ABAP_TABLEDESCR (内表)
│ └── CL_ABAP_REFDESCR (引用)

├── CL_ABAP_OBJECTDESCR (对象类型)
│ ├── CL_ABAP_CLASSDESCR (类)
│ │ attributes → 属性列表
│ │ methods → 方法列表
│ └── CL_ABAP_INTFDESCR (接口)

└── CL_ABAP_ENUMDESCR (枚举)

动态数据类型分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
" 分析结构类型
DATA: BEGIN OF ls_flight,
carrid TYPE sflight-carrid,
connid TYPE sflight-connid,
fldate TYPE sflight-fldate,
END OF ls_flight.

" 获取类型描述
DATA(lo_descr) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_data( ls_flight ) ).

" 访问组件信息
LOOP AT lo_descr->components INTO DATA(ls_comp).
WRITE: / ls_comp-name, ls_comp-type_kind.
ENDLOOP.
📌Down-cast 必须操作
DESCRIBE_BY_DATA 返回 CL_ABAP_TYPEDESCR 类型引用。
需要用 CAST (或 ?=) down-cast 到具体子类才能访问特定属性。

动态对象类型分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
" 分析对象类型
METHOD on_vehicle_created FOR EVENT vehicle_created OF lcl_vehicle
IMPORTING sender.

" 获取 sender 的动态类型描述
DATA(lo_descr) = CAST cl_abap_classdescr(
cl_abap_typedescr=>describe_by_object_ref( sender ) ).

" 获取类名
DATA(lv_classname) = lo_descr->get_relative_name( ).

" 只处理 Bus
IF lv_classname = 'LCL_BUS'.
APPEND sender TO mt_vehicles.
ENDIF.
ENDMETHOD.

常用方法对比

方法 所属类 返回 用途
describe_by_data( data ) CL_ABAP_TYPEDESCR 类型描述 分析数据类型
describe_by_object_ref( ref ) CL_ABAP_TYPEDESCR 类描述 分析对象类型
describe_by_name( name ) CL_ABAP_TYPEDESCR 类型描述 通过名称获取
get_relative_name( ) CL_ABAP_OBJECTDESCR 类名 获取相对类名
components CL_ABAP_STRUCTDESCR 内表 结构组件列表

RTTC (Run Time Type Creation)

从 SAP Web AS 6.40 开始,可以动态创建类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
" 动态创建结构类型
DATA: lt_components TYPE cl_abap_structdescr=>component_table.

" 添加组件
APPEND VALUE #( name = 'CARRID' type = cl_abap_elemdescr=>get_c( 3 ) )
TO lt_components.
APPEND VALUE #( name = 'PRICE' type = cl_abap_elemdescr=>get_p( length = 15 decimals = 2 ) )
TO lt_components.

" 创建结构描述
DATA(lo_struct) = cl_abap_structdescr=>create( lt_components ).

" 创建数据引用
DATA: lo_data TYPE REF TO data.
CREATE DATA lo_data TYPE HANDLE lo_struct.

" 使用
FIELD-SYMBOLS: <ls_struc> TYPE any.
ASSIGN lo_data->* TO <ls_struc>.

RTTC 创建方法

方法 用途
CL_ABAP_ELEMDESCR get_c( len ), get_i( ), get_p( ) 创建基本类型
CL_ABAP_STRUCTDESCR create( components ) 创建结构类型
CL_ABAP_TABLEDESCR create( line_type ) 创建内表类型
CL_ABAP_REFDESCR create( base_type ) 创建引用类型

RTTI vs 替代方案

方法 优点 缺点
RTTI 类型安全,不触发异常 代码稍多
Down-cast + TRY 简单直接 可能触发运行时错误
DESCRIBE FIELD 简单 已过时,功能有限

考试/测试模式

场景/关键词 答案
“RTTI全称” Run Time Type Identification
“RTTC全称” Run Time Type Creation
“根描述类” CL_ABAP_TYPEDESCR
“describe_by_data返回什么” CL_ABAP_TYPEDESCR 引用,需要down-cast
“如何获取类名” get_relative_name( )
“动态创建类型用什么” RTTC,各描述类的 create( ) 方法
“RTTI替代DESCRIBE FIELD” 是的,RTTI覆盖了所有DESCRIBE功能

运行时类型服务 练习

#practice #rtts #rtti #rttc #dynamic-types #abap-objects

📌核心模式 (点击展开)
关键词 答案
RTTS Run Time Type Services,运行时类型服务总称
RTTI Run Time Type Identification,运行时类型识别
RTTC Run Time Type Creation,运行时类型创建
根类 CL_ABAP_TYPEDESCR
describe_by_data 根据数据对象获取类型描述
describe_by_name 根据类型名获取类型描述
describe_by_object_ref 根据对象引用获取类描述

题目1 - RTTS 概述与组成 [记忆]

什么是 RTTS?它由哪两个部分组成?各自的功能是什么?

📌查看答案
RTTS(Run Time Type Services) 是 ABAP 提供的运行时类型服务,允许程序在运行时检查和创建数据类型。

两个组成部分

组成 全称 功能
RTTI Run Time Type Identification 在运行时识别和检查数据类型信息
RTTC Run Time Type Creation 在运行时动态创建新的数据类型

核心类体系

1
2
3
4
5
6
7
8
9
CL_ABAP_TYPEDESCR            " 类型描述基类
├── CL_ABAP_ELEMDESCR " 基本类型(I, C, D, T, ...)
├── CL_ABAP_STRUCTDESCR " 结构体类型
├── CL_ABAP_TABLEDESCR " 内表类型
├── CL_ABAP_REFDESCR " 引用类型
├── CL_ABAP_CLASSDESCR " 类类型
├── CL_ABAP_INTFDESCR " 接口类型
├── CL_ABAP_DATADESCR " 数据描述基类
└── CL_ABAP_ENUMDESCR " 枚举类型

RTTI 用于读取类型信息(属性、组件等),RTTC 用于动态创建类型描述对象并由此创建数据引用。


题目2 - CL_ABAP_TYPEDESCR 的静态方法 [记忆]

CL_ABAP_TYPEDESCR 提供了哪些核心静态方法用于获取类型描述?请列出并说明各自的用途。

📌查看答案
CL_ABAP_TYPEDESCR 核心静态方法
方法 用途 示例
DESCRIBE_BY_DATA( ) 根据数据对象获取类型描述 获取变量的类型信息
DESCRIBE_BY_NAME( ) 根据类型全名获取类型描述 获取已知类型名的描述
DESCRIBE_BY_OBJECT_REF( ) 根据对象引用获取类描述 获取对象的类信息
DESCRIBE_BY_DATA_REF( ) 根据数据引用获取类型描述 获取引用指向的数据类型

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DATA: lv_num    TYPE i VALUE 42,
lt_table TYPE STANDARD TABLE OF sflight.

" 根据数据对象获取描述
DATA(lo_type) = cl_abap_typedescr=>describe_by_data( lv_num ).
WRITE: / lo_type->type_kind, " 输出: 'I' (整数类型)
lo_type->absolute_name. " 输出: 'I'

" 根据类型名获取描述
DATA(lo_struct) = cl_abap_structdescr=>describe_by_name( 'SFLIGHT' ).

" 根据对象引用获取描述
DATA(lo_obj) = NEW zcl_my_class( ).
DATA(lo_class) = cl_abap_typedescr=>describe_by_object_ref( lo_obj ).
WRITE: / lo_class->absolute_name. " 输出: 'ZCL_MY_CLASS'

返回类型为 CL_ABAP_TYPEDESCR 的引用,可根据需要向下转型为具体子类。


题目3 - type_kind 属性值含义 [记忆]

CL_ABAP_TYPEDESCR 的 type_kind 属性返回单字符类型代码。请列出常见的 type_kind 值及其含义。

📌查看答案
type_kind 常见值
type_kind 含义 对应 ABAP 类型
I 整数 TYPE I
P 压缩小数 TYPE P
F 浮点数 TYPE F
C 字符 TYPE C / N
N 数字文本 TYPE N
D 日期 TYPE D
T 时间 TYPE T
X 十六进制 TYPE X
g 字符串 TYPE STRING
y 字节串 TYPE XSTRING
u 结构体(扁平) TYPES: BEGIN OF …
v 结构体(深层) 含字符串/内表组件
h 内表 TYPE TABLE / STANDARD TABLE
r 数据引用 TYPE REF TO DATA
l 对象引用 TYPE REF TO object
k 类引用 TYPE REF TO class
e 枚举类型 TYPE ENUM

题目4 - 结构体类型描述 [记忆]

如何使用 CL_ABAP_STRUCTDESCR 获取结构体的组件信息?请写出代码示例并说明返回的组件表结构。

📌查看答案
获取结构体组件信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
" 方式1:通过数据对象
DATA: ls_flight TYPE sflight.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_data( ls_flight ) ).

" 方式2:通过类型名
DATA(lo_struct2) = CAST cl_abap_structdescr(
cl_abap_structdescr=>describe_by_name( 'SFLIGHT' ) ).

" 获取组件列表
DATA(lt_components) = lo_struct->get_components( ).

" 遍历组件
LOOP AT lt_components INTO DATA(ls_comp).
WRITE: / ls_comp-name, " 组件名(如 CARRID)
ls_comp-type->type_kind, " 类型代码(如 'C')
ls_comp-type->length. " 长度
ENDLOOP.

组件表(COMPONENT_TABLE)结构

字段 类型 说明
NAME STRING 组件名称
TYPE REF TO CL_ABAP_DATADESCR 组件的类型描述引用
AS_INCLUDE ABAP_BOOL 是否为 INCLUDE 结构
SUFFIX STRING INCLUDE 的后缀
DECIMALS I 小数位数(仅 P 类型)
TYPE_KIND TYPEKIND 内部类型代码

其他有用的方法

  • GET_COMPONENT( iv_name ) — 获取指定名称的组件
  • GET_SYMBOL_TABLE( ) — 获取符号表
  • STRUCT_KIND 属性 — 结构类型(FLAT/DEEP)

题目5 - 内表类型描述 [记忆]

如何使用 CL_ABAP_TABLEDESCR 获取内表的类型信息(如表类型、行类型、键定义)?

📌查看答案
内表类型描述
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
DATA: lt_flights TYPE SORTED TABLE OF sflight
WITH NON-UNIQUE KEY carrid connid.

" 获取内表类型描述
DATA(lo_table) = CAST cl_abap_tabledescr(
cl_abap_typedescr=>describe_by_data( lt_flights ) ).

" 获取行类型
DATA(lo_line_type) = lo_table->get_table_line_type( ).
WRITE: / '行类型:', lo_line_type->absolute_name. " SFLIGHT

" 获取表类别
CASE lo_table->table_kind.
WHEN cl_abap_tabledescr=>tablekind_std.
WRITE: / '标准表'.
WHEN cl_abap_tabledescr=>tablekind_sorted.
WRITE: / '排序表'.
WHEN cl_abap_tabledescr=>tablekind_hashed.
WRITE: / '哈希表'.
WHEN cl_abap_tabledescr=>tablekind_any.
WRITE: / '任意表类型'.
ENDCASE.

" 获取键信息
DATA(lt_keys) = lo_table->key.
LOOP AT lt_keys INTO DATA(ls_key).
WRITE: / '键组件:', ls_key-name.
ENDLOOP.

" 检查唯一性
IF lo_table->has_unique_key = abap_true.
WRITE: / '键是唯一的'.
ENDIF.

CL_ABAP_TABLEDESCR 关键属性

属性 说明
TABLE_KIND 表类别(STD/SORTED/HASHED)
KEY 键组件表
HAS_UNIQUE_KEY 键是否唯一
INITIAL_SIZE 初始大小

题目6 - describe_by_object_ref 使用 [记忆]

describe_by_object_refdescribe_by_class_name 有什么区别?在什么场景下使用 describe_by_object_ref

📌查看答案
两种方法对比
方法 输入 返回 用途
describe_by_object_ref 对象引用实例 对象实际运行时类的描述 获取对象运行时类型
describe_by_class_name 类名字符串 指定名称的类描述 按名称查询类信息

关键区别

  • describe_by_object_ref 返回的是对象的 实际运行时类型(考虑多态)
  • describe_by_class_name 返回的是 指定名称 的类描述

多态场景示例

1
2
3
4
5
6
7
8
9
10
11
12
DATA: lo_animal TYPE REF TO lcl_animal.

" 实际创建的是子类 lcl_dog 的实例
lo_animal = NEW lcl_dog( ).

" describe_by_object_ref 返回 lcl_dog 的描述
DATA(lo_type) = cl_abap_typedescr=>describe_by_object_ref( lo_animal ).
WRITE: / lo_type->absolute_name. " 输出: LCL_DOG(而非 LCL_ANIMAL)

" describe_by_class_name 返回指定类的描述
DATA(lo_type2) = cl_abap_classdescr=>describe_by_class_name( 'LCL_ANIMAL' ).
WRITE: / lo_type2->absolute_name. " 输出: LCL_ANIMAL

使用场景

  • 多态编程中确定对象的实际类型
  • 动态方法调用前检查对象是否实现了某个接口
  • 调试和日志记录中获取对象的类名

题目7 - RTTC 动态创建数据类型 [记忆]

什么是 RTTC(Run Time Type Creation)?如何使用 RTTC 动态创建结构体类型和内表类型?

📌查看答案
RTTC 概念
RTTC 允许在运行时动态创建新的数据类型,并使用这些类型创建数据引用。

动态创建结构体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
" 定义组件
DATA(lt_components) = VALUE cl_abap_structdescr=>component_table(
( name = 'ID' type = cl_abap_elemdescr=>get_i( ) )
( name = 'NAME' type = cl_abap_elemdescr=>get_c( 20 ) )
( name = 'PRICE' type = cl_abap_elemdescr=>get_p(
p_length = 10 p_decimals = 2 ) )
).

" 创建结构体类型
DATA(lo_struct_type) = cl_abap_structdescr=>create( lt_components ).

" 创建数据引用并分配
DATA: lo_data TYPE REF TO data.
CREATE DATA lo_data TYPE HANDLE lo_struct_type.

" 使用字段符号访问
FIELD-SYMBOLS: <ls_data> TYPE any.
ASSIGN lo_data->* TO <ls_data>.
ASSIGN COMPONENT 'ID' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_id>).
<lv_id> = 100.

动态创建内表类型

1
2
3
4
5
6
7
8
9
10
" 基于已创建的结构体类型创建内表类型
DATA(lo_table_type) = cl_abap_tabledescr=>create(
p_line_type = lo_struct_type
p_table_kind = cl_abap_tabledescr=>tablekind_std
p_unique = abap_false ).

" 创建内表数据引用
CREATE DATA lo_table TYPE HANDLE lo_table_type.
FIELD-SYMBOLS: <lt_table> TYPE ANY TABLE.
ASSIGN lo_table->* TO <lt_table>.

关键方法

  • CL_ABAP_ELEMDESCR=>GET_I( ) — 获取基本类型描述
  • CL_ABAP_ELEMDESCR=>GET_C( length ) — 获取字符类型描述
  • CL_ABAP_STRUCTDESCR=>CREATE( components ) — 创建结构体
  • CL_ABAP_TABLEDESCR=>CREATE( ... ) — 创建内表

题目8 - RTTI 与 RTTC 协作分析 [分析]

分析以下场景:一个程序需要从外部配置表读取字段定义(字段名、类型、长度),然后动态创建对应的内表并填充数据。请说明 RTTI 和 RTTC 在此场景中分别扮演什么角色,以及完整的处理流程。

📌查看答案
场景分析:RTTI + RTTC 协作

RTTI 的角色(读取已有类型信息)

  • 从配置表中读取的字段类型名(如 ‘CHAR’, ‘INT’)需要转换为对应的类型描述对象
  • 可使用 CL_ABAP_ELEMDESCR=>DESCRIBE_BY_NAME( ) 获取基本类型信息

RTTC 的角色(动态创建新类型)

  • 根据配置动态组合组件,创建新的结构体类型
  • 基于结构体类型创建内表类型
  • 使用 CREATE DATA ... TYPE HANDLE 创建实际的数据对象

完整流程

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
" 第1步:从配置表读取字段定义(数据层)
SELECT field_name, abap_type, length, decimals
FROM zfield_config
INTO TABLE @DATA(lt_config).

" 第2步:使用 RTTC 构建组件表
DATA: lt_components TYPE cl_abap_structdescr=>component_table.
LOOP AT lt_config INTO DATA(ls_conf).
DATA(lo_elem) = cl_abap_elemdescr=>get_c( ls_conf-length ).
" 根据类型分发
CASE ls_conf-abap_type.
WHEN 'CHAR'.
lo_elem = cl_abap_elemdescr=>get_c( ls_conf-length ).
WHEN 'INT'.
lo_elem = cl_abap_elemdescr=>get_i( ).
WHEN 'PACK'.
lo_elem = cl_abap_elemdescr=>get_p(
p_length = ls_conf-length p_decimals = ls_conf-decimals ).
ENDCASE.
APPEND VALUE #( name = ls_conf-field_name type = lo_elem )
TO lt_components.
ENDLOOP.

" 第3步:RTTC 创建动态结构体类型
DATA(lo_struct) = cl_abap_structdescr=>create( lt_components ).

" 第4步:RTTC 创建动态内表类型
DATA(lo_table) = cl_abap_tabledescr=>create( lo_struct ).

" 第5步:创建数据对象
DATA: lo_data_ref TYPE REF TO data.
CREATE DATA lo_data_ref TYPE HANDLE lo_table.
FIELD-SYMBOLS: <lt_dynamic> TYPE STANDARD TABLE.
ASSIGN lo_data_ref->* TO <lt_dynamic>.

" 第6步:填充数据(使用字段符号动态访问)
DATA(lo_row_ref) = cl_abap_structdescr=>create( lt_components ).
CREATE DATA lo_row TYPE HANDLE lo_row_ref.
ASSIGN lo_row->* TO FIELD-SYMBOL(<ls_row>).
ASSIGN COMPONENT 'FIELD1' OF STRUCTURE <ls_row>
TO FIELD-SYMBOL(<lv_val>).
<lv_val> = 'Hello'.
APPEND <ls_row> TO <lt_dynamic>.

总结:RTTI 识别现有类型 → RTTC 组合创建新类型 → CREATE DATA 实例化。


题目9 - 动态类型创建综合应用 [应用]

请编写完整的 ABAP 程序:根据输入参数动态创建一个包含任意数量字段的结构体,然后创建对应的内表,插入若干行数据,并使用 RTTI 输出内表的类型信息。

📌查看答案
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
REPORT zrttc_dynamic_table.

PARAMETERS: p_fields TYPE i DEFAULT 3.

START-OF-SELECTION.
PERFORM create_dynamic_table.

FORM create_dynamic_table.
DATA: lt_components TYPE cl_abap_structdescr=>component_table.

" 动态构建组件
DO p_fields TIMES.
DATA(lv_name) = |FIELD{ sy-index }|.
DATA(lo_type) = COND #(
WHEN sy-index = 1 THEN cl_abap_elemdescr=>get_i( )
WHEN sy-index = 2 THEN cl_abap_elemdescr=>get_c( 30 )
WHEN sy-index = 3 THEN cl_abap_elemdescr=>get_p(
p_length = 10 p_decimals = 2 )
ELSE cl_abap_elemdescr=>get_c( 20 ) ).

APPEND VALUE #(
name = lv_name
type = lo_type
) TO lt_components.
ENDDO.

" 创建结构体和内表类型
DATA(lo_struct) = cl_abap_structdescr=>create( lt_components ).
DATA(lo_table_type) = cl_abap_tabledescr=>create(
p_line_type = lo_struct
p_table_kind = cl_abap_tabledescr=>tablekind_std ).

" 创建数据对象
DATA: lo_table_ref TYPE REF TO data,
lo_row_ref TYPE REF TO data.
CREATE DATA lo_table_ref TYPE HANDLE lo_table_type.
CREATE DATA lo_row_ref TYPE HANDLE lo_struct.

FIELD-SYMBOLS: <lt_table> TYPE STANDARD TABLE,
<ls_row> TYPE any.
ASSIGN lo_table_ref->* TO <lt_table>.
ASSIGN lo_row_ref->* TO <ls_row>.

" 插入数据
DO 5 TIMES.
ASSIGN COMPONENT 'FIELD1' OF STRUCTURE <ls_row>
TO FIELD-SYMBOL(<lv_f1>).
<lv_f1> = sy-index * 10.

ASSIGN COMPONENT 'FIELD2' OF STRUCTURE <ls_row>
TO FIELD-SYMBOL(<lv_f2>).
<lv_f2> = |记录 { sy-index }|.

APPEND <ls_row> TO <lt_table>.
ENDDO.

" 使用 RTTI 输出类型信息
DATA(lo_rtti) = cl_abap_tabledescr=>describe_by_data( <lt_table> ).
DATA(lo_tab_info) = CAST cl_abap_tabledescr( lo_rtti ).

WRITE: / '表类型:',
SWITCH string( lo_tab_info->table_kind
WHEN cl_abap_tabledescr=>tablekind_std THEN '标准表'
WHEN cl_abap_tabledescr=>tablekind_sorted THEN '排序表'
WHEN cl_abap_tabledescr=>tablekind_hashed THEN '哈希表' ).

WRITE: / '行数:', lines( <lt_table> ).
WRITE: / '--- 组件 ---'.
DATA(lo_line) = lo_tab_info->get_table_line_type( ).
LOOP AT CAST cl_abap_structdescr( lo_line )->get_components( )
INTO DATA(ls_comp).
WRITE: / ls_comp-name, ls_comp-type->type_kind.
ENDLOOP.
ENDFORM.

题目10 - RTTI 用于通用编程 [应用]

请编写一个通用工具方法,接收任意内表参数,自动输出内表的元信息(行数、列数、每列名称和类型)。这体现了 RTTI 在通用编程中的什么价值?

📌查看答案
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
CLASS lcl_table_info DEFINITION.
PUBLIC SECTION.
CLASS-METHODS: display_info
IMPORTING it_data TYPE ANY TABLE.
ENDCLASS.

CLASS lcl_table_info IMPLEMENTATION.
METHOD display_info.
" 获取内表类型描述
DATA(lo_table) = CAST cl_abap_tabledescr(
cl_abap_typedescr=>describe_by_data( it_data ) ).

" 获取行类型
DATA(lo_line) = CAST cl_abap_structdescr(
lo_table->get_table_line_type( ) ).

" 获取组件
DATA(lt_comp) = lo_line->get_components( ).

" 输出元信息
WRITE: / '===== 内表元信息 ====='.

" 表类型
WRITE: / '表类型:',
SWITCH string( lo_table->table_kind
WHEN cl_abap_tabledescr=>tablekind_std THEN '标准表'
WHEN cl_abap_tabledescr=>tablekind_sorted THEN '排序表'
WHEN cl_abap_tabledescr=>tablekind_hashed THEN '哈希表' ).

" 行数和列数
WRITE: / '行数:', lines( it_data ).
WRITE: / '列数:', lines( lt_comp ).

" 列详情
WRITE: / '--- 列详情 ---'.
WRITE: / |{ '列名' WIDTH=20 } { '类型' WIDTH=6 } { '长度' WIDTH=6 }|.
WRITE: / '-------------------------------------------'.

LOOP AT lt_comp INTO DATA(ls_col).
DATA(lv_type) = SWITCH string( ls_col-type->type_kind
WHEN 'I' THEN 'INT'
WHEN 'C' THEN 'CHAR'
WHEN 'P' THEN 'PACK'
WHEN 'D' THEN 'DATE'
WHEN 'T' THEN 'TIME'
WHEN 'g' THEN 'STRG'
WHEN 'h' THEN 'TTAB'
ELSE ls_col-type->type_kind ).

WRITE: / |{ ls_col-name WIDTH=20 } { lv_type WIDTH=6 } |
&& |{ ls_col-type->length WIDTH=6 }|.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

" 使用示例
START-OF-SELECTION.
SELECT * FROM sflight INTO TABLE @DATA(lt_flights) UP TO 10 ROWS.
lcl_table_info=>display_info( lt_flights ).

RTTI 在通用编程中的价值

  1. 真正的多态TYPE ANY TABLE 使方法可接受任意内表,RTTI 在运行时确定具体类型
  2. 消除重复代码:一个方法处理所有内表,不需要为每种表结构写单独的版本
  3. 动态框架:这是 ALV、序列化框架等通用组件的基础技术
  4. 调试工具:快速检查任意数据对象的结构信息
  5. 元数据驱动:基于类型信息自动生成 UI、文档或映射逻辑

这种模式在企业级 ABAP 开发中非常常见,特别是在框架和工具类的开发中。


📌模式总结 (点击展开)
关键词 答案
RTTS 运行时类型服务总称(RTTI + RTTC)
RTTI 运行时类型识别(读取类型信息)
RTTC 运行时类型创建(动态构建类型)
CL_ABAP_TYPEDESCR 类型描述根类
CL_ABAP_ELEMDESCR 基本类型描述
CL_ABAP_STRUCTDESCR 结构体类型描述
CL_ABAP_TABLEDESCR 内表类型描述
describe_by_data 根据数据对象获取描述
describe_by_name 根据类型名获取描述
describe_by_object_ref 根据对象引用获取描述
type_kind 单字符类型代码(I/C/P/D/h 等)
get_components 获取结构体组件列表
get_table_line_type 获取内表行类型
CREATE(RTTC) 动态创建类型描述
CREATE DATA … TYPE HANDLE 根据类型描述创建数据
通用编程 TYPE ANY/TABLE + RTTI 实现多态工具

异常处理 练习

#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
最佳实践 捕获具体类型,不空处理,记录日志

异常处理概念 (★★★)

#abap-objects #exception #try-catch #cx-hierarchy

概览表

项目 关键点
类异常 基于类的异常处理,替代 sy-subrc
异常类层次 CX_ROOT → CX_STATIC_CHECK / CX_DYNAMIC_CHECK / CX_NO_CHECK
TRY结构 TRY ... CATCH ... CLEANUP ... ENDTRY
抛出异常 RAISE EXCEPTION TYPE cx_class

类异常概念

SAP Web AS 6.10 引入了基于类的异常处理:

  • 异常 = 异常对象 (Exception Object)
  • 异常对象 = 异常类的实例
  • 属性值包含错误信息
1
2
3
4
5
6
7
传统方式:                      类异常方式:
sy-subrc = 4. RAISE EXCEPTION TYPE cx_error.
IF sy-subrc <> 0. TRY.
" 处理错误 " 正常代码
ENDIF. CATCH cx_error INTO lo_exc.
" 处理异常
ENDTRY.
📌类异常不仅限于OO上下文
类异常可以在所有处理块中使用,包括过程式代码。所有可捕获的运行时错误也可以作为类异常处理。

异常类层次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CX_ROOT (根异常类)
│ GET_TEXT( ) → 获取异常文本
│ GET_SOURCE_POSITION( ) → 获取出错位置

├── CX_NO_CHECK
│ " 不可声明检查 — 不需要在 RAISING 中声明
│ " 总是可以传播

├── CX_STATIC_CHECK (默认)
│ " 静态检查 — 必须在 RAISING 中声明或在 TRY 中处理
│ " 最严格,编译器检查
│ " 推荐默认使用

└── CX_DYNAMIC_CHECK
" 动态检查 — 运行时检查
" 不强制声明,但传播时检查
" 运行时错误属于此类

└── CX_SY_ARITHMETIC_ERROR
└── CX_SY_ARITHMETIC_OVERFLOW

三种异常类对比

类型 编译时检查 RAISING声明 适用场景
CX_STATIC_CHECK 必须 默认推荐
CX_DYNAMIC_CHECK 不强制 运行时错误
CX_NO_CHECK 不需要 资源短缺等不可避免的情况
⚠️不能直接继承 CX_ROOT
必须从 CX_NO_CHECKCX_DYNAMIC_CHECKCX_STATIC_CHECK 之一继承。

TRY-ENDTRY 控制结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TRY.
" 可能抛出异常的代码
DATA(result) = 1 / 0.

CATCH cx_sy_arithmetic_overflow INTO DATA(lo_exc).
" 处理算术溢出
WRITE: / lo_exc->get_text( ).

CATCH cx_root INTO DATA(lo_root).
" 处理其他所有异常
WRITE: / lo_root->get_text( ).

CLEANUP.
" 当前TRY块没有匹配CATCH时的清理
" (异常被外层捕获时执行)

ENDTRY.

TRY块执行流程

1
2
3
4
5
6
7
1. 执行 TRY 块中的语句
2. 如果发生异常:
a. 从当前TRY块开始搜索匹配的 CATCH
b. 按 CATCH 顺序检查(匹配 = 异常类本身或其父类)
c. 找到 → 执行该 CATCH 块
d. 找不到 → 向外层 TRY 块扩展搜索
3. 如果没有匹配且没有外层 → 运行时错误
⚠️CATCH 的顺序
CATCH 按出现顺序检查。更具体的异常类应放在更通用的前面
如果先 CATCH cx_root,后面的 CATCH 永远不会执行。

分析异常对象

1
2
3
4
5
6
7
8
9
CATCH cx_sy_arithmetic_overflow INTO DATA(lo_exc).
" 获取异常文本
DATA(lv_text) = lo_exc->get_text( ).

" 获取出错位置
lo_exc->get_source_position(
IMPORTING
program_name = DATA(lv_prog)
line_number = DATA(lv_line) ).
💡INTO 的重要性
只有使用 INTO 时,系统才会实际创建异常对象。不用 INTO 时,为性能考虑不会创建。

异常类命名约定

范围 前缀 示例
系统预定义 CX_SY_ CX_SY_ARITHMETIC_OVERFLOW
全局异常类 CX_ (或 ZCX_) ZCX_VALIDATION_ERROR
局部异常类 LCX_ LCX_INVALID_INPUT

考试/测试模式

场景/关键词 答案
“CX_STATIC_CHECK vs CX_DYNAMIC_CHECK” STATIC必须声明/处理;DYNAMIC不强制
“CATCH顺序” 按出现顺序检查,具体类在前
“CLEANUP何时执行” 异常被外层或调用者捕获时
“CX_ROOT能直接继承吗” 不能,必须从三个子类之一继承
“INTO的作用” 创建异常对象引用以便分析
“get_text( )” 返回异常文本(来自IF_MESSAGE接口)

持久化对象 练习

#practice #persistent-objects #persistence-service #class-actor #oo-transactions

📌核心模式 (点击展开)
关键词 答案
持久化服务 ABAP Persistence Service,对象自动映射到数据库
持久化类 在 Class Builder 中标记为 Persistence
Class Actor ZCA_ 前缀,代理持久化类的创建和管理
OO 事务 面向对象的事务管理,替代传统 CALL TRANSACTION

题目1 - 持久化服务概述 [记忆]

什么是 ABAP Persistence Service?它的主要目的是什么?它解决了传统数据库访问中的哪些问题?

📌查看答案
ABAP Persistence Service 是 SAP 提供的一种对象关系映射(ORM)机制,将 ABAP 对象自动映射到数据库表记录。

主要目的

  • 将对象的状态自动存储到数据库,无需手动编写 SQL
  • 实现对象与关系数据库之间的透明映射
  • 让开发者以面向对象的方式操作数据库数据

解决的问题

  1. 消除 SQL 编码:不需要手动编写 INSERT/UPDATE/DELETE/SELECT
  2. 类型安全:属性类型与数据库字段类型自动对应
  3. 关注点分离:业务逻辑不需要关心存储细节
  4. 状态管理:自动跟踪对象的修改状态(脏标记)
  5. 一致性:通过事务保证数据一致性

核心组件

  • 持久化类(Persistent Class):映射到数据库表的类
  • Class Actor(ZCA_):管理持久化对象的生命周期
  • Persistence Agent:在后台处理实际的数据库操作

题目2 - 持久化类的创建 [记忆]

如何在 Class Builder (SE24) 中创建一个持久化类?创建过程中需要配置哪些内容?

📌查看答案
创建持久化类的步骤
  1. 创建类:在 SE24 中创建类(如 ZCL_PERS_EMPLOYEE),选择类类型为 Persistent Class
  2. 映射数据库表
    • 在 SE24 中进入「Persistence」视图
    • 指定要映射的数据库表(如 ZEMPLOYEE
    • 系统自动生成与表字段对应的属性
  3. 配置映射
    • 将表字段映射到类属性
    • 设置主键字段
    • 定义属性的读写模式(READ-ONLY / READ-WRITE)

自动生成的内容

  • Class Actor:系统自动生成 ZCA_<类名> 类(如 ZCA_PERS_EMPLOYEE
  • GET/SET 方法:每个映射属性自动生成 getter 和 setter 方法
  • 代理类:后台处理数据库操作的代理

重要限制

  • 持久化类不能有自定义的 CONSTRUCTOR
  • 属性类型必须与数据库字段兼容
  • 只能映射透明表(Transparent Table)

题目3 - Class Actor (ZCA_) 的作用 [记忆]

Class Actor 是什么?命名规范是什么?它提供了哪些关键方法?

📌查看答案
Class Actor 定义
Class Actor 是持久化服务的代理类,负责创建、查找和管理持久化对象的生命周期。

命名规范

  • 持久化类:ZCL_PERS_<名称>(如 ZCL_PERS_EMPLOYEE
  • Class Actor:ZCA_PERS_<名称>(如 ZCA_PERS_EMPLOYEE
  • Class Agent:ZCB_PERS_<名称>(内部代理类)

Class Actor 提供的关键方法

方法 功能
CREATE_PERSISTENT( ) 创建新的持久化对象并写入数据库
GET_PERSISTENT( ) 根据主键查找已存在的持久化对象
DELETE_PERSISTENT( ) 从数据库中删除持久化对象
GET_PERSISTENT_BY_QUERY( ) 通过查询条件查找对象

使用示例

1
2
3
4
5
6
7
8
9
" 通过 Class Actor 创建对象
DATA(lo_employee) = zca_pers_employee=>agent->create_persistent(
i_employee_id = '001'
i_name = '张三'
i_department = 'IT' ).

" 查找已有对象
DATA(lo_emp) = zca_pers_employee=>agent->get_persistent(
i_employee_id = '001' ).

题目4 - 持久化对象的状态管理 [记忆]

持久化对象有哪些状态(State)?Persistence Service 如何跟踪这些状态?

📌查看答案
持久化对象的状态
状态 说明
NEW(新建) 通过 CREATE_PERSISTENT 创建,尚未提交到数据库
LOADED(已加载) 从数据库加载的对象,尚未修改
CHANGED(已修改) 已加载的对象,属性被 setter 方法修改
DELETED(已删除) 通过 DELETE_PERSISTENT 标记为删除
NOT_LOADED 对象已知存在但属性尚未从数据库加载(延迟加载)

状态跟踪机制

  • Persistence Service 通过 Class Agent 在内部维护对象的状态
  • 当调用 setter 方法修改属性时,状态自动变为 CHANGED
  • 当提交事务时,Service 根据状态执行相应的 SQL 操作:
    • NEW → INSERT
    • CHANGED → UPDATE
    • DELETED → DELETE
    • LOADED → 不执行任何操作

这种机制实现了 脏标记(Dirty Checking),只同步真正修改过的对象。


题目5 - OO 事务管理 [记忆]

在持久化对象中如何管理数据库事务?列出关键的事务管理语句。

📌查看答案
OO 事务管理
持久化服务使用自己的事务管理机制,与传统的 COMMIT WORK / ROLLBACK WORK 配合使用。

关键事务语句

1
2
3
4
5
6
7
8
9
10
11
" 开始事务——通常隐式开始

" 创建或修改持久化对象
DATA(lo_obj) = zca_pers_employee=>agent->create_persistent( ... ).
lo_obj->set_name( '李四' ).

" 提交事务——将所有挂起的更改写入数据库
COMMIT WORK.

" 回滚事务——撤销所有未提交的更改
ROLLBACK WORK.

事务管理要点

  1. COMMIT WORK 触发 Persistence Service 将所有 NEW/CHANGED 状态的对象同步到数据库
  2. ROLLBACK WORK 撤销当前事务中所有未提交的更改,对象状态恢复
  3. 在提交前,所有更改只存在于内存中
  4. 提交后,对象的临时状态(NEW/CHANGED)重置为 LOADED

与 LUW 的关系

  • 持久化对象的事务是 SAP LUW(逻辑工作单元)的一部分
  • 可以与传统数据库操作混合使用,但需注意一致性

题目6 - 持久化服务 vs 传统 OPEN SQL [分析]

比较 ABAP Persistence Service 和传统 OPEN SQL 方式的优缺点。在什么场景下应该选择持久化服务,什么场景下不应使用?

📌查看答案
对比分析
维度 Persistence Service OPEN SQL
编码量 少(自动生成) 多(手动编写)
类型安全 高(编译时检查) 中(使用结构体)
性能 较低(额外抽象层) 较高(直接访问)
批量操作 不适合 非常适合
复杂查询 受限(需 Query 类) 灵活(JOIN、子查询)
学习曲线 较陡
调试难度 较高(自动化代码)
灵活性 低(映射固定)

适合使用持久化服务的场景

  • 对象模型与数据库表高度匹配
  • 需要严格的 OOP 封装
  • 单条记录的 CRUD 操作为主
  • 业务逻辑集中在对象方法中

不应使用持久化服务的场景

  • 批量数据处理(如报表)
  • 复杂的多表 JOIN 查询
  • 性能敏感的操作
  • 已有大量 OPEN SQL 代码的项目
  • 数据库特定功能(如 HINT、聚合函数)

题目7 - 持久化对象编程 [应用]

请编写代码,使用持久化服务完成以下操作:1) 创建一个新的员工记录;2) 修改该员工的部门;3) 查询该员工信息并输出;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
DATA: lo_employee TYPE REF TO zcl_pers_employee,
lo_exception TYPE REF TO cx_root.

TRY.
" 1. 创建新员工记录
lo_employee = zca_pers_employee=>agent->create_persistent(
i_employee_id = 'EMP001'
i_name = '王五'
i_department = 'SALES'
i_salary = 5000 ).

" 2. 修改部门
lo_employee->set_department( 'IT' ).

" 3. 查询并输出
DATA(lo_found) = zca_pers_employee=>agent->get_persistent(
i_employee_id = 'EMP001' ).

WRITE: / '员工ID:', lo_found->get_employee_id( ),
/ '姓名:', lo_found->get_name( ),
/ '部门:', lo_found->get_department( ),
/ '薪资:', lo_found->get_salary( ).

" 提交事务
COMMIT WORK.
WRITE: / '事务已提交'.

CATCH cx_root INTO lo_exception.
" 4. 出错回滚
ROLLBACK WORK.
WRITE: / '错误:', lo_exception->get_text( ).
WRITE: / '事务已回滚'.
ENDTRY.

关键点:

  • CREATE_PERSISTENT 创建新记录
  • GET_PERSISTENT 查找已有记录
  • setter 方法修改后需要 COMMIT WORK 提交
  • 异常处理中使用 ROLLBACK WORK 回滚

题目8 - Class Actor 的查询功能 [应用]

说明如何使用 Class Actor 的查询功能(Query)来查找满足特定条件的持久化对象。请编写代码示例:查找所有部门为 ‘IT’ 且薪资大于 5000 的员工。

📌查看答案
使用 GET_PERSISTENT_BY_QUERY 方法进行条件查询:
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
DATA: lo_query      TYPE REF TO if_os_query,
lt_employees TYPE osreftab,
lo_employee TYPE REF TO zcl_pers_employee.

TRY.
" 创建查询对象
lo_query = cl_os_system=>get_query_manager( )->create_query(
iv_object = 'ZCL_PERS_EMPLOYEE'
iv_language = sy-langu ).

" 使用条件查询
" 方式1: 通过 Query Manager 创建带条件的查询
DATA(lo_query_manager) = cl_os_system=>get_query_manager( ).
DATA(lo_condition) = lo_query_manager->create_query_condition(
iv_attribute = 'DEPARTMENT'
iv_operator = '='
iv_value = 'IT' ).

" 获取结果
lt_employees = zca_pers_employee=>agent->get_persistent_by_query(
i_query = lo_query ).

" 遍历结果
LOOP AT lt_employees INTO lo_employee.
IF lo_employee->get_salary( ) > 5000.
WRITE: / lo_employee->get_employee_id( ),
lo_employee->get_name( ),
lo_employee->get_department( ),
lo_employee->get_salary( ).
ENDIF.
ENDLOOP.

CATCH cx_root INTO DATA(lo_err).
WRITE: / '查询错误:', lo_err->get_text( ).
ENDTRY.

注意:复杂条件查询也可以在获取结果集后在 ABAP 层进一步过滤(如上例中的薪资条件)。对于大量数据,建议使用传统的 OPEN SQL 而非持久化查询。


📌模式总结 (点击展开)
关键词 答案
持久化服务 ABAP ORM,对象自动映射到数据库
持久化类 SE24 中创建,映射到透明表
Class Actor ZCA_ 前缀,管理对象生命周期
Class Agent ZCB_ 前缀,内部代理处理 DB 操作
CREATE_PERSISTENT 创建新持久化对象
GET_PERSISTENT 按主键查找对象
DELETE_PERSISTENT 删除持久化对象
对象状态 NEW / LOADED / CHANGED / DELETED
事务提交 COMMIT WORK 同步到数据库
事务回滚 ROLLBACK WORK 撤销更改
适用场景 单记录 CRUD、严格 OOP
不适用场景 批量处理、复杂查询、性能敏感

异常处理进阶 (★★★)

#abap-objects #exception #raise-exception

概览表

项目 关键点
RAISE EXCEPTION 抛出类异常
异常传播 通过 RAISING 声明向上传播
创建异常类 全局/局部异常类
异常文本 OTR文本或消息类
异常映射 将一种异常映射为另一种

抛出异常

基本语法

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

带自定义属性

1
2
3
4
RAISE EXCEPTION TYPE zcx_validation_error
EXPORTING
message = 'Invalid input'
value = lv_input.

重新抛出

1
2
3
4
5
CATCH cx_root INTO DATA(lo_exc).
" 记录日志
WRITE: / lo_exc->get_text( ).
" 重新抛出原始异常
RAISE EXCEPTION lo_exc.

异常传播 (Propagation)

当方法不想处理异常时,可以将其传播给调用者:

1
2
3
4
5
6
7
8
CLASS lcl_calculator DEFINITION.
PUBLIC SECTION.
METHODS: divide
IMPORTING !iv_a TYPE i
!iv_b TYPE i
RETURNING VALUE(rv_result) TYPE decfloat16
RAISING cx_sy_arithmetic_overflow. " 声明传播
ENDCLASS.

传播规则

异常类型 是否需要RAISING 说明
CX_STATIC_CHECK 必须 编译器强制检查
CX_DYNAMIC_CHECK 不强制 但建议声明
CX_NO_CHECK 不需要 总是可以传播
💡RAISING 的作用
RAISING 声明是方法接口的一部分。它告诉调用者”这个方法可能抛出以下异常”,调用者必须处理或继续传播。

创建异常类

全局异常类

  1. Class Builder 中创建(前缀 ZCX_ 或 YCX_)
  2. 继承 CX_STATIC_CHECK(推荐默认)
  3. 定义属性(错误信息)
  4. 维护异常文本

异常文本

方式 说明
OTR文本 Object Text Repository,SAP标准方式
消息类 引用 T100 消息
内联文本 简单场景
1
2
3
4
5
6
" 使用消息类的异常
METHODS: constructor
IMPORTING
!textid LIKE if_t100_message=>t100key OPTIONAL
!previous TYPE REF TO cx_root OPTIONAL
!msgv1 TYPE sy-msgv1 OPTIONAL.

局部异常类

1
2
3
4
5
6
CLASS lcx_error DEFINITION INHERITING FROM cx_static_check.
PUBLIC SECTION.
DATA: mv_message TYPE string.
METHODS: constructor
IMPORTING !iv_message TYPE string.
ENDCLASS.

异常映射 (Exception Mapping)

将一种异常映射为另一种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
METHODS: process_data
RAISING zcx_application_error.

METHOD process_data.
TRY.
" 调用可能抛出 CX_SY_ARITHMETIC_OVERFLOW 的方法
DATA(result) = calculate( ).

CATCH cx_sy_arithmetic_overflow INTO DATA(lo_exc).
" 映射为应用异常
RAISE EXCEPTION TYPE zcx_application_error
EXPORTING
previous = lo_exc " 保留原始异常
textid = zcx_application_error=>calculation_failed.
ENDTRY.
ENDMETHOD.

previous 参数

  • previous 参数保留原始异常链
  • 可以通过 lo_exc->previous 向上追溯
  • 实现异常的链式追踪

调试中的异常处理

  • 异常抛出时,调试器显示异常类名称
  • 如果被 CATCH 捕获,显示成功消息
  • 有两个按钮:分析异常对象、跳转到出错源码

考试/测试模式

场景/关键词 答案
“RAISING声明是必须的吗” CX_STATIC_CHECK必须
“previous参数的作用” 保留异常链,可追溯原始异常
“异常映射的目的” 将底层异常转换为应用层异常
“重新抛出异常” RAISE EXCEPTION lo_exc (不用 TYPE)
“CX_NO_CHECK传播规则” 不需要RAISING声明,总是传播

Class Builder 与全局类 (★★★)

#abap-objects #global-class #class-builder

概览表

项目 关键点
Class Builder SAP GUI 中创建和维护全局类/接口的工具
全局类 Repository对象,有版本管理、传输系统
全局接口 Repository中的接口,可被任何程序使用
Object Navigator 集成开发环境,包含Class Builder

局部类 vs 全局类

特性 局部类 全局类
定义位置 程序内部 Repository 中的独立对象
可见性 仅在定义的程序中 所有程序都可使用
命名空间 自由命名 Y*/Z* 或客户命名空间
工具 代码编辑器 Class Builder (SE24/SE80)
版本管理
传输 随程序传输 独立传输

Class Builder 的主要功能

通过 SE80 (Object Navigator)SE24 (Class Builder) 访问:

标签页 功能
Properties 类属性(类型、最终/抽象等)
Attributes 定义属性
Methods 定义方法及签名
Events 定义事件
Types 定义公有/私有类型
Friends 定义友元关系
Interfaces 实现的接口

创建全局类

步骤

  1. 在 Object Navigator 中选择包(Package)
  2. 右键 → Create → Class
  3. 填写名称(Z*/Y*开头)和描述
  4. 选择属性(最终类、抽象类等)
  5. 在各标签页定义组件

方法定义流程

  1. Methods 标签 → 输入方法名、级别、可见性
  2. Parameters 按钮 → 定义签名(IMPORTING/EXPORTING/RETURNING等)
  3. Source Code 按钮 → 编写实现

全局接口

1
2
3
4
5
" 全局接口在 Class Builder 中创建
" 命名: ZIF_<name> 或 YIF_<name>

" 使用:
DATA: lo_ref TYPE REF TO zif_partner.

从局部类迁移到全局类

迁移过程

  1. 在 Class Builder 中创建新的全局类
  2. 将局部类的定义复制到全局类(使用Class Builder界面)
  3. 将局部类的实现复制到全局类
  4. 调整程序中的引用(局部类名 → 全局类名)
  5. 使用 Refactoring Assistant 辅助重构

Refactoring Assistant

  • 可以将局部类的组件移动到全局类
  • 可以将方法定义移动到接口
  • 自动调整引用关系

测试全局类

Class Builder 内置测试功能:

  • 创建测试类 (Test Classes)
  • 使用 ABAP Unit 框架
  • 直接执行方法进行快速测试

考试/测试模式

场景/关键词 答案
“全局类命名规则” Y* 或 Z* 开头
“Class Builder在哪个事务码” SE24 或 SE80
“局部类 vs 全局类” 局部类只在程序内可见,全局类是Repository对象
“Refactoring Assistant” 辅助将局部类迁移到全局类或接口

考试陷阱

#dashboard #exam-traps

⚠️本笔记目的
BC402 高级ABAP 课程中常见的易错点和考试陷阱集锦。

算术与数据类型

陷阱:** 运算符导致整数除法变化
int = ( 1 / 4 ) * ( 2 * 2 )0(整数算术取整)
int = ( 1 / 4 ) * ( 2 ** 2 )1** 触发二进制浮点运算!)
** 运算符即使所有操作数都是 i 类型也会强制使用二进制浮点运算。
算术表达式
陷阱:大数字字面量超出类型 i 范围
10000000000 超出类型 i 范围,因此变为类型 p,改变了算术类型。
int = 1000000000 / 300000000 * 39(整数算术)
int = 10000000000 / 3000000000 * 310(压缩算术)
算术表达式
陷阱:定点算术程序属性
如果未设置程序属性”定点算术”(Fixed Point Arithmetic),类型 p 的计算将忽略小数位。结果看起来不正确,因为小数点仅在输入/输出时考虑。
算术表达式

程序调用与内存

陷阱:LEAVE TO TRANSACTION 销毁 ABAP 内存
CALL TRANSACTION(插入模式)不同,LEAVE TO TRANSACTION 会销毁整个内部会话栈,同时清除 ABAP 内存
程序调用
陷阱:函数组在不同内部会话中分别加载
如果两个内部会话调用同一函数组的函数模块,每个会话获得独立的全局数据副本。无法跨会话共享数据。
运行时与内存管理
陷阱:外部子程序调用共享程序组
外部 PERFORM 将子程序的程序加载到同一程序组中,共享接口工作区。这可能导致非预期的数据耦合。
运行时与内存管理

内表

陷阱:标准表使用 BINARY SEARCH 需先排序
READ TABLE ... BINARY SEARCH 仅在表按键字段排序后才能正确工作。否则结果不可预测——不会报错。
特殊表技术
陷阱:DELETE ADJACENT DUPLICATES 需要已排序的表
仅删除相邻的重复行。必须先排序,使用 COMPARING 指定比较字段。
特殊表技术
陷阱:COLLECT 仅适用于扁平行类型
COLLECT 对数值字段求和,将所有非数值字段作为键进行比较。深层行类型会导致运行时错误。
特殊表技术
陷阱:哈希表不支持索引访问
不能对哈希表使用 READ TABLE ... INDEXLOOP AT ... FROM/TOINSERT ... INDEXDELETE ... INDEX
内表类型

动态编程

陷阱:动态标识符需要括号,不能有空格
CALL FUNCTION (lv_name) — 正确。CALL FUNCTION ( lv_name ) — 错误。动态令牌的括号内不允许有空格。
动态编程技术
陷阱:泛型数据引用必须使用 ASSIGN 解引用
TYPE REF TO DATA 变量不能用 ->* 直接解引用。必须使用 ASSIGN dref->* TO <fs>
泛型类型与动态访问
陷阱:RTTC 不能生成类或接口
RTTC 仅限于数据类型。不能动态创建类或接口类型。
运行时类型创建

Open SQL

陷阱:FOR ALL ENTRIES 空驱动表
如果驱动表为空,FOR ALL ENTRIES 会读取整个数据库表——没有 WHERE 限制。务必先检查 itab IS NOT INITIAL
SQL 多表访问
陷阱:JOIN 绕过表缓冲
Open SQL JOIN 不使用 SAP 表缓冲。即使表已配置缓冲也会被忽略。对缓冲表应使用单表访问。
数据库接口与缓冲
陷阱:DISTINCT 与聚合函数
SELECT COUNT( DISTINCT col ) 计算不同值的数量。SELECT DISTINCT col COUNT(*) 无效。DISTINCT 作用于结果集,而非聚合中的个别列。
SQL 聚合与处理
陷阱:外连接——右侧只允许一个表
LEFT OUTER JOIN 右侧只能有一个表。不能在右侧外连接到视图或另一个连接。
SQL 多表访问

检查点与测试

陷阱:静态断点留在生产系统
不带 IDBREAK-POINT 始终激活。传输到生产系统前必须移除。改用 BREAK-POINT ID group
检查点与断言
陷阱:LOG-POINT 必须指定检查点组
LOG-POINT 必须始终带有 ID group。与断点不同,日志点不能在没有检查点组的情况下使用。
检查点与断言

性能

陷阱:循环中的 SELECT *
最常见的性能杀手。始终用 JOIN、FOR ALL ENTRIES 或预缓冲到内表来替代。
数据库性能
陷阱:标准表线性搜索处理大数据集
| 不带 BINARY SEARCHREAD TABLE 对标准表是 O(n)。按键访问应使用排序表或哈希表。
ABAP 性能

相关链接

  • [[_posts/SAP_ABAP/BC402_ABAP高阶技巧/00-仪表盘/学习地图]] → 薄弱环节部分
  • [[_posts/SAP_ABAP/BC402_ABAP高阶技巧/00-仪表盘/快速参考]]
0%