
简介这是一份基于Spring Boot构建的SAP RFC交互后台管理系统源码适合需要对接SAP系统的Java开发者、后端工程师及项目架构师。项目完整集成MyBatis、MyBatis-Plus、Shiro、Quartz、thymeleaf以及SAPJCO3通过调用SAP RFC函数实现SAP与外部系统之间的双向数据同步同时提供定时任务调度、权限控制、单页面响应式前端等企业级后台常用能力。资源包共336个文件大小约9.46MB包含98个Java源文件作为核心业务与配置逻辑另配套HTML/CSS/JS等前端页面、SQL初始化脚本及YML配置文件整体目录分层清晰便于直接导入IDE研读与二次扩展。通过这份代码读者可以重点学习SAPJCO3连接属性配置与RFC调用封装方法、Quartz定时任务的触发与执行逻辑、基于Shiro的认证授权整合方式以及Spring Boot后台管理系统的分层架构与MyBatis数据持久化实践。资源还覆盖了后台管理系统的基础模块能帮助快速搭建一个具备SAP数据获取、维护和定时同步能力的实用脚手架该资源已有430人学习/下载适合作为企业级SAP集成项目起步模板供中等及以上水平的Java开发者参考。1. 用Spring Boot搭一套SAP RFC交互系统它到底解决了什么问题大多数Java项目第一次接SAP最容易翻车的不是业务逻辑而是连不上、连上后又不知道数据长什么样。SAP是核心系统动一个字段都要走变更流程业务方只会给你开放几个RFC/BAPI接口剩下的全靠中间层自己消化。这套资源就是用Spring Boot MyBatis Thymeleaf Quartz 2 Shiro SAP JCo 搭了一个完整的RFC交互底座从连接SAP、调用BAPI取数到定时同步、权限控制、页面展示整条链路都铺好了。适合两类人一类是有Java后端基础、突然被安排做SAP集成的开发者不需要懂ABAP但得看得懂JCo调用结构和回调数据另一类是已经在做SAP接口想省掉从零搭框架时间的从业者可以直接在这个架子上填业务函数、加定时任务、配权限菜单。下文按我个人拆这套资源时的顺序把每个模块的落法和坑位写清楚照着搭一遍你就能知道哪里容易翻车、哪里需要改参数。2. SAP JCo接入与RFC调用链路连接配置、连接池与避坑排查JCo是SAP官方的Java连接器Java程序就是通过它和SAP做RFC通信的。这套资源把JCo封装成独立连接模块上层Service只负责传函数名和参数不直接接触底层连接细节。思路是对的因为RFC调用一旦散落在业务代码里后面想统一加日志、加限流、切连接池都会很痛苦。2.1 JCo依赖与连接参数先在本地把SAP连上JCo不是单纯把jar丢进Maven就能跑它底层带C的本地库。Windows环境放sapjco3.dllLinux放libsapjco3.so而且位数必须和JDK一致。这套资源的做法是把sapjco3.jar放在项目的lib目录通过system scope引入本地库放在启动脚本指定的目录。好处是版本握在项目手里不会被公共仓库里五花八门的版本污染。dependency groupIdcom.sap/groupId artifactIdsapjco3/artifactId version3.1/version scopesystem/scope systemPath${project.basedir}/lib/sapjco3.jar/systemPath /dependency这里用systemPath是因为SAP JCo官方不发布到公共Maven仓库你只能从SAP支持站点下载对应平台的包。版本3.1只是个落位示例实际以你下载到的包名为准不同版本对应不同的RFC协议兼容性后面避坑部分会展开。连接参数统一放在jco.properties里内容大致是这样jco.client.ashost10.20.30.40 jco.client.sysnr00 jco.client.client800 jco.client.userRFC_USER jco.client.passwd****** jco.client.langZH jco.pool.max_connections30 jco.pool.max_used_connections20ashost是SAP应用服务器地址sysnr是系统编号client是集团号langZH是避免中文乱码的关键参数。如果SAP用了负载均衡可能还要补mshost和group这套资源默认走单机直连先把最基础的通路打通再考虑消息服务器方式。2.2 RFC调用的三段式取目的地、设参数、读回参JCo调用RFC函数的标准流程是固定的三段式先从JCoDestinationManager拿到目的地再从仓储里取函数模板设完导入参数后执行最后从导出参数或表参数里读结果。这段代码是整个系统的核心模板后面所有业务函数调用都照着这个结构套。public CompanyInfo callBapi(String companyCode) throws JCoException { // 从统一管理的目的地池拿连接SAP_ERP这个名称在jco.properties里定义 JCoDestination dest JCoDestinationManager.getDestination(SAP_ERP); // 从仓储里取函数模板函数名要和SAP侧SE37里的一致大小写敏感 JCoFunction fn dest.getRepository().getFunction(BAPI_COMPANYCODE_GET_DETAIL); // 所有导入参数放在ImportParameterList里字段名以SAP函数文档为准 fn.getImportParameterList().setValue(COMPANYCODE, companyCode); // 执行之后返回值分散在Export和Table两个列表里 fn.execute(dest); JCoTable table fn.getTableParameterList().getTable(COMPANYCODE_DETAIL); CompanyInfo info new CompanyInfo(); while (table.next()) { info.setCode(table.getString(COMP_CODE)); info.setName(table.getString(COMP_NAME)); info.setCity(table.getString(CITY)); } return info; }三个容易忽略的细节第一getRepository()的结果是有缓存的同一个函数不要每次重新加载第二SAP返回的字段名通常是大写table.getString(COMP_NAME)大小写写错直接返回null第三JCoTable的游标移动依赖next()方法刚获取时游标在第一条数据之前不调next()直接取值会拿不到数据。这套资源里把上面这段模板抽了一个RfcClient类所有业务Service都注入它统一管异常和日志。2.3 连接池参数别把SAP主机打满JCo自带的连接池是复用机制。如果每次调用都临时新建JCoDestination相当于每次做一次完整的RFC握手SAP侧会积累大量短连接很容易触发SAP的日志量和连接限制。队列一多SAP管理员就会来找你喝茶。所以这套系统在jco.properties里配好容量应用启动时初始化连接池之后所有调用共用。jco.pool.max_connections是上限jco.pool.max_used_connections是峰值限制。后者尤其重要因为定时任务和人工查询可能叠在同一时间点跑如果峰值限制不设连接会超过SAP侧授权数。SAP的RFC用户授权是收费点超出后通常报权限类错误那个问题不是Java代码逻辑错是连接配额满了。2.4 避坑排查JCo接入和RFC调用的四个高频错误第一启动时报sapjco3找不到或者JCoException 127。现象应用启动直接抛UnsatisfiedLinkError提示找不到sapjco3。原因本地库没有放在JVM能扫描到的目录或者JDK位数和.dll/.so不匹配。解决确认本地库放在jdk.library.path指定的目录Windows用64位JDK就放64位sapjco3.dll版本和jar严格配套不要混着用。第二getDestination抛103/104错误提示目的地不存在。现象JCoRuntimeException说Destination SAP_ERP not found。原因jco.properties没被加载或者ashost/sysnr写错。解决先在代码里打印System.getProperty(jco.client.ashost)确认配置是否进classpath再检查SAP主机能否ping通。我遇到过把sysnr00写成sysnr0的错误码看着是连接问题实际是配置格式问题。第三execute抛128错误提示RFC函数不可用。现象调用某个BAPI时报128原因多是当前RFC用户没有该函数的授权或者函数在SAP侧没有启用远程调用属性。解决先在SE37里查函数名确认存在且已激活再看RFC用户绑定的角色是否包含该函数的授权对象。这个问题最容易误判成代码问题实际上跟Java一点关系都没有。第四返回的中文字符串乱码。现象从SAP读出来的公司名称是乱码。原因SAP系统是非Unicode编码或者lang参数没设对。解决连接参数里固定langZH如果SAP侧本身就是非Unicode尽量在SAP侧用CONVERT_TO之类的函数先转码本地再按UTF-8读取。这套资源在日志里也做了统一转码处理确保落库不乱。3. 用Shiro做登录与权限治理会话、加密与接口鉴权的落地方案RFC交互系统如果没权限控制等于把SAP的窗口直接暴露给所有能访问这个系统的人。这套资源用Shiro管两层一是用户的登录认证和会话管理二是接口权限和页面按钮的可见性。Shiro相比Spring Security更轻量配置直观对中小型系统足够用。3.1 Shiro配置与过滤器链anon、authc与静态资源Shiro的核心是过滤器链所有请求在进Controller之前先过一遍ShiroFilter。这套系统里配置的过滤器链很重要顺序错了整个权限体系就崩了。Configuration public class ShiroConfig { Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean bean new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); MapString, String filterMap new LinkedHashMap(); // 登录页和登录接口匿名访问静态资源也要放行 filterMap.put(/login, anon); filterMap.put(/static/**, anon); // 其余所有接口走认证放到最后兜底 filterMap.put(/**, authc); bean.setFilterChainDefinitionMap(filterMap); return bean; } }LinkedHashMap保证了顺序性/login和/static/**放前面/**放最后。一个常见的翻车点是有人把/**写在前面导致登录页都被拦截形成死循环。如果前端是前后端分离还要额外处理Ajax请求的未登录返回值不能默认重定向到登录页否则前端拿到的是一整段HTML而不是JSON。3.2 自定义Realm与密码加盐别用明文比密码Shiro的认证要自定义Realm从数据库里查用户信息和权限角色。这套资源里用户表存的是加盐MD5后的密码哈希不是明文。判断密码时Shiro会用配置好的HashedCredentialsMatcher自动做同一套加盐和哈希计算。protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { UsernamePasswordToken upToken (UsernamePasswordToken) token; User user userMapper.findByUsername(upToken.getUsername()); if (user null) { return null; } // 数据库里存的是 盐值密码 的哈希盐值单独存一列 ByteSource salt ByteSource.Util.bytes(user.getSalt()); return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), salt, getName()); }ByteSource.Util.bytes(user.getSalt())这行是核心盐字串必须从同一个用户记录里取出来否则永远校验不过。加盐MD5的迭代次数在HashedCredentialsMatcher里配常见做法是MD5迭代两次。这套资源的用户表里password和salt两列都有新建用户时用随机数生成盐再用同样的算法算哈希。这样就算数据库泄露明文密码也不会直接暴露。3.3 权限粒度与页面控制按钮级授权怎么定权限模型是典型的角色-权限两级结构用户表、角色表、用户角色关联表、角色权限关联表。后端接口用RequiresRoles(admin)或RequiresPermissions(rfc:call)做细粒度控制。这里有个常见的坑Shiro的注解默认不生效必须配置AuthorizationAttributeSourceAdvisor的Bean把Shiro的注解与Spring AOP桥接起来否则注解写了也白写。页面按钮的显隐则用Thymeleaf配合Shiro的#subject对象判断。比如某个页面上的执行RFC同步按钮只有拥有rfc:sync权限的人才能看到前端用th:if${#subject.hasPermission(rfc:sync)}包起来。注意按钮隐藏不等于接口安全后端接口的权限校验必须同时存在前端隐藏只是用户体验真正拦人的是过滤器链和后端注解。3.4 会话边界超时、并发与登录状态会话管理要注意两个边界。第一是超时时间SAP同步任务可能在凌晨跑但人工查询界面的会话不必开太久默认30分钟无操作就失效。第二是并发登录控制同一账号是否允许多人同时登录这套资源默认禁止通过DefaultWebSecurityManager配置会话管理器时限制同时在线数。前端拿到401响应后要跳转登录页不能只是控制台报个错就完事否则用户会以为系统卡死了。4. Quartz 2调度SAP同步任务定义、触发规则与运行状态落地SAP主数据同步基本都是批量的——每天凌晨拉一次公司代码每小时拉一次库存月底拉一次财务数据。这套资源用Quartz 2做调度管理配合前面封装好的RFC调用模块把定时任务变成配置化的东西改触发时间不用改代码。4.1 Quartz 2.x的Bean装配与Job写法项目标题里明确写的是Quartz 2这个版本和新版Quartz的API有一些差异。新版本Quartz 4.x把很多FactoryBean废弃了但Quartz 2时代的标准做法就是用JobDetailFactoryBean和CronTriggerFactoryBean不要在新老版本之间混用API否则会导致任务创建成功但调度不触发。Bean public JobDetailFactoryBean rfcSyncJobDetail() { JobDetailFactoryBean factory new JobDetailFactoryBean(); factory.setJobClass(RfcSyncJob.class); factory.setName(rfc-sync-job); factory.setGroup(erp-sync); return factory; } Bean public CronTriggerFactoryBean rfcSyncTrigger() { CronTriggerFactoryBean factory new CronTriggerFactoryBean(); factory.setJobDetail(rfcSyncJobDetail().getObject()); // 每天凌晨2点跑一次cron表达式按实际业务需求改 factory.setCronExpression(0 0 2 * * ?); return factory; }注意Quartz的cron表达式是6到7位的最后两位分别表示星期和年份类字段其中?表示不指定只在日或星期这两个位置上使用。如果原来只写过Spring的Scheduled第一次看Quartz的cron会有点懵0 0 2 * * ?的含义是每天凌晨2点整?替代了星期的位置。4.2 Job里注入Service别直接new写Job类的时候最容易翻的坑就是Service注入进去是null。原因Quartz框架自己实例化JobDetail的任务对象它不受Spring容器管理直接Autowired注入的Service全是null。这套资源的做法是在SchedulerFactoryBean里把ApplicationContext放进调度上下文Job执行时再取出来。public class RfcSyncJob extends QuartzJobBean { Override protected void executeInternal(JobExecutionContext context) { // Job是Quartz实例化的Spring不会自动注入Service ApplicationContext app (ApplicationContext) context.getScheduler() .getContext().get(applicationContext); RfcService rfcService app.getBean(RfcService.class); // 调用SAP拉主数据落库 rfcService.syncMasterData(); } }这块代码建议直接照抄。getContext().get(applicationContext)这个key和SchedulerFactoryBean里设置的名字必须一致。资源里的做法是在配置类里用factory.setApplicationContext(applicationContext)把上下文塞进去然后Job这边再取出来两次的key都是applicationContext到位。4.3 动态调度与运行状态表定时任务不能写死在配置文件里业务方可能临时要加一个每天中午跑一次的库存同步任务。这套资源的做法是把任务配置存在MyBatis管理的表里运行时通过Scheduler的API动态添加、暂停、恢复、删除。CREATE TABLE job_run_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, job_name VARCHAR(100) NOT NULL, trigger_time DATETIME NOT NULL, finish_time DATETIME, status VARCHAR(20) NOT NULL, error_msg TEXT, INDEX idx_job_name (job_name) );job_run_log这张状态表很关键它是判断任务是否还在运行的依据。状态字段取值建议用RUNNING、SUCCESS、FAILED三种error_msg存SAP返回的错误码和消息文本。展示页面上要能看到最近几次任务触发的时间、耗时、成功还是失败比任务丢失了一个晚上强得多。4.4 与RFC联动避免把SAP打爆SAP侧的资源是有限的定时任务如果没有并发控制就可能出现凌晨2点手动点了个查询、同时定时批处理也触发的情况导致SAP连接池爆炸。第一道防线是在Job类上加DisallowConcurrentExecution让同一个Job在同一时间只能有一个实例在跑错过触发时间就等下一轮不排队硬挤。第二道防线是调度前先查job_run_log如果上次任务还没结束这次直接跳过并在日志里记录一条SKIPPED状态。第三道防线是失败重试要有间隔这套资源默认重试3次、每次间隔1分钟重试次数过多或者无间隔重试会把SAP的连接配额直接打满。5. Thymeleaf JavaScript渲染RFC结果页面展示、回调和联动技巧调度和权限都有之后最后一步是把RFC返回的数据呈现在页面上。这套资源用Thymeleaf做服务端渲染再通过JavaScript做局部刷新和动态交互。两个技术配合起来既能保证首屏加载不会白屏也能避免整个页面频繁刷新带来的糟糕体验。5.1 Thymeleaf模板结构放在templates下的标准用法Thymeleaf模板统一放在src/main/resources/templates目录下HTML片段用th:开头的属性做服务端渲染。查询类页面直接服务端渲染是合理的因为首屏数据就一次成型不用多一次Ajax请求。table thead trth公司代码/thth公司名称/thth城市/th/tr /thead tbody tr th:eachrow : ${companyList} td th:text${row.code}/td td th:text${row.name}/td td th:text${row.city}/td /tr /tbody /tableth:each就是Thymeleaf的循环${companyList}对应Controller里Model中塞进去的集合对象。th:text默认会做HTML转义所以SAP返回的公司名称里如果带、之类的字符不会破坏页面结构也算意外做了一次防XSS。静态资源要用th:href{/static/js/main.js}带了上下文路径也能自动拼对。5.2 JavaScript把RFC结果表格化前后端数据对接服务端渲染适合首屏但刷任务执行记录、重新拉取SAP数据这类操作不适合整页刷新用JavaScript做局部刷新更实用。这套资源里写了一个查询接口返回JSON前端fetch之后填表。async function loadCompanyList() { const resp await fetch(/rfc/company/list, { headers: { X-Requested-With: XMLHttpRequest } }); // 未登录时后端会返回401这里跳转登录页 if (resp.status 401) { window.location.href /login; return; } const data await resp.json(); const tbody document.getElementById(companyTbody); tbody.innerHTML ; data.forEach(row { const tr document.createElement(tr); // 注意字段名大小写要和后端JSON保持一致 tr.innerHTML td${row.code}/tdtd${row.name}/tdtd${row.city}/td; tbody.appendChild(tr); }); }请求头里带X-Requested-With: XMLHttpRequest是让Shiro判断这是一个Ajax请求。这样未登录时后端返回401 JSON而不是整页重定向前端拿到401自己决定跳转登录页体验顺畅很多。innerHTML在这里拼的是自己后端返回的字段值不直接拼用户输入相对安全如果拼了可疑内容记得先做文本转义再插进DOM。5.3 常见问题日期、时区、编码与局部刷新第一日期格式不对。现象SAP返回的日期字段是YYYYMMDD数字串直接渲染到表格里显示一大串数字用户看不懂。原因RFC表结构里DATE类型就是8位字符。解决服务端统一转成LocalDate后再传给前端或者前端格式化一下再显示。第二时区差一天。现象凌晨批处理拉的数据日期显示成前一天。原因SAP服务器和本地服务器时区设置不一致。解决统一在服务端按Asia/Shanghai格式化时间字段不要依赖Java默认时区。我后来写所有日期处理逻辑都强制指定时区不加的话到了夏令时之类的节点还会再出问题。第三页面乱码或控制台乱码。现象页面中文乱码或者后端日志打印RFC返回的中文都是???。原因启动参数缺少-Dfile.encodingUTF-8或者响应头没加charsetUTF-8。解决启动脚本统一加编码参数Controller层返回JSON时显式声明produces application/json;charsetUTF-8。这条问题排查起来很烦但其实查一下启动命令就能确认。6. 验证RFC调用是否成功从SAP侧和日志侧双重确认做到这一步系统能跑、页面能看但能跑不等于调用是正确的。RFC调用经常像个黑匣子你只看到Java代码里返回了数据但没法确定这数据是SAP真实返回的还是缓存里碰巧有的。所以这套资源在最后留了一个双重验证的环节值得沿用。先看日志侧。rfc_call_log表是每次RFC调用的完整记录字段设计成这五个维度函数名、目标系统、调用开始时间、结束时间、状态码。每次执行RFC前后都要写开始和结束日志特别是异常时要把SAP的JCoException错误码和消息文本完整记录下来。有了这张表就能回答昨晚凌晨2点的同步任务到底跑没跑成功这类灵魂拷问。再看SAP侧。SE37事务代码可以直接测试单个函数入参先把业务参数填进去执行一次看SAP侧自己的返回结果。ST05可以打开RFC跟踪看Java应用与SAP之间实际传递的消息内容。这里有一个实战技巧故意传一个必定报错的参数比如不存在的公司代码让系统报错。如果Java侧收到的错误码和SE37里调试报的错误码一致说明链路本身是通的问题出在业务数据上如果两边的错误码对不上多半是字段类型或长度在传参时被改坏了。最后是字段映射核对。SAP的表结构用SE11打开对照实体类的字段定义特别关注两个地方CHAR类型字段的长度按字节计算中文字符会占2到3个字节所以SAP侧字段长40的Java侧字符串不要按40个字符去截断数字字段要确认是NUMC还是DEC前者是前导零补齐的字符传值时要保留0直接用Integer会丢掉前导零导致SAP识别错误。从那以后我每次改完RFC接口都不会直接让业务方验证而是先在SE37里回放一遍业务参数再回来看Java日志里的调用记录。两边都对上了才敢说这个接口真的通了。希望帮到你。本文还有配套的精品资源点击获取