简介这是一份面向SAP Cloud Platform Integration开发者的Groovy脚本示例集聚焦CPI集成场景中的常见编码需求既能充当新项目脚本的起始模板也能帮助快速查找与CPI主题相关的通用函数减少重复检索时间。压缩包共36个文件以Groovy脚本与Markdown说明文档为主配合properties、txt、xml、csv等输入输出样例整体体积仅29KB每个子目录内通常包含script.groovy、README.md及对应的测试输入文件目录结构清晰便于按功能模块定位。内容覆盖excel转xml、xml转csv、SFTP同步读取、MPL有效载荷记录与日志级别调整、SOAP错误体获取、凭据读取等典型用例示例均提供背景说明、输入输出对照和可运行脚本读者既能理解逻辑又能直接改造复用。目前已有912人学习下载适合刚开始接触CPI Groovy开发的新手作为系统入门教材也适合有经验的集成工程师作为代码片段速查手册减少常见任务的搜索引擎时间。1. cpi-groovy-examplesSAP CPI开发中最实用的Groovy脚本示例集一线做集成的开发者应该都有过这种经历深夜值班群突然报警说供应商推送的JSON报文格式又变了下游ERP解析直接罢工。排查了半天发现只是日期字段格式对不上流程里却没有转换环节只能临时在CPI的集成流里塞一个Groovy脚本改完重新部署再盯日志确认。这个名为cpi-groovy-examples的示例工程就是把这些高频场景沉淀成了一套可复用的Groovy脚本集合。它收纳了JSON与XML互转、报文头与属性读写、Base64编码、AES加解密、SHA-256哈希、HTTP外呼、异常兜底这些SAP CPI集成开发里反复重复的需求适合SAP集成实施顾问、CPI流程开发者和在Java与Groovy之间来回切换的开发者。拿到脚本后改一改输入输出就能直接贴进Integration Flow的Script step里跑起来。2. 技术栈与工程结构从Integration Flow到脚本执行的完整链路2.1 CPI集成流程与Script step的调用机制SAP CPICloud Platform Integration也就是后来集成到SAP Integration Suite里的Cloud Integration组件上开发的核心产物是Integration Flow集成流。画布上的流程从Sender通道进入经过Content Modifier、Script、Mapping等步骤最后从Receiver通道出去。这里的Script step是一个非常特殊的节点它不像Content Modifier那样只能做静态的键值替换也不像Mapping那样依赖预定义的消息结构而是可以写任意Groovy代码做动态处理。CPI运行时会把集成流编译成Camel路由脚本节点被包装成一个Processor消息以Camel Exchange的形式流入脚本执行完后再把结果塞回Exchange继续往下走。脚本执行时CPI会向Groovy上下文注入几个特定的绑定变量message代表当前消息对象body是消息体headers是消息头Mapproperties是流程属性Maplog是日志对象。这些都不是Groovy标准库自带的东西而是CPI运行时接进来的。很多人第一次写CPI脚本时习惯写System.out.println输出调试信息结果在运行日志里翻半天什么都找不到就是因为没走log对象。脚本是按请求执行的每次消息流过Script step都会重新执行一遍所以脚本里不要保留跨请求的共享状态。常见做法是把它当成一个无状态函数从message里读body做完变换再setBody回去不持有任何静态缓存。Script step在流程里的定位要把握一个原则——能在Content Modifier里配置完成的就不要写脚本脚本写多了流程对业务同事来说就是一个黑匣子排查问题时只能靠日志推断。真正需要脚本的场景是那些需要动态计算、循环拼接、调用外部服务拿凭证再解密之类的逻辑。比如根据当天日期生成文件名或者根据报文字段决定走哪个分支这些用配置项做不出来才交给Groovy。示例工程里的脚本也遵循这个原则每个脚本只解决一个窄问题没有那种几百行包罗万象的大脚本。CPI运行环境对脚本还有一些硬限制脚本有执行时长约束超大循环和大数据处理在脚本里做并不合适容易被运行时中断不要随意在脚本里起线程集成平台对线程的回收不可控容易造成资源泄漏文件系统写入也不可靠本地临时目录在不同节点上是不一致的。这些限制项目README里都写到了算是给刚接触CPI的人提前打预防针。2.2 目录设计按业务场景组织脚本而不是按技术分层这个示例工程的目录结构很朴素没有后端项目常见的service、dao、controller分层而是直接按业务场景分目录。因为CPI的Script step只能粘贴单个脚本脚本之间不允许互相importclasspath里不会包含你工程里的其他文件所以任何公共逻辑都不得不完整写进一个脚本文件里。与其写一堆互相依赖的脚本自我感动不如从一开始就按单文件自包含来组织。目录长这样cpi-groovy-examples/ ├── script/ │ ├── content-conversion/ # 报文体转换JSON、XML、CSV │ │ ├── json-to-xml.groovy │ │ ├── xml-to-json.groovy │ │ └── csv-to-json.groovy │ ├── header-property/ # 消息头header与流程属性property操作 │ │ ├── set-header-with-charset.groovy │ │ └── preserve-property.groovy │ ├── crypto/ # 加密、解密、签名、哈希 │ │ ├── aes-encrypt.groovy │ │ ├── aes-decrypt.groovy │ │ └── sha256-hash.groovy │ ├── database/ # 外部JDBC数据查询 │ │ └── jdbc-query.groovy │ ├── http/ # 带认证的HTTP外呼 │ │ └── http-with-basic-auth.groovy │ ├── error-handling/ # 异常捕获与错误兜底 │ │ └── fallback-json.groovy │ └── util/ # 通用工具类脚本 │ ├── base64-encode.groovy │ └── pretty-print-xml.groovy ├── src/test/groovy/ # Spock单元测试做脚本逻辑回归 ├── build.gradle # 本地测试构建脚本 └── README.md # 脚本索引与使用说明每个子目录里的脚本都是独立文件命名规则统一是动词-名词.groovy一看文件名就知道这个脚本干什么。README里附带了一张索引表列了脚本名、适用场景、输入输出格式、注意事项。找脚本的时候直接翻索引比登录云平台逐个流程点开看高效得多。这个目录结构不是一次定死的最开始只有content-conversion和util两个目录后来在处理对接时发现加密逻辑频繁复用才把crypto单独拆出来。整理示例工程的过程其实就是收敛场景的过程每增加一个真实对接需求就沉淀一个脚本目录会自然生长。2.3 本地构建配置对齐CPI运行环境的Groovy版本build.gradle里最需要注意的是Groovy版本选择。CPI运行时内置的Groovy版本比开源社区慢不少示例工程里默认对齐到Groovy 2.4.x而不是赶时髦用3.x或4.x。血泪教训是本地用高版本语法写爽了部署到CPI上编译不过报错信息还不直观。下面是本地测试用的最小构建配置plugins { id groovy } repositories { mavenCentral() } dependencies { implementation org.codehaus.groovy:groovy:2.4.21 testImplementation org.spockframework:spock-core:1.3-groovy-2.4 } test { useJUnitPlatform() }版本对齐很重要。Groovy 2.4以来的闭包语法和GDK扩展方法在CPI上都能用但2.5之后新增的语法特性就要慎重了。Spock依赖选1.3-groovy-2.4是因为Spock 2.x版本要求更高的Groovy版本和2.4搭不到一起。如果本地不想引入Gradle直接装一个Groovy 2.4的SDK用命令行跑脚本也可以。工程里带上Gradle的主要目的是方便在CI流水线里跑测试每次提交代码自动回归所有脚本。脚本这种东西改起来很容易但回归测试往往没人做等到线上出问题时才发现把别的场景改挂了。3. 核心代码拆解报文转换、头部处理与异常兜底的关键实现3.1 脚本入口消息读取、日志与回写约定每个脚本文件的开头都有统一的入口方法这是从示例工程里所有脚本中抽出来的公共约定。下面这段代码是最小骨架def processData(message) { // CPI运行时注入的绑定变量可以直接用不需要import任何SAP类 def body message.getBody(String.class) def headers message.getHeaders() def properties message.getProperties() log.info(收到消息长度 body.length()) // 中间处理逻辑写在这里 def newBody body.trim() // 回写消息体并返回这个返回值会成为流程下一跳的输入 message.setBody(newBody) return message }getBody(String.class)是按字符串类型读取消息体。如果上游是二进制流比如PDF或图片这里会得到乱码字符串正确做法是先判断消息类型再选择读取方式。log.info是CPI提供的日志入口会写入集成日志中心System.out.println不会。message.setBody之后必须把message对象返回否则流程下游拿到的还是旧的消息体。参数说明getBody支持String、byte[]、InputStream几种常见类型按业务需要指定即可。3.2 JSON转XML递归构建报文体并处理数组节点JSON和XML互转是集成场景里最刚需的能力。很多外部系统只吐JSON而下游ERP只收XML中间就得靠脚本转换。下面这段递归代码是整个content-conversion目录的核心import groovy.json.JsonSlurper import groovy.xml.MarkupBuilder def processData(message) { def body message.getBody(String.class) def json new JsonSlurper().parseText(body) def sw new StringWriter() def xml new MarkupBuilder(sw) xml.setEscapeAttributes(true) buildXml(xml, root, json) message.setBody(sw.toString()) return message } def buildXml(MarkupBuilder xml, String nodeName, def value) { if (value instanceof Map) { // 对象节点key作为子标签名value递归展开 xml.${nodeName}() { value.each { k, v - buildXml(xml, k, v) } } } else if (value instanceof List) { // 数组节点每个元素复用同一个标签名 value.each { item - buildXml(xml, nodeName, item) } } else { // 简单值直接作为标签文本 xml.${nodeName}(value) } }JsonSlurper解析后的JSON对象在Groovy里表现为Map、List、String、BigDecimal等类型。递归时对Map生成嵌套节点对List复用当前标签名简单值直接作为文本节点输出。MarkupBuilder对GString动态节点名是原生支持的这是Groovy构建XML的惯用写法。setEscapeAttributes(true)是为了防止属性值里的特殊字符破坏XML结构。这段代码有几个边界要注意Map的key如果包含空格或非法XML字符生成的标签名不合法空数组会生成一个空标签如果顶层是数组root节点会出现两层的嵌套。实际使用时可以把root节点名和数组元素名抽成方法参数不同接口的差异只是传参不同逻辑本体不用改。这里也藏着一个坑数组元素如果本身是Map会再包一层同名节点后面避坑章节专门讲。3.3 报文头处理与Base64编码适配文件上传场景crypto和header-property这两个目录里的脚本经常配合使用。比如对接一个要求二进制安全的文件上传接口需要把原始报文做Base64编码同时覆盖出站HTTP请求的头信息import java.nio.charset.StandardCharsets def processData(message) { def headers message.getHeaders() // 覆盖出站HTTP请求的Header在CPI里会生效到后续HTTP Sender步骤 headers.put(CamelHttpMethod, POST) headers.put(Content-Type, application/json; charsetUTF-8) headers.put(Accept, application/json) // 对原始报文做Base64编码常用于接口要求二进制安全的场景 def body message.getBody(String.class) def encoded body.getBytes(StandardCharsets.UTF_8).encodeBase64().toString() message.setBody(encoded) return message }Content-Type里带charsetUTF-8是关键不带的话中文会被按ISO-8859-1解析下游拿到乱码。getHeaders()返回的Map在不同CPI版本下行为有差异有的版本是可修改的有的版本修改了也不生效这个坑在避坑章节会细说。Base64编码会把换行符也编码进去如果对端接口要求单行字符串需要在编码后做去除换行的处理。encodeBase64()是Groovy给byte[]加的扩展方法底层是JDK的Base64实现不需要引第三方库。3.4 异常捕获与错误兜底不让流程裸奔集成脚本最容易翻车的地方是异常处理。示例工程里error-handling目录的fallback-json.groovy展示了一个标准写法def processData(message) { def headers message.getHeaders() try { def body message.getBody(String.class) def json new groovy.json.JsonSlurper().parseText(body) // 业务字段校验字段缺失时抛异常 if (!json.orderNo) { throw new IllegalArgumentException(缺少订单号) } message.setBody({\status\:\OK\,\orderNo\:\${json.orderNo}\}) } catch (Exception e) { log.severe(脚本执行异常: ${e.message}) // 回写一个下游能识别的业务错误报文而不是让流程直接挂掉 message.setBody({status:ERROR,code:500,message:处理失败}) headers.put(Content-Type, application/json; charsetUTF-8) } return message }脚本内部的try-catch只能覆盖脚本自身的执行范围比如解析失败、字段缺失这类可控的业务异常。下游HTTP连接超时、映射器类型不一致这些基础设施类异常脚本捕获不到应该交给集成流画布上的Error Handler分支统一处理。原则是业务异常转成标准错误报文返回给调用方基础设施异常让流程进入异常分支两者职责不同不要混在一起。3.5 安全加密AES加解密与SHA-256哈希crypto目录里的脚本解决的是接口签名和数据加密需求。很多企业对外的API要求请求体先用AES加密再传输或者要对报文做SHA-256哈希生成签名。示例里的实现全部基于JDK自带API不引第三方库这很重要。CPI运行时不支持添加自定义jar包用第三方库会直接ClassNotFoundException。下面是AES加密的写法import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec def processData(message) { def body message.getBody(String.class) // 密钥从流程属性里读取避免硬编码在脚本里 def key properties[encryptKey] ?: 0123456789abcdef def encrypted aesEncrypt(body, key) message.setBody(encrypted) return message } def aesEncrypt(String plainText, String key) { def secretKey new SecretKeySpec(key.getBytes(UTF-8), AES) def cipher Cipher.getInstance(AES/ECB/PKCS5Padding) cipher.init(Cipher.ENCRYPT_MODE, secretKey) return cipher.doFinal(plainText.getBytes(UTF-8)).encodeBase64().toString() }AES/ECB/PKCS5Padding是很多外部系统接口文档里约定死的模式虽然从安全角度看ECB不如GCM但对接时只能迁就对端。密钥从流程属性properties里读取而不是硬编码这是示例工程里坚持的习惯——密钥轮换时只需要在CPI流程配置里改属性不需要重新部署脚本。properties这个绑定变量在CPI运行时代表流程属性集合在集成流的Property步骤里预先定义好即可。4. 跑起来本地测试环境搭建与CPI部署操作4.1 环境准备JDK、Groovy与Gradle的版本组合要把这份示例工程在本地跑起来先准备基础环境。JDK用8或11都行Groovy用2.4.xGradle用4.x或5.x这个组合是Groovy 2.4时代兼容性最好的搭配。如果不想在系统里装一堆SDK用Docker起一个Groovy 2.4容器更干净docker run --rm -v $PWD:/workspace -w /workspace groovy:2.4 groovy script/util/base64-encode.groovy为什么非要Groovy 2.4因为CPI运行时内置的Groovy版本就在2.4这个代际附近。本地用2.5、2.6甚至更高版本的新语法写部署到CPI上大概率编译失败。Groovy 2.4以来的闭包、GString、扩展方法在CPI上都没问题但2.5之后新增的语法特性就要慎重。如果项目里现有脚本可以在本地跑通建议先打开脚本文件看一眼头部有没有import第三方包有的话趁早换成JDK自带实现。4.2 本地Mock与Spock测试让脚本在CI里跑起来这个示例工程最有价值的地方就是自带了一套本地测试设施。CPI脚本不好调试每次改完部署到云上再触发流程看日志来回至少十分钟本地测试秒级反馈。测试的核心思路是模拟CPI注入的绑定变量用GroovyShell直接执行脚本文件。下面这段Spock测试覆盖了json-to-xml.groovyimport spock.lang.Specification class JsonToXmlTest extends Specification { def JSON转XML应该生成合法的XML结构() { given: def scriptText new File(script/content-conversion/json-to-xml.groovy).text def binding new Binding() def mockMessage new MockMessage({name:张三,items:[{id:1},{id:2}]}) binding.setVariable(message, mockMessage) binding.setVariable(log, new MockLog()) when: new GroovyShell(binding).evaluate(scriptText) then: def xml new XmlSlurper().parseText(mockMessage.body) xml.name.text() 张三 xml.items.id.size() 2 } }MockMessage需要实现getBody、setBody、getHeaders、getProperties这几个方法内部用一个String字段保存消息体。MockLog只需要实现info和severe方法。binding.setVariable对应CPI运行时的绑定变量注入GroovyShell.evaluate执行脚本文本时脚本里访问的message和log就会指向Mock对象。脚本执行完MockMessage的body字段被改写测试就能直接断言转换结果。这套Mock实现很轻不需要引入CPI的SDK依赖纯Groovy就能跑。本地测试的价值不止是验证逻辑正确性。脚本里的边界分支可以全部覆盖到比如数组为空、字段缺失、密钥长度不对这些情况不用部署到CPI就能触发。等到真正要上线时脚本在CPI上只需要验证运行时兼容性逻辑正确性已经由本地测试兜底了。4.3 把脚本部署到CPI的完整步骤本地跑通了接下来就是把脚本接到CPI集成流里。整体步骤如下登录Integration Suite创建一个新的Integration Flow从Sender通道开始拖一个流程骨架。在需要插入逻辑的位置拖入Script step双击打开脚本编辑区。把示例工程里对应脚本的全部内容粘贴进去确认运行语言选Groovy。如果脚本依赖流程属性比如crypto里的encryptKey先在流程配置里加一个Property步骤或Externalized Parameter把密钥值配置好。为脚本步骤配置异常分支把Error Handler接到错误处理脚本或返回错误报文的分支上。点击Deploy部署等Artifact状态从Starting变成Started这个过程通常需要一两分钟。用Postman或其他HTTP客户端发一条测试报文到Operate页面的监控日志里查看执行记录重点看log.info输出的关键信息。第4步经常有人忽略。脚本里用了properties[encryptKey]但流程里没有定义这个属性运行时会拿到null如果脚本里没做默认值处理密钥就是空的加密结果对端根本解不开。示例脚本里都写了?:默认值但生产环境建议还是在流程属性里显式配置密钥轮换时不用改脚本。调试时有个小技巧在脚本末尾临时把body设置成固定字符串比如message.setBody(DEBUG-MARKER)用来跳过中间逻辑确认流程链路是通的。确认后再把调试代码改回来。这个习惯能省很多排查时间尤其是多分支流程里不知道消息走到哪一步的时候。4.4 日志分级与线上排查CPI的日志体系分几个级别脚本里能用的是log.info、log.severe和log.warning。info记录正常流程的关键节点severe记录异常warning记录需要注意但不影响主流程的情况。线上排查时到Monitor界面的集成流运行实例里可以看到每一步的输入输出内容和脚本日志。这里踩过的一个坑是日志级别设置不当导致大量info日志刷屏把真正有用的severe日志淹没在几百条记录里。示例工程里的脚本日志只记录关键节点比如消息长度、转换后的根节点名、加解密是否成功不做无意义的逐字段打印。5. 避坑记录CPI脚本从本地到上线的五个典型翻车点5.1 ClassNotFoundException第三方库在CPI上不存在现象脚本在本地用Gradle跑测试完全正常打包完粘贴到CPI的Script step里编译也通过了但运行时报ClassNotFoundException指向脚本里import的某个第三方库类。原因CPI运行时不允许添加自定义jar包classpath里只有平台内置的类库和Groovy/JDK自带API。本地开发时Gradle会从Maven仓库拉取依赖所以编译运行都没问题但CPI上没有这些依赖。解决所有加解密、哈希、Base64、HTTP工具类场景一律改用JDK自带API。AES用javax.cryptoSHA-256用java.security.MessageDigestBase64用java.util.Base64或Groovy的encodeBase64()。如果某个功能JDK实现不了需要评估是否真的要在脚本里做或者改走HTTP调用一个现成的公共服务。5.2 本地中文正常部署到CPI后乱码现象脚本在本地测试输出中文正常部署到CPI后下游收到的是乱码比如“张三”变成“å¼ ä¸‰”。原因CPI的getBody(String.class)会按消息头里的Content-Type字符集解码如果上游请求没带charset或带的不是UTF-8解码出来就是乱码。本地测试直接用字符串赋值绕过了字符集问题。解决在脚本前面加一个Content Modifier步骤显式把Content-Type设置为application/json; charsetUTF-8再进入Script step。脚本内部涉及编码转换时统一用StandardCharsets.UTF_8显式指定不要依赖隐式转换。示例工程里的set-header-with-charset.groovy就是专门干这个的。5.3 System.out.println在CPI日志里看不到现象脚本里写了System.out.println(调试信息)部署运行后到监控日志里翻不到任何输出。原因CPI的日志采集走的不是标准输出流System.out不会进入集成运行日志。这是一个让人很恼火的特性第一次搞CPI开发的人基本都中招。解决统一用log.info、log.warning、log.severe替代System.out。本地测试时MockLog里把输出打到控制台保持和CPI一致。如果已经有脚本写满了System.out.println写个简单脚本全局替换成log.info即可注意log对象在CPI注入环境下不需要声明。5.4 修改getHeaders()返回的Map下游Sender不生效现象脚本里对message.getHeaders()返回的Map执行put设置了CamelHttpMethod、Content-Type日志打印也确认写入成功但下游HTTP Sender发出的请求头还是旧的。原因CPI某些版本下getHeaders()返回的是不可变Map修改操作不会同步到Camel Exchange的消息头。或者Headers已被下游组件拷贝了一份快照脚本里改的Map不是Sender真正读取的那个。解决改用message.setHeader(key, value)逐个设置消息头。示例工程里header-property目录的脚本全部采用setHeader写法避免这个版本差异。设置完成后用log.info把最终headers内容打印出来确认一遍这个习惯能省去大量排查时间。5.5 JSON转XML时数组节点被包了两层现象用3.2节的递归脚本转JSON时凡是数组类型都会多出一层父节点。比如items数组有两个元素生成的XML变成itemsitemsid1/id/itemsitemsid2/id/items/items。原因递归函数里List分支调用buildXml(xml, nodeName, item)当item是Map时进入Map分支又生成了一个nodeName节点。数组里的对象节点被重复包了一次。解决List分支里要判断item类型如果item是Map直接展开它的内容而不是再套一层标签。修正后的逻辑def buildXml(MarkupBuilder xml, String nodeName, def value) { if (value instanceof List) { value.each { item - if (item instanceof Map) { // 数组中的对象直接展开字段避免多包一层节点 item.each { k, v - buildXml(xml, k, v) } } else { buildXml(xml, nodeName, item) } } } else if (value instanceof Map) { xml.${nodeName}() { value.each { k, v - buildXml(xml, k, v) } } } else { xml.${nodeName}(value) } }这个坑在示例工程早期版本里真实存在过后来调整递归逻辑并在测试用例里加了对数组结构的断言防止再犯。本地测试的意义就在这里数组结构这种边界场景在CPI上手动触发一次很费劲在Spock测试里一行断言就解决了。6. 二次开发把示例脚本沉淀成自己的集成工具库拿到这份示例工程第一步不要急着往CPI里贴脚本。先按自己团队的接口规范把脚本里的通用约定统一改一遍根节点名、数组节点名、密钥读取位置、日志输出格式。比如json-to-xml.groovy里固定传的root节点名在自己项目里可能要改成Order或者Response这个参数从脚本方法里抽出来放到CPI流程属性里配置这样不同接口复用同一个脚本时只需要改流程属性不需要复制脚本。具体做法是在脚本开头加一段默认值读取def ROOT_NODE properties[rootNode] ?: root def ARRAY_NODE properties[arrayNode] ?: item这样脚本就成了一个模板不同接口的差异全部由流程属性控制。工程里维护一张脚本索引表也值得做。每新增一个对接场景沉淀的脚本就登记一条记录包括脚本文件、适用场景、输入输出格式、依赖项。三个月后再来找脚本翻索引比逐步打开流程图快得多。索引表长这样脚本文件适用场景输入输出依赖json-to-xml.groovy供应商JSON转标准XMLapplication/jsontext/xml无第三方库aes-encrypt.groovy请求体AES加密明文JSONBase64密文JDK javax.cryptosha256-hash.groovy接口签名哈希任意文本64位十六进制哈希JDK MessageDigestfallback-json.groovy业务异常兜底返回任意报文标准错误JSON无还有一个习惯值得养成每个脚本至少配一个Spock测试放在src/test/groovy对应目录下。脚本这种代码最容易被随手改动今天改了一个日期格式以为只影响当前接口结果另一个流程也复用了同一份脚本上线当天下游报表全是错的。从那以后我每次在CPI里新建或修改脚本都强制先本地跑一遍测试再部署这条流程救了我很多次。希望帮到你。本文还有配套的精品资源点击获取