简介:SAP接口集成场景下,基于Spring Boot的Java后台管理系统往往承担数据互通与权限管理双重职责。sapweb项目完整演示了如何整合Spring Boot、MyBatis-Plus、Shiro、Thymeleaf、Quartz 2与SAPJCO3,通过RFC函数调用实现SAP数据对外暴露和外部数据回写SAP,并配合定时任务完成数据同步,适合Java后端开发及SAP实施人员参考。
压缩包共336个文件,约9.46MB。98个Java源码是核心控制器、服务与RFC调用实现;41个JS文件处理管理后台的交互与MVVM逻辑,配合28个HTML、25个CSS组成Thymeleaf渲染的响应式页面;13个XML承担MyBatis映射与Spring配置,另有SQL脚本、YML/Properties环境配置及适配不同系统的JCo链接库(dll/so/jnilib),便于直接移植调试。
资源已有428人浏览学习。目录按后台模块、前端资源、数据库脚本等分层组织,包含Shiro权限、Druid数据源、Quartz任务调度等企业级配置示例,可作为SAP接口开发或Spring Boot后台项目的可运行脚手架,减少从零搭建与SAP联调中的常见踩坑。 做企业系统集成的朋友,应该对SAP不陌生。这些年我经手了不少SAP外围系统,最常用也最稳的一条路,就是用Java技术栈搭一个web应用,通过SAP官方提供的JCo连接器,把RFC接口的能力暴露给内部用户或其他业务系统。今天聊的这个项目,就是一套完整的参考实现:后端用Springboot 2.x + MyBatis做业务和数据持久化,前端用Thymeleaf渲染页面,任务调度交给Quartz,权限控制用Shiro,核心的SAP交互则通过SAP JCo 3.x完成。
这套组合最大的好处是:每一层技术选型都足够成熟,社区资料多,踩坑成本低,非常适合那些需要快速交付又要求稳定运行的SAP周边系统。
1. 整体架构与思路拆解
1.1 为什么是这套技术栈
先说Springboot。它在这个项目里的角色是“胶水层”,把HTTP接口、依赖注入、数据库连接池、外部配置全部统一管理起来。相比传统的SSM(Spring + SpringMVC + MyBatis)手写一大堆XML配置,Springboot能省掉至少30%的搭建时间,尤其是处理SAP连接池这种需要管理生命周期的资源时,Springboot的@ConfigurationProperties配合@Bean可以很干净地完成初始化。
MyBatis在这里不是主角,但非常重要。SAP的RFC接口返回的数据往往是多表结构(比如物料主数据、BOM、库存列表),这些数据在写入本地数据库或做二次加工时,用一个灵活的ORM比JPA更顺手。MyBatis允许手写SQL,对复杂查询的掌控力更强,而且配合PageHelper分页插件,做列表展示非常方便。
Thymeleaf选它是因为Springboot对它支持最好,页面模板和Java代码的交互很自然。有人喜欢前后端分离,但在这个场景里,内部管理系统的用户量通常不大,服务端渲染维护成本更低,也不需要考虑跨域和Token刷新的问题。
Quartz 2负责两类任务:一是定时从SAP拉取数据(比如每天凌晨同步物料库存),二是定时向SAP推送状态(比如定时触发某个审批流程)。用Quartz而不是Spring自带的@Scheduled,是因为Quartz支持持久化、集群部署、动态修改触发时间,真实项目里这些能力早晚会用到。
Shiro管登录认证和接口权限。SAP系统通常已经有了一套账号体系,但外围系统不建议直接使用SAP账号,最好在本地维护一份用户表,通过Shiro把登录态管理起来,再按角色划分菜单和按钮权限。
1.2 SAP JCo在架构中的位置
SAP JCo(Java Connector)是整个系统的心脏。它分两个部分:SAP JCo Middleware(中间件)和SAP JCo API(Java工具库)。前者负责与SAP服务器建立RFC连接,后者是Java开发者直接调用的接口。
JCo的连接管理有两种方式:单连接模式和连接池模式。实际项目里必须用连接池,一方面是因为SAP的license对并发连接数有严格限制,另一方面是频繁建立和释放连接的开销非常大。JCo自带的JCoDestinationManager本身就支持连接池配置,我们只需要在项目中给它提供一份配置文件即可(通常是sap-config.properties)。
我把整个SAP交互层设计成三块:
sapjco-layer ├── config # JCo连接配置、自定义连接池封装 ├── client # RFC调用客户端,封装JCoDestination获取、错误处理 └── dto # 请求/响应模型的映射对象这样的好处是:业务代码里不直接出现JCo的API,全部通过自定义的SapClient调用,未来如果SAP侧接口变了,只需要改对应DTO和翻译层即可。
2. 环境准备与核心依赖配置
2.1 SAP JCo的安装与部署
JCo最大的坑在于它依赖本机原生库。JCo的jar包(sapjco3.jar)只是一个Java wrapper,真正干活的是sapjco3.dll(Windows)或libsapjco3.so(Linux)。所以安装分三步:
- 到SAP官网下载对应操作系统的JCo压缩包,解压后你会看到
lib和docs两个目录。 - 把
sapjco3.jar放进项目的lib目录,如果你的构建工具是Maven,可以把它手动安装到本地仓库:
mvn install:install-file -Dfile=lib/sapjco3.jar -DgroupId=com.sap -DartifactId=sapjco3 -Dversion=3.1.0 -Dpackaging=jar- 把原生的
.dll或.so文件放到JVM的运行目录下,或者放到系统的PATH路径中。我在Linux服务器上习惯把libsapjco3.so放到/usr/lib64下,然后重启Java进程,避免每次部署还要手动指定-Djava.library.path。
有个细节需要注意:JCo区分32位和64位,并且要求JDK的位数和原生库完全一致。java -version显示的是64-Bit,就一定要用64位的JCo包,不然启动时直接报UnsatisfiedLinkError。
2.2 Springboot核心配置(application.yml)
配置文件的重点有三个:数据库连接、MyBatis映射、Quartz线程池。这是最影响运行稳定性的一层。
spring: datasource: url: jdbc:mysql://localhost:3306/sapweb?useUnicode=true&characterEncoding=utf8 username: root password: 123456 hikari: maximum-pool-size: 20 minimum-idle: 5 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.sapweb.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl sap: jco: destination: name: RFC_DEST client: 800 user: SAP_USER passwd: "***" lang: ZH host: 192.168.1.100 sysnr: 00 poolCapacity: 10 peakLimit: 20这里解释一下JCo配置里的几个关键参数:
poolCapacity:连接池中最多保持的RFC连接数。如果业务高峰时并发调RFC,这个值太大会造成SAP侧的压力;太小则会出现JCoException: PeakLimit reached。peakLimit:同一时刻最多可以创建的连接数(包括借出中还没归还的)。这个值必须 >=poolCapacity,否则空闲连接还没复用完,新请求就被拒绝。sysnr:SAP系统编号,通常为00或01,要和SAP Basis同事确认,不能凭猜。
2.3 MyBatis多模块配置的坑
如果项目是多模块(比如api模块、rpc模块、admin模块分开),MyBatis的Mapper扫描路径很容易出问题。要么在启动类上明确指定@MapperScan("com.example.**.mapper"),要么在MyBatis配置文件中用typeAliasesPackage指定实体包路径。
我在这个项目里用的是多数据源方案(本地库 + SAP接口数据缓存库),给每个数据源单独配了一个SqlSessionFactory,避免事务管理器混淆。有个经验:MyBatis的Mapper接口和XML文件的namespace必须完全一致,中文注释里的分号、引号也不能复制错,不然接口注入时报BindingException是最难排查的。
3. 核心业务实现:RFC交互的全流程
3.1 RFC接口的分类与调用封装
SAP的RFC接口可以粗略分成两类:同步RFC和异步RFC(aRFC / tRFC)。这个项目里主要用到同步RFC,比如:
BAPI_MATERIAL_GETLIST:获取物料列表BAPI_MATERIAL_AVAILABILITY:查库存可用量(这个对应热词里的MD07/MD04常见场景)BAPI_GOODSMVT_CREATE:创建货物移动凭证BAPI_ACC_DOCUMENT_POST:生成会计凭证BAPI_BOM_GETDETAIL:查BOM明细
封装调用的核心代码不复杂,关键点是异常处理和返回结构解析。
public RfcResult execute(RfcFunction function) { JCoDestination destination = JCoDestinationManager.getDestination("RFC_DEST"); JCoFunction jcoFunction = destination.getRepository().getFunction(function.getName()); if (jcoFunction == null) { throw new SapRfcException("RFC函数不存在: " + function.getName()); } // 填入请求参数 JCoParameterList importParams = jcoFunction.getImportParameterList(); for (Map.Entry<String, Object> entry : function.getImportParams().entrySet()) { importParams.setValue(entry.getKey(), entry.getValue()); } // 表格类型参数(比如条件行、多行数据) JCoParameterList tableParams = jcoFunction.getTableParameterList(); JCoTable table = tableParams.getTable(function.getTableParamName()); ... // 填充表格行 try { jcoFunction.execute(destination); } catch (AbapException e) { JCoFunction abortFunction = e.getFunction(); // 解析BAPI的RETURN表,获取具体错误消息 JCoTable returnTable = abortFunction.getTableParameterList().getTable("RETURN"); ... } return convertToRfcResult(jcoFunction); }这里有个很关键的细节:调用BAPI时,必须检查RETURN表里的消息类型。如果TYPE为E(错误)或A(异常),即使JCo的execute()没有抛异常,业务上也算失败。我在封装层里统一做了这个判断,并且把MESSAGE字段(可能带**号占位符)用参数列表里的实际值做了替换,这样前端或者日志里看到的错误信息才可读。
3.2 RFC返回数据的解析与本地化
SAP返回的数据结构很“经典”:有单值字段、有内表(类似Java的List)、还有嵌套结构和嵌套表。我第一次对接BOM展开接口时,返回的嵌套深度高达四层,直接用JCoStructure一层层get,代码写得非常啰嗦。
后来的做法是:为每个RFC接口写一个独立的DTO转换器,把JCo的结构体转成Java的POJO,下层业务只面对POJO。
public class MaterialStockDto { private String materialCode; private String plant; private String storageLocation; private BigDecimal stockQty; private String baseUnit; }再配合一个简单工具类,用反射或者手动映射,完成JCoRecord到MaterialStockDto的转换。这样做还有一个额外的好处:SAP字段名通常是全大写带下划线(比如WERKS),而Java习惯是驼峰命名,转换层正好把这个差异消化掉。
3.3 动态调用与元数据缓存
JCo的JCoRepository会对已调用的函数做缓存。但有个常见问题:如果SAP侧修改了函数接口(增加了新参数),本地JCo的缓存不会自动失效。在开发联调阶段,这会让明明SAP已经更新了的接口一直报参数不存在。
解决方案有两种:一是每次调用前不通过destination.getRepository().getFunction(name),而是用destination.getRepository().clear()强制清空缓存再获取;二是在生产环境使用JCoDestinationManager.getDestination()并设置合理的缓存过期策略。在实际项目中,我推荐在启动时主动预加载频繁使用的函数到内存,这样还能提前暴露开发阶段没发现的接口不匹配问题。
3.4 Quartz任务调度的集成要点
这个项目的Quartz用得非常克制,只开发了三个任务:
- 每日凌晨同步物料库存快照(从SAP拉所有物料可用量,写入本地表)
- 每小时同步销售订单状态
- 每周同步BOM变更记录
集成Quartz时,有两个必须处理的点:
第一,任务持久化。如果你直接用Springboot整合Quartz,会发现默认的RAMJobStore不持久化,服务一重启所有计划都丢失。生产系统必须用JDBCJobStore,也就是把job和trigger信息存在数据库表里。Quartz官方提供了一套建表脚本(tables_mysql_innodb.sql),在MySQL里执行一遍即可。
第二,Job里拿不到Spring的Bean。Quartz的Job实例是它自己通过反射创建的,不在Spring容器里。我写了一个SpringJobFactory,重写newJob()方法,从ApplicationContext里直接获取Job实例,这样Job里就能正常注入Mapper和SapClient。
public class SpringJobFactory extends SpringBeanJobFactory { @Autowired private ApplicationContext applicationContext; @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { Object jobInstance = super.createJobInstance(bundle); applicationContext.getAutowireCapableBeanFactory().autowireBean(jobInstance); return jobInstance; } }配置完成后,每次任务触发都会拿到一个Spring托管的全新实例,依赖注入完全正常。
4. Shiro权限设计与Session管理
4.1 登录认证和密码策略
Shiro在这套系统里负责兜底安全。我在ShiroConfig里配置了三层过滤链:/login匿名访问、/static/**匿名访问、其余路径一律需要认证。
密码存库时用的是BCryptPasswordEncoder(Spring Security的这个类可以直接拿来用),比MD5安全很多,而且自带随机盐,同一个密码每次加密结果都不同,防止彩虹表攻击。
Shiro默认的HashCredentialsMatcher不适用BCrypt,所以自定义了一个Matcher:
public class BcryptMatcher implements CredentialsMatcher { private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { return encoder.matches(new String((char[]) token.getCredentials()), (String) info.getCredentials()); } }4.2 权限粒度控制
权限设计不用太复杂,但按钮级权限值得做。我在数据库里维护了三张表:sys_user、sys_role、sys_permission,中间用两张关联表连接。Shiro的AuthorizationInfo里,把role和permission都填上,页面里用Thymeleaf的sec:authorize标签控制按钮显示隐藏。
Thymeleaf和Shiro的整合有个现成的库thymeleaf-extras-shiro,引入后在HTML里就能写:
<button sec:authorize="hasPermission('sap:stock:sync')">立即同步库存</button>这样就避免了“只隐藏、不校验”的尴尬。后端接口方法上再配一层@RequiresPermissions,两层保护。
4.3 会话管理的坑
Shiro的DefaultSecurityManager默认使用DefaultWebSessionManager,会创建自己的一套Session,和HttpSession是两码事。这在集群部署时需要特别注意,如果多节点负载均衡,登录态会丢,因为另外一台服务器不认识你第一次登录创建的Session。
我当时的处理方式是统一使用Redis共享Session,同时用Shiro的AbstractSessionDAO把SessionId存到Redis。这样只要用户请求带着同一个Cookie,无论落到哪个节点都能通过认证。代码改动不多,但解决了未来的扩容问题。
5. 常见问题排查与避坑指南
5.1 JCo连接异常
现象:JCoException: (104) JCO_ERROR_RESOURCE_IN_USE或PeakLimit reached。
原因:连接池配置过小,同时并发RFC的数量超过了peakLimit。
解决思路:首先看监控,是瞬时峰值还是持续增长。如果是瞬时峰值,调大peakLimit和poolCapacity;如果是持续增长,多半是代码里借了连接没释放。JCo的execute()方法执行完之后要保证JCoDestinationManager的释放逻辑被执行,建议把调用包在try-finally里,或者使用try-with-resources风格。
经验补充:JCo调用本身是线程安全的,JCoDestination实例可以被多个线程共享。但JCoFunction不是线程安全的,不能把Function对象缓存在静态变量里复用,每次调用都要从Repository获取新实例,这个坑我踩过,并发一高就会出现参数串号。
5.2 SAP返回数据乱码
现象:从SAP读到的物料描述或文本字段里的中文乱码。
原因:JCo连接配置里的lang参数没设置,或者本地的JVM默认编码不是UTF-8。
解决思路:配置文件里指定lang=ZH,并且确保Springboot的server.servlet.encoding是强制UTF-8。如果数据还是乱码,检查SAP的classic表字段是否是LANG依赖的(比如MAKTX跟语言相关),在不同语言环境下返回的内容本身就不同。
5.3 MyBatis Mapper扫描不到
现象:启动报Invalid bound statement (not found): com.example.mapper.StockMapper.selectByMaterial。
原因:绝大多数情况是Mapper接口和XML文件没有正确绑定。
解决思路:按顺序检查三点:
- XML文件是否在
mapper-locations参数指定的路径下 - Mapper接口的包名和XML的
namespace是否完全一致 - XML中的
id和接口方法名是否一致
经验补充:多模块项目里,如果非启动模块放了Mapper XML,构建工具(Maven)默认不会把src/main/java下的XML文件打进jar包。需要在pom.xml里加一段<resources>配置,单独把**/*.xml也当作资源文件打包。
5.4 一个典型的联调时间线
对接RFC接口时,我习惯于这样安排进度:
- 第一个半天:确认接口清单,和SAP开发(或Basis)对参数定义
- 第二个半天:用JCo的
JCoFunction.getImportParameterList()打印一下,确认参数名和嵌套结构 - 第三天:写完转换器,联调核心场景(成功路径)
- 第四天:处理异常场景,尤其是SAP弹出来的
E/RETURN消息怎么翻译成中文提示
这个节奏在多个项目里都是稳稳的。
6. 部署与调优笔记
6.1 服务器层面的几个参数
JCo是重I/O的组件,我在部署时重点关注三个Linux参数:
ulimit -n:默认1024太小,RFC连接池加上数据库连接池,很容易突破,建议调大到65535。net.core.somaxconn:如果RFC调用频繁经过网络层,TCP的连接队列满了会导致连接超时,调整为1024以上。- JVM堆内存:SAP返回大表数据(比如一次拉几万行物料),内存吃紧是常事。我把-Xms和-Xmx都设为物理内存的一半,并且预留了逃逸分析的空间。GC策略上,JCo调用通常会产生大量短命对象,用G1比CMS(已废弃)更合适。
6.2 数据库表设计的两个建议
一是所有和SAP交互的日志表,字段类型尽量用String而不是Text。MySQL的Text类型不能直接加索引,排查问题时想用material_code查日志都难。
二是同步过来的SAP数据建议加一个sync_batch_no字段,每次同步都生成一个新的批次号。这样即使上游数据源有主数据变更,也能通过批次号快速做全量对账,而不是一条条人工核对。
6.3 监控与预警
在调度任务里加一个简单的看板表,记录每次同步的开始时间、结束时间、成功/失败状态、同步行数。每天早上到办公室第一件事就是看这张表,如果某天同步失败但没有告警,影响会延迟被发现,后续对账会非常麻烦。
我在项目里用了springboot的Actuator暴露/health端点,再配合阿里云监控报警,对关键任务做了失败告警。不要为了省事跳过这一步。
7. 这个项目的扩展方向
如果后续要往更深的方向走,有几个比较顺其自然的演进:
- 引入Flowable工作流引擎。SAP的审批流程如果涉及多级审批、超时自动提醒,Quartz倒也能做,但Flowable的管理能力明显更适合。Springboot下通过
flowable-spring-boot-starter接入并不难,网上可以找到完整的整合教程。 - 做一个通用RFC网关模块。前端或外部系统如果也需要调SAP,与其每个系统单独对接JCo,不如由这个sapweb提供一组统一REST接口,内部做RFC路由和限流,这样SAP侧的改动只需改这一处。
- 引入消息队列(如Kafka或RabbitMQ)。如果经常要批量拉取大数据或者做异步回写,引入MQ做削峰填谷是个不错的选择。JCo调RFC是同步的,但可以先把请求扔到队列里,消费者按SAP可承受的速率去消费,这样不会压垮SAP。
我在做类似项目时通常会先画一张“哪些是稳定的、哪些是可替换的”设计图。SAP相关的接口协议、字段含义是相对稳定的,而Java技术栈那一层,只要拆得足够干净,未来不管是换ORM还是换前端方案,代价都很小。
做SAP集成的核心思路,其实就是把SAP当成一个“只有RFC接口的大黑盒”,系统设计上保证外围功能不会因为SAP侧一次配置变更而崩掉。与其反复猜,不如把契约定清楚,把错误处理做到位。上面这些经验都是在项目线上运行时一点点沉淀下来的。如果你也正好在折腾SAP和Java集成的项目,照着这套思路去做,至少能在环境构建和踩坑排查上少花一半时间。
本文还有配套的精品资源,点击获取