unionj-generator是开源的一套基于openAPI 3.0规范的一套代码生成器,核心组件包括unionj-generator-openapi、unionj-generator-backend、unionj-generator-service等。
unionj-generator-openapi包含一套dsl语言,用于定义接口和数据模型。
unionj-generator-backend可以基于编写的dsl表达式生成spring boot的controller层接口(proto类)和入参出参vo类。
unionj-generator-service可以基于编写的dsl表达式或者openapi3规范的json文档生成typescript的http客户端代码。
我们已经将这套工具应用于公司项目的开发中,节省了前后端开发的工作量,也提高了前后端同事沟通协作的效率。
我们做这套工具出于以下几条核心理念:
- 设计优先:产品经理给到原型图后,由开发组长负责分析需求和分解任务。后端同学根据各自领到的任务,对照原型图理解任务要求,设计接口的路径、入参和出参
- 契约精神:前后端同学协作时,以openapi 3.0规范的接口文档为契约,通过代码生成方式实现对接口请求的封装,采用typescript有类型约束的Javascript超集,实现前端工程对契约的遵守;同时生成后端的接口类和vo类代码,实现后端服务对契约的遵守
下面以一个用户管理服务的demo项目为例,通过介绍四种常用的接口:
入参为multipart/form-data格式POST接口
入参为json格式POST接口
查询字符串传参GET接口
下载文件接口
来说明如何快速集成到项目开发中。
准备
IDE:Intellij idea
Java: java version "1.8.0_281"
Maven: Apache Maven 3.6.0
postman:推荐用最新版
安装unionj-generator到本地repository:
git clone git@github.com:unionj-cloud/unionj-generator.git
mvn clean install -Dmaven.test.skip=true
- 安装unionj-generator-maven-plugin到本地repository:
git clone git@github.com:unionj-cloud/unionj-generator-maven-plugin.git
mvn clean install -Dmaven.test.skip=true
- 安装unionj-java-archetype到本地repository
git clone git@github.com:unionj-cloud/unionj-java-archetype.git
mvn clean install
初始化工程
用准备的unionj-java-archetype初始化一个项目guide。执行命令:
mvn archetype:generate \
-DarchetypeGroupId=cloud.unionj \
-DarchetypeArtifactId=unionj-java-archetype \
-DarchetypeVersion=0.0.1-SNAPSHOT \
-DinteractiveMode=false \
\
-DgroupId=cloud.unionj \
-DartifactId=guide \
-Dversion=0.0.1-SNAPSHOT \
-Dpackage=cloud.unionj.guide
然后用idea打开
cd guide
idea .
项目结构如图:
- guide-api:服务接口模块,启动类是cloud.unionj.guide.api.ApiApplication类
- guide-gen:接口设计模块,入口类是cloud.unionj.guide.gen.Openapi3Designer类
需求说明
只有五个接口:
注册用户接口
用户信息编辑接口
用户信息查询接口
用户头像图片下载接口
用户分页列表接口
开始设计
首先要修改guide-gen模块的入口文件,通过dsl包里的静态方法定义接口的基本信息,比如标题,版本,已经服务地址,最后返回Openapi3对象。最终是用这个对象来生成代码和json接口文档的。
需要注意的点:
用到表单的场景中,暂时不支持Content-Type是x-www-form-urlencoded的表单入参,只支持multipart/form-data,因为multipart/form-data既可以上传文件,也可以传递键值对,比较通用,作者就偷了个懒。
接口入参的request body那里,如果不指定SchemaType(下文会介绍),默认Content-Type为application/json
总的来说,推荐大家设计入参请求体是formdata或者json的接口
接口设计的流程一般是先设计schema,也就是入参请求体(如果需要的话)和出参的vo类,再设计接口
package cloud.unionj.guide.gen;
import cloud.unionj.generator.openapi3.PathConfig;
import cloud.unionj.generator.openapi3.expression.paths.ParameterBuilder;
import cloud.unionj.generator.openapi3.model.Openapi3;
import cloud.unionj.generator.openapi3.model.Schema;
import cloud.unionj.generator.openapi3.model.paths.Parameter;
import static cloud.unionj.generator.openapi3.PathHelper.get;
import static cloud.unionj.generator.openapi3.PathHelper.post;
import static cloud.unionj.generator.openapi3.dsl.Generic.generic;
import static cloud.unionj.generator.openapi3.dsl.Openapi3.openapi3;
import static cloud.unionj.generator.openapi3.dsl.Schema.schema;
import static cloud.unionj.generator.openapi3.dsl.SchemaHelper.*;
import static cloud.unionj.generator.openapi3.dsl.info.Info.info;
import static cloud.unionj.generator.openapi3.dsl.servers.Server.server;
public class Openapi3Designer {
public static Openapi3 design() {
Openapi3 openAPI3 = openapi3(ob -> {
info(ib -> {
ib.title("用户管理模块");
ib.version("v1.0.0");
});
server(sb -> {
sb.url("http://unionj.cloud");
});
});
return openAPI3;
}
}
注册用户接口
此接口为了说明multipart/form-data入参的POST接口如何设计。入参是注册表单数据,出参是返回用户id。
先设计schema
private static Schema ResultVO = schema(sb -> {
sb.type("object");
sb.title("ResultVO");
sb.properties("code", int32);
sb.properties("msg", string);
sb.properties("data", T);
});
public static Schema UserRegisterFormVO = schema(sb -> {
sb.type("object");
sb.title("UserRegisterFormVO");
sb.description("用户注册表单");
sb.properties("username", string("用户名"));
sb.properties("password", string("密码"));
});
public static Schema UserRegisterRespVO = schema(sb -> {
sb.type("object");
sb.title("UserRegisterRespVO");
sb.description("用户注册结果");
sb.properties("id", int64("用户ID"));
});
public static Schema ResultVOUserRegisterRespVO = generic(gb -> {
gb.generic(ResultVO, ref(UserRegisterRespVO.getTitle()));
});
需要注意的点:
title必填,title是后续生成的代码的类名和json文档里的schema名
vo类的schema定义里type必须是"object"
properties方法里定义属性名和属性类型
属性类型有helper类,java里的基本且常用的类型的schema已经封装好了,开箱即用,只要静态导入即可:
import static cloud.unionj.generator.openapi3.dsl.SchemaHelper.*;
- generic方法用于生成泛型,最后生成的代码是ResultVO<UserRegisterRespVO>
- ResultVO类的data属性是泛型属性。一个schema最多只能有一个T类型的属性
具体有哪些请查看源码或者代码库的readme
再设计接口
post("/api/user/register", PathConfig.builder()
.summary("用户注册接口")
.tags(new String[]{"用户管理模块", "User"})
.reqSchema(UserRegisterFormVO)
.reqSchemaType(PathConfig.SchemaType.FORMDATA)
.respSchema(ResultVOUserRegisterRespVO)
.build());
需要注意的点:
summary里写接口说明,方便调用方理解接口是做什么用的
tags传字符串数组,第一个元素传语义化的标签,一般用于其他UI版本的接口文档用来做锚点或者是菜单分组,第二个元素如果有,则会用于生成代码,做接口名称。tags非必填,不需要刻意忽略不写。代码的接口名称默认是取请求地址用"/"分割后的第一个非空字符串元素
reqSchema传入参的schema
reqSchemaType传上文提到的schema的类型,是作者定义的。暂时只有三种
public enum SchemaType {
JSON("json"),
FORMDATA("formdata"),
STREAM("stream");
//省略
}
- respSchema传出参的schema
到这里,有些同学可能已经迫不及待想看效果。莫急。全部设计完再看生成效果.
用户信息编辑接口
此接口为了说明multipart/form-data入参,同时包含文件上传的POST接口如何设计。入参是用户信息编辑的表单数据,用户ID放进查询字符串里,出参只返回字符串(比如"ok")。
先设计schema
public static Schema UserEditFormVO = schema(sb -> {
sb.type("object");
sb.title("UserEditFormVO");
sb.description("用户信息编辑表单");
sb.properties("name", string("真实姓名"));
sb.properties("age", int32("年龄"));
sb.properties("sex", enums("性别", new String[]{"BOY", "GIRL"}));
sb.properties("avatar", file("用户头像"));
});
public static Schema ResultVOstring = generic(gb -> {
gb.generic(ResultVO, string);
});
需要注意的点:
- file:文件类型的schema
- enums:枚举类型的schema。枚举类型的属性会作为内部enum类生成
再设计接口
post("/api/user/edit", PathConfig.builder()
.summary("用户信息编辑接口")
.tags(new String[]{"用户管理模块", "User"})
.parameters(new Parameter[]{
ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
})
.reqSchema(UserEditFormVO)
.reqSchemaType(PathConfig.SchemaType.FORMDATA)
.respSchema(ResultVOstring)
.build());
需要注意的点:
- Parameter.InEnum枚举类表示参数放在哪里,暂时只支持放在请求链接的查询字符串里
例如:?name=jack&id=1
用户信息查询接口
此接口为了说明GET请求查询字符串传参的接口如何设计。入参是用户ID,出参返回用户详情。
先设计schema
public static Schema UserDetailVO = schema(sb -> {
sb.type("object");
sb.title("UserDetailVO");
sb.description("用户详情");
sb.properties("id", int64("用户ID"));
sb.properties("name", string("真实姓名"));
sb.properties("age", int32("年龄"));
sb.properties("sex", enums("性别", new String[]{"BOY", "GIRL"}));
sb.properties("avatar", string("用户头像下载地址"));
});
public static Schema ResultVOUserDetailVO = generic(gb -> {
gb.generic(ResultVO, ref(UserDetailVO.getTitle()));
});
再设计接口
get("/api/user/detail", PathConfig.builder()
.summary("用户信息查询接口")
.tags(new String[]{"用户管理模块", "User"})
.parameters(new Parameter[]{
ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
})
.respSchema(ResultVOUserDetailVO)
.build());
用户头像图片下载接口
此接口为了说明文件下载的接口如何设计。入参是用户ID,出参返回文件二进制流,这里是GET请求,实际用GET请求还是POST请求都无所谓。
先设计schema
无
再设计接口
get("/api/user/avatar", PathConfig.builder()
.summary("用户头像下载接口")
.tags(new String[]{"用户管理模块", "User"})
.parameters(new Parameter[]{
ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
})
.respSchema(file("用户头像"))
.build());
用户分页列表接口
此接口为了说明json格式的请求体传参的接口如何设计。入参是分页和查询条件,出参返回用户列表。
先设计schema
public static Schema UserPageReqVO = schema(sb -> {
sb.type("object");
sb.title("UserPageReqVO");
sb.description("用户列表分页查询条件");
sb.properties("size", int32("每页多少条数据"));
sb.properties("current", int32("第几页"));
sb.properties("sort", string("排序条件字符串:排序字段前使用'-'(降序)和'+'(升序)号表示排序规则,多个排序字段用','隔开",
"+age,-create_at"));
sb.properties("sex", string("筛选条件:用户性别"));
});
public static Schema PageResultVO = schema(sb -> {
sb.type("object");
sb.title("PageResultVO");
sb.properties("items", ListT);
sb.properties("total", int64("总数"));
sb.properties("size", int32("每页多少条数据"));
sb.properties("current", int32("当前页码"));
sb.properties("pages", int32("总页数"));
});
public static Schema PageResultVOUserDetailVO = generic(gb -> {
gb.generic(PageResultVO, ref(UserDetailVO.getTitle()));
});
public static Schema ResultVOPageResultVOUserDetailVO = generic(gb -> {
gb.generic(ResultVO, ref(PageResultVOUserDetailVO.getTitle()));
});
需要注意的点:
- 泛型支持嵌套,例如:ResultVOPageResultVOUserDetailVO生成的代码是ResultVO<PageResultVO<UserDetailVO>>
再设计接口
post("/api/user/page", PathConfig.builder()
.summary("用户分页列表接口")
.tags(new String[]{"用户管理模块", "User"})
.reqSchema(UserPageReqVO)
.respSchema(ResultVOPageResultVOUserDetailVO)
.build());
需要注意的点:
- reqSchemaType不设置的情况下,默认是json格式的请求体
至此,我们接口和vo类全部设计完成,完整代码如下:
package cloud.unionj.guide.gen;
import cloud.unionj.generator.openapi3.PathConfig;
import cloud.unionj.generator.openapi3.expression.paths.ParameterBuilder;
import cloud.unionj.generator.openapi3.model.Openapi3;
import cloud.unionj.generator.openapi3.model.Schema;
import cloud.unionj.generator.openapi3.model.paths.Parameter;
import static cloud.unionj.generator.openapi3.PathHelper.get;
import static cloud.unionj.generator.openapi3.PathHelper.post;
import static cloud.unionj.generator.openapi3.dsl.Generic.generic;
import static cloud.unionj.generator.openapi3.dsl.Openapi3.openapi3;
import static cloud.unionj.generator.openapi3.dsl.Schema.schema;
import static cloud.unionj.generator.openapi3.dsl.SchemaHelper.*;
import static cloud.unionj.generator.openapi3.dsl.info.Info.info;
import static cloud.unionj.generator.openapi3.dsl.servers.Server.server;
public class Openapi3Designer {
private static Schema ResultVO = schema(sb -> {
sb.type("object");
sb.title("ResultVO");
sb.properties("code", int32);
sb.properties("msg", string);
sb.properties("data", T);
});
public static Schema UserRegisterFormVO = schema(sb -> {
sb.type("object");
sb.title("UserRegisterFormVO");
sb.description("用户注册表单");
sb.properties("username", string("用户名"));
sb.properties("password", string("密码"));
});
public static Schema UserRegisterRespVO = schema(sb -> {
sb.type("object");
sb.title("UserRegisterRespVO");
sb.description("用户注册结果");
sb.properties("id", int64("用户ID"));
});
public static Schema ResultVOUserRegisterRespVO = generic(gb -> {
gb.generic(ResultVO, ref(UserRegisterRespVO.getTitle()));
});
public static Schema UserEditFormVO = schema(sb -> {
sb.type("object");
sb.title("UserEditFormVO");
sb.description("用户信息编辑表单");
sb.properties("name", string("真实姓名"));
sb.properties("age", int32("年龄"));
sb.properties("sex", enums("性别", new String[]{"BOY", "GIRL"}));
sb.properties("avatar", file("用户头像"));
});
public static Schema ResultVOstring = generic(gb -> {
gb.generic(ResultVO, string);
});
public static Schema UserDetailVO = schema(sb -> {
sb.type("object");
sb.title("UserDetailVO");
sb.description("用户详情");
sb.properties("id", int64("用户ID"));
sb.properties("name", string("真实姓名"));
sb.properties("age", int32("年龄"));
sb.properties("sex", enums("性别", new String[]{"BOY", "GIRL"}));
sb.properties("avatar", string("用户头像下载地址"));
});
public static Schema ResultVOUserDetailVO = generic(gb -> {
gb.generic(ResultVO, ref(UserDetailVO.getTitle()));
});
public static Schema UserPageReqVO = schema(sb -> {
sb.type("object");
sb.title("UserPageReqVO");
sb.description("用户列表分页查询条件");
sb.properties("size", int32("每页多少条数据"));
sb.properties("current", int32("第几页"));
sb.properties("sort", string("排序条件字符串:排序字段前使用'-'(降序)和'+'(升序)号表示排序规则,多个排序字段用','隔开",
"+age,-create_at"));
sb.properties("sex", string("筛选条件:用户性别"));
});
public static Schema PageResultVO = schema(sb -> {
sb.type("object");
sb.title("PageResultVO");
sb.properties("items", ListT);
sb.properties("total", int64("总数"));
sb.properties("size", int32("每页多少条数据"));
sb.properties("current", int32("当前页码"));
sb.properties("pages", int32("总页数"));
});
public static Schema PageResultVOUserDetailVO = generic(gb -> {
gb.generic(PageResultVO, ref(UserDetailVO.getTitle()));
});
public static Schema ResultVOPageResultVOUserDetailVO = generic(gb -> {
gb.generic(ResultVO, ref(PageResultVOUserDetailVO.getTitle()));
});
public static Openapi3 design() {
Openapi3 openAPI3 = openapi3(ob -> {
info(ib -> {
ib.title("用户管理模块");
ib.version("v1.0.0");
});
server(sb -> {
sb.url("http://unionj.cloud");
});
post("/api/user/register", PathConfig.builder()
.summary("用户注册接口")
.tags(new String[]{"用户管理模块", "User"})
.reqSchema(UserRegisterFormVO)
.reqSchemaType(PathConfig.SchemaType.FORMDATA)
.respSchema(ResultVOUserRegisterRespVO)
.build());
post("/api/user/edit", PathConfig.builder()
.summary("用户信息编辑接口")
.tags(new String[]{"用户管理模块", "User"})
.parameters(new Parameter[]{
ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
})
.reqSchema(UserEditFormVO)
.reqSchemaType(PathConfig.SchemaType.FORMDATA)
.respSchema(ResultVOstring)
.build());
get("/api/user/detail", PathConfig.builder()
.summary("用户信息查询接口")
.tags(new String[]{"用户管理模块", "User"})
.parameters(new Parameter[]{
ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
})
.respSchema(ResultVOUserDetailVO)
.build());
get("/api/user/avatar", PathConfig.builder()
.summary("用户头像下载接口")
.tags(new String[]{"用户管理模块", "User"})
.parameters(new Parameter[]{
ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
})
.respSchema(file("用户头像"))
.build());
post("/api/user/page", PathConfig.builder()
.summary("用户分页列表接口")
.tags(new String[]{"用户管理模块", "User"})
.reqSchema(UserPageReqVO)
.respSchema(ResultVOPageResultVOUserDetailVO)
.build());
});
return openAPI3;
}
}
生成代码和json文档
在你的idea右上角有如图所示的maven按钮,点击打开窗口
双击图中画红勾的compile,执行代码生成命令,可以看到下方控制台有如下输出:
看到Code generated和BUILD SUCCESS就说明执行成功了。生成的代码在这里:
还没有导入工程中。打开pom.xml文件,格式化一下,在modules标签里加入箭头所示的两行代码。
再点击下图所示按钮即可导入工程:
生成的proto代码如下:
package cloud.unionj.guide.proto;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import cloud.unionj.guide.vo.*;
public interface UserProto {
@GetMapping("/api/user/avatar")
ResponseEntity<byte[]> getApiUserAvatar(
@RequestParam("id") Long id
);
@GetMapping("/api/user/detail")
ResultVO<UserDetailVO> getApiUserDetail(
@RequestParam("id") Long id
);
@PostMapping("/api/user/edit")
ResultVO<String> postApiUserEdit(
@RequestParam(value="name", required=false) String name,
@RequestParam(value="age", required=false) Integer age,
@RequestParam(value="sex", required=false) String sex,
@RequestParam("id") Long id,
@RequestPart(value="avatar", required=false) MultipartFile avatar
);
@PostMapping("/api/user/page")
ResultVO<PageResultVO<UserDetailVO>> postApiUserPage(
@RequestBody UserPageReqVO body
);
@PostMapping("/api/user/register")
ResultVO<UserRegisterRespVO> postApiUserRegister(
@RequestParam(value="username", required=false) String username,
@RequestParam(value="password", required=false) String password
);
}
生成的json文档在这里:
如何使用
代码如何使用
还是以文本的guide项目为例,在guide-api模块的pom.xml文件里加入上文生成的guide-proto和guide-vo模块的依赖:
在guide-api模块里创建controller包和UserController类,实现UserProto接口,点implement methods按钮,可以自动生成打桩代码。
然后就可以实现业务需求了!
json文档如何使用
导入postman
点击import按钮,弹出模态框,将生成的openapi3.json文件拖入上传区域:
点击import按钮导入,效果是这样的:
User文件夹是多余的,因为postman默认是用json文档里的每个接口的tags值来做分组的。因为咱们设计的时候tags里传了两个值,所以生成了多余的User文件夹,可以删掉或者忽略。