
简介面向金蝶 EAS 8.2 平台的二次开发场景该文档聚焦 Webservice 开发过程中的关键经验与常见难点适合需要编写接口或维护 EAS 系统的后端开发及实施工程师阅读。内容围绕搜索源数据、查看工作流、运行 EAS 客户端、启动 Bos 客户端等核心环节把从实体字段定位、业务流程确认到环境异常排错的经验整理成一份可快速查阅的总结。资源以单个 docx 文档形式打包共 1 个文件压缩包大小约 1.67MB文件结构简洁、内容紧凑既方便在团队内部分享也适合作为个人长期备查手册。该资源已有 345 人学习或下载其内容覆盖从环境配置到接口编写的多个常见问题能够帮助读者少走弯路尤其对刚接触 EAS Webservice 的开发者有直接参考价值。文档还涉及平台基础架构、安全机制与权限控制、SQL 查询工具使用以及客户端异常处理方法并给出若干排除思路与实际命令涵盖查询编辑器快捷键、kdupdater.jar 处理登录异常等经验可直接用于提升开发效率与稳定性。1. EAS 8.2 webservice开发为什么值得单独写一份技巧EAS 8.2的webservice表面上是把内部业务接口暴露成SOAP服务实际上整条调用链需要经过EAS的登录服务、BOS远程服务注册、WS报文转换与业务Facade四层。只拿到一个WSDL地址就生成客户端代码通常第一晚跑不通问题往往并不在代码本身而在发布配置和入参约定上。这篇文章把EAS 8.2 webservice的发布、报文封装、会话认证、排错和自测这五段经验拆开写适合正在EAS上做二次开发的工程师也适合对接方负责接WSDL的同事。命令和代码可以直接抄走能少踩几个坑。2. EAS 8.2 webservice发布从业务接口到WSDL真正可访问在EAS 8.2中对外暴露一个webservice常见做法并不是给现有业务Bean加上注解就完事而是走“定义接口、写实现类、在BOS IDE里登记服务、部署后拉WSDL”这条固定链路。很多从Spring项目转过来的开发者第一步就会踩坑写了一个带Service的类就认为能被自动发现结果WSDL永远拉不出来。2.1 WebService、Facade和本地Bean的边界先想清楚一个问题调用方到底是谁。这个判断决定你要不要进入webservice发布流程也决定后面所有写法。调用方是EAS内部的其他模块直接走EAS本地Facade或EJB不绕WebService省掉序列化开销事务边界也更完整。调用方是外部OA、SAP或自研系统并且要标准SOAP协议EAS 8.2 webservice才值得发布。调用方坚持RESTful风格EAS 8.2并没有把WS接口自动转REST的开关。常见做法是在EAS外层再放一个协议转换网关内部保持SOAP对外暴露REST。弄清边界之后下面的代码结构和配置项才有意义。凡是外部系统不需要跨网络访问的接口就不用进入WS发布环节进入之后你就要接受三个约束方法签名尽量简单、入参不能直接带EAS实体、异常必须可序列化。2.2 接口与实现类的代码骨架EAS 8.2的WS代码并不复杂难点在包组织和运行容器。接口放在独立包实现放在impl子包。实际工程里我一般会沿用这个结构package com.yourcompany.eas.ws; public interface IOutStockQueryWS { /** * 按来源单号查询出库单状态 * * param sourceBillNo 外部来源单号 * param needDetail 是否返回明细 * return 出库单状态结果 * throws Exception 业务异常统一抛出 */ public String queryOutStockStatus(String sourceBillNo, boolean needDetail) throws Exception; }实现类package com.yourcompany.eas.ws.impl; import com.kingdee.bos.Context; import com.yourcompany.eas.ws.IOutStockQueryWS; public class OutStockQueryWS extends AbstractWSBean implements IOutStockQueryWS { Override public String queryOutStockStatus(String sourceBillNo, boolean needDetail) throws Exception { Context ctx this.getContext(); // WS层只做薄壳业务逻辑交给Facade方法保持短小 return this.queryFromFacade(ctx, sourceBillNo, needDetail); } private String queryFromFacade(Context ctx, String sourceBillNo, boolean needDetail) throws Exception { // 按第3章说明把EAS实体转成DTO对外返回 return OK; } }逻辑说明接口上的throws Exception是为了让WS容器把异常包装进SOAP Fault。如果直接在方法里捕获后吞掉客户端只能收到一个空响应连基本错误都拿不到。AbstractWSBean是EAS webservice容器内置基类getContext()返回当前登录用户上下文。不确定这个类在本地包里的具体位置时在EAS安装目录的lib包里搜关键词“AbstractWS”或者直接看BOS IDE的服务描述模板生成出来的父类。不要在这里堆业务代码。EAS的WS调用有超时上限WS方法体越长可排查的问题就越多。2.3 BOS IDE登记服务和WSDL部署验证代码写完只是第一步。EAS 8.2不会自动扫描这个实现类必须先在BOS IDE里登记服务配置项 | 建议取值 | 说明 服务名称 | IOutStockQueryWS 或对外约定名 | 会成为WSDL中的port名称 命名空间 | http://ws.yourcompany.com | 改动会影响客户端生成代码 实现类 | com.yourcompany.eas.ws.impl.OutStockQueryWS | 必须是编译后的全类名 会话策略 | 需要认证 | 外部系统必须先调EAS登录服务 超时秒 | 30到60 | 批量查询再提高到120登记完成后按下面顺序验证把包含WS类所在的部署单元发布到EAS应用服务器。打开EAS管理控制台在远程服务列表中刷新确认服务处于已发布状态。在浏览器访问WSDL地址http://{eas_host}:{port}/ormrpc/services/OutStockQueryWS?wsdl确认WSDL中能看到queryOutStockStatus方法名和对应的入参顺序。注意port取EAS实际部署端口不同环境别照抄。如果WSDL打不开按“服务列表、部署日志、浏览器地址”三个顺序查不要只改配置反复试。2.4 集群部署下的发布一致性EAS多节点部署时WS必须每个app节点都发布。只发其中一台另外节点会出现服务找不到或服务版本不一致。发布后建议逐个节点访问WSDL比对返回内容里的服务地址。如果两台返回的host或port不同说明节点配置有偏差这个检查要放在发布确认阶段做不能等外部系统联调时再暴露。3. EAS 8.2 webservice入参出参封装DTO比直接传EAS实体稳定十倍很多第一次做EAS 8.2 webservice的人会直接把业务实体类放到方法签名里比如把SalesOrderInfo作为入参或返回类型。这条路在技术上是能跑的但是在生产环境维护几天就想改回来。3.1 直接传实体对象的三个麻烦当Axis扫描SalesOrderInfo时会把所有getter跑一遍几十个字段全部序列化。带来的麻烦有三个报文体积大一些虚拟字段被触发计算性能浪费明显。实体字段一变客户端必须跟着重新生成Stub联调成本高。EAS实体内部的Uuid、Timestamp、嵌套集合类型在SOAP映射上稳定性差外部系统经常拿到解析不了的结构。所以EAS 8.2 webservice开发里的约定是入参只用基本类型、String或请求DTO返回只用String、DTO数组或满足序列化要求的简单对象。3.2 最小可复现的DTO写法package com.yourcompany.eas.ws.dto; import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class OutStockInfoDTO implements Serializable { private static final long serialVersionUID 1L; private String billNo; // 单据编号 private String bizDate; // 业务日期字符串格式: yyyy-MM-dd HH:mm:ss private String status; // 单据状态 private ListOutStockItemDTO items; // 明细行 public OutStockInfoDTO() { this.items new ArrayListOutStockItemDTO(); } public String getBillNo() { return billNo; } public void setBillNo(String billNo) { this.billNo billNo; } // 其他getter/setter省略实际编码时补齐 }为什么集合要在构造方法里初始化因为null集合在XML中会生成xsi:niltrue一部分旧版SOAP客户端把这个值解析成null后直接NPE。而new出空集合后WSDL节点对应的是空数组兼容性反而好。3.3 字段规则对照表字段类型 | 容易踩的坑 | 推荐做法 java.util.Date | 序列化会带上服务器时区跨时区客户端差8小时 | 出参统一String固定为 yyyy-MM-dd HH:mm:ss BigDecimal | 精度位数不可控小数尾部出现意外0 | 统一setScale再按需转字符串 boolean参数 | WSDL生成的isXxx语义容易混 | 参数名用needDetail不要用isDetail 集合/数组 | List在WSDL中产生ArrayOf嵌套解析困难 | 方法返回DTO[]不用List如果对接方是Delphi XE2这类老客户端上面这组规则尤其重要。老版本SOAP解释器对复杂类型和自引用结构的支持不完整EAS侧越简单客户端越顺利。我一般会把时间、金额、状态全部转成String客户端只做展示不参与额外计算。3.4 实体转DTO的转换器位置把实体转DTO的代码单独拆出来是长期维护上最值得的一笔投资。package com.yourcompany.eas.ws.dto; import com.yourcompany.eas.bo.outstock.OutStockBillInfo; import com.yourcompany.eas.bo.outstock.OutStockBillEntry; public class OutStockConvertor { public static OutStockInfoDTO toDTO(OutStockBillInfo bill) { OutStockInfoDTO dto new OutStockInfoDTO(); dto.setBillNo(bill.getBillNo()); // 日期转字符串避免时区偏差 dto.setBizDate(DateUtil.format(bill.getBizDate(), yyyy-MM-dd HH:mm:ss)); dto.setStatus(bill.getStatus()); // 明细行防止WS线程里懒加载失败 if (bill.getEntry() ! null) { for (OutStockBillEntry entry : bill.getEntry()) { OutStockItemDTO item new OutStockItemDTO(); item.setMaterialNumber(entry.getMaterialNumber()); dto.getItems().add(item); } } return dto; } }逻辑说明转换器放在独立的dto包下后续EAS升级导致实体字段变更时只改转换器。bill.getEntry()如果是懒加载集合要在Facade层先触发一次加载否则脱离事务边界之后在WS层再读明细很容易报LazyLoad异常。EAS 8.2里最典型的现象就是接口前一半数据正常后一半明细丢失。4. EAS 8.2 webservice认证与会话处理调不通多半卡在这层4.1 先登录拿到sessionId再调业务方法EAS 8.2对webservice的认证链路几乎固定是两段式先调用EAS登录接口拿到sessionId业务方法的第一个参数传这个sessionId。不要试图在业务方法里直接传用户名密码。EAS收到没有会话令牌的请求时会直接抛出未登录或会话无效错误。客户端调用代码示例基于wsdl2java生成的Stub// 1. 登录获得会话 EASLoginProxyService loginService new EASLoginProxyServiceLocator(); EASLogin login loginService.getEASLogin(); Context ctx login.login(wsuser, password, eas_db, lcl); String sessionId ctx.getSessionID(); // 2. 调用业务WebService首参传sessionId OutStockQueryWSSoapBindingStub stub (OutStockQueryWSSoapBindingStub) new OutStockQueryWSLocator().getOutStockQueryWS(); String result stub.queryOutStockStatus(sessionId, SO-2025-001, true);参数说明lcl是EAS中常见语言环境编码表示中文简体。登录接口的WSDL地址一般在/ormrpc/services/EASLogin下与业务WS同域。生成的Stub里sessionId可能直接作为方法第一个参数也可能由工具放到_setProperty里。最省事的确认方式就是直接查看WSDL中方法的参数顺序。4.2 服务端账号授权与服务发布不绑定WSDL发布成功并不代表调用通畅。要让某个外部系统用wsuser调用这个WS还要在管理控制台的远程服务授权中把用户加进去。授权和发布是两个独立操作。很多团队在发布后忘记授权结果WSDL能打开、登录也正常偏偏业务调用返回无权限。授权建议为外部系统单独建账号不要共用管理员。只授权相关业务服务不要整站授权。每季度核对一遍外部系统账号避免已废弃的对接方继续保留访问权限。顺带说一点如果团队在对比EAS本地私有化部署和云端构建的差别注意EAS 8.2的远程服务授权只认EAS本地用户表云端网关往往需要单独做账号映射。不要指望8.2的本地WSDL发布到云端网关之后就自动带上同一套鉴权这是两个体系。4.3 登录态失效的特征与排查顺序报错特征 | 指向 Session invalid / session expired | 会话超时需要重新login Unauthorized / 无权限 | 服务端授权没配或用户被禁用 多节点下时好时坏 | 会话没有在所有节点间同步 EAS重启后所有外部调用失败 | 内存会话被清空客户端需要重连排查顺序先确认登录服务地址和业务服务地址同域同端口。再确认sessionId是刚拿到的不是缓存了几个小时的旧值。看EAS日志中是否有AuthenticationException。最后才去管理控制台检查授权列表。4.4 异常序列化与错误码约定BOSException直接抛给外部时SOAP Fault里会带出EAS内部类名、数据源名称甚至完整堆栈对客户端既不友好也不安全。EAS 8.2的WS开发中普遍做一层异常转换。public class ServiceFault extends RuntimeException { private static final long serialVersionUID 1L; private String code; public ServiceFault(String code, String message) { super(message); this.code code; } public String getCode() { return code; } }WS方法里这样用try { // 业务查询 return queryList(ctx, billNo); } catch (BOSException e) { logger.error(query error, billNo{}, billNo, e); throw new ServiceFault(1002, 单据查询失败请稍后重试); }当客户端抓到ServiceFault时判断code即可message只给用户看。错误码建议分几段管理1001参数校验失败、1002业务处理失败、1003会话失效、2001权限不足。避免客户端靠解析中文文本来定位问题。5. EAS 8.2 webservice上线前自测与提速技巧5.1 用webservice测试工具先行验证SoapUI Community版可以直接用WSDL地址创建项目自动列出所有方法请求模板也能直接生成。配合EAS的login服务先取sessionId再填入请求模板发送能在不写Java代码的情况下验证整条链路。Postman也能导入WSDL但复杂arrayOf结构的展示不如SoapUI直观。网上那些免费webservice接口可以用来练SOAP语法但生产对接必须以EAS自己发布的WSDL为准否则sessionId首参和字段顺序完全对不上。5.2 服务卡死时的线程快照外部调用一直卡住不返回时不要只等超时直接在EAS服务器上抓线程栈jps -l jstack pid ws_stack_$(date %Y%m%d_%H%M%S).txt看栈里是否存在自己的WS实现类以及阻塞在什么方法。EAS 8.2里很多卡死并非死循环而是WS线程在等待数据库连接池释放栈里通常能搜到waiting for connection。遇到这种情况要连同连接池配置和并发压测结果一起分析。5.3 sessionId复用池每次调用都重新login会产生很大的会话切换开销。接口调用频率高时我会在客户端维护一个简易会话复用池public class EasSessionHolder { private static volatile String sessionId; private static volatile long expireAt; public static synchronized String getSessionId() throws Exception { // 预留2分钟缓冲避免临界点失效 if (sessionId ! null expireAt System.currentTimeMillis() 120_000L) { return sessionId; } Context ctx loginService.login(wsuser, password, eas_db, lcl); sessionId ctx.getSessionID(); expireAt System.currentTimeMillis() 30 * 60 * 1000L; return sessionId; } }说明用synchronized保证高并发时只有一个线程触发重新登录避免登录风暴。会话接近过期时间前2分钟就重登防止外部正好在最后一秒发起调用时失败。多个外部系统或不同业务方不要共用同一个sessionId一是权限归属分不清二是某一边操作导致会话释放后另一边全部失效。本文还有配套的精品资源点击获取