前面我们用过mybatis生成器帮我们生成了model和mapper组件,那我们的DTO和API组件有没有相关的生成器帮我们生成呢?那就是本节的主角swagger生成器啦。废话不多说,开干!
为了让swagger的文档更好维护,swagger官方为我们提供了一个代码生成器工具,可以基于openApi3规范所编写的yml文件来生成带swagger注解的API接口。下面我们将把该生成器插件集成进来。
集成步骤
首先我们在项目最外层创建一个swaggerGen.gradle
的脚本文件,用于编写swagger插件生成任务,具体的脚本小卷直接贴出来:
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
中,看我们对这个文件的调整:
...
configurations {
...
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
...
}
...
apply from: 'swaggerGen.gradle'
再来看基于openApi3规范的文档定义yml文件:
mall.yml
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: xiaojuan@162.com
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
...
ext.templateDir = "$projectDir/src/main/resources/openapi/templates"
task generateApi {
doLast {
...
clientOps.setProperties([
...
'templateDir' : project.templateDir
])
...
}
}
这样配置以后,生成的代码是基于我们所维护的模板了。接下来就可以实现生成代码的一些定制了。
简化返回值类型
现在我们将原先的返回值类型ResponseEntity
简化为实际的类型。为此我们需要先修改下swagger-codegen
的源码。我们把当前的3.0.42
版本所依赖的模块swagger-codegen-generators:1.0.39
,修改其中SpringCodegen
的postProcessOperations(objs)
方法:
package io.swagger.codegen.v3.generators.java;
import ...
public class SpringCodegen extends ... {
...
public static Map 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的要求重新声明下:
org.apache.maven.plugins
maven-compiler-plugin
3.3
1.8
1.8
把maven-javadoc-plugin
插件注释掉。将testng
的依赖注释掉,把test
源码包改名为test0
,以便忽略所有单元测试,因为它使用了基于jdk11的依赖。
重新在本地安装改后的模块:
ok,在本地安装成功!
现在在swaggerGen.gradle中增加修改源码后重新安装的模块:
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
了。
生成校验注解
在beanValidationCore.mustache
中加扩展属性指定的校验注解声明:
...
{{{ vendorExtensions.x-validation }}}
在pojo.mustache
文件中也加上扩展内容:
...
{{{ vendorExtensions.x-validation }}}
public class {{classname}} ...
在model.mustache
中增加必要的导包语句:
...
{{#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
中增加同样的导包语句:
...
{{#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.42
的swagger-codegen
模块中,我们将其源码下载下来,在idea中打开用maven构建好后,增加swagger-codegen
模块的定制版本:
在CodegenConsts
类中增加常量:
public static final String IS_FORM_OBJ_EXT_NAME = PREFIX_IS + "form-obj";
在CodegenObject
类中增加一个获取vendorExtensions
中此key的方法:
public Boolean getIsFormObj() { return getBooleanValue(CodegenConstants.IS_FORM_OBJ_EXT_NAME); }
改完后,我们将定制版安装到本地:
在我们的swagger-codegen-generators:1.0.39.custom
中将swagger-codegen
依赖改为我们定制的版本即可:
io.swagger.codegen.v3
swagger-codegen
3.0.42.custom
因为我们之前改的fromRequestBody
所在类会被一个类继承并覆盖该方法,方法入参也要调整:
原先在生成器解析openApi3文档所保存的requestBody内容对象中,对于表单形式取的是formParams
,而我们改造后对表单对象接收形式并没有在formParams
中存参数,而是存在了bodyParams
中,因此取值逻辑调整下:
调整后,我们在本地重新安装。
看下现在我们的模块引入生成器依赖的情况,
swaggerGen.gradle:
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
模块,拷贝过来即可)并进行更新:
{{#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的调整:
@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
中引入定制版本:
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文件:
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: xiaojuan@162.com
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)"