swagger生成器

前面我们用过mybatis生成器帮我们生成了model和mapper组件,那我们的DTO和API组件有没有相关的生成器帮我们生成呢?那就是本节的主角swagger生成器啦。废话不多说,开干!

为了让swagger的文档更好维护,swagger官方为我们提供了一个代码生成器工具,可以基于openApi3规范所编写的yml文件来生成带swagger注解的API接口。下面我们将把该生成器插件集成进来。

集成步骤

首先我们在项目最外层创建一个swaggerGen.gradle的脚本文件,用于编写swagger插件生成任务,具体的脚本小卷直接贴出来:

groovy 复制代码
buildscript {
    repositories {
        maven{ url 'https://maven.aliyun.com/repository/public'}
        maven{ url 'https://maven.aliyun.com/repository/gradle-plugin'}
        maven{ url 'https://maven.aliyun.com/repository/spring'}
        maven{ url 'https://maven.aliyun.com/repository/spring-plugin'}
        mavenCentral()
    }
    dependencies {
        classpath('io.swagger.codegen.v3:swagger-codegen-maven-plugin:3.0.42')
    }
}

import io.swagger.codegen.v3.CodegenConfigLoader
import io.swagger.codegen.v3.DefaultGenerator
import io.swagger.codegen.v3.ClientOptInput
import io.swagger.codegen.v3.ClientOpts
import io.swagger.v3.parser.OpenAPIV3Parser

ext.output = "$projectDir"
ext.apiPackage   = 'com.xiaojuan.boot.web.api.generated'
ext.modelPackage = 'com.xiaojuan.boot.dto.generated'
ext.swaggerFile  = "$projectDir/mall.yaml"

task generateApi {
    doLast {
        def openAPI = new OpenAPIV3Parser().read(project.swaggerFile.toString(), null, null)
        def clientOpts = new ClientOptInput().openAPI(openAPI)
        def codegenConfig = CodegenConfigLoader.forName('spring')
        codegenConfig.setOutputDir(project.output)
        codegenConfig.setLibrary('spring-mvc')
        clientOpts.setConfig(codegenConfig)
        def clientOps = new ClientOpts()
        clientOps.setProperties([
//                'dateLibrary'           : 'java8', // Date library to use 不想生成空实现必须要注释掉
                'useTags'               : 'true',  // Use tags for the naming
                'interfaceOnly'         : 'true',   // Generating the Controller API interface and the models only
                'apiPackage'            : project.apiPackage,
                'modelPackage'          : project.modelPackage,
                'modelNameSuffix'       : 'DTO',
                'performBeanValidation' : 'false',
                'java8'                 : 'false', // 不生成接口默认实现
                'skipDefaultInterface'  : 'true',
                'openApiNullable'       : 'false'
        ])
        clientOpts.setOpts(clientOps)

        def generator = new DefaultGenerator().opts(clientOpts)
        System.setProperty('generateModels', 'true')
        System.setProperty('generateApis', 'true')
        System.setProperty('supportingFiles', 'false')
        generator.generate() // Executing the generation
    }
}

dependencies {
    // 仅用于插件源码调试
    developmentOnly 'io.swagger.codegen.v3:swagger-codegen-maven-plugin:3.0.42'
}

脚本说明

这里我们用到一个swagger-codegen-maven-plugin插件,这个是swagger生成器的maven插件,我们无法直接集成使用。这里我们引入了其核心的API,并通过编写一个task的形式来定义了一个generateAPI的任务。这里我们将生成基于spring mvc框架的代码形式,生成的组件包括DTO类和Rest API接口。这里的生成设置我们做了一些配置,确保尽量符合我们想要的生成结构。

这里我们会基于一个遵循openApi3规范的yml文件格式的配置来生成,也就是项目根路径下的mall.yml,生成的类和接口的路径我们也通过常量做了指定。

注意,最后我们加了一个developmentOnly类型的依赖,该依赖使得我们在开发阶段可以对生成器源码进行调试,以便更高的做一些生成功能的定制。这种依赖的引入方式和引入spring-boot-devtools类似。

然后我们将该脚本应用到gradle主配置build.gradle中,看我们对这个文件的调整:

groovy 复制代码
...

configurations {
    ...
    developmentOnly
    runtimeClasspath {
        extendsFrom developmentOnly
    }
    ...
}

...
apply from: 'swaggerGen.gradle'

再来看基于openApi3规范的文档定义yml文件:

mall.yml

yaml 复制代码
openapi: 3.0.1
info:
  title: 小卷生鲜OpenApi
  description: 小卷生鲜在线API文档
  version: 1.0.0
  termsOfService: https://edu.xiaojuan.com
  contact:
    name: xiaojuan
    url: https://edu.xiaojuan.com
    email: [email protected]
servers:
  - url: 'http://localhost:8080'
    description: dev
paths:
  /mall-api/user/register:
    post:
      tags:
        - user
      summary: 用户注册接口
      description: 注册一个普通用户
      operationId: register
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserRegister'
      responses:
        200:
          description: 注册成功

components:
  schemas:
    UserRegister:
      description: 用户注册DTO类
      type: object
      properties:
        username:
          description: 用户名
          type: string
        password:
          description: 密码
          type: string
        age:
          description: 年龄
          type: integer
          format: int32
        email:
          description: 邮箱
          type: string
        mobileNo:
          description: 手机号
          type: string

以上我们给了一个简单的示例,关于具体的格式可以参考相关文档。这里我们暂时没有加上校验,校验我们将采用扩展的配置形式。

这样我们通过点击生成任务就得到我们想要的DTO类和API接口了:

修改生成模板

基于spring的模板地址:github.com/swagger-api...

我们将要调整的mustache模板文件拷贝到项目中一个固定前缀的路径下:

因为mustache模板是基于handlebar的小胡子语法的,为了让idea支持这种语法高亮,我们再装一个插件:

现在要生成器按照我们的模板目录来加载相关文件,则做如下设定即可

swaggerGen.gradle

groovy 复制代码
...
ext.templateDir  = "$projectDir/src/main/resources/openapi/templates"
    
task generateApi {
    doLast {
        ...
        clientOps.setProperties([
            ...
                'templateDir'           : project.templateDir
        ])
        ...
    }
}

这样配置以后,生成的代码是基于我们所维护的模板了。接下来就可以实现生成代码的一些定制了。

简化返回值类型

现在我们将原先的返回值类型ResponseEntity<T>简化为实际的类型。为此我们需要先修改下swagger-codegen的源码。我们把当前的3.0.42版本所依赖的模块swagger-codegen-generators:1.0.39,修改其中SpringCodegenpostProcessOperations(objs)方法:

java 复制代码
package io.swagger.codegen.v3.generators.java;

import ...

public class SpringCodegen extends ... {
    ...
    public static Map<String, String> returnTypeMap;
    ...
    static {
        returnTypeMap = new HashMap<>();
        returnTypeMap.put("Byte", "byte");
        returnTypeMap.put("Short", "short");
        returnTypeMap.put("Integer", "int");
        returnTypeMap.put("Long", "long");
        returnTypeMap.put("Float", "float");
        returnTypeMap.put("Double", "double");
        returnTypeMap.put("Boolean", "boolean");
        returnTypeMap.put("Void", "void");
    }
    ...
    
}

修改版本以表明这是动过源码的版本:

我们将maven编译插件的版本和对jdk的要求重新声明下:

xml 复制代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.3</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
    </configuration>
</plugin>

maven-javadoc-plugin插件注释掉。将testng的依赖注释掉,把test源码包改名为test0,以便忽略所有单元测试,因为它使用了基于jdk11的依赖。

重新在本地安装改后的模块:

ok,在本地安装成功!

现在在swaggerGen.gradle中增加修改源码后重新安装的模块:

groovy 复制代码
buildscript {
    repositories {
        mavenLocal()
        ...
    }
    dependencies {
        classpath('io.swagger.codegen.v3:swagger-codegen-generators:1.0.39.custom')
        classpath('io.swagger.codegen.v3:swagger-codegen-maven-plugin:3.0.42') {
            exclude group: 'io.swagger.codegen.v3', module: 'swagger-codegen-generators'
        }
    }
}

...

dependencies {
    // 仅用于插件源码调试
    developmentOnly 'io.swagger.codegen.v3:swagger-codegen-generators:1.0.39.custom'
    developmentOnly('io.swagger.codegen.v3:swagger-codegen-maven-plugin:3.0.42') {
        exclude group: 'io.swagger.codegen.v3', module: 'swagger-codegen-generators'
    }
}

这样我们重新运行generateAPI任务后,就能生成原始的返回值类型了,不会再用ResponseEntity<T>了。

生成校验注解

beanValidationCore.mustache中加扩展属性指定的校验注解声明:

handlebars 复制代码
...
{{{ vendorExtensions.x-validation }}}

pojo.mustache文件中也加上扩展内容:

handlebars 复制代码
...
{{{ vendorExtensions.x-validation }}}
public class {{classname}} ...

model.mustache中增加必要的导包语句:

handlebars 复制代码
...
{{#useBeanValidation}}
...
import com.xiaojuan.boot.common.validation.PatternConst;
import com.xiaojuan.boot.common.validation.annotation.*;
import static com.xiaojuan.boot.consts.ValidationConst.*;
{{/useBeanValidation}}
...

同样在api.mustache中增加同样的导包语句:

handlebars 复制代码
...
{{#useBeanValidation}}
{{#jakarta}}
...
{{/jakarta}}
{{^jakarta}}
...
{{/jakarta}}
import com.xiaojuan.boot.common.validation.PatternConst;
import com.xiaojuan.boot.common.validation.annotation.*;
import static com.xiaojuan.boot.consts.ValidationConst.*;
{{/useBeanValidation}}
...

支持生成表单对象

默认swagger生成器会对我们在openApi文档中通过$ref指定的表单形式提交的对象,生成其各个属性的api参数列表,而不是我们所期望的DTO对象,因此这里我们还必须对其源码进行改造。我们对定制的swagger-codegen-generators:1.0.39.custom版本继续修改:

DefaultCodegenConfig.java

这里我们将原来判断isForm的分支与下面处理requestBody的逻辑进行合并。

fromRequestBody(...)方法中再进行提交表单对象情况的判断:

这里我们主要对表单提交形式的额外信息记录到vendorExtensions中,其中CodegenConstants.IS_FORM_OBJ_EXT_NAME是我们新定义的一个key,用于在formParams.mustache模板中增加我们基于表单对象的判断。

该常量的key需要我们维护到swagger-codegen-3.0.42swagger-codegen模块中,我们将其源码下载下来,在idea中打开用maven构建好后,增加swagger-codegen模块的定制版本:

CodegenConsts类中增加常量:

java 复制代码
public static final String IS_FORM_OBJ_EXT_NAME = PREFIX_IS + "form-obj";

CodegenObject类中增加一个获取vendorExtensions中此key的方法:

java 复制代码
public Boolean getIsFormObj() { return getBooleanValue(CodegenConstants.IS_FORM_OBJ_EXT_NAME); }

改完后,我们将定制版安装到本地:

在我们的swagger-codegen-generators:1.0.39.custom中将swagger-codegen依赖改为我们定制的版本即可:

xml 复制代码
<dependency>
    <groupId>io.swagger.codegen.v3</groupId>
    <artifactId>swagger-codegen</artifactId>
    <version>3.0.42.custom</version>
</dependency>

因为我们之前改的fromRequestBody所在类会被一个类继承并覆盖该方法,方法入参也要调整:

原先在生成器解析openApi3文档所保存的requestBody内容对象中,对于表单形式取的是formParams,而我们改造后对表单对象接收形式并没有在formParams中存参数,而是存在了bodyParams中,因此取值逻辑调整下:

调整后,我们在本地重新安装。

看下现在我们的模块引入生成器依赖的情况,

swaggerGen.gradle:

groovy 复制代码
buildscript {
    repositories {
        mavenLocal()
        ...
    }
    dependencies {
        classpath('io.swagger.codegen.v3:swagger-codegen-generators:1.0.39.custom')
        classpath('io.swagger.codegen.v3:swagger-codegen:3.0.42.custom')
        classpath('io.swagger.codegen.v3:swagger-codegen-maven-plugin:3.0.42') {
            exclude group: 'io.swagger.codegen.v3', module: 'swagger-codegen-generators'
            exclude group: 'io.swagger.codegen.v3', module: 'swagger-codegen'
        }
    }
}

...

dependencies {
    // 仅用于插件源码调试
    developmentOnly('io.swagger.codegen.v3:swagger-codegen-maven-plugin:3.0.42') {
        exclude group: 'io.swagger.codegen.v3', module: 'swagger-codegen-generators'
        exclude group: 'io.swagger.codegen.v3', module: 'swagger-codegen'
    }
    developmentOnly 'io.swagger.codegen.v3:swagger-codegen:3.0.42.custom'
    developmentOnly 'io.swagger.codegen.v3:swagger-codegen-generators:1.0.39.custom'
}

最后我们再引入表单模板(模板来源于swagger-codegen-generators模块,拷贝过来即可)并进行更新:

handlebars 复制代码
{{#isFormParam}}{{#isFormObj}}{{#useOas2}}@ApiParam(value = "{{{description}}}"{{#required}}, required=true{{/required}} {{^isContainer}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}){{/useOas2}}{{^useOas2}}@Parameter(in = ParameterIn.DEFAULT, description = "{{{description}}}"{{#required}}, required=true{{/required}}, schema=@Schema({{#allowableValues}}{{> allowableValues }}{{/allowableValues}}{{#defaultValue}}{{#allowableValues}},{{/allowableValues}} defaultValue="{{{defaultValue}}}"{{/defaultValue}})){{/useOas2}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} {{{dataType}}} {{paramName}}{{/isFormObj}}{{^isFormObj}}{{^isBinary}}{{#useOas2}}@ApiParam(value = "{{{description}}}"{{#required}}, required=true{{/required}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}){{/useOas2}}{{^useOas2}}@Parameter(in = ParameterIn.DEFAULT, description = "{{{description}}}"{{#required}}, required=true{{/required}},schema=@Schema({{#allowableValues}}{{> allowableValues }}{{/allowableValues}}{{#defaultValue}}{{#allowableValues}},{{/allowableValues}} defaultValue="{{{defaultValue}}}"{{/defaultValue}})){{/useOas2}} @RequestParam(value="{{baseName}}"{{#required}}, required=true{{/required}}{{^required}}, required=false{{/required}})  {{{dataType}}} {{paramName}}{{/isBinary}}{{#isBinary}}{{#useOas2}}@ApiParam(value = "file detail"){{/useOas2}}{{^useOas2}}@Parameter(description = "file detail"){{/useOas2}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart("file") MultipartFile {{baseName}}{{/isBinary}}{{/isFormObj}}{{/isFormParam}}

生成扩展错误消息

api.mustache的调整:

handlebars 复制代码
@ApiResponses(value = { {{#responses}}
        @ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#vendorExtensions.x-has-errCode}},extensions = @Extension(name = "errCode", properties = { {{#vendorExtensions.x-errCode}}@ExtensionProperty(name = "{{code}}", value="{{msg}}"){{#hasMore}},{{/hasMore}} {{/vendorExtensions.x-errCode}} }){{/vendorExtensions.x-has-errCode}}){{#hasMore}},
            {{/hasMore}}{{/responses}} })

修改Model生成类型

因为之前我们用的mybatis生成器对数据库的tinyint类型默认生成了byte类型,和我们swagger生成器生成DTO的Integer类型不一致,因此我们按照之前修改swagger生成器的流程,把org.mybatis.generator:mybatis-generator-core:1.4.1源码也改下。把源码下到本地,用idea打开用maven构建ok。

版本改下:

修改源码:

执行本地安装

mbgen.gradle中引入定制版本:

groovy 复制代码
configurations {
    mybatisGenerator
}

dependencies {
    // 生成器工具
    mybatisGenerator 'org.mybatis.generator:mybatis-generator-core:1.4.1.custom'
    ...
}

执行重新生成后,我们将原先对用户实体类的角色字段role的类型都调整为int类型。改完后,把所有单元测试跑一遍。

应用swagger生成器

经历了一路折腾,现在我们的swagger生成器终于能为API接口生成简单的返回值,并且能生成校验注解、生成的类文件中类型也导入了,也能接收表单对象了,ok!最后我们将之前实现的API模块全部维护到mall.yml的openApi3定义文件中。

我们将用户模块之前定义的DTO类和API接口都删除,用swagger帮我们生成的,整好后把所有单元测试跑一遍。ok!

本节又是考验小伙伴们实践的耐性的一节,当我们发现第三方的工具依赖不能满足我们对API使用的要求时,我们可以通过在本地对其源码构建的方式来修改一些功能甚至做二次开发。通过调试学习这些优秀的开源框架或工具,也让我们从大牛们对代码的设计中受益良多。

最后是完整的mall.yml文件:

yaml 复制代码
openapi: 3.0.1
info:
  title: 小卷生鲜OpenApi
  description: 小卷生鲜在线API文档
  version: 1.0.0
  termsOfService: https://edu.xiaojuan.com
  contact:
    name: xiaojuan
    url: https://edu.xiaojuan.com
    email: [email protected]
servers:
  - url: 'http://localhost:8080'
    description: dev
paths:
  /mall-api/admin/users:
    get:
      tags:
        - admin
      summary: 用户列表查询接口
      description: 查询所有的用户列表
      operationId: list
      responses:
        200:
          description: 查询成功
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UserInfo'
  /mall-api/user/profile:
    get:
      tags:
        - user
      summary: 用户信息查询接口
      description: 用户登录情况下查询个人信息
      operationId: profile
      responses:
        200:
          description: 查询成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInfo'
  /mall-api/user/admin/login:
    post:
      tags:
        - user
        - admin
      summary: 管理员登录接口
      description: 管理员后台登录
      operationId: adminLogin
      parameters:
        - name: username
          in: query
          description: 用户名
          schema:
            type: string
          x-validation: "@NotBlank(message = MSG_USERNAME_REQUIRED)"
        - name: password
          in: query
          description: 密码
          schema:
            type: string
          x-validation: "@NotBlank(message = MSG_PASSWORD_REQUIRED)"
      responses:
        200:
          description: 登录成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInfo'
  /mall-api/user/login:
    post:
      tags:
        - user
      summary: 普通用户登录接口
      description: 使用用户名/密码登录
      operationId: login
      parameters:
        - name: username
          in: query
          description: 用户名
          schema:
            type: string
          x-validation: "@NotBlank(message = MSG_USERNAME_REQUIRED)"
        - name: password
          in: query
          description: 密码
          schema:
            type: string
          x-validation: "@NotBlank(message = MSG_PASSWORD_REQUIRED)"
      responses:
        200:
          description: 登录成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInfo'
  /mall-api/user/signature:
    post:
      tags:
        - user
      summary: 更新个性签名接口
      description: 登录情况下修改个性签名
      operationId: signature
      parameters:
        - name: signature
          in: query
          description: 个性签名
          schema:
            type: string
          x-validation: '@NotBlank(message = MSG_PERSONAL_SIGNATURE_REQUIRED)'
      responses:
        200:
          description: 更新个性签名成功
  /mall-api/user/logout:
    post:
      tags:
        - user
      summary: 退出登录接口
      description: 用户退出登录
      operationId: logout
      responses:
        200:
          description: 退出登录成功
  /mall-api/user/register:
    post:
      tags:
        - user
      summary: 用户注册接口
      description: 注册一个普通用户
      operationId: register
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/UserRegister'
      responses:
        200:
          description: 注册成功
        500:
          description: 注册失败
          x-errCode: '10001:参数校验失败,10002:用户已注册'

components:
  schemas:
    UserInfo:
      description: 用户信息DTO类
      type: object
      properties:
        id:
          description: 用户id
          type: integer
          format: int64
        username:
          description: 用户名
          type: string
        role:
          description: 角色
          type: integer
          format: int32
        personalSignature:
          description: 个行签名
          type: string
    UserRegister:
      description: 用户注册DTO类
      type: object
      x-validation: '@NotAllBlank(value = {"mobileNo", "email"}, message = MSG_NOT_ALL_EMPTY_MOBILE_EMAIL)'
      properties:
        username:
          description: 用户名
          type: string
          x-validation: "@NotBlank(message = MSG_USERNAME_REQUIRED)"
        password:
          description: 密码
          type: string
          x-validation: "@NotBlank(message = MSG_PASSWORD_REQUIRED)"
        age:
          description: 年龄
          type: integer
          format: int32
          x-validation: "@Min(value = 18, message = MSG_AGE_LIMIT)"
        email:
          description: 邮箱
          type: string
          x-validation: "@Email(message = MSG_EMAIL_FORMAT_BAD)"
        mobileNo:
          description: 手机号
          type: string
          x-validation: "@MyPattern(regexp = PatternConst.MOBILE, message = MSG_MOBILE_FORMAT_BAD)"
相关推荐
中国lanwp21 分钟前
springboot logback 默认加载配置文件顺序
java·spring boot·logback
cherishSpring32 分钟前
在windows使用docker打包springboot项目镜像并上传到阿里云
spring boot·docker·容器
苹果酱05671 小时前
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
java·vue.js·spring boot·mysql·课程设计
慧一居士3 小时前
Kafka HA集群配置搭建与SpringBoot使用示例总结
spring boot·后端·kafka
uncofish3 小时前
springboot不连接数据库启动(原先连接了mysql数据库)
数据库·spring boot·mysql
_BugMan3 小时前
Spring Boot集成RocketMQ
spring boot·rocketmq·java-rocketmq
bing_1583 小时前
Spring Boot 应用中如何避免常见的 SQL 性能陷阱 (例如:SELECT *, NOT IN, 隐式类型转换)?
spring boot·sql·性能优化
xbhog4 小时前
Java大厂面试突击:从Spring Boot自动配置到Kafka分区策略实战解析
spring boot·kafka·mybatis·java面试·分布式架构
bug菌4 小时前
面十年开发候选人被反问:当类被标注为@Service后,会有什么好处?我...🫨
spring boot·后端·spring
Java水解6 小时前
详细分析SpringBootTest中的测试类(附Demo)
spring boot·后端