ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RAP附件上传下载实战:CDS注解与Stream Handler开发指南

RAP附件上传下载实战:CDS注解与Stream Handler开发指南 1. 为什么一个注解就能“自动生成”上传下载在传统 ABAP 里做附件上传我最怕的就是 MIME 仓库那套流程先 SMW0 建对象再写 RFC 把文件内容塞进数据库最后前端还得单独处理二进制流、自己拼 HTTP 请求。到了 RAP 时代事情变得简单得多——CDS 视图里的内容字段只要打上Semantics.largeObject注解OData 层就会把该属性暴露成Edm.Stream类型。SAP Fiori Elements 一看到这种类型会自动渲染出上传按钮、下载链接和文件名展示不需要写一行前端代码。这个注解背后做的事情其实是把“媒体对象”的概念带进了 RAP 业务对象。OData 里普通字段是放在 JSON/Atom payload 里传输的而 Stream 类型字段走的是另一条通道文件内容通过/$value这个特殊 endpoint 单独流式读写不会塞进业务数据的主请求里。所以 Fiori Elements 才能对内容字段做出“上传附件”和“下载文件”的交互而不是把它当成一个普通字符串文本框。要启用这个能力CDS 视图里通常需要三个字段协同工作字段用途CDS 类型建议注解说明文件内容abap.string( 0 )或abap.rawstring( 0 )Semantics.largeObject : true实际存文件流的字段文本文件用 string二进制用 rawstring文件名称abap.char( 255 )Semantics.fileName : true下载时生成Content-Disposition的文件名MIME 类型abap.char( 100 )Semantics.mimeType : true告诉浏览器/前端该文件是什么类型例如application/pdf文字版说“三个字段协同”实际项目里我通常再加一个文件大小字段虽然它不是注解必须的但列表展示、前后端校验都用得上。这里有个细节容易踩坑文件内容字段的类型选择。如果你只传文本文件、XML、日志abap.string( 0 )没问题但如果业务上要传 PDF、图片、Office 文档必须用abap.rawstring( 0 )并且在 handler 里把内容当xstring处理。别想着用一个通用字段通吃所有文件类型HANA 上 char 大对象和二进制大对象的存储方式完全不同到时候乱码了排查成本很高。数据库表的定义也要跟上。CDS 里用abap.rawstring( 0 )对应表字段建议用RAWSTRING用abap.string( 0 )则对应STRING。这两个类型在 HANA 上分别落成 BLOB 和 CLOB容量足够承载几十上百 MB 的文件。如果哪一天你要存视频、大安装包这类几百 MB 甚至 GB 级的东西建议别直接放业务表里而是把文件丢对象存储RAP 里只存下载 URL这一条经验后面我还会再展开。2. 行为定义让后台“知道”这是流媒体对象光在 CDS 视图上打注解还不够。RAP 是行为驱动的编程模型你得在行为定义Behavior Definition里显式声明这个 BO 支持哪些流式操作后台才知道 create / update / read 这三种场景下要怎么处理文件流。行为定义里加下面这一段define behavior for ZI_Attachment alias Attachment persistent table zatt_file lock master authorization master ( instance ) { create; update; delete; stream create, update, read; }stream create表示允许上传新文件的流内容stream update表示允许覆盖已有文件的流内容stream read表示允许下载文件内容。三个都写上是最省事的但如果业务上只允许上传不允许下载那stream create和stream update留着、stream read去掉就行。有几个选型问题在这里必须提前想清楚不然后期返工很麻烦第一要不要启用 draftFiori Elements 的标准 List Report 默认不强制 draft但如果行为定义里写了draft table前端编辑就会进入草稿机制。媒体对象加上 draft 之后文件流会分两个版本存储一份在激活表一份在草稿表stream handler 里也要区分草稿态和激活态复杂度直接翻倍。我的建议是第一版先做非 draft 的简单版本把整条链路跑通再考虑要不要加 draft。很多内部管理类页面上传附件之后直接提交保存draft 并不是刚需。第二persistent table 还是 managed我上面写的是带persistent table的 managed 实现方式这也是最稳的。让 RAP 帮你管理字段映射create/update操作自动落库stream handler 只负责文件流本身的写入和读出。如果你用 unmanaged所有读写都要手写 SQL为了一个上传功能没必要。第三服务定义和服务绑定。行为定义写完还需要把它暴露到 OData 服务里Service Definition 里加上ZI_ATTACHMENT这个投影视图或者直接暴露 CDS 视图Service Binding 里绑定成 OData V2 / V4。Fiori Elements 两种协议都支持不过我用 V4 多处理流式请求时更干净。绑定完之后先别急着去前端预览打开 metadata 看一眼。找到附件实体对应的属性定义正常情况下content字段的类型应该显示为Edm.Stream。如果这里显示成了Edm.String说明注解没生效多半是 CDS 视图没激活或者内容字段类型用错了。3. Handler 实现与数据落库create_stream / update_stream / read_stream 完整链路行为定义只是声明真正处理文件流的是 Behavior Implementation 里的三个 stream handler 方法。这里我给出一套我在项目里验证过的最小实现逻辑。先说create_stream。这个方法的职责是当 Fiori Elements 上传一个新附件时接收文件流并把它写入数据库。RAP 的做法比较特别它会把create行为业务数据和create_stream行为文件流分开触发你先创建一条附件记录比如存文件名、MIME 类型、业务关联的外键然后再单独上传流内容。所以 handler 里做的事情通常是“根据 key 找到刚创建的那条记录再把流内容更新进去”。METHOD create_stream. DATA: ls_attach TYPE zatt_file. SELECT SINGLE * FROM zatt_file WHERE attachment_uuid is_info-key-attachment_uuid INTO ls_attach. IF sy-subrc 0. ls_attach-content it_content. ls_attach-mime_type is_info-mime_type. ls_attach-filename is_info-filename. MODIFY zatt_file FROM ls_attach. ENDIF. es_response VALUE #( filename is_info-filename mime_type is_info-mime_type ). ENDMETHOD.注意早期版本里 stream handler 的入参结构可能不是cl_rap_event_stream_createty_create_info这种写法不同 SPS 版本 API 差异挺大。你在 Eclipse 里写方法时直接用 FOR STREAM CREATE 的模板生成器让它帮你把参数结构带出来别去手抄网上的老代码。IT_CONTENT 就是文件二进制内容类型是xstring无论你 CDS 里用的是 string 还是 rawstringhandler 里拿到手都是字节流。update_stream的逻辑和 create_stream 基本一致区别只是它针对已存在的记录做覆盖。这里有一个实战技巧当用户在 Fiori Elements 里“重新上传”一个附件时前端可能只发文件流不发业务字段。所以 update handler 里不要先读前端传过来的文件名而是直接读数据库里已有记录把新流内容写进去同时保留旧文件名然后根据前端是否带了新的 filename 再决定要不要更新文件名字段。read_stream是下载链路的核心写得不好会出现“能上传不能下载”的怪问题METHOD read_stream. SELECT SINGLE filename, mime_type, content FROM zatt_file WHERE attachment_uuid is_info-key-attachment_uuid INTO DATA(ls_attach). IF sy-subrc 0. es_response VALUE #( content ls_attach-content filename ls_attach-filename mime_type ls_attach-mime_type ). ENDIF. ENDMETHOD.这个方法本身很简单但有几个细节必须注意文件名编码。如果文件名里有中文、空格或特殊字符下载时浏览器经常出现乱码或截断。我建议在read_stream里不要只返回 filename 字段还要检查网关侧的Content-Disposition处理方式。SAP BTP 环境通常没问题但 On-Prem 网关如果做了额外代理文件名要看是否走了 RFC 5987 编码。实在不行就在前端拿到下载 URL 后用encodeURIComponent再做一次编码双保险。大文件的内存问题。xstring是一次性把整个文件读进内存的几十 MB 没问题几百 MB 就会比较吃紧。上传大文件时网关默认请求体大小也有限制这里等于是双重瓶颈。如果业务确定有大文件场景我建议在 CDS 视图里直接存对象存储的 URL而不是文件内容本身然后单独做一个下载跳转接口别让 RAP 去缓冲大文件流。业务数据与流内容的原子性。这句话可能听起来很抽象但实际场景很现实用户点了上传前端先创建附件记录再传文件流。如果此时流传输失败业务表里可能留下一条“有文件名没内容”的脏记录。我现在的处理习惯是在 create 行为里先不提交最终状态把附件记录状态标成“处理中”等create_stream成功后才把状态改成“已完成”前端查询列表时过滤掉“处理中”的数据。虽然没有完美的事务保证但从用户体验上能避免一大半脏数据问题。4. Fiori Elements 侧看到的界面与效果当你把 OData 服务绑定好、metadata 里 content 属性确实变成了Edm.StreamFiori Elements 会怎么表现直接说结论List Report 和 Object Page 都不用写自定义扩展标准功能就能覆盖大部分上传下载需求。在 List Report 的表格里如果 CDS 视图给 filename 字段加了UI.lineItem注解这一列会自动渲染成文件名并且在行尾出现下载图标。单击图标触发read_stream下载逻辑就走通了。这里要注意不要把 content 字段加到lineItem或identification里它已经是 Stream 类型塞到表格里会让前端解析出问题。经验做法是让 content 字段在 UI 上保持隐藏只展示 filename 和 mime_type。在 Object Page 的编辑界面Fiori Elements 会根据 Stream 字段自动生成文件上传控件。用户选择一个文件后前端先把文件暂存等用户点了保存按钮RAP 行为才真正触发 create / create_stream。这个交互模式和传统 ABAP 那种“选完立刻传”不一样刚上手的人可能会觉得“怎么点了没反应”其实是保存后才提交。如果你用的是 SAP Fiori Tools 或者 Business Application Studio 做本地预览可以直接用预览服务跑这个应用不需要额外做 UI 配置。但有一个前提预览工程里要配置好 destination 指向你的后端服务否则前端根本拿不到 metadata。我见过不少人卡在这一步明明后端 SAp 都通了页面却一直白屏最后发现是 Fiori Tools 的 mta.yaml 里 destination 没配上。前端的请求路径也值得看一眼。上传时Fiori Elements 会先发一个 POST 创建业务数据再发一个 PUT 或 POST 到类似/ZI_ATTACHMENT(Key)/content/$value的 endpoint 传文件流。下载时则是 GET/ZI_ATTACHMENT(Key)/content/$value。你可以在浏览器的 Network 面板里看到这两个请求排错的时候特别有用。尤其是 CSRF token——Fiori Elements 会自动带上x-csrf-token头但如果你拿 Postman 直接调接口必须先 GET 一次/$metadata拿 token再带着 token 去上传或下载否则网关直接 403。这个坑我在测试接口时踩了好几次。还有一个和 UI 相关的体验细节如果你的业务场景是“只下载不上传”比如只读展示某个合同附件的原文那就别在行为定义里加stream create/update只保留stream read。反而这样前端不会出现编辑按钮整个页面也更干净权限上也更安全。5. 我踩过的 stream 坑断流、超时与并发问题写 RAP 流式功能的那段时间我几乎把网上常见的stream disconnected类报错都撞了一遍。分享几个典型场景和我的排查链路比直接贴报错有用得多。场景一上传到一半报stream disconnected before completion: stream closed before response.completed。这个错误出现在大文件上传场景症状是前端传了十几秒后请求中断后端日志里没有明显的 ABAP dump。排查链路是这样的先看是谁断开的流——如果是前端浏览器多半是用户手动取消了或网络抖动如果是网关层断的要看 Web Dispatcher 或负载均衡的请求超时配置。SAP BTP 环境下Cloud Connector 和 Destination 的超时时间默认值并不高大文件上传容易被中间层超时掐断。我的处理方式是能改配置就调大超时时间不能改的就限制上传文件大小比如前端加上传前校验超过 50MB 直接提示不让传。与其让用户在十几秒后看到失败不如一开始就拦住。场景二报transport error: network error后台日志甚至出现error decoding response body。这类报错往往是跨网络域调用导致的比如 Fiori 前端在 BTP Cloud Foundry后端 ABAP 环境在 On-Prem中间走了 Cloud Connector。上传文件流经过多个跳转任何一跳网络抖动都可能让响应体解析失败。我建议你先用 Postman 或 curl 直接打后端的$valueendpoint绕开前端框架看裸接口通不通。如果裸接口没问题问题基本出在网络链路或前面的代理上如果裸接口也断那就是后端 stream handler 的问题去查 ABAP 应用日志里的异常。场景三并发更新同一条附件记录后一个请求覆盖了前一个。RAP 默认有锁机制lock master声明就是干这个的。但在非 draft 模式下前端上传和编辑业务数据是两个独立请求锁的持有时间很短。实际项目里出现过两个用户同时给一条订单补传附件后上传的覆盖了先上传的。我的做法是附件记录里加一个版本号或者last_changed_at字段update 时用 SQL 条件更新判断版本号更新行数为 0 就说明版本冲突直接给前端抛业务错误。听起来老套但在这个场景下就是最有效的兜底。场景四stream disconnected before completion: 由于目标计算机积极拒绝无法连接。这个报错最初是我在自己电脑上测试时遇到的。字面意思是连接目标时被拒绝通常不是 ABAP 逻辑的问题而是后端服务没起来、端口不通或者 SAP Fiori Tools 的预览服务指向了错误的 destination。别傻乎乎去调 stream handler先检查服务绑定有没有激活、Gateway 能不能 ping 通再去看代码。场景五OData 响应里 stream 字段总是空。这是纯注解问题。我遇到过 CDS 视图里 content 字段类型用了abap.string( 255 )这种定长类型结果 metadata 里 content 被映射成Edm.StringFiori Elements 根本不生成上传控件。后来改成abap.string( 0 )重新激活才正常。检查步骤其实很简单看 metadata 里该属性是不是Edm.Stream不是就赶紧排查字段类型和注解。最后分享一个我留下的习惯每次配好一个 RAP 流式对象我都会先用 Postman 完整打一遍“创建业务数据 - 上传流 - 下载流 - 覆盖流”四条链路确认无误后再让前端对接。这样把“网络问题”和“业务代码问题”彻底分开后面排查效率能提高不少。文件上传下载这种功能代码本身并不复杂复杂的是链路里的各种边界条件。把Semantics.largeObject这条核心链路摸透了以后遇到图片预览、PDF 在线预览、附件替换这些变体需求都是在这套骨架上加加减减的事。
返回列表