ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SAP BTP ABAP调用On-Premise RFC:ACO_PROXY与Service Consumption Model实践

SAP BTP ABAP调用On-Premise RFC:ACO_PROXY与Service Consumption Model实践 上个月帮一个老客户做集成需求听上去很朴素把 ERP 里的一个 On-Premise RFC Function Module 接到 SAP BTP 的 ABAP 环境里。但真正动手之后才发现这条链路里最关键的两块是 ACO_PROXY 元数据的导出以及 Service Consumption Model 在云侧的落地。这两步只要错一个后面全部白搭。前前后后磨了几天踩了不少坑今天把这条完整的落地路径和过程写出来希望能帮你少走几条弯路。这篇文章适合两类人一类是准备把云 ABAP 环境和老 ERP 打通却不知道从哪下手的顾问另一类是已经做完基本配置但在生成 Service Consumption Model 时反复报错、不知道代理类怎么调用的开发。我会把设计思路、桥接方案、ADT 里的实际操作、调用代码模板以及我实际遇到过的坑全部讲清楚。1. 为什么不能直接在 BTP ABAP 环境里调 RFC云环境的通信边界1.1 Steampunk 没有传统 RFC 端口别再用 SM59 的思路了很多从传统 ABAP 过来的人第一反应就是建一个 SM59 RFC Destination然后 CALL FUNCTION。这个思路在 SAP BTP ABAP 环境里直接失效。BTP 上的 ABAP 环境也就是大家常说的 Steampunk它跑在云基础设施上没有传统意义上的 CPI-C 端口也不能像老系统那样随便建一个 IP 地址加实例号的连接。这里要理解一个本质区别云 ABAP 环境与外部系统通信默认只走 HTTP/HTTPS。它对外提供的通信能力是经过抽象和受控的不像老系统那样直接暴露 RFC 接口。所以你要把 On-Premise 系统里的 RFC Function Module 变成云侧能调用的东西中间必须有一个转换层。这个转换层可以是你自己维护的网关、可以是你已有的 SAP PO/CPI也可以是一个专门写的 OData 服务。总之RFC 协议本身过不去但 RFC 里的业务逻辑和数据却可以转换过去。这一点想通了后面就不会在“为什么我的 RFC Destination 出不来”这种问题上浪费时间。Steampunk 里根本没有那个界面你要做的是建立一个 HTTP 出站连接然后在云侧生成对应的 Service Consumption Model再通过这个模型生成 ABAP 代理类来调用。1.2 ACO_PROXY 到底是什么我习惯理解的“代理元数据仓”说到 ACO_PROXY首先要说明这不是某个标准的事务代码而是我在实际项目里对一个“代理对象集合”的约定叫法。我们当时为了让老系统里的 RFC 接口能被云侧识别需要先生成一系列描述 RFC 函数签名、导入导出参数、异常结构的元数据对象我们统一放在名字以 ACO_PROXY 开头的包下面所以整个方案就叫 ACO_PROXY。更准确地说这里的元数据来源于 ESR 或 SPROXY 生成出来的 ABAP Proxy 对象。Proxy 对象本质上是接口定义它把 RFC Function Module 的参数结构描述成了 ABAP 类、接口、类型组并保存了命名空间、消息类型、异常类等信息。这些信息就是 Service Consumption Model 真正需要的“素材”云侧生成消费模型时必须拿到接口定义才能自动生成对应的 ABAP 类。所以整条链路可以简化为老系统遗留的函数模块先生成标准描述ACO_PROXY 元数据再通过某种中间协议暴露给云侧最后在 ABAP 环境里用 Service Consumption Model 消费。没有中间描述云侧代码就只能手写 HTTP 报文那不仅工作量大而且根本算不上“模型落地”。2. 动手前的网络与桥接设计中间层怎么选2.1 三种桥接方案对比选错了后期很痛苦把 RFC 暴露给云 ABAP 环境通常有下面三种做法。我实际都试过各有适用条件。方案落地方式优点缺点适用场景SAP PO/CPI 中介在 PO 里把 RFC 包装成 REST 或 SOAP 服务云侧再消费不修改老系统保留现有 ESB 架构权限集中管理中间多一跳排查链路较长PO 维护成本高企业已经统一走 PO/CPI且接口数量较多On-Premise Gateway 发布 OData用 SEGW 把 RFC/BAPI 包成 OData 服务实现直观云侧生成 Service Consumption Model 最顺手老系统需要安装/配置 SAP Gateway且要处理 OData 注解老系统已有 SAP Basis 能力不想额外引入 ESBCloud Connector 自定义 HTTP 桥用 Cloud Connector 打通网络老系统写一个 HTTP Handler 调用本地 RFC链路短适合单个接口快速验证需要自己写安全层且没有统一标准原型验证、偶尔一两个接口的轻量集成我这次客户用的是第三种思路但外部套了一层类似 API 管理的逻辑。当时选择它的原因很简单客户没有 PO 许可Gateway 又在推荐配置之外而 BTP 与老系统之间已经有 Cloud Connector 的标准连接。在 Cloud Connector 上配置好一个受信任的虚拟资源路径云 ABAP 环境的出站 HTTP 请求就能到达老系统上的一个 ABAP 程序该程序内部再启动一个异步 RFC 调用。注意这里说的是“启动异步 RFC 调用”因为如果是同步的大数据量 RFCHTTP 连接很容易超时性能很难看。2.2 Cloud Connector 背后的访问控制配置用 Cloud Connector 时最容易忽略的是“访问映射”和“访问策略”这两个区。你需要在访问控制里新增一条对后端系统资源的映射把云侧看到的虚拟路径映射到老系统的实际路径。比如云侧访问/rfc/material/get_list实际到达后端时是/sap/bc/http_rfc/get_list。这块有两个体会虚拟路径尽量带上一层语义不要暴露老系统的真实内部路径。虽然没有绝对安全但至少可以少给攻击面。云 ABAP 环境里的 Communication System 配置的主机名一定要和 Cloud Connector 里配置的名字一致。我当时在这里栽过一次Communication System 里填了内部别名结果请求到达 Cloud Connector 时匹配不到资源映射直接 404。这个错很隐蔽因为从 BTP 方看目标系统是通的但实际链路根本没建立起来。2.3 认证与信任链要提前约定中间层一旦确定马上要约定认证方式。最常见的两种Basic Auth 和 OAuth 2.0 Client Credentials。如果是轻量接口Basic Auth 够用但用户名密码放在配置里要管好凭证。如果客户安全要求高老老实实用 OAuth 2.0用 Client ID 和 Client Secret 换取 Token再带着 Token 访问桥接服务。在云 ABAP 环境里这些凭证最终会落到 Communication Arrangement 对应的 Communication User 上。我建议一个通信安排只绑定一个通信用户不要多个接口共享一个用户。这样出了问题在日志里能精确定位是哪个接口、哪个用户、在哪个时间段发起的请求。3. 核心落地过程从 ACO_PROXY 元数据到 Service Consumption Model3.1 在老系统侧导出代理元数据的正确姿势这一步看似琐碎却是后续模型导入质量的基础。在传统 ABAP 系统或 PO 里用 SPROXY 生成 ABAP Proxy 时系统会产生很多对象接口、代理类、消息类型、命名空间、数据类型等。这些东西共同构成了接口的元数据。想用接口编号查对应函数SPROXY 本身并没有直接的搜索按钮你可以按命名空间或者对象类型浏览。我通常的做法是在浏览器里找到 ESR 接口编号后在 SPROXY 的“对象列表”输入接口名。如果你不知道完整名字可以用通配符式地按前缀查比如输入ACO_PROXY*。查到之后重点检查“Interface Pattern”确认它是同步还是异步并核对 Request 和 Response 结构里的字段是否与你预期一致。这里有一个容易踩的坑很多 RFC 函数模块在开发时把多余的结构也挂在导出参数里导致生成的元数据又大又乱。这会让云侧的 Service Consumption Model 生成一大堆无用结构后续开发调用时看着就头疼。所以一定要先清理函数模块的导出导入参数只保留真正需要的宁可多建几个方法也不要在一个函数里堆所有字段。但如果你和我一样项目是从 ABAP PO 接手那还要注意 PO 侧 ABAP Proxy Generation 的一个特点一旦接口定义发生变化你要在 PO 里重新生成并激活代理类。老接口的缓存不刷掉云侧永远拿到的是旧结构。这个也很坑因为在云侧看不到 PO 的缓存状态排查方向很容易跑偏。后来我养成了一个习惯在 PO 上每次重新生成 ABAP Proxy 后用事务代码 SPROXY 检查一下接口的生成时间确认是最新时间戳再继续。3.2 桥接实现让 RFC 函数模块能通过 HTTP 被访问如果采用“Cloud Connector 自定义 HTTP 桥”的方式那么桥接程序本身就是一个 ABAP 类里面实现一个 IF_HTTP_EXTENSION 接口在 HANDLE_REQUEST 里解析 JSON然后调用本地的 RFC 函数模块。这里要特别提一下同步和异步的选择。同步情况直接在 HANDLE_REQUEST 里 CALL FUNCTION把返回结果转成 JSON写回 HTTP Response。这样做最简单一个函数模块就是一个接口。但问题也很明显如果函数模块本身执行要五秒以上HTTP 请求端很容易重试重复调用导致老系统里产生脏数据。所以对那种更新型 RFC比如创建销售订单我实际不建议同步。异步情况桥接程序收到 HTTP 请求后只做一个动作把入参存到一张自定义日志表然后立即返回“已收到”。后台 scheduled job 每隔几秒扫描这张表调用 RFC 函数模块完成后把结果更新到日志表中云侧再通过另一个查询接口轮询状态。这样虽然代码量翻倍但成功率、幂等性都强很多。这个设计听起来笨但在跨云环境里反而是最稳的。不过如果是这次标题场景大多数项目更愿意直接用标准方式在网关里把 RFC 发布成 OData 服务然后在云侧用 Service Consumption Model 向导生成代理。这种方式你不需要自己维护 HTTP Handler直接以 OData 元数据为输入生成的代理类内部会处理 HTTP 细节。如果你的桥接方案是 PO也是类似思路PO 导出 OpenAPI/SOAP/WSDL 元数据云侧再导入。所以下面的 Service Consumption Model 才是真正通用的收口环节。3.3 用 ADT 创建 Service Consumption Model一步步操作在 Eclipse 的 ABAP Development Tools 里选中你的包名右键 New Other搜索 Service Consumption Model。创建向导会问你要读取哪种类型的元数据最常见的两类是 ODATA_V2 和 SOAP 或 OPENAPI。如果是经典 RFC 发布的 OData 服务就选 ODATA_V2。输入元数据的方式有两种从本地文件导入或直接填服务 URL 让系统拉取元数据。我用的是本地文件方式因为可以提前检查 XML 内容是否完整。元数据文件有时候会被网关截断尤其是大型函数模块结构一多文件体积超过几 MB 就会莫名其妙少尾标签。你直接让系统联网拉取报错很难看出是网络问题还是文件问题。所以我的做法是先用网关返回元数据另存到本地再通过 ADT 导入。这一步看起来绕但能筛掉相当多的格式问题。生成 Service Consumption Model 后向导会要求你命名一个前缀比如Z_ACO_PROXY_SE。系统会自动生成代理类和相关结构。这些类的命名一般是Z_ACO_PROXY_SE_...类里包含了调用远端服务的方法以及输入输出结构。生成完毕记得立刻打开生成的类看一遍成员方法。如果你发现类名下没有可用的调用方法大概率是元数据里没有定义“操作”。OData 的 Function Import 对应一个可执行的方法如果桥接服务没定义好函数导入Service Consumption Model 生成的只是一个空壳。3.4 生成后的代码结构长什么样以我正在做的一个物料主数据查询接口为例元数据导入后生成了这些对象接口类ZIF_ACO_PROXY_MATERIAL实现类ZCL_ACO_PROXY_MATERIAL输入结构ZACO_MAT_READ_IN包含物料号MATNR工厂WERKS输出结构ZACO_MAT_READ_OUT包含物料描述、基本计量单位等关键调用方法通常在接口类里叫做EXECUTE或READ_MATERIAL。方法内部会处理 HTTP 调用、错误响应、反序列化等。你业务代码里基本不用管底层细节只要给它输入结构然后接收输出结构就行。这里我特别想说生成的代理类虽然不用改但它的性能不一定符合你的预期。每次调用相当于一次完整的 HTTP 往返所以最好在一个会话里复用代理实例。不要每次处理一行数据就 new 一个实例那性能一定崩。正确做法是在 LOOP 之前创建一次实例循环内部反复调用。4. 云侧调用代码怎么写一个可复制的模板4.1 代理类调用的最小骨架以读取物料描述接口为例云侧 ABAP 环境里写一个方法直接调用生成的代理类METHOD get_material_description. DATA: lo_proxy TYPE REF TO zcl_aco_proxy_material. DATA: ls_input TYPE zaco_mat_read_in. DATA: ls_output TYPE zaco_mat_read_out. DATA: lv_http_status TYPE i. DATA: lv_error_text TYPE string. CREATE OBJECT lo_proxy. ls_input-matnr iv_matnr. ls_input-werks 1000. TRY. CALL METHOD lo_proxy-read_material IMPORTING ev_http_status lv_http_status ev_error_text lv_error_text es_output ls_output. CATCH cx_root INTO DATA(lx_ex). RAISE EXCEPTION TYPE zcx_aco_proxy_error EXPORTING text lx_ex-get_text( ). ENDTRY. IF lv_http_status 200. RAISE EXCEPTION TYPE zcx_aco_proxy_error EXPORTING text lv_error_text. ENDIF. rv_description ls_output-maktx. ENDMETHOD.这段代码看起来很短但关键点全在异常和 HTTP 状态码处理上。生成的代理类未必会把 4xx、5xx 抛成异常有的实现只把状态码放在返回值里。所以每次调用后必须先检查 HTTP 状态码然后再取业务输出结构。我还在代理类外面包了一层自定义异常ZCX_ACO_PROXY_ERROR好处是业务代码里只需要捕获这个异常不用关心底层是网络超时、HTTP 500 还是 JSON 解析失败。实际上底层发生错误时我把异常文本设置为适合业务用户理解的短文本而不是把原始堆栈直接抛给用户。4.2 CSRF Token 这个隐形需求如果你桥接的是 OData 服务且服务端开启了 CSRF 防护那么调用 GET 方法还好如果是 POST 或 PUT第一步必须先发一个不带业务数据的请求去拿 CSRF Token然后在真实请求头里带上这个 Token。代理类不一定完整处理这个流程要看生成时的设置和桥接服务是否校验。最稳妥的方式是在桥接服务端关闭 CSRF 校验吗除非接口只在受信内网网段用否则我不推荐。更合理的做法是在云侧代理类初始化时自动获取 Token 并存到一个静态属性里超时后再重新获取。这样一个代理实例只需要一次 Token 获取不会每个请求都多一次往返。如果生成 Service Consumption Model 时没有自动处理你也不方便修改代理类那就再写一个 HTTP Client 包装类。逻辑很简单GET csrf_token把 Token 放到后续请求的x-csrf-token头里。这个包装类在云 ABAP 环境里用标准类cl_web_http_client就能实现。4.3 日志与调试工具顺便聊聊 ALV 增强集成接口测试时我习惯把每次调用的请求参数、返回状态、响应报文、耗时都记录到一张应用日志表。查询日志时想直观一点很多人直接用cl_salv_tablefactory把日志表包装成 ALV。但你很快会发现一个问题请求文本里可能有长报文默认 ALV 显示时根本没法快速查找某个关键字段尤其是一屏数据几十条想按物料号过滤却没有搜索帮助。有人会问reuse_alv_grid_display能不能加 F4答案是能加但标准函数里的it_fieldcat如果你给某个字段设置了no_merging或者自定义styleF4 不一定生效。更灵活的做法是用 SALV 的if_salv_wd_component注册 F4 事件但很多旧系统升级上来的代码并不兼容。我的建议是结合实际环境如果只是调试用不如直接用cl_salv_display_metadata加一个列过滤或者在日志查询界面做一个简单的搜索字段前端用RANGES筛选。顺带说一个容易被忽略的点ALV 显示变式类似事务代码相关的变式维护能大幅度减少调试等待时间。当你经常查同一个日志表、看同一批列、用同样的排序就把这个布局保存成默认变式。但云 ABAP 环境里涉及到系统字段差异直接把老系统的 ALV 变式传上来不一定兼容最好在目标环境里重新维护一次。这不是什么高深技术却能让现场支持人员少一点烦躁。4.4 源码导出与静态检查调试告一段落后我习惯把 Service Consumption Model 生成的代理类和自己的业务方法在 ADT 里用“导出源代码”功能备份到本地。这有两个用处现在 BTP 环境的开发对象也有人喜欢用 abapGit 管理但云侧的很多对象并不支持传统传输捆绑。源码本地留档至少在代码迁移、代码审查时多一份对照。用静态检查可以提前发现类型不匹配和严重废弃方法。Eclipse 里的 ABAP 静态检查面板虽然默认开着但对生成的代理类经常报一些无关紧要的提示比如不检查或强制转换。这时不要直接全局忽略而应该只看自己业务代码的检查结果避免把真正的错误漏过去。生成代理类里的代码最好当作黑盒不要手动修改否则模型重新导入时会被覆盖。5. 我踩过的坑常见问题与排查思路实录5.1 常见报错速查表报错信息可能原因处理方式404 Not FoundCloud Connector 虚拟路径映射不匹配检查 Communication System 的主机名、映射路径前后缀401 Unauthorized通信用户密码错误、认证模式没对上重置密码重新生成 Communication Arrangement403 Forbidden缺少相应角色或服务权限在通信用户角色里增加对桥接服务的调用权限500 Internal Server Error后端 RFC 处理逻辑报错查看后端应用日志和 RFC 短转储Timeout后端 RFC 执行时间超过 HTTP 超时设置改成异步调用或用 PO 做异步转发Meter 数据导入失败元数据文件被网关截断本地另存元数据文件再导入 ADTAllowed host list 错误云 ABAP 环境出站连接请求头里 host 不在白名单在通信配置中配置出站服务白名单这里面最常见的就是 404 和 401。404 的问题我刚说过主机名和虚拟路径两边要对齐401 则多数时候不是你密码错了而是认证类型不一致。桥接服务要求 OAuth 2.0你却在 Communication User 里配置了 Basic Auth那每次能连接但永远无法通过身份校验。5.2 与老系统保存增强的冲突问题业务侧经常会要求主数据保存时自动把新字段同步给云侧。比如资产主数据 AS02 保存增强很多老系统里是由自定义增强和 BADI 处理。一旦我们把 RFC 调用挂在这些保存事件里要非常小心循环调用。我当时遇到的情况是资产主数据保存成功后增强里调用了一个写回云侧的函数模块但那个函数模块内部因为网络异常又去读了一遍老系统资产主数据这个读操作又触发了一次查询查询后的日志更新又写回了云侧。结果就是一个简单保存动作产生了十几次 HTTP 调用日志表炸满了。建议是在所有从 RFC 桥接程序发起的读写请求头里加一个自定义标记字段例如X-ACO-SOURCEBACKGROUND。后端 RFC 逻辑检测到这个标记就跳过所有保存增强、失效增强和更新操作。也就是说增强只对人工操作生效不对系统间同步生效。这块不提前约定联调阶段会非常痛苦。5.3 大字符串字段的序列化问题另一个让我头疼的问题是长文本字段。RFC 函数的导出参数如果有STRING或LRAW类型在桥接层转 JSON 时容易出现换行丢失或中文乱码。解决方案有两个桥接层将长文本作为 base64 编码后放入 JSON 字段云侧拿到后再解码。限制导出字段长度比如只返回前 200 个字符一般用于摘要显示。如果业务确实需要完整长文本我采用过更稳妥的办法把长文本进数据库表RFC 只返回一个 GUID云侧后续再用另一个查询接口按 GUID 取内容。这个模式在老系统同步场景里也很常见能极大降低 HTTP 报文体量。6. 性能、应用日志与后续扩展建议6.1 性能上的两个关键数字跨云链路无论怎么优化一个请求的耗时也很难低于 100ms这还只是链路来回不包括 RFC 执行本身。如果在一个批量 LOOP 里同步调用200 条数据就可能超过 20 秒再算上网络波动几乎肯定会超时。所以批量场景一定要异步化或分批处理。我建议的另一个性能手段是结果缓存。对于基础数据查询比如物料描述、客户名称、成本中心文本完全可以在云侧做本地缓存用一个表存键和值刷新频率一天一次不要每次从老系统实时查。如果业务允许一天延迟缓存策略能减少约 80% 的请求量。6.2 应用日志怎么记才够用在 BTP ABAP 环境里没有事务代码 SLG1但你可以用 ABAP 环境提供的事务代码或者在代码里写完日志记录。我的习惯是写一张自己的接口日志表字段包含唯一请求号 UUID调用方程序名与方法名请求时间戳与耗时毫秒HTTP 状态码、错误文本请求报文摘要JSON 截断到 255 字符响应报文摘要调试时最有用的一列是“请求报文摘要”。如果参数被序列化后传递错误摘要里立刻能看出来是谁的锅。我们曾排查过一个问题云侧传了物料号000000000100020005但 RFC 接收端读到的却是00000000010002000A反复折腾后发现是桥接层把数值当成了数字自动做了类型转换尾数丢了精度。所以说接口日志里保留原始报文摘要能让你快速发现这类串数据类型的问题。6.3 后续接口的扩展路径如果后面还要接第二个、第三个 RFC 函数模块我建议不要每次重新去搭一套桥接程序。在桥接层做一个动态路由用一个接口配置表把 URL 路径映射到 Function Module 名称。这样新增接口时只需要维护配置表和一张字段映射表不用改桥接类主体。但要注意Service Consumption Model 与桥接服务是一一对应的。新增接口场景建议在云侧也重新生成一个新的消费模型而不是在一个模型里堆几十个方法。原因是模型的变更有自己的版本维护和激活性如果全堆在一起一个接口调整会牵连其他所有接口。保持接口模型粒度的一致性后续维护会轻松很多。回到文章开头的那个问题从 ACO_PROXY 元数据到 Service Consumption Model其实没有一步是真正难到需要发明新技术的但每一步都有很多“你以为透了其实没透”的细节。我个人最大的体会是跨环境集成的调试成本远高于开发成本。链条上有老系统、桥接层、云 ABAP 环境、网络、认证这么多环任何一个环节的信息不对称都会造成半天甚至几天的排查。所以建议你从设计阶段就把接口清单、元数据版本、认证方式、日志规范写在文档里不要指望代码能解释一切。最后再分享一个小技巧我给桥接层和云侧代理类都加了统一的请求 ID一旦出问题只要把这个 ID 丢给两边不到两分钟就能定位到具体是哪个环境出了问题。这个做法不花什么成本效果却出奇的好。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进