资讯动态

SpringBoot整合SAP RFC实战:JCo连接池与Quartz定时同步

发布时间:2026/10/20 1:47:50 来源:尧图企业网站定制
简介这是一套基于SpringBoot的Java后台管理系统源码面向需要打通SAP系统与外部业务数据的企业开发者和Java后端学习者。项目整合Spring、Spring MVC、MyBatis-Plus、Druid、Shiro、Thymeleaf与Quartz并通过sapjco3调用SAP的RFC函数实现数据获取与回传再以接口形式对外提供服务同时借助定时任务完成数据定时同步适合用于学习企业级SAP集成方案与权限、调度等模块的落地写法。资源包共336个文件以98个java源码、41个js脚本、28个html页面、25个css样式及13个xml配置为主另含sql、yml、properties等配置与字体、图片资源压缩包约9.46MB目录结构完整。目前已有432人学习下载可帮助读者快速理解RFC交互流程、接口设计与定时同步任务的实现思路。1. 从一次对接翻车说起这个 SAP 交互系统到底解决什么问题去年帮一家制造企业做 ERP 周边系统时我遇到一个典型场景业务部门要在自研的 Web 平台上发起采购申请、查询物料主数据、回写生产工单状态而这些数据全都躺在 SAP 里。最原始的做法是让业务员在 SAP GUI 里手工录一遍再在 Web 系统里录一遍两边数据对不上就互相甩锅。后来我们决定用 Java 直接打通 SAP 的 RFC 接口把双向同步做成后台服务。这套sapweb项目就是干这个的——用 SpringBoot 做骨架MyBatis 管持久化Thymeleaf 渲染页面Quartz 跑定时同步任务Shiro 做权限控制最核心的是通过 SAP JCo 调用 RFC 函数完成与 SAP 系统的实时交互。它适合两类人一是手里有 SAP 环境、需要做外围系统集成的后端开发二是想找一个「SpringBoot 全家桶 企业级中间件」完整参考项目的学习者。项目本身不复杂但把 RFC 调用、连接池管理、定时任务、权限过滤这几块串成了一条能跑的链路拿来当脚手架改一改就能上生产。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份源码拆开讲。2. 技术栈与整体架构五个组件怎么各司其职2.1 分层结构与依赖关系这个项目是标准的单体分层架构没有上微服务那套。原因很实际SAP RFC 调用本身是同步阻塞的拆成微服务反而增加网络开销和事务一致性麻烦。整体分四层表现层Thymeleaf 模板 Controller负责页面渲染和表单提交。选 Thymeleaf 而不是 JSP是因为它天然支持 HTML 原型前端改完直接浏览器打开就能看效果不用等后端编译。业务层Service 接口与实现封装 RFC 调用的参数组装、结果解析、异常转换。持久层MyBatis Mapper XML管本地业务表和同步日志表。基础设施层Shiro 安全过滤、Quartz 调度、SAP JCo 连接管理。依赖注入全部走 SpringBoot 自动配置pom.xml里关键依赖就五个spring-boot-starter-web、mybatis-spring-boot-starter、thymeleaf、quartz、shiro-spring外加一个本地安装的sapjco3.jar。注意 JCo 这个包不在 Maven 中央仓库得从 SAP 官方渠道拿到对应操作系统的版本手动mvn install:install-file装进本地仓库这是第一个容易卡住新手的点。2.2 SAP JCo 连接配置与 RFC 调用封装JCo 的核心是JCoDestination和JCoFunction。项目里我一般会写一个SapRfcService统一管理连接避免每次调用都重新建连。配置放在application.ymlsap: jco: # SAP 应用服务器地址按实际环境填写 ashost: 192.168.1.100 sysnr: 00 client: 100 user: RFC_USER passwd: RFC_PASS lang: ZH pool: max-connections: 10 peak-limit: 5对应的 Java 配置类读取这些参数生成Properties并交给JCoDestinationManagerConfiguration public class SapConfig { Value(${sap.jco.ashost}) private String ashost; Value(${sap.jco.sysnr}) private String sysnr; Value(${sap.jco.client}) private String client; Value(${sap.jco.user}) private String user; Value(${sap.jco.passwd}) private String passwd; Bean public JCoDestination jCoDestination() throws JCoException { Properties props new Properties(); props.setProperty(DestinationDataProvider.JCO_ASHOST, ashost); props.setProperty(DestinationDataProvider.JCO_SYSNR, sysnr); props.setProperty(DestinationDataProvider.JCO_CLIENT, client); props.setProperty(DestinationDataProvider.JCO_USER, user); props.setProperty(DestinationDataProvider.JCO_PASSWD, passwd); // 连接池参数生产环境必须配否则并发一上来就崩 props.setProperty(DestinationDataProvider.JCO_POOL_CAPACITY, 10); props.setProperty(DestinationDataProvider.JCO_PEAK_LIMIT, 5); return JCoDestinationManager.getDestination(props); } }参数说明ashost是 SAP 应用服务器 IPsysnr是系统编号通常 00 或 01client是集团号。连接池的POOL_CAPACITY控制最大空闲连接数PEAK_LIMIT控制峰值并发这两个值要根据 SAP 那边rdisp/max_wprun_time和对话进程数来调配大了会把 SAP 的对话进程占满配小了高并发时请求排队。调用 RFC 的通用方法长这样public JCoFunction callRfc(String functionName, MapString, Object imports) throws JCoException { JCoDestination destination jCoDestination(); JCoFunction function destination.getRepository().getFunction(functionName); if (function null) { throw new RuntimeException(RFC 函数不存在: functionName); } // 填充导入参数 imports.forEach((k, v) - function.getImportParameterList().setValue(k, v)); function.execute(destination); return function; }逻辑说明先从仓库拿到函数模板再逐字段塞入导入参数最后执行。返回的JCoFunction里可以取getExportParameterList()和getTableParameterList()。这里有个血泪经验——SAP 的 RFC 函数参数名大小写敏感而且结构体字段类型必须严格匹配传错一个类型直接抛JCoRuntimeException排查起来很费劲。2.3 Shiro 权限与 Quartz 定时任务的集成方式Shiro 的配置走ShiroFilterFactoryBean拦截/sap/**路径需要rfc:invoke权限登录页和静态资源放行。Realm 从数据库查用户和权限密码用HashedCredentialsMatcher加盐哈希。Quartz 这边项目把同步任务定义在数据库里通过SchedulerFactoryBean注入 Spring 上下文Job 类里直接Autowired业务 Service。Configuration public class ShiroConfig { Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager sm) { ShiroFilterFactoryBean bean new ShiroFilterFactoryBean(); bean.setSecurityManager(sm); MapString, String chain new LinkedHashMap(); chain.put(/login, anon); chain.put(/css/**, anon); chain.put(/sap/**, perms[rfc:invoke]); // 关键RFC 接口必须鉴权 chain.put(/**, authc); bean.setFilterChainDefinitionMap(chain); return bean; } }Quartz 的 Job 里调用 RFC 时要注意Job 实例默认不是 Spring 管理的得用AutowiringSpringBeanJobFactory把依赖注入进去否则Autowired全是 null。这个坑我在三个项目里踩过每次都是任务跑起来报空指针才想起来。3. 从零跑起来环境准备与启动步骤3.1 本地环境与 SAP JCo 安装跑这个项目的前提是你得有一个能连的 SAP 环境或者至少有一套 RFC 目标配置。本地需要 JDK 8 或 11JCo 3.x 对高版本 JDK 支持有限别用 17、Maven 3.6、MySQL 5.7。JCo 的安装分三步# 1. 把 sapjco3.jar 和对应平台的 native 库放到项目 lib 目录 # 2. 安装 jar 到本地 Maven 仓库 mvn install:install-file -Dfilelib/sapjco3.jar \ -DgroupIdcom.sap -DartifactIdsapjco3 -Dversion3.1.6 -Dpackagingjar # 3. 把 native 库路径加到启动参数 java -Djava.library.path./lib -jar sapweb.jarWindows 下 native 库是sapjco3.dllLinux 下是libsapjco3.so。如果启动报UnsatisfiedLinkError九成是java.library.path没指对或者 dll 和 so 版本跟 jar 不匹配。3.2 数据库初始化与配置修改项目自带schema.sql和data.sql建表语句包含用户表、权限表、同步日志表、任务配置表。执行顺序是先建库再跑脚本CREATE DATABASE sapweb DEFAULT CHARACTER SET utf8mb4; USE sapweb; -- 然后执行项目 resources/db/schema.sqlapplication.yml里要改三处数据源连接、SAP JCo 参数、Quartz 的org.quartz.jobStore.class配置。如果不想用数据库存任务可以改成RAMJobStore但生产环境不建议重启任务就丢了。3.3 启动验证与第一个 RFC 调用启动命令mvn spring-boot:run看到Started SapwebApplication就算起来了。验证 RFC 是否通最直接的办法是写一个测试接口GetMapping(/sap/ping) RequiresPermissions(rfc:invoke) public String ping() { try { // 调用 SAP 标准函数 RFC_PING只验证连通性 callRfc(RFC_PING, Collections.emptyMap()); return SAP 连接正常; } catch (JCoException e) { return 连接失败: e.getMessage(); } }访问http://localhost:8080/sap/ping返回「SAP 连接正常」说明链路通了。如果报logon denied检查用户名密码和 client报connection refused检查 ashost 和 sysnr以及 SAP 的rdisp/gwserv是否允许外部连接。4. 核心模块拆解RFC 调用链路与数据同步实现4.1 物料主数据查询的完整链路以查询物料主数据为例SAP 标准函数是BAPI_MATERIAL_GET_DETAIL。前端 Thymeleaf 页面提交物料号Controller 接收后调 ServiceService 组装参数调 RFC再把返回的JCoStructure转成 Java Bean 返回页面。public MaterialDto getMaterialDetail(String materialNo) throws JCoException { MapString, Object imports new HashMap(); imports.put(MATERIAL, materialNo); JCoFunction function callRfc(BAPI_MATERIAL_GET_DETAIL, imports); // 取导出参数中的结构体 JCoStructure detail function.getExportParameterList() .getStructure(MATERIAL_GENERAL_DATA); MaterialDto dto new MaterialDto(); dto.setMaterialNo(detail.getString(MATERIAL)); dto.setDescription(detail.getString(MATL_DESC)); dto.setBaseUnit(detail.getString(BASE_UOM)); return dto; }逻辑说明getStructure拿到的是嵌套结构字段名必须跟 SAP 里定义的一模一样。getString对空值会返回空字符串而不是 null这点比 JDBC 友好。如果字段是数值类型用getDouble或getBigDecimal别用getString再 parse精度会丢。4.2 定时同步任务的 Quartz 实现同步任务的核心是把 SAP 的变更数据拉到本地表。项目里定义了一个MaterialSyncJob每 30 分钟跑一次调用BAPI_MATERIAL_GETLIST拿增量列表再逐条查详情入库。DisallowConcurrentExecution // 防止上一次没跑完下一次又进来 public class MaterialSyncJob implements Job { Autowired private MaterialService materialService; Override public void execute(JobExecutionContext context) { try { ListString changed materialService.fetchChangedMaterials(); for (String no : changed) { MaterialDto dto materialService.getMaterialDetail(no); materialService.upsertLocal(dto); // 本地有则更新无则插入 } } catch (Exception e) { // 记录同步日志别让异常吞掉 materialService.logSyncError(e); } } }参数说明DisallowConcurrentExecution是必须加的否则任务执行时间超过间隔时会并发跑导致重复插入。upsertLocal用 MyBatis 的INSERT ... ON DUPLICATE KEY UPDATE实现比先查后插少一次数据库往返。4.3 异常处理与事务边界RFC 调用失败分两类网络层异常JCoException和业务层异常SAP 返回BAPIRET2错误表。项目里统一在 Service 层捕获转成自定义的SapRfcException带上错误码和消息。事务边界要特别注意RFC 调用本身不在本地事务里所以「调 RFC 成功但本地入库失败」这种情况必须靠补偿机制项目里用同步日志表记录每次调用的状态失败的重试。Transactional public void syncWithRetry(String materialNo) { try { MaterialDto dto getMaterialDetail(materialNo); upsertLocal(dto); logSync(materialNo, SUCCESS, null); } catch (Exception e) { logSync(materialNo, FAILED, e.getMessage()); throw new SapRfcException(同步失败, e); } }注意Transactional只覆盖本地数据库操作RFC 调用不受事务控制。如果 RFC 成功但本地回滚下次重试会重复调用 RFC所以 RFC 函数最好设计成幂等的或者用 SAP 的BAPI_TRANSACTION_COMMIT显式控制提交时机。5. 避坑与常见问题排查5.1 JCo 连接池耗尽导致请求阻塞现象系统运行一段时间后所有 RFC 调用都卡住日志里出现No free connection available。原因连接池的PEAK_LIMIT设得太小或者有代码拿到连接后没释放。JCo 的连接是自动归还的但如果在execute之后又调了getExportParameterList并持有引用连接可能被占用。解决把PEAK_LIMIT调到跟 SAP 对话进程数匹配的值同时检查代码里有没有在循环里反复getDestination应该复用同一个JCoDestination实例。5.2 中文乱码与字符集不匹配现象从 SAP 取回的中文描述显示成问号或乱码。原因JCo 默认用 UTF-8但 SAP 系统如果是非 Unicode 的字符集可能是 GBK 或 Latin1。解决在 JCo 配置里加JCO_CODEPAGE参数或者在application.yml里指定sap.jco.codepage: 8400对应 GBK。另外 Thymeleaf 页面要声明meta charsetUTF-8数据库连接串加useUnicodetruecharacterEncodingutf8。5.3 Quartz 任务在集群环境下重复执行现象部署两个节点后同一个同步任务在两个节点上各跑了一遍数据重复。原因Quartz 默认用RAMJobStore每个节点独立调度。解决改用JobStoreTX并配置数据库表设置org.quartz.jobStore.isClusteredtrue同时给任务加DisallowConcurrentExecution。集群模式下 Quartz 会通过数据库锁保证同一时刻只有一个节点执行。5.4 Shiro 权限缓存导致修改不生效现象给用户加了新权限但用户不重新登录就还是提示无权限。原因Shiro 的AuthorizingRealm默认缓存授权信息doGetAuthorizationInfo不会每次调用。解决在修改权限后手动调realm.clearCachedAuthorizationInfo(principals)或者把缓存实现换成 Redis 并设置较短的过期时间。开发阶段可以直接关掉缓存securityManager.setCacheManager(null)。5.5 RFC 函数参数类型不匹配现象调用 RFC 时报JCoRuntimeException: Field X has wrong type。原因SAP 的字段类型是CHAR、NUMC、DATS等Java 传 String 进去时 JCo 会尝试转换但NUMC类型如果传了非数字字符就会失败。解决调用前用function.getImportParameterList().getFieldMetaData(FIELD_NAME)查一下字段类型按类型传值。日期字段用JCoDate或yyyyMMdd格式的字符串别传java.util.Date。6. 二次开发改造从能跑到好用的几个关键动作拿到这套源码后最值得改的地方有三个。第一是把 RFC 调用做成异步。现在所有调用都是同步阻塞的页面提交后要等 SAP 返回才能渲染物料多的时候体验很差。改造思路是用CompletableFuture包一层Controller 返回DeferredResult前端轮询或走 WebSocket 拿结果。注意 JCo 的JCoDestination是线程安全的但JCoFunction不是每个线程要独立创建。第二是加一层 RFC 调用日志和重试。生产环境最怕的是「调了但不知道成没成」。我一般会在callRfc方法里埋点记录函数名、入参摘要、耗时、返回码写到rfc_call_log表。失败的重试用 Spring Retry配置Retryable(maxAttempts 3, backoff Backoff(delay 1000))但要注意幂等性——查询类函数随便重试创建类函数重试前得先查一下是否已创建。第三是把 SAP 连接配置外置到配置中心。现在参数写死在application.yml里换环境要改代码重新打包。改成从 Nacos 或 Apollo 读取配合RefreshScope实现动态刷新。但连接池参数改了之后需要重建JCoDestination得写一个监听器在配置变更时销毁旧实例。验证改造效果的方法很简单写一个压测脚本用 JMeter 并发调/sap/ping接口看连接池是否稳定、响应时间是否线性增长。我一般会跑 50 并发持续 5 分钟观察 SAP 那边的对话进程占用和本地内存。如果PEAK_LIMIT设成 5 而并发是 50响应时间会飙升这时候要么加连接数要么把同步调用改成异步队列。从那以后我每次接 SAP 集成的活都强制先跑一遍RFC_PING和连接池压测确认链路和容量再写业务代码。这套sapweb的骨架不算新但把 JCo、Shiro、Quartz 这几个容易出问题的点都串起来了拿来当起点能省不少搭环境的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑