☰
SAP RFC集成实战:sapweb框架的Spring Boot连接与定时同步方案
2026/10/16 5:04:29 网站建设 项目流程

简介:一个基于Spring Boot的Java后台管理系统完整源码,面向需要将SAP数据与外部系统双向同步的Java开发人员。项目整合Spring Boot、MyBatis/MyBatis-Plus、Shiro、Thymeleaf、Quartz与SAPJCO3,实现通过RFC函数获取和写入SAP数据,并对外提供接口服务与定时同步任务,同时涵盖用户、角色等常规后台管理模块;前端采用单页面响应式设计,兼顾后台管理与SAP集成场景,可直接作为企业级开发脚手架。

资源共336个文件,压缩包约9.46MB,其中Java源码占98个,用于实现业务逻辑与SAP交互;41个JavaScript脚本、28个HTML页面与25个CSS样式构成前端界面;13个XML配置、4个YAML、4个properties及3个SQL脚本帮助快速配置环境并初始化数据,另有大量GIF演示动图辅助理解运行效果。

目前已有430人学习下载。读者可对照项目结构学习Shiro权限控制、Quartz定时调度、SAPJCO3调用RFC的完整写法,以及MyBatis-Plus与Druid数据源的集成细节;SQL脚本与配置文件齐全,便于导入数据库后快速启动调试,适合正在搭建SAP集成中间层或后台管理系统的后端工程师参考。

1. 接到 SAP 集成需求时,sapweb 这套方案是最稳的起点

做企业级开发的同行大概率都遇到过这种需求:业务系统要和 SAP 做 RFC 交互,拿物料主数据、同步生产订单状态、读取财务凭证。SAP 的 RFC 接口不像 REST 那样随手一调就通,它依赖 JCo 这个专有连接器,配置复杂、坑还多,很多开发者在第一个目的地配置上就卡了两三天。sapweb 项目就是围绕这个场景搭建的完整骨架:Spring Boot 负责整体应用,MyBatis 管数据持久化,Thymeleaf 做后台页面,Quartz 2 处理定时抓取任务,Shiro 控制登录与权限,sapjco 负责与 SAP 系统的 RFC 通信。它适合那些需要快速把 SAP 接口接进 Web 系统的团队,尤其是前后端不分离、重后台管理的企业应用。下面我把这个项目的拆解过程完整过一遍,包括选型逻辑、RFC 调用写法、定时任务和安全集成的细节,以及那几条只有踩过才知道的坑。

2. 技术选型和项目架构:为什么是这五个框架凑在一起

2.1 数据持久化:MyBatis 在 SAP 集成场景下的优势

SAP 的 RFC 接口返回的数据结构非常固定,通常是表结构或者扁平化的字段集合,这种数据形态和关系型数据库的表天然匹配。MyBatis 相比 JPA 更合适的原因在于:映射关系完全由你控制,SQL 写在 XML 里,遇到复杂的关联查询或者 SAP 数据落地前的清洗逻辑,调整起来远比 JPA 的自动映射灵活。另一个现实因素是,做 SAP 集成的企业项目很多还挂着老旧的 Oracle 或 SQL Server,MyBatis 对这类数据库的方言兼容处理比 JPA 那一套实体关系映射更直接。

实际项目中我通常把 RFC 返回的数据先落到一张临时表,再做业务表更新,这样出错时可以对比数据、手动纠正。MyBatis 的批量插入在这种场景下性能优势很明显。看一下 Mapper 层的基本写法:

<insert id="batchInsertMaterial" parameterType="list"> INSERT INTO material_temp ( matnr, maktx, matkl, meins, erdat ) VALUES <foreach collection="list" item="item" separator=","> (#{item.matnr}, #{item.maktx}, #{item.matkl}, #{item.meins}, #{item.erdat}) </foreach> </insert>

这里把 SAP 物料主数据的五个核心字段写入临时表。foreach的collection对应接口传入的 List 参数名,separator=","让它生成一条多值 INSERT,避免循环单条插入带来的网络往返。注意batchInsertMaterial这个 id 要和 Mapper 接口方法名保持一致,MyBatis 就是靠这个绑定 SQL 和 Java 方法的。

2.2 视图层:Thymeleaf 为什么够用

前后端不分离的项目里,Thymeleaf 是 Spring Boot 生态里最顺手的模板引擎。它支持在 HTML 里直接写服务端表达式,对做过 JSP 的老手几乎零学习成本。作为 SAP 管理后台这种重 CRUD、重列表展示的系统,Thymeleaf 足够撑起所有页面渲染需求。

关键点是 Thymeleaf 处理 SAP 长文本字段时的标签写法。SAP 的物料描述、订单备注经常包含换行和特殊字符,直接输出到 HTML 会打乱布局。我习惯用th:inline="text"配合 JavaScript 做二次处理,这也是我在这个项目里用到 JavaScript 的主要场景:

<div class="material-desc" th:inline="text"> [[${material.maktx}]] </div> <script th:inline="javascript"> const descEle = document.querySelector('.material-desc'); if (descEle) { descEle.innerHTML = descEle.textContent.replace(/\n/g, '<br/>'); descEle.innerHTML = descEle.textContent.replace(/\s{2,}/g, '&nbsp;&nbsp;'); } </script>

服务端把maktx字段输出到 div 里,然后由 JavaScript 把原始文本里的换行符替换成<br/>,连续空格替换为不间断空格。这样 SAP 侧带格式的长文本在页面上不至于挤成一团或者丢失换行。th:inline="javascript"是 Thymeleaf 专为页面内嵌脚本设计的模式,它允许在 JS 代码里直接引用服务端变量,比拼接字符串安全得多。

2.3 调度与安全:Quartz 2 和 Shiro 的分工逻辑

SAP RFC 接口很多是批量数据同步场景,比如每天晚上定时拉取物料或库存,这时候就要 Quartz 2 来驱动。倒不是 Spring 自带@Scheduled不够用,而是 Quartz 2 支持持久化任务存储、集群部署时的任务分配、以及更细粒度的 cron 表达式控制。做 SAP 集成的系统往往需要凌晨跑批,跑批任务失败还要支持手动重跑,Quartz 的JobDetail和Trigger分离设计让这些需求实现得很干净。

Shiro 在这个架构里负责的是 Web 层的登录认证和接口权限。SAP 集成接口属于敏感操作,直接暴露在外网等于把企业核心数据送人。Shiro 的ShiroFilterFactoryBean可以做到对特定 URL 做拦截,未登录用户直接跳登录页。它比 Spring Security 轻量,配置也更直观。来看核心配置:

@Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); factoryBean.setLoginUrl("/login"); factoryBean.setSuccessUrl("/index"); factoryBean.setUnauthorizedUrl("/403"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/api/sap/**", "authc"); filterChainDefinitionMap.put("/**", "authc"); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; }

anon表示匿名可访问,authc表示需要认证,logout是 Shiro 内置的退出过滤器。/api/sap/**单独拎出来拦截,是因为这个路径下的接口都是 SAP 数据查询或同步入口,必须登录后才能调。LinkedHashMap保证过滤器链从上到下匹配,顺序写错会导致拦截规则失效。

2.4 整体目录结构与启动流程

项目的包结构可以参考下面的划分,按职责分层,后续加功能不会乱:

com.sapweb ├── config // Shiro、Quartz、JCo 配置类 ├── controller // 页面跳转和 REST 接口 ├── service // RFC 调用、业务处理 ├── mapper // MyBatis Mapper 接口 ├── model // 实体类和 DTO ├── job // Quartz 定时任务 └── util // JCo 工具封装、日期处理等

启动流程是:Spring Boot 启动时加载 JCo 配置类完成 SAP destination 初始化,Shiro 配置类建立安全过滤链,Quartz 配置类扫描@Component注解的 Job 类注册触发器。三者互不阻塞,但 JCo 配置如果失败,Spring Boot 不会自动终止启动,这一点后面避坑章节会详细说。

3. SAP JCo 的 RFC 交互实现:从连接配置到数据映射

3.1 JCo destination 配置:一种必须掌握的连接方式

SAP JCo 的核心概念是 destination,它代表一个到 SAP 系统的命名连接配置。JCo 3.0 支持在属性文件里配置 destination,也支持通过代码动态注册。项目里我采用的是文件配置,通过DestinationDataProvider注册一个自定义的DestinationDataProvider实现,这样可以动态读取数据库里的连接配置,不用改代码就能切换 SAP 环境(开发、测试、生产)。

public class SapDestinationConfigurer { private static final String ABAP_AS = "abap"; public static void register() { if (JCoDestinationManager.isDestinationAvailable(ABAP_AS)) { return; } try { DestinationDataProvider provider = new DestinationDataProvider() { @Override public Properties getDestinationProperties(String destinationName) { Properties props = new Properties(); props.setProperty(DestinationDataProvider.JCO_ASHOST, "192.168.1.100"); props.setProperty(DestinationDataProvider.JCO_SYSNR, "00"); props.setProperty(DestinationDataProvider.JCO_CLIENT, "800"); props.setProperty(DestinationDataProvider.JCO_USER, "RFCHandler"); props.setProperty(DestinationDataProvider.JCO_PASSWD, "encrypted-password"); props.setProperty(DestinationDataProvider.JCO_LANG, "ZH"); return props; } }; JCoDestinationManager.registerDestinationDataProvider(provider); } catch (Exception e) { throw new RuntimeException("SAP destination 注册失败", e); } } public static JCoDestination getDestination() throws JCoException { return JCoDestinationManager.getDestination(ABAP_AS); } }

这段代码手动注册了一个名为abap的 destination。JCO_ASHOST是 SAP 应用服务器 IP,JCO_SYSNR是系统编号,JCO_CLIENT是客户端编号,JCO_USER和JCO_PASSWD是 SAP 通信账号。JCO_LANG=ZH让 SAP 返回中文描述文本。注意isDestinationAvailable方法先做了判断,防止重复注册导致异常。

3.2 同步调用 RFC 函数:以读取物料主数据为例

SAP 的 RFC 函数通过JCoDestination.getRepository()获取函数模板,然后创建实例、传入参数、执行。这是一套固定流程。关键点在于参数的传递方式:导入参数(Import)直接赋值,表参数(Table)需要先获取 JCoTable 对象再逐行填充。BAPI 的返回结果通常包含RETURN表,里面是消息类型(S/E/W)和消息文本,必须处理。

public List<MaterialDto> fetchMaterialData(String matnrPrefix) { List<MaterialDto> resultList = new ArrayList<>(); try { JCoDestination dest = SapDestinationConfigurer.getDestination(); JCoFunction function = dest.getRepository().getFunction("BAPI_MATERIAL_GETLIST"); if (function == null) { throw new RuntimeException("RFC 函数不存在,请检查函数名"); } function.getImportParameterList().setValue("MATL_NUMBER", matnrPrefix + "*"); function.getImportParameterList().setValue("MAX_ROW", 1000); JCoParameterList tables = function.getTableParameterList(); JCoTable materialTable = tables.getTable("MATERIAL_DATA"); function.execute(dest); if (materialTable.getNumRows() > 0) { for (int i = 0; i < materialTable.getNumRows(); i++) { materialTable.setRow(i); MaterialDto dto = new MaterialDto(); dto.setMatnr(materialTable.getString("MATERIAL")); dto.setMaktx(materialTable.getString("MATERIAL_DESC")); dto.setMatkl(materialTable.getString("MATERIAL_TYPE")); dto.setMeins(materialTable.getString("BASE_UOM")); dto.setErdat(materialTable.getString("CREATED_DATE")); resultList.add(dto); } } JCoTable returnTable = tables.getTable("RETURN"); if (returnTable != null && returnTable.getNumRows() > 0) { returnTable.setRow(0); String type = returnTable.getString("TYPE"); if ("E".equals(type)) { throw new RuntimeException("SAP RFC 调用失败: " + returnTable.getString("MESSAGE")); } } } catch (JCoException e) { log.error("RFC 调用异常,错误码: {}", e.getGroup(), e); throw new RuntimeException("JCo 调用失败,错误分组: " + e.getGroup(), e); } return resultList; }

这段代码的要点:getFunction拿到 RFC 函数模板,setValue设置导入参数,getTable获取输出表,execute执行调用,最后遍历结果集并检查RETURN表。MATL_NUMBER支持通配符,传*可以模糊匹配物料编码。MAX_ROW限制最大返回行数,防止一次拉取太多数据撑爆内存。JCoException.getGroup()可以拿到错误分组,连接失败、权限不足、参数错误都属于不同分组,方便排查。

3.3 参数配置与常见参数说明

RFC 调用时参数名称必须和 SAP 侧函数定义完全一致,大小写敏感,差一个字符都调不通。下面整理了项目里最常用的几类参数:

参数名所属类别作用示例
MATL_NUMBERImport物料编号,支持通配符1500*
MAX_ROWImport最大返回行数1000
MATERIAL_DATATable物料主数据输出表遍历读取
RETURNTable函数执行消息检查返回类型
PLANTImport工厂编号,用于库存查询1000

一个值得注意的边界是:MATL_NUMBER传*会扫描所有物料,数据量大时 SAP 侧会非常吃资源,生产环境一定要有限制条件。我一般要求业务侧必须传物料编码前缀或者最后修改日期范围,避免全表扫描。

3.4 数据落地:从 JCo 表对象到 MyBatis 批量写入

RFC 拿回来的数据不会直接塞进业务表,先插入临时表做二次校验是常见做法。这里有一个性能优化细节:每次循环里setRow(i)切换游标,取值后直接塞进批量 List,攒够一千条就执行一次批量插入,比全部攒完再插入更稳,内存占用可控。

public void syncMaterialData(String matnrPrefix) { List<MaterialDto> materialList = fetchMaterialData(matnrPrefix); if (CollectionUtils.isEmpty(materialList)) { return; } List<List<MaterialDto>> partitions = Lists.partition(materialList, 500); for (List<MaterialDto> partition : partitions) { materialMapper.batchInsertMaterial(partition); materialMapper.deleteDuplicateFromTemp(); } }

Lists.partition是 Guava 提供的分片工具,把大 List 拆成每片 500 条的小列表,分批写入。deleteDuplicateFromTemp在每批插入后清理临时表里重复的物料编码,这是为了防止 SAP 侧返回了历史重复数据。实际项目里这个清洗逻辑会更复杂,可能还要比对ERDAT日期取最新,这里给出的是最简版本。

4. 系统集成:定时任务、权限控制和前端渲染

4.1 Quartz 调度:让 RFC 定时自动执行

RFC 同步任务通常是凌晨跑批,Quartz 2 的好处在于即使应用重启,任务状态也不会丢。下面是一个典型的物料同步 Job 设计,重点看 Job 里如何拿到 Spring 管理的 Service——直接用@Autowired是空指针,因为 Quartz 实例化 Job 不走 Spring 容器。

@Component public class MaterialSyncJob extends QuartzJobBean { @Autowired private MaterialSyncService materialSyncService; @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { String materialPrefix = context.getMergedJobDataMap().getString("materialPrefix"); try { materialSyncService.syncMaterialData(materialPrefix); } catch (Exception e) { log.error("物料同步任务失败", e); JobExecutionException ex = new JobExecutionException(e); ex.setUnscheduleAllTriggers(false); ex.setRefireImmediately(true); throw ex; } } }

QuartzJobBean是 Spring 提供的适配类,它让 Job 实例的创建交给 Spring 容器,@Autowired才能生效。getMergedJobDataMap可以取到触发任务时传入的参数,这样同一份 Job 逻辑可以通过不同参数跑不同业务。setRefireImmediately(true)表示任务失败后自动重试一次,setUnscheduleAllTriggers(false)表示重试失败也不停掉触发器,避免一次失败导致后续所有同步任务被禁用。

4.2 注册 Quartz 触发器的配置写法

任务的调度配置我习惯写在配置类里,方便集中管理 cron 表达式。两个典型触发器:一个每天凌晨两点跑增量同步,一个每周日做全量刷新。

@Configuration public class QuartzSchedulerConfig { @Bean public JobDetail materialSyncJobDetail() { JobDetail job = JobBuilder.newJob(MaterialSyncJob.class) .withIdentity("materialSyncJob") .usingJobData("materialPrefix", "1*") .storeDurably() .build(); return job; } @Bean public Trigger materialSyncTrigger() { return TriggerBuilder.newTrigger() .forJob(materialSyncJobDetail()) .withIdentity("materialSyncTrigger") .withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")) .build(); } @Bean public Trigger materialFullSyncTrigger() { return TriggerBuilder.newTrigger() .forJob(materialSyncJobDetail()) .withIdentity("materialFullSyncTrigger") .withSchedule(CronScheduleBuilder.cronSchedule("0 30 1 ? * SUN")) .build(); } }

使用usingJobData传入了物料前缀参数,在 Job 里通过context.getMergedJobDataMap().getString("materialPrefix")读取。storeDurably()表示即使没有触发器关联,JobDetail 也要保留,方便后续动态添加新触发器。cron 表达式按顺序是秒、分、时、日、月、周,0 0 2 * * ?就是每天凌晨两点整,0 30 1 ? * SUN是每周日凌晨一点半。

4.3 Shiro 权限控制细化到接口级别

sapweb 里的 SAP 接口不能所有登录用户都能调,通常要区分管理员、业务员、只读角色。Shiro 支持在方法上通过注解做权限校验,前提是开启@RequiresPermissions注解支持。

@RequiresRoles("admin") @GetMapping("/api/sap/material/sync") public Result triggerMaterialSync(@RequestParam String prefix) { materialSyncService.syncMaterialData(prefix); return Result.success("同步任务已触发"); }

@RequiresRoles("admin")表示只有 admin 角色的用户可以访问这个接口,其他人直接 403。角色在 Shiro 的 Realm 里通过AuthorizationInfo.addRole()赋值。这样一个普通的业务账号就不能手动触发全量同步,只有管理员有这个权限。如果不加这个注解,任何登录用户都可以调同步接口,生产环境很容易出事。

4.4 Thymeleaf 渲染 RFC 查询结果的完整页面逻辑

页面端把 RFC 查询结果用表格展示,支持按物料编码模糊搜索。服务端 controller 返回一个ModelAndView,页面通过 Thymeleaf 表达式直接渲染。下面给出查询接口和页面片段:

@GetMapping("/material/list") public String listMaterial(@RequestParam(defaultValue = "") String keyword, Model model, @RequestParam(defaultValue = "1") int pageNum) { PageHelper.startPage(pageNum, 20); List<MaterialDto> list = materialMapper.searchMaterial(keyword); PageInfo<MaterialDto> pageInfo = new PageInfo<>(list); model.addAttribute("pageInfo", pageInfo); model.addAttribute("keyword", keyword); return "material/list"; }
<table class="table table-hover"> <thead> <tr> <th>物料编码</th> <th>物料描述</th> <th>物料类型</th> <th>基本单位</th> <th>创建日期</th> </tr> </thead> <tbody> <tr th:each="mat : ${pageInfo.list}"> <td th:text="${mat.matnr}"></td> <td th:text="${mat.maktx}"></td> <td th:text="${mat.matkl}"></td> <td th:text="${mat.meins}"></td> <td th:text="${mat.erdat}"></td> </tr> </tbody> </table>

PageHelper 是 MyBatis 的分页插件,PageHelper.startPage后面紧跟的第一条查询会被自动加上 limit 分页。PageInfo封装了总记录数、总页数、当前页等分页信息。Thymeleaf 的th:each遍历分页列表,th:text输出字段值。这里分页的页码参数pageNum从页面请求里带过来,前端需要处理点击页码时重新发起请求。

5. 避坑与排查:JCo 环境下最容易翻车的五个场景

5.1 现象:系统运行几天后 SAP 调用全部卡死

排错时的表现是所有 RFC 调用都超时,应用日志里全是JCoException: JCO_ERROR_COMMUNICATION,但应用本身没有崩溃,后台页面还能打开。原因基本是 SAP 侧 RFC 连接数被占满,没有释放。查 SAP 侧 sm50 看到的 RFC 会话数接近上限,应用池视角看是 JCo 的连接池配置太小或者长连接泄漏。

解决方法是双层处理:SAP 侧给通信账号增加 RFC 并发会话上限,应用侧设置合理的连接池容量。JCo 的JCoDestination默认连接池参数需要手动调,连池空闲连接回收时间不要设太长:

Properties props = new Properties(); props.setProperty(DestinationDataProvider.JCO_POOL_CAPACITY, "10"); props.setProperty(DestinationDataProvider.JCO_PEAK_LIMIT, "30"); props.setProperty(DestinationDataProvider.JCO_EXPIRATION_TIME, "300000");

JCO_POOL_CAPACITY是连接池的基本容量,JCO_PEAK_LIMIT是最大峰值连接数,JCO_EXPIRATION_TIME是连接最大空闲时间(毫秒)。从那以后我每次上线前都会确认这两个参数,避免无上限增长把 SAP 连接耗死。

5.2 现象:RFC 返回几千行数据直接内存溢出

物料查询时没有加行数限制,SAP 返回两万行数据,应用直接 OOM。原因是MAX_ROW参数没设置,SAP 默认全量返回,加上 JCo 的 JCoTable 会把数据全部加载到内存。

解决方法是在调用 BAPI 之前强制设置MAX_ROW,并且配合分页逻辑。SAP 侧很多 BAPI 支持传入行数限制,但不支持游标分页,所以数据量确实大时只能按物料前缀拆分查询。我在fetchMaterialData方法里固定传了 1000 行上限,如果业务需要更多数据,就按前缀拆成多次调用。

5.3 现象:RFC 调用成功了,但数据库里没有数据

事务边界问题。服务方法上有@Transactional注解,而 RFC 调用完成后、事务提交之前,MyBatis 写入的数据还没有真正落库。如果后面代码抛异常,整个事务回滚,RFC 那边已经执行过的操作无法回滚,数据就对不上了。

解决方法是把 RFC 调用和本地数据库写入拆到两个事务里。RFC 调用方法不要加@Transactional,写完临时表后再开启本地事务做业务更新。如果业务上确实要保证 SAP 和数据库的一致性,那就要引入补偿机制,定期比对 SAP 侧数据和本地数据。这个坑在同步类项目里出现频率非常高。

5.4 现象:SAP 返回的中文描述在页面上乱码

读取的物料描述在数据库里正常,导出 Excel 或渲染到页面时变成乱码。原因一般是 JCo 层的字符编码没配套,SAP 侧返回的是非 Unicode 编码,而 JVM 默认 UTF-8。

处理方法是在 destination 配置里显式指定语言和编码,JCO_LANG设置成ZH,同时在读取物料描述后做一次字符集转换兜底:

String rawText = materialTable.getString("MATERIAL_DESC"); String desc = new String(rawText.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);

不过这个转换并非所有情况都适用,关键还是要 SAP 侧用 Unicode 字符串返回文本。如果是老系统非 Unicode 模式,只能在 JCo 配置侧想办法。排错时先确认 SAP 系统是不是 Unicode 模式,再看 JCo 的代码页设置。

5.5 现象:Shiro 登录后在定时任务里报未授权

Quartz 任务的执行线程不在 Shiro 的Subject上下文里,任务里访问用户信息或者做权限校验会拿到 null 或者抛出UnavailableSecurityContextException。这是因为 Shiro 把安全上下文绑定在ThreadLocal上,Quartz 的工作线程继承不到。

解决方式分两种:如果任务本身不需要用户上下文,直接在任务里用系统服务账号操作;如果必须要用户身份,可以在任务触发时手动绑定 Subject:

Subject subject = SecurityUtils.getSubject(); subject.runAs(userPrincipal); try { // 执行需要权限的操作 } finally { subject.releaseRunAs(); }

runAs让当前线程临时获得指定用户身份,releaseRunAs结束后释放。不过实际项目里我很少这么干,更常见的做法是定时任务用独立服务账号跑,越权风险更小。

6. 验证与进阶:把同步任务做成可观测的

RFC 同步类功能光跑通不够,还要能验证任务到底执行了什么、执行到什么程度。我的习惯是从三个层面做观测,这三步验证在 sapweb 上线初期帮我发现了不少隐性问题。

第一步是给每次 RFC 同步加一个批次号。从 Quartz 触发器到 Service 方法,再到数据库临时表,都带同一个批次号。这样某一次数据异常,可以直接按批次号查日志和数据库,定位是 SAP 返回的问题还是本地清洗的问题。实现方式是在 JobDetail 的 JobDataMap 里放一个 UUID,Job 启动时写入 ThreadLocal,Service 层插入临时表时带上这个值。

第二步是记录同步结果的计数。每次 RFC 调用返回后,把拉取到的行数、写入成功的行数、清洗掉的行数分别打到日志里。时间长了可以按天看趋势,比如某天同步行数异常变少,可能就是 SAP 侧数据没更新,或者筛选条件被谁改过。

第三步是 SAP 侧和本地侧各留一份汇总对比。SAP 侧用另一个 RFC 函数统计物料总数,本地也用 SQL 统计数量,两个数字不一致说明同步有漏,触发告警逻辑。这个对账逻辑不复杂但对数据完整性意义很大。同步数据如果数量对不上,最麻烦的不是后来发现数据不对,而是用户基于不完整的数据做了业务决策。

每次上线新的 RFC 同步任务,我都会强制走一遍这三步验证:批次号能不能贯穿全链路,计数日志是否能对上,对账是否能稳定跑通。之前吃过一次亏,看着日志全是满了,其实写入失败的记录被吞掉了,从那以后我对同步完成率就特别敏感,宁可多打几行日志也不愿意对着黑匣子猜。希望这套拆解帮到你,无论是接入 SAP 还是排查集成问题,关键是把连接、事务、事务边界、可观测性这四个点想清楚。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询