版本实现方式
某项目需针对商品、商店和用户实现REST接口。
虽然大家约定通过URL Path方式实现API版本控制,但实现不一:
@GetMapping("/api/item/v1") @GetMapping("/api/v1/shop") @GetMapping("/v1/api/merchant")
显然,商品、商店和商户的接口开发没有按一致URL格式实现接口的版本控制,这时/api/v1/user和/api/user/v1,这到底是一个接口还是两个?
相比于在每个接口的URL Path中设置版本号,更理想的方式是在框架层面实现统一。使用Spring按下面方式自定义RequestMappingHandlerMapping。
首先,创建一个注解来定义接口的版本。@APIVersion自定义注解应用于方法或Controller上:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface APIVersion { String[] value(); }
然后,定义一个APIVersionHandlerMapping类继承RequestMappingHandlerMapping。
RequestMappingHandlerMapping的作用,是根据类或方法上的**@RequestMapping来生成RequestMappingInfo**的实例。
重写registerHandlerMethod方法的实现,从**@APIVersion**自定义注解中读取版本信息,拼接上原有的、不带版本号的URL Pattern,构成新的RequestMappingInfo,来通过注解的方式为接口增加基于URL的版本号:
要通过实现WebMvcRegistrations接口,来生效自定义的APIVersionHandlerMapping:
这样,就实现了在Controller上或接口方法上通过注解,来实现统一的Pattern进行版本号控制:
@GetMapping(value = "/api/user") @APIVersion("v4") public int right4() { return 4; }
使用框架来明确API版本的指定策略,不仅实现了标准化,更实现了强制的API版本控制。对上面代码略做修改,我们就可以实现不设置@APIVersion接口就给予报错提示。
接口明确同步/异步
某文件上传服务,上传接口特慢,因为该接口在内部需两步操作
- 首先上传原图
- 压缩后上传缩略图
如果每一步都耗时5s,则该接口返回至少需10s。
于是把接口改为异步,每步操作都限定超时时间,即分别把上传原文件和上传缩略图的操作提交到线程池,然后等待一定时间。
上传接口的请求和响应比较简单,传入二进制文件,传出原文件和缩略图下载地址。
这种实现ok吗?
接口命名虽然是同步上传操作,但其内部通过线程池进行异步上传,并因为设置了较短超时所以接口整体响应挺快。但一旦遇到超时,接口就不能返回完整的数据,不是无法拿到原文件下载地址,就是无法拿到缩略图下载地址,接口的行为变得不可预测。
所以,这种优化接口响应速度的方式并不可取,更合理的方式是,让上传接口要么完全同步处理,要么完全异步处理:
- 同步处理,接口一定是同步上传原文件和缩略图的,调用方可自己选择调用超时,如果来得及可以一直等到上传完成,如果等不及可以结束等待,下一次重试
- 异步处理,接口是两段式,上传接口本身只是返回一个任务ID,然后异步做上传操作,上传接口响应很快,客户端需要之后再拿着任务ID调用任务查询接口查询上传的文件URL
同步上传接口的实现,超时选择留给客户端:
接口的入参和出参DTO的命名,推荐使用接口名+Request和Response后缀
异步的上传文件接口不再返回文件URL,而是返回一个任务ID:
@Data public class AsyncUploadRequest { private byte[] file; } @Data public class AsyncUploadResponse { private String taskId; }
把上传任务提交到线程池处理,但不会同步等待任务完成,而是完成后把结果写入一个HashMap,任务查询接口通过查询这个HashMap获得文件URL:
文件上传查询接口则以任务ID作为入参,返回两个文件的下载地址,因为文件上传查询接口是同步的,所以直接命名为syncQueryUploadTask:
改造后的FileService提供很明确的:
- 同步上传接口syncUpload
- 异步上传接口asyncUpload,搭配syncQueryUploadTask查询上传结果
使用方可以根据业务性质选择合适的方法:
- 如果是后端批处理使用,那么可使用同步上传,多等待一些时间问题不大
- 如果是面向用户的接口,那么接口响应时间不宜过长,可以调用异步上传接口,然后定时轮询上传结果,拿到结果再显示
总结
第一,针对响应体的设计混乱、响应结果的不明确问题,服务端需要明确响应体每一个字段的意义,以一致的方式进行处理,并确保不透传下游服务的错误。
第二,针对接口版本控制问题,主要就是在开发接口之前明确版本控制策略,以及尽量使用统一的版本控制策略两方面。
第三,针对接口的处理方式,我认为需要明确要么是同步要么是异步。如果API列表中既有同步接口也有异步接口,那么最好直接在接口名中明确。
一个良好的接口文档不仅需说明如何调用接口,更需要补充接口使用的最佳实践以及接口的SLA标准。
太多接口文档只给参数定义,但诸如幂等性、同步异步、缓存策略等看似内部实现相关的一些设计,其实也会影响调用方对接口的使用策略,最好也体现在接口文档。
对于服务端出错时是否返回200响应码,从RESTful设计原则看,应该尽量利用HTTP状态码表达错误,但现实都不是这么绝对。
如果认为HTTP 状态码是协议层面的契约,那当这个错误已经不涉及HTTP协议时(即服务端已收到请求进入服务端业务处理后产生的错误),不一定需要硬套协议本身的错误码。
但涉及非法URL、非法参数、没有权限等无法处理请求的情况,还是应该使用正确的响应码来应对。