unionj-generator快速上手-后端篇

Photo by Hack Capital on Unsplash

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 [email protected]:unionj-cloud/unionj-generator.git
mvn clean install -Dmaven.test.skip=true
  • 安装unionj-generator-maven-plugin到本地repository:
git clone [email protected]:unionj-cloud/unionj-generator-maven-plugin.git
mvn clean install -Dmaven.test.skip=true
  • 安装unionj-java-archetype到本地repository
git clone [email protected]: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 .

项目结构如图:

image
  • 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
  • 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>

再设计接口

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文档

image

在你的idea右上角有如图所示的maven按钮,点击打开窗口

image

双击图中画红勾的compile,执行代码生成命令,可以看到下方控制台有如下输出:

image

看到Code generated和BUILD SUCCESS就说明执行成功了。生成的代码在这里:

image

还没有导入工程中。打开pom.xml文件,格式化一下,在modules标签里加入箭头所示的两行代码。

image

再点击下图所示按钮即可导入工程:

image
image

生成的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 getApiUserAvatar(
        @RequestParam("id") Long id
    );

    @GetMapping("/api/user/detail")
    ResultVO getApiUserDetail(
        @RequestParam("id") Long id
    );

    @PostMapping("/api/user/edit")
    ResultVO 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> postApiUserPage(
        @RequestBody UserPageReqVO body
    );

    @PostMapping("/api/user/register")
    ResultVO postApiUserRegister(
        @RequestParam(value="username", required=false) String username,
        @RequestParam(value="password", required=false) String password
    );

}

生成的json文档在这里:

image

如何使用

代码如何使用

还是以文本的guide项目为例,在guide-api模块的pom.xml文件里加入上文生成的guide-proto和guide-vo模块的依赖:

image

在guide-api模块里创建controller包和UserController类,实现UserProto接口,点implement methods按钮,可以自动生成打桩代码。

image

image

然后就可以实现业务需求了!

json文档如何使用

导入postman

image

点击import按钮,弹出模态框,将生成的openapi3.json文件拖入上传区域:

image

点击import按钮导入,效果是这样的:

image.png

User文件夹是多余的,因为postman默认是用json文档里的每个接口的tags值来做分组的。因为咱们设计的时候tags里传了两个值,所以生成了多余的User文件夹,可以删掉或者忽略。

生成前端http请求客户端

《unionj-generator快速上手-前端篇》已发布。

你可能感兴趣的:(unionj-generator快速上手-后端篇)