ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

NC65开发Restful接口全流程:从注册到调试避坑指南

NC65开发Restful接口全流程:从注册到调试避坑指南 NC65上做Restful接口开发听着不算新鲜可真到动手的时候模块怎么注册、路径怎么映射、分页怎么做、日志怎么查每一个环节都可能卡住一两天。我自己在几个ERP集成项目里反复折腾过这条路从小程序商城到三方WMS都是清一色Restful接口加JSON数据。这篇文章不聊虚的就把在NC65里从零开发一个Restful接口、再做调试的全过程写出来顺便记录几个我踩过好几次的坑。适合正在做NC65二次开发、或者准备让NC65和外部系统做HTTP接口对接的朋友。1. 项目概述与核心需求拆解1.1 背景为什么放着WebService不用非要写Restful在这个项目里采购订单、销售出库、库存查询这些核心数据都要和一家第三方WMS做实时对接。对方要求的接口风格非常明确所有接口走Restful请求和响应统一用JSON而且查询类接口必须分页返回不能一次把全量数据塞过去。NC65平台本身确实不反对Restful问题是平台默认提供的那套对外服务封装得不够用主要对外通道还是WebService。真要在两边系统之间走SOAP光是报文组装和解析就是一笔不小的开销联调时两边看着一坨xml也会头疼。再加上前端小程序要直接调NC接口从微信小程序里发SOAP请求几乎等于自找麻烦于是项目组才决定在NC65模块里自己写一套轻量的Restful接口层对外只暴露HTTP和JSON所有业务逻辑都包在NC模块内部。这个决定不是拍脑袋做出来的而是从对接成本和后期维护两个角度反复权衡后的结果。对接成本方面Restful接口文档简洁、测试工具多外部系统的开发人员上手快维护方面接口类放在NC模块里升级、加字段、改查询逻辑都方便不需要动平台底层。而且NC65本身是Java EE体系跑在应用服务器里对外提供HTTP服务本来就有天然的基础我们只是把平台没做好的那层统一收口自己做掉。1.2 需求拆解一个Restful接口层到底要做什么把需求摊开看这个任务可以拆成四层。第一层是接口发布要让NC65能在应用服务器进程中响应外部HTTP请求走的是HTTP路径而不是内部的EJB或RMI调用。第二层是数据封装NC的业务数据要么是VO对象要么是数据库记录不能把内部字段直接扔出去必须做一层DTO转换只把外部系统需要的字段暴露出来。第三层是权限体系NC的登录校验和功能权限是平台级资产我们不想为了保护Restful接口再单独做一套账号密码体系所以要和NC自身的会话机制打通。第四层是异常与日志接口一旦发布出去外部系统半夜调不通是会直接打电话的没有一套清晰的错误码和日志规范后面的日子会非常难过。最初我拿到这个需求时第一反应是“NC65上写个Servlet响应不就完了”但后来发现事情没有这么简单。NC65模块之间的类加载机制、事务管理器、session获取都有平台自己的规矩如果完全绕开平台机制接口可以通但业务逻辑一复杂就会遇到各种奇怪的问题。所以技术选型这一步比写代码本身更关键。2. 技术方案选型与原理剖析2.1 三种常见实现思路对比技术选型这件事我前前后后权衡了挺久。NC65下做Restful接口市面上常见的方案大致有三种一种是直接用NC65内置的REST框架来发布接口另一种是在NC模块里自建Servlet或Filter层自己处理HTTP请求还有一种是干脆在NC外部单独部署一个Spring Boot服务做中间层。方案优点缺点适用场景NC65内置REST框架和平台session、权限打通部署简单走NC模块类加载不同版本API有差异文档少踩坑要靠自己主流选择自建Servlet/Filter层逻辑可控不依赖平台封装需要自己处理JSON序列化、鉴权、日志工作量大对接口形态有特殊要求独立Spring Boot服务中间层开发效率高生态好NC业务对象在外面不好用session体系和事务要额外适配只是简单取数三种方案我都做过实验。自建Servlet看着自由实际上后面每个接口都要重复处理鉴权、JSON转换、异常包装写到最后全是重复代码。独立Spring Boot服务要远程调NC的业务服务网络开销和维护成本都不低。最后我选择方案一直接用NC65内置的REST能力来发布接口。这样写出来的接口跑在NC模块的进程内拿session、调NC的DAO、用事务管理都是从同一个容器走省掉大量边界问题。2.2 内置REST框架的核心原理说到NC65内置的REST支持它的核心是遵循了JAX-RS风格的一套注解系统比如Path定义资源路径GET、POST定义HTTP方法参数通过QueryParam、PathParam或者请求体传入。不同NC65版本的具体实现类、包名会有差异有的版本要求接口类继承平台提供的基础服务类才能拿到上下文有的版本则不需要所以动手前最好先看一眼本地NC的jar里到底有哪些注解和相关类。原理上大概是这样的外部HTTP请求到NC容器之后先经过NC自带的一串过滤器链条包括登录状态检查、功能权限校验然后路由到平台的REST处理组件。REST组件根据URL路径匹配到我们写的资源类再把HTTP请求参数绑定成方法的入参方法返回的对象最终通过JSON序列化器写入HTTP响应。我们写的接口类并不直接接收Socket请求它只是整个请求管线里最内层的处理器。这个设计带来的好处是我们不需要为Restful接口单独处理“用户是否登录”的问题。只要请求里带了有效的NC会话标识平台会把当前操作员环境自动塞进上下文代码里直接获取当前登录用户、公司、组织这些信息和NC内部调用没有任何区别。但需要注意一点内置REST框架自动完成的校验只是“是否登录、是否有对应功能权限”更细粒度的数据权限必须在业务代码里自己控制。2.3 方案确定后的接口设计思路接口设计上我参照常规的Restful API规范把接口按资源维度划分。比如客户档案查询、订单头查询、订单行查询分别放在各自的资源路径下。但这里想提醒一下NC65场景下做REST接口不必死板地套RESTful资源概念简单查询用GET就行复杂业务提交用POST。因为很多对接方最关心的是“这个URL能不能调通、参数格式是什么”而不是“你的接口是不是严格符合RESTful规范”。项目上线几个月后回头再看这种务实的风格确实让联调过程顺畅不少。开发前我还专门做了一张接口清单给每个接口定好URL前缀、HTTP方法、入参、返参、错误码。这份清单在项目里发挥了很大作用因为NC65模块多、路径长没有一张清单后面联调时经常要翻代码才能对上参数名。接口清单建议用在线表格维护每次增删字段都同步更新比写在Word里或者只在代码注释里留一行好用得多。3. 接口开发实现全流程3.1 开发环境搭建NC65的Restful接口开发本质上还是NC65模块开发。我用的开发环境是传统的UAP Studio也就是Eclipse基础上改造的版本JDK版本和NC65要求保持一致我们项目里是JDK 8。开发机上需要配好NC65的本地部署目录也就是常说的home目录模块代码编译后直接放到模块的classes目录下重启或热部署后就能被加载。有一点值得强调NC65的模块目录结构比普通Java Web项目复杂一个模块下面通常有classes、lib、META-INF、config等目录。Restful服务类编译后的class文件必须放到模块的classes目录里同时要确保模块是启用状态。如果模块没启用或者模块名写错接口类根本不会被加载外部访问就是404。这个环节看似简单但在多人协作、手工打包部署的情况下特别容易出错。调试方面我建议直接在本地把NC65跑起来再用开发工具连接远程调试端口。具体做法是在NC65的启动脚本里加一段JVM参数-Xdebug -Xrunjdwp:transportdt_socket,servery,suspendn,address8788然后在Eclipse里配置一个Remote Java Application调试项端口填8788。这样在本地代码里打上断点外部调用接口时就能看到完整的调用链和变量值。接上调试端口以后热部署配合断点调试开发效率会提升一大截。3.2 基础REST接口骨架下面给一个最简单的REST服务类示例。需要注意不同NC65小版本里注解的包名可能不一样建议用Eclipse的Open Type功能按CtrlShiftT直接搜索Path、GET、POST这些注解类看看它们究竟在哪个jar里再确定正确的import路径。这种查jar的方式看着笨却是最不会出错的。// 简单示例包名、注解import以本地NC版本为准 Path(/demo) Produces(MediaType.APPLICATION_JSON) public class DemoRestService { GET Path(/hello) public String hello(QueryParam(name) String name) { return {\message\:\hello name \}; } }这个类不复杂但它代表了REST接口的最小要素资源路径/demo、HTTP方法GET、子路径/hello、一个查询参数name。外部调用时就是GET http://host:port//demo/hello?namenc。如果接口返回的是JSON字符串直接以字符串形式返回即可框架不会做额外处理但如果想返回一个Java对象让框架自动转JSON就要看本地版本的序列化配置是否符合预期。我在项目里偏向于自己封装一层返回结果对象而不是直接把业务VO扔给框架序列化。原因很简单NC的VO里经常有特殊类型、懒加载关联对象直接序列化极可能报错或者序列化成一大堆内部字段。自己定义一个统一返回结构包含code、message、data三个字段外部对接和联调都会清爽很多遇到异常时也能往code里塞业务错误码而不是让前端解析一堆英文异常信息。3.3 分页查询接口实战“NC65查询接口实现分页”这个需求几乎在每次对接中都会遇到。我这个项目里的查询分页接口一开始想直接依赖平台的查询组件但不同版本的NC对查询组件分页支持差异比较大。与其被平台封装绊住不如在服务层手工控制分页SQL这样可靠性最高数据库切换时也容易调整。可能有人觉得手写SQL不够“平台化”但在接口对接这种场景里稳定可控比优雅更重要。分页接口的处理流程是固定的第一步校验入参pageNo和pageSize的合法性必须提前判断避免负数或超大的分页参数把数据库拖垮第二步组装查询条件执行count查询拿到总记录数第三步按数据库方言执行分页查询取出当前页数据第四步把数据行转成DTO组装成分页结果对象返回。以Oracle数据库为例count和分页的SQL可以这样写。-- 总数统计 SELECT COUNT(1) FROM customer t WHERE t.status 1; -- 分页查询ORACLE ROWNUM写法 SELECT * FROM ( SELECT t.*, ROWNUM rn FROM customer t WHERE t.status 1 AND ROWNUM ? ) WHERE rn ?第一个?是pageNo乘以pageSize第二个?是pageNo减1再乘以pageSize。如果数据库换成MySQL就要改成LIMIT如果用的是达梦、人大金仓这类国产库也有各自的分页方言写之前建议先在数据库客户端里试通SQL再搬到代码里。分页返回结构我定义为{code, message, data:{total, pageNo, pageSize, rows}}。其中total字段是前端小程序做上拉加载时必须用到的没有它前端就不知道还有没有下一页。3.4 参数传递与鉴权设计Restful接口的参数实际开发中分为三类URL路径参数、Query参数、Body里的JSON参数。NC65的REST框架对这三类都支持但我建议对POST接口统一使用JSON Body传参并且定义一个统一的请求体类。比如查询请求体里包含pageNo、pageSize、过滤条件这些字段以后不管增加多少查询维度都不需要改URL定义非常利于接口版本演进。这里就涉及“restful api 参数怎么写”的实际问题了。使用Postman或者小程序端调用时最稳的做法是GET接口参数直接拼在URL后面例如?pageNo1pageSize10statusActivePOST接口参数放在请求体里请求头必须带Content-Type: application/jsonBody写一个合法的JSON字符串比如{pageNo:1,pageSize:10,keyword:abc}。很多联调卡在参数上并不是代码写错了而是请求头的Content-Type没设置对服务端根本读不到Body。鉴权方面考虑到外部系统不是浏览器环境NC默认的Session Cookie机制不一定方便携带我在项目里做了token转发方案外部系统先调用一个登录接口传账号密码换取token后续请求都把token放到请求头Authorization: Bearer xxx里。服务端在REST层统一写一个过滤器校验token校验通过后再把用户上下文塞进去供后续业务代码使用。这种方案不必改动NC平台本身的登录机制只是在我们自己的REST层加了一道壳。需要注意的是token的过期策略和安全存储不能让token明文长时间有效否则泄露风险很大。3.5 插件注册和模块配置这部分是我觉得最容易被忽略却最容易出问题的。NC65不是把class文件丢进classes目录就能被识别为REST服务的它要求你在模块配置里把接口类声明成一个可用的服务。不同项目的NC65版本写法有差异但核心思路一致。我当时是翻遍了模块原来的代码找到一个老旧的REST服务示例把它的配置完整复制了一份只改类名和路径。这种方法看着笨却是最稳妥的因为不同版本在配置文件名和标签名上有差异拿本版本已有的配置当模板一定不会错。我当时的完整步骤是这样先确认模块名再在模块已有配置里搜索rest关键字找到同类REST服务的注册示例然后把示例复制一份改成自己的类名和URL路径最后重新启动NC观察启动日志里有没有出现接口加载相关的字样。如果没出现优先排查配置文件名是不是不对、路径是不是拼错、模块有没有启用。这个环节特别容易碰到“nc65 cannot instantiate plugin”这类报错后面我会专门展开讲。4. 调试方法与工具实测4.1 远程调试接口代码接口不是写完就能交差的调试这步才是真正消耗时间的地方。我用的第一手段是远程调试前面提到在启动脚本里加了JDWP参数这样在Eclipse里打断点就能看到整套调用链。调试时有一个很实用的经验不要只在自己的代码里打断点还要在REST框架的入口、权限过滤器的关键位置打断点。因为很多问题不是业务逻辑错了而是请求根本没走到你的方法里面。返回404大概率是路由没匹配上返回401说明鉴权没过。只有在框架层看到请求的路径和匹配过程才能快速判断是哪一步把请求掐断了。远程调试还有一个好处可以直接观察NC内部对象的取值。比如外部传过来的参数是个JSON字符串你可以在进入业务方法之前先看一下框架把它解析成了什么对象字段名是否对应。有一次我们排查了半天最后发现是外部系统把一个字段名中的下划线写成了驼峰前端看着差不多Java对象解析出来却是null这种问题不打断点根本发现不了。4.2 Postman联调与接口文档化Postman是我日常联调最常用的工具。新建一个请求很简单但要高效使用还是有几个小技巧。首先建一个公共的环境变量文件把host、port、token这些值放进去这样在集合里切换测试环境和生产环境时只需要改一个配置文件不用每个请求都改一遍URL。其次登录接口返回的token可以用Postman的Tests脚本自动存到环境变量里脚本大概是pm.environment.set(token, pm.response.json().data.token)这行逻辑这样后续所有请求都能引用{{token}}。最后是所有需要鉴权的接口在集合级的Authorization里统一设置Bearer Token不需要每个接口手动填。Postman还有一个好用的功能是自动生成接口文档。把每个接口的入参、出参、示例请求全部整理好可以直接导出来发给对接方。比起在Word里手写文档这种方式的准确性高很多因为文档里的字段一定和实际请求体是一致的不会出现“文档里写的是name代码里用的是custName”这种低级问题。接口文档化这件事建议在开发阶段就同步做不要等到联调开始时才补。4.3 JMeter压测与参数化接口联调通了以后还要做一轮基本的并发测试。尤其是查询类接口如果被多个门店同时调用SQL写得稍差一点NC的数据库连接池很容易被打满。JMeter在这个场景里非常实用。有人经常问“jmeter restful 参数怎么写”我在JMeter里的操作流程是先添加线程组设置线程数和循环次数再添加HTTP请求Method选POST路径填接口地址请求体里写JSON参数接着添加HTTP头管理器增加一行Content-Type: application/json然后添加响应断言判断响应内容里是否包含code:200最后添加查看结果树和聚合报告跑完看平均响应时间、TPS、错误率。如果要做更真实的压测可以把JSON参数里的页码和关键字做参数化比如页码用${__Random(1,5,pageNo)}关键字段用CSV数据文件来驱动。这里有一个特别常见的坑JMeter发JSON请求时如果不设置Content-Type: application/json服务端读取Body时可能拿到null整个接口就会报参数缺失。从查看结果树里看请求体明明没写错但响应一直是参数错误十有八九就是请求头没设置对。压测不是跑一次就完事还要顺手观察数据库连接数、JVM内存和GC情况否则接口响应慢的时候你根本分不清是SQL慢还是应用卡顿。4.4 日志跟踪与异常定位接口发布以后日志是线上排查问题最重要的依据。NC65的日志体系依赖log4j但模块默认的日志级别往往不是DEBUG导致生产环境看不到多少有用输出。我的习惯是在REST服务类里显式声明一个Logger然后在关键位置打日志接口入参打DEBUG级别业务结果打INFO级别异常打ERROR级别。这样平时生产日志不至于太吵出问题时还能通过调低日志级别来定位。排查异常时还有一个笨但有效的办法保持NC控制台实时可见。NC65启动后控制台里的异常堆栈往往比日志文件更全尤其是启动阶段的插件加载问题很多异常只在启动时打印一次等你再去翻日志文件可能已经被覆盖了。所以调试期间不要动不动关掉服务器窗口也不要每次重启都清空控制台保持可见很多线索一眼就能看到。5. 常见问题与避坑实录5.1 cannot instantiate plugin插件实例化失败热词里的“nc65 cannot instiate plugin”按原意应该是cannot instantiate plugin这个报错我在开发早期几乎天天碰到。它表示NC在启动或部署时想实例化某个插件类但没有成功。这个报错一般出现在整个模块重启或者补丁发布的时候本来想更新一个REST接口结果服务器一启动控制台抛出一片红色堆栈整个模块都没加载所有接口全线404。所以遇到这种报错不能只盯着代码改得先确认模块有没有成功加载。出现这个问题的原因通常有三类。第一类是类路径写错配置里的类名和实际类的全限定名不一致或者类不在当前模块的classes目录里。第二类是类没有默认构造方法或者构造方法里抛出异常NC实例化插件时基本使用无参构造如果你的类只写了带参构造直接就会失败。第三类是类依赖的jar不在模块的lib目录下REST服务类依赖了某个第三方库但这个库没有被部署到目标环境。排查时先看完整异常堆栈定位到具体是哪个类实例化失败然后按这三步逐项排查。必要时把classes目录的class文件和源码目录对照一下确认编译输出没有错位。5.2 接口404请求没到你的方法里接口地址访问返回404先不要怀疑自己的业务逻辑大概率是路由或注册层面的问题。很多新手一上来就在自己方法里找bug浪费大量时间。其实可以在类的最上面加一行静态日志如果日志都没打印说明类根本没被加载。我的排查顺序是先确认URL里的模块名和配置文件里的模块名完全一致大小写都要一致再确认REST服务类是否被加载启动日志里搜索类名然后确认路径注解没有写错比如子路径末尾多了一个斜杠有时候也会导致匹配失败最后确认模块是否启用NC控制台里模块没启用配置再对也没有用。我曾遇到过一次特别隐蔽的问题本地调试代码改好了接口也能通但打包发到服务器后同样的路径就是404。最后发现是打包时少了一个模块的class文件服务类根本没被完整上传。这类问题最难查因为本地永远复现不了。从那以后我每次部署完第一件事就是看远程启动日志里有没有输出接口加载成功的标记确认无误再做功能测试。5.3 跨域预检请求被拦截前端页面尤其是部署在另一个域名下的管理后台调用NC接口时浏览器会先发一个OPTIONS预检请求。如果NC的REST层没有处理OPTIONS浏览器就会报跨域错误。解决方法是加一个跨域过滤器在响应头里设置允许的来源和请求方法核心代码就是设置三个响应头Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers。其中Allow-Headers里一定要把自定义的Authorization头列出来否则前端带token的请求依然过不了预检。生产环境不建议把Access-Control-Allow-Origin设置成星号最好配置成固定的前端域名白名单。因为接口一旦开放跨域任何网页都可以在用户浏览器里发起请求配合登录态可能会被利用做跨站请求。虽然NC内部还有业务权限控制但安全边界能收紧就收紧不要图省事。5.4 JSON中文乱码与返回结构不一致又一个高频问题接口返回的中文变成一串问号或者乱码。这通常不是业务代码的问题而是编码参数没设置对。解决方案有几个在服务类或方法上显式声明Produces(application/json;charsetUTF-8)如果框架不支持在注解里指定字符集就手动把响应对象转成UTF-8的字节数组返回还要确保数据库连接串里配置了UTF-8尤其Oracle数据库字符集不一致时数据源头就是乱的后面怎么处理都白搭。返回结构不一致也是高频问题。有的接口返回统一封装的{code, message, data}有的接口直接返回数组对接方第一个接口调通了第二个接口解析就报错。开发时一定要守住规范所有正常响应和异常响应都封装成同一个结构不能让业务层的异常直接以框架默认的错误JSON形式返回。否则前端错误捕获逻辑很难写联调时也容易扯皮。统一异常处理这件事从一开始就要做不要等对接方催了再补。5.5 事务与数据库连接接口成功但数据没落库Restful接口的方法默认不在NC的业务事务边界内这是新手最容易踩的坑。我第一次写一个保存单据的POST接口时调完返回成功、日志也没有异常但数据库里就是没有数据。后来发现数据并不是没写进去而是事务没有提交连接关闭时被回滚了。在NC65里要让业务逻辑在一个可控的事务边界中执行通常要调用平台提供的事务代理方法来包装业务方法或者在统一拦截层处理事务。还有一个相关问题就是数据库连接管理。不要自己在方法里直接通过JDBC的getConnection乱开连接一个请求里多次取连接很容易把连接池耗尽。推荐做法是整个业务操作只用一个连接全部完成后再统一commit或rollback然后释放。排查这类问题的思路也不复杂打开数据库的连接日志如果事务操作正常但最后没有commit记录基本就能锁定是事务没有被正确提交。这时候不要怀疑数据库或SQL回头检查代码里的事务边界才是正路。最后再说一个我个人的习惯每次新增一个Restful接口都顺手用Postman导出一份集合文件连同接口清单一起放到项目文档目录里。这样即使换了开发者新同事也不用再从代码里反推参数直接导入集合就能联调。NC65的Restful开发本身不神秘最耗时间的往往不是写接口代码而是处理那些平台机制相关的小坑。希望这篇文章能帮你少走一点弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进