前一阵在公司做 GIS 服务定制化需要把一段复杂的业务校验逻辑塞进 ArcGIS Server 的地图服务里让浏览器直接通过 REST 接口调用。折腾了一轮 Java 版本的 REST SOE从环境配置、接口设计到打包上线踩了不少坑。这篇文章是 ArcGIS SOE 系列的第 5 篇重点讲怎么用 Java 开发 REST 型的 Server Object Extension。如果你已经掌握 ArcGIS Server 的基础操作想给现有地图服务增加自定义逻辑那么这篇文章可以直接跟着做如果你是没用过 SOE 的新手也可以把前面的背景部分读一读再从代码实录开始复现。文章会聊清楚内部处理链路、关键代码、部署方式、常见报错以及一些文档里不会写的经验。1. 为什么要用 Java 开发 REST SOE1.1 内置服务满足不了的需求先说我遇到的真实场景。某项目里有个需求前端在地图上画一个多边形后端要把范围内的地块要素和相关权属信息一起返回。直接开一个要素服务也能做查询但它返回的是标准要素 JSON想把另一个业务库里的人、电话、抵押状态拼进来就只能把所有要素拉到前端再做二次关联。数据量小的时候没问题到了几十万行浏览器直接卡死接口延迟也没法看。这个场景下需要的是“服务端聚合输出”接收参数后在 ArcGIS Server 进程里完成空间查询再访问业务库做关联最后输出一个紧凑的定制 JSON。标准服务没有这种扩展点GP 服务又太重每次跑一个模型都要额外交互参数和返回格式还都是固定那一套。最后选型落在了 SOE 上。SOE 的好处非常直接它被挂在一个具体的 GIS 服务上比如 MapServer、GeoDataService能拿到该服务背后的 server object可以直接调用 ArcObjects 去查数据、改渲染、做空间分析也可以访问外部系统。输出内容完全自己控制REST 型 SOE 直接暴露给前端或者第三方系统调用轻量、跨语言、容易联调。1.2 REST SOE 和 SOAP SOE、动态图层方案怎么选做技术选型时最容易混淆的是 REST SOE、SOAP SOE 和动态图层。我列了一个对比方便照着判断维度REST SOESOAP SOE动态图层调用方式URL 资源 JSON 参数SOAP XML WSDL标准 REST 服务但能力固定适合场景前端地图应用、第三方轻量系统接入企业总线、强类型客户端、SOA 环境改子图层、改符号、临时合并图层开发成本中等资源模型要自己设计偏高要处理 WSDL 和复杂类型最低配置即可输出控制力强完全自定义 JSON/XML强但类型化约束多弱只能按服务内置结构输出部署位置服务端扩展注册后挂服务服务端扩展注册后挂服务服务配置不写代码如果你只是想让前端少请求几次把几个图层临时组合一下动态图层就够用。但如果要“自定义输入参数、自定义返回 JSON、访问外部业务库”就必须走 SOE。SOAP 型和 REST 型在能力上没有本质区别真正影响选择的是消费端。现在大多数产品都是 Web 端或者移动端直接调 REST没必要再包一层 SOAP所以我后面都围绕 REST 来讲。至于用 Java 而不是 .NET原因也很实际我们后续要把服务部署到 Linux 环境团队已有的后台接口也全是 Java 技术栈。SOE 虽然挂在 ArcGIS Server 上但内部逻辑完全可以复用公司的公共 Java 库统一做日志、统一做错误码比另外维护一套 .NET 扩展舒服得多。2. 开发环境与项目模板2.1 版本匹配是第一道门槛SOE 开发最让人头疼的不是代码而是环境。ArcGIS Server 的 Java SDK 非常讲究版本对应稍不留神就会在注册扩展时看到不友好的错误。我这里以 10.8.1 为例实际项目就是这么跑的组件版本说明ArcGIS Server10.8.1扩展最终部署到哪个版本开发就用哪个版本ArcObjects Java SDK10.8.1可以从 ArcGIS Server 安装介质里找到JDK1.8不要随意升级到 11 或 17Eclipse2020-12 或兼容版本官方插件在这个环境里最稳为什么强调 JDK 1.8因为 ArcObjects Java API 底层是通过互操作桥接机制调用 ArcObjects 的SDK 里的很多 class 都是基于旧字节码生成的高版本 JDK 虽然名义上兼容但实际编译和运行阶段可能踩到不可控的问题。我见过有人用 JDK 11 在 IDE 里编译通过部署到 Server 之后一直提示某个类加载失败换回 1.8 就好了。不要在这个点上冒险。ArcObjects Java SDK 的安装方式就是在开发机上单独安装一份然后在 Eclipse 里配置 SDK 路径。配置位置在 Window 菜单的 Preferences 下找到 ArcGIS 相关项指定 SDK 的安装根目录。如果配置完成后新建项目时看不到 SOE 相关的向导说明 SDK 插件没有正确加载先确认 Eclipse 启动时使用的是 JDK 1.8。2.2 搭建可复现的项目结构项目不建议直接用默认包名尽量按公司规范来比如com.company.soe。Eclipse 新建项目的路径大致是File - New - Other - ArcGIS Server Object Extension Project选择 Java 和 REST 类型。SDK 会自动生成了一个扩展骨架里面有生命周期方法、REST 处理方法的空实现同时还生成了项目级的配置文件这个配置文件在打包时会变成 .soe 的内容。生成好的项目结构大致是这样FeatureCountSOE ├── src │ └── com/company/soe │ ├── FeatureCountSOE.java │ └── util/QueryHelper.java ├── lib │ └── 第三方依赖比如 JDBC 驱动 └── 工程配置文件一个容易被忽略的点SOE 里如果要用 JDBC 连接外部业务数据库驱动 jar 一定要放到项目的 lib 目录并加进构建路径最终打包时让 SDK 把你这些依赖一起塞进 .soe。不要指望 Server 上某个公共目录里刚好有驱动。我就犯过这个错本地跑得好好的传上 Server 之后一直提示找不到驱动类最后重新打包才解决。这里还建议如果团队里有人用 IntelliJ IDEA也可以直接建普通 Java 工程手动引入 SDK 里的 jar。但是官方向导生成的 SOE 配置、打包脚本都是围绕 Eclipse 这组流程转的IDEA 能编译不等于能正确生成 .soe。新手阶段老老实实按向导来能少碰很多暗坑。3. 理解 REST SOE 的请求链路3.1 一次浏览器请求到达 SOE 的全过程很多 SOE 开发的问题其实出在没理解请求怎么走。搞清楚了链路排错会快很多。假设你的服务地址是http://server:6080/arcgis/rest/services/demo/MapServerSOE 挂上去后访问入口长这样http://server:6080/arcgis/rest/services/demo/MapServer/exts/FeatureCount/count?layerId0fjson请求到达 ArcGIS Server 后大概是这么几件事Server 先解析出服务名demo确认这是一个 MapServer 服务。检查这个服务开启了哪些能力明确 SOE 扩展是否被勾选。根据 URL 中的exts/FeatureCount找到已注册的扩展实例名称。把 URL 剩余部分拆成资源名count和操作名也就是resourceName和operationName。把查询参数layerId0fjson整理成 JSON 字符串作为operationInput。调用 SOE 对应的处理接口拿到返回的字节内容再按 JSON 或 XML 形式回给客户端。这个链路里最容易忽略的地方是第三步。SOE 命名很重要getSchema()里声明的资源根名称要和实际 URL 里的扩展名、以及服务配置里勾选的扩展名称保持一致。三个地方只要有一个对不上在前面某个步骤就会直接断掉最常见的表现就是 404。3.2 Schema 与资源设计REST SOE 不是靠代码反射自动生成接口文档的服务目录里能看到什么完全取决于getSchema()返回的 JSON。这个 Schema 会告诉 ArcGIS Server这个 SOE 有哪些资源、每个资源支持哪些操作、每个操作需要什么参数。服务目录里的 REST 页面包括?fpjson返回的元数据也都是从 Schema 来的。举个例子一个只提供一个count操作的 SOESchema 可以设计成{ FeatureCount: { operations: { count: true }, parameters: { layerId: true, where: false } } }这里layerId标记为必填where标记为可填。实际操作时浏览器访问/count这个资源再传入layerId和whereServer 就会把参数整理后送到handleRESTRequest里。设计资源时我个人的建议是层级尽量浅一个 SOE 最多两三层就够了。资源名字用名词操作名用动词比如FeatureCount/count、Query/execute。太深的路径会增加匹配复杂度而且前端调用时不容易记忆。参数方面尽量保持简单能走查询字符串就走查询字符串不要在 body 里塞一坨复杂结构除非你的客户端本身就是受控的。4. 核心代码实现与部署4.1 初始化与生命周期管理Server Object Extension 和普通 Java Servlet 不一样它不只是一个请求处理器还和一个具体服务共存亡。服务启动时SOE 实例被创建init()方法被调用可以通过IServerObjectHelper拿到当前服务对应的 server object。服务停止时shutdown()被调用释放资源。所以生命周期方法里适合做这些事情init()保存 helper 引用做轻量配置读取。shutdown()释放数据库连接、清理线程池、断开外部系统。切记不要在init()里做重活。比如一上去就遍历全图建立空间索引缓存那服务启动会变得异常慢而且如果初始化失败这个服务可能直接起不来。我的习惯是真正用到某个资源时才去获取 server object 或者连接数据库把初始化成本分摊到第一次请求上。另外SOE 实例是单例的多个请求会并发进入同一个实例的方法。所有成员变量如果会变化必须考虑线程安全性。能用局部变量就用局部变量不要为了省几行代码把查询条件放到类字段里否则高并发下一个请求的数据可能污染另一个请求。4.2 getSchema 与 handleRESTRequest 的 Java 实现下面这段代码是一个功能简化版的 REST SOE业务场景是“统计指定图层满足条件后的要素数量并返回一个自定义 JSON”。我把查库和查图的真实逻辑省略了只保留请求映射和参数解析方便看清 SOE 的骨架。package com.company.soe; import java.io.IOException; import java.nio.charset.StandardCharsets; import com.esri.arcgis.server.IRESTRequestHandler; import com.esri.arcgis.server.IServerObjectExtension; import com.esri.arcgis.server.IServerObjectHelper; import com.esri.arcgis.server.json.JSONObject; import com.esri.arcgis.system.AutomationException; public class FeatureCountSOE implements IServerObjectExtension, IRESTRequestHandler { private IServerObjectHelper serverObjectHelper; Override public void init(IServerObjectHelper soh) throws IOException, AutomationException { this.serverObjectHelper soh; System.out.println(FeatureCountSOE initialized.); } Override public void shutdown() throws IOException, AutomationException { System.out.println(FeatureCountSOE shutdown.); } Override public String getSchema() { JSONObject resource new JSONObject(); JSONObject operation new JSONObject(); operation.put(count, true); JSONObject params new JSONObject(); params.put(layerId, true); params.put(where, false); resource.put(operations, operation); resource.put(parameters, params); JSONObject schema new JSONObject(); schema.put(FeatureCount, resource); return schema.toString(); } Override public byte[] handleRESTRequest( String capabilities, String resourceName, String operationName, String operationInput, String outputFormat, String[] responseProperties) throws IOException, AutomationException { JSONObject input new JSONObject(operationInput); int layerId input.getInt(layerId); String where input.optString(where, ); // 这里实际应该通过 serverObjectHelper 拿到 MapServer // 再访问要素类做真实统计下面先返回占位数据 JSONObject result new JSONObject(); result.put(layerId, layerId); result.put(where, where); result.put(count, 0); return result.toString().getBytes(StandardCharsets.UTF_8); } }注意一下不同小版本 SDK 里IRESTRequestHandler的方法签名可能有细微差别编写前先看 SDK 里的接口定义和自带的示例代码以你实际环境为准。在真实代码里handleRESTRequest拿到operationInput之后第一步一定是解析参数。因为 Server 传进来的可能是空字符串、JSON 对象也可能是普通字符串针对不同来源要做兼容。开发时可以把operationInput原样返回先验证路由是否通再加业务逻辑这样能区分是链路问题还是代码问题。拿到 server object 的方式也很直观在需要真正访问数据时这样写Object serverObject serverObjectHelper.getServerObject(); if (serverObject instanceof IMapServer) { // 这里就可以调用图层信息、地图范围等 ArcObjects 接口 }getServerObject()返回的到底是IMapServer还是IGeoDataServer取决于你的 SOE 挂在什么服务上。MapServer 就转IMapServerGeoDataService 就转IGeoDataServer不要乱转。类型不对时尽早返回一个带业务错误码的 JSON而不是让异常抛到外层变成 500 页面。4.3 打包 .soe 与注册到 Server代码写完接下来是打包上线。Java 版的 SOE 最终产出的不是普通 jar而是.soe文件。Eclipse 里右键项目选择 Export找到与 ArcGIS Server 相关的导出类型填好目标 Server 版本和文件名SDK 会自动把你的 class、资源配置文件和依赖 jar 一起打进去。这里有一个很容易被坑的细节打包时选择的版本必须和你要部署的 ArcGIS Server 版本一致。SDK 检测到版本不匹配时可能不会阻止你但上传到 Server 后完全可能出现“扩展已注册但服务启动失败”的情况。登录 ArcGIS Server Manager先到扩展管理页面上传.soe文件然后再到具体服务的 Capabilities 配置里勾选这个 SOE。这里有两项要填扩展名以及是否启用。扩展名就是要出现在 URL 里的那一段比如FeatureCount建议和getSchema()里的资源根名称保持一致。注册完成后可以先用浏览器打开http://server:6080/arcgis/rest/services/demo/MapServer/exts/FeatureCount?fpjson如果能正常看到 Schema 信息说明 SOE 注册成功接下来就可以直接访问具体操作。用 curl 测试更方便curl -G http://server:6080/arcgis/rest/services/demo/MapServer/exts/FeatureCount/count \ --data-urlencode layerId0 \ --data-urlencode whereSTATUS 可售 \ --data-urlencode fjson如果服务开启了权限控制URL 里还要带上 token 参数。调试阶段为了省事可以先建一个不设权限的测试服务跑通后再把正式权限加上。5. 常见问题与排查心得5.1 打开服务报 404 或资源不存在SOE 部署阶段出现频率最高的报错就是 404。我把碰到过的几种情况整理成了速查表现象可能原因处理方法/exts/FeatureCount直接 404服务没有勾选该 SOE到服务 Capabilities 里启用URL 里扩展名和配置不一致注册名称写错统一扩展名尽量让三者一致服务能启动但访问具体操作 404Schema 里没有声明该操作检查 getSchema 里 operations 是否写对浏览器能打开目录curl 调通但前端调不通安全配置或跨域问题先检查 token再检查跨域配置还有一个容易被忽略的问题就是服务类型匹配。SOE 挂在 MapServer 上但如果 URL 用的是GPServer开头自然找不到扩展名。写代码时也建议在handleRESTRequest里对serverObjectHelper.getServerObject()做一次类型判断类型不对就返回“服务类型不支持”的提示比莫名其妙的空指针好排查得多。5.2 并发访问下的状态管理线上环境最怕的就是“并发把 SOE 打垮”。前面说过SOE 是单例每次请求都进入同一个对象成员变量一旦被多个线程写入就会出现数据串线的诡异问题。我自己的实践规则是三条能用局部变量绝对不用成员变量。数据库连接用连接池不要在请求里新建连接也不要在shutdown()前一直持有同一个连接。对 server object 的访问尽量只读。如果要写需要额外考虑锁和并发冲突尽量把写操作集中到专门的服务或后台任务里。另外返回值也要注意大小。SOE 的好处是能输出自定义 JSON但不要借着这个能力一口气输出全量数据。如果查询结果可能很大加上分页参数或者在结果里只给必要字段。Server 进程的资源有限一个 SOE 把内存占满会影响同一个服务实例上的其他请求。5.3 调试技巧与日志调试 SOE 不能像普通 Java Web 项目那样随时断点尤其服务已经部署到 Server 上之后很多问题只能靠日志和返回结果判断。最快的验证方法是“最小回路”先让handleRESTRequest原样返回operationInput看请求是否到达参数是否完整。这一步通了说明路由、注册、服务类型都没问题然后再往里加真实的查询逻辑。日志方面开发阶段可以直接用System.out打印到 Server 的输出日志里看。业务稳定后再考虑接入标准日志组件。不过我建议扩展包里的依赖越少越好每多一个第三方 jar打包和部署的风险就高一分。只要能完成业务目标能少依赖就少依赖。还有一个小技巧在返回的 JSON 里加一个debug字段把关键路径上的耗时、参数、server object 类型放进去联调阶段非常有用。上线前再把这个字段去掉或者放到内部版本里。之前有个问题前端说“同样的参数有时候有结果有时候没结果”就是靠这个 debug 字段发现某个请求多带了一个空格参数导致 SQL 查询条件不一致。最后再分享一个个人习惯正式开发前永远先做一个只返回固定 JSON 的“空壳 SOE”走通上传、注册、调用整条链路再开始填充业务代码。这一步看着简单实际能省掉后面大量排队等待的时间。毕竟 SOE 的瓶颈往往不在代码写法而在环境和链路先把环境打通后面所有操作都会顺很多。