如花是前端开发同学,精通各种框架和JS工具链,雷卷是Java程序猿,对Spring Boot比较熟悉而已,他们经常配合做一些项目开发。一天

产品经理

要求开发一个新的Social项目,准备试水一下, 项目之旅由此开始啦。

前后端分离

如花主要负责前端,如界面、交换、REST API 调用等,雷卷主要负责后端,数据库、Redis、输出REST API等,分工非常明确。 这没有什么难的,访问 start.aliyun.com/

创建项目,然后编写对应的Controller,输出REST API, 第一个Controller不到5分钟就出炉啦,如下:

@RestController @RequestMapping("/user") @CrossOrigin("*") public class UserController { @GetMapping("/nick/{id}") public Mono<String> findNickById(@PathVariable("id") Integer id) { return Mono.just("nick: " 1); } }

Copy

雷卷钉钉给如花,告诉该REST API如何调用。 如花也很迅速,创建一个Node项目,然后webpack等都设置好,他选择了axios这个package,告诉雷卷这个JS的http client多么好,什么浏览器和Node自适配,支持Promise,而且API简洁,一个demo,确实非常简单。

const axios = require('axios').default; (async () => { let userId = 1; let nick = await axios.get('/user/nick/' userId) .then(function (response) { return response.data; }) console.log(nick); })()

Copy

OpenAPI 3来啦

没过几天,REST API已经有十几个啦,钉钉发来发去,已经不行啦。 经常URL要调整、参数要变更、返回的json对象字段也在调整,钉钉上说不明白,而且如果有其他同学加入怎么办? 没有文档说明。

他们决定使用OpenAPI 3规范,只要有该规范,如花就不用钉钉雷卷,只要参考标准OpenAPI文档就可以啦。 这个也不麻烦,当然不会手写这个规范的json,有springdoc-openapi项目,可以直接输出OpenAPI规范文件,非常easy。 pom.xml中添加一下依赖,如下:

<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-webflux-ui</artifactId> <version>1.3.0</version> </dependency>

Copy

然后对应的Controller方法上添加一下@Operation进行说明,就可以啦。

@GetMapping("/nick/{id}") @Operation(description = "find nick by id") public Mono<String> findNickById(@PathVariable("id") Integer id) { return Mono.just("nick: " 1); }

Copy

代码稍作调整,雷卷将OpenAPI规范地址发给如花,http://xxxx:8080/swagger-ui.html ,你自己根据看这个规范就可以啦。 界面如下:

springboot 实现开放api(如何让JavaScript更优雅地访问Spring)(1)

如花中途找到雷卷,主要是描述完善、增加tag等,方便定位等。 在此过程中,如花有点受伤,API一旦调整,雷卷就会钉钉给如花,XXX API调整,你OpenAPI Console上看一下新的规范,你自己调整代码就可以啦。

OpenAPI不那么简单

API越来越多,已经有50个啦,一天如花来找到雷卷,讨论一下HTTP请求约定的问题。 如花抱怨到,现在的这些REST API,太灵活啦,一些参数要放到URL的path中,一些参数要在query中,还有一些是放在http header中,GET和POST不停的在切换,一些API返回的是text/plain,一些返回的是json,虽然OpenAPI上也多明确说明啦,如花还觉得自己非常多的时间都花在构建axios请求的config对象上啦,而且代码非常不好看,到处都是这些config对象的构建,而且这些参数和返回值类型,都没法做代码提示,一次不小心把参数名写错啦,花费了一个上午来调试才找到。 ”你钉钉上说一句API调整啦,我要花半天看哪些调整啦,又不能代码diff,

IDE

又不能提示,我眼睛都累死啦。” 如花有一堆的抱怨。

雷卷告诉如花, 你根据OpenAPI规范生成Node.js代码不就可以啦,json文件就在那里,自己搞呗。 你知道的,我也没法统一,token必须在header中,考虑URL友好型,一些变量放在URL的Path中还是有道理的,还有一些安全考量,没有办法,同时还有性能要求,Servlet Filter规范等,这就是HTTP。

如花铁了心不想看什么OpenAPI规范,对照着写这些无聊的代码,API调整后还要去找不同,简直是拿前端不当人。和雷卷讨论一上午,即便有OpenAPI规范,要从OpenAPI json中自动生成这些固定格式代码,还是有些麻烦,需要后端配合,输出非常多的信息,才能做到代码提示等需求。而且如何配合webpack自动化这个流程也麻烦,下载最新的OpenAPI json,调用脚本生成js代码,也不方便自动化等。

让Spring Boot生成npm package

最后两个觉得能否让Spring Boot应用自动生成一个npm package,然后如花在代码调用该package提供的API就可以。package.json添加一个依赖,该依赖关联到Spring Boot应用提供的URL,如下:

"dependencies": { "@UserService/UserController": "http://localhost:8080/npm/@UserService/UserController" }

Copy

接下来就是要定服务接口啦。既然JavaController已经提供了API,为何不复用该API作为Service API,反正不能让如花再构建什么http请求,写那么多axios无聊代码。 最终结论如下:

const userController = require("@UserService/UserController"); (async () => { let nick = await userController.findNickById(1); console.log(nick); })()

Copy

import userController from "@UserService/UserController" userController.findUserById(1).then(user => { console.log(user.nick) });

Copy

基于接口通讯,你就是想查找会员nick,那么findNickById(id) 这个是最好的API,鬼才想知道背后使用什么技术实现的,以后URL调整,参数调整,http还是https,哪怕是HTTP/2,都不能让我改什么代码。

Spring Boot生成npm package背后的技术

技术上完全没有问题,而且相当靠谱。 对于REST Controller,我们已经使用@GetMapping, @PostMapping, @PathVariable, @RequestParam,@RequestBody等,这些元信息已经存在啦,我们只需要找到对应的Controller Spring Bean,然后使用反射机制将这些信息转换为JS Code代码就可以啦。

一些技术实现点:

详细代码实现,如何从Java是生成JS,类型如何匹配等,这里不细说,这里有一个问题要澄清一下,对于生成的代码,主要是强烈流程正确,而不太讲究代码的可阅读性,这些生成的代码都是给机器阅读的,如你看protobuf生成的代码,没有人能读懂。 但是我会竭力将代码可阅读性提供,毕竟还有一些调试问题。

让我们看一下最终的效果。

最终效果

springboot 实现开放api(如何让JavaScript更优雅地访问Spring)(2)

springboot 实现开放api(如何让JavaScript更优雅地访问Spring)(3)

springboot 实现开放api(如何让JavaScript更优雅地访问Spring)(4)

通过Spring Boot生成npm package和基于服务接口调用,将所有的底层细节全部屏蔽。 如花不用再去看那个OpenAPI控制台,直接调用接口就可以, 也不问这个参数是path变量,还是请求变量,还是在http header中的name,这些全部不用考虑,至于是GET还是POST,更不要考虑。 所有的这些细节都从后端的Controller直接生成即可。

当然特性还不止这些,包括OpenAPI的annotation整合,JS函数的optional,default value支持,@Deprecated API废弃等,当然TypeScrip支持也是第一位的。总结一下,调用更简单,而且全面的审查和代码提示。

虽然项目还是提供OpenAPI的swagger-ui.html,但是如花基本不怎么去看啦,代码编写这么简单,看那个Swagger UI干什么,虽然swagger控制台可以做REST API测试,但是怎么比得过jest来的简单和快捷。

test('findNickById', async () => { let nick = await userController.findNickById(1); console.log(nick); });

Copy

考虑到一些同学可能不知道这个HTTP URL地址,这些如花也想好啦,提交到npm repository就可以,生成的package.json中version是基于时间点的,我们测试完成后,命令行自动提交一下就可以啦,其他同学想用,就是标准的package name就可以。

总结

最后说明一下, 这个项目的灵感来自于Vaadin两天前的blog,不少同学都在使用TypeScript编写浏览器端应用,那么TypeScrip如何后后端通讯?

最好的方式当然是import后直接API调用,而不是如何封装XMLHttpRequest,提供更便捷的API,axios已经提供非常好的API啦。 基于接口的代码如下:

import * as helloEndpoint from './generated/HelloEndpoint' async makeCall() { const greeting = await helloEndpoint.sayHello('Vaadin'); console.log(greeting); }

Copy

Java服务端的代码如下:

@Endpoint public class HelloEndpoint { public String sayHello(String name) { return "Hello " name; } }

Copy

这种基于接口方式写代码非常简洁,完全是无脑的那种。 Vaadin能做到这点,主要就是在编译过程中,如HelloEndpoint生成对应的npm package,然后提供给TS调用。

Spring Boot npm package generator是在运行期生成,node开发同学不用安装JDK,执行Maven编译,然后将npm package拷贝过来等,现在只需一个依赖声明就可以。

借鉴于这个思想,RSocket的npm package也是自动生成。如果我是前端同学,我只想调用一下findNickById(id)实现功能,至于底层通讯是REST API, gRPC还是RSocket,管我什么事情。 即便再好的http client封装,也没有接口约定来的最直接。

当然如果是Python,Ruby等语言,可以使用同样的机制,完全为开发者屏蔽各种http client, gRPC client, RSocket Client等。

原文链接:https://www.tuicool.com/articles/y2uURn3

,