14天自定义3个注解扩展Swagger的3个功能的经历
前言
(一)本文针对的小伙伴
点开此文章的小伙伴们请注意了,本片文章针对的是对Swagger有一定基础的小伙伴;
你应该具备或者已经具备的:
- 1、有过Swagger的使用经历;
- 2、了解过Swagger,动手去集成过SpringBoot;
- 3、对自定义注解解决业务需求有想迫切了解的兴趣。
- 4、…。
(二)通过本文能了解或者学到什么
本篇文章是我通过大量的实践,摸索、查资料、Debug源代码一步一步的摸索出来的,整理一份比较详细的资料,以便也有利于他人,少走弯路。
通过本文你将会:
- 1、了解到SpringBoot项目中如何自定义注解并且使用;
- 2、掌握如何扩展Swagger的功能,并成功的用在项目上;
- 3、了解到自定义注解的流程,以及如果应用的过程;
- 4、少走一些坑。
一、第一部分:基础(可跳过)
(一)swagger简介
swagger确实是个好东西。
为什么是个好东西呢?
因为:
- 1、可以跟据业务代码自动生成相关的api接口文档,尤其用于restful风格中的项目;
- 2、开发人员几乎可以不用专门去维护rest api,这个框架可以自动为你的业务代码生成restfut风格的api;
- 3、而且还提供相应的测试界面,自动显示json格式的响应。
- 4、大大方便了后台开发人员与前端的沟通与联调成本。
1、springfox-swagger简介
鉴于swagger的强大功能,java开源界大牛spring框架迅速跟上,它充分利用自已的优势,把swagger集成到自己的项目里,整了一个spring-swagger,后来便演变成springfox。springfox本身只是利用自身的aop的特点,通过plug的方式把swagger集成了进来,它本身对业务api的生成,还是依靠swagger来实现。
关于这个框架的文档,网上的资料比较少,大部分是入门级的简单使用。本人在集成这个框架到自己项目的过程中,遇到了不少坑,为了解决这些坑,我不得不扒开它的源码来看个究竟。此文,就是记述本人在使用springfox过程中对springfox的一些理解以及需要注意的地方。
2、springfox大致原理
springfox的大致原理就是,在项目启动的过程中,spring上下文在初始化的过程,框架自动跟据配置加载一些swagger相关的bean到当前的上下文中,并自动扫描系统中可能需要生成api文档那些类,并生成相应的信息缓存起来。如果项目MVC控制层用的是springMvc那么会自动扫描所有Controller类,跟据这些Controller类中的方法生成相应的api文档。
(二)SpringBoot集成Swagger
springfox-swagger-ui依赖并不是必须的,可以使用第三方的UI,也可以自己写一套前端的UI集成进来。
我们就可以使用一个基于bootstrap写的UI。
1、引入相关依赖
<!-- 引入swgger相关依赖 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <!-- springfox的UI。此依赖不是必须的 --> <!-- <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> -->
截至到目前2020.9.9日springfox-swagger2:2.9.2的版本主要引入了下面这些依赖:
- io.swagger:swagger-annotations:1.5.20
- io.swagger:swagger-models:1.5.20
- io.springfox:springfox-spi:2.9.2
- io.springfox:springfox-schema:2.9.2
- io.springfox:springfox-swagger-common:2.9.2
- io.springfox:springfox-spring-web:2.9.2
- com.google.guava:guava:20.0
- com.fasterxml:classmate:1.3.3
- org.slf4j:slf4j-api:1.7.24
- org.springframework.plugin:spring-plugin-core:1.2.0.RELEASE
- org.springframework.plugin:spring-plugin-metadata:1.2.0.RELEASE
- org.mapstruct:mapstruct:1.2.0.Final
- io.springfox:springfox-core:2.9.2
- net.bytebuddy:byte-buddy:1.8.12
为了页面好看,我们也可以引入这个基于Bootstrap的前端UI:
<!--基于BootStrap的UI框架--> <!--2.x.x版本的swagger-bootstrap-ui引用包方式如下 1.9.x和2.x.x选择一个包引用就行--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <!--在引用时请在maven中央仓库搜索最新版本号--> <version>2.0.4</version> </dependency>
默认的swagger界面:
引入第三方Bootstrap编写的UI:
2、配置相关配置文件
/** * SwaggerConfig file * zhenghui */ @Configuration @EnableSwagger2 @EnableKnife4j //UI public class Swagger2Config { @Bean public Docket appApi() { return new Docket(DocumentationType.SWAGGER_2) .useDefaultResponseMessages(false) //去掉默认的状态响应码 .groupName("知识库") .apiInfo(apiInfo()) .select() //扫描指定的包 // .apis(RequestHandlerSelectors.basePackage("com.glodon.demo.mybatis")) //扫描的包 //扫描只包含Swagger的注解,这种方式灵活 .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) .paths(PathSelectors.any()) .build(); } /** * 配置Swagger信息 * @return */ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("知识库接口文档") .description("该文档主要提供知识库后端的接口 \r\n\n") .contact(new springfox.documentation.service.Contact("我们是机器人<----q'(^_^)'p---->业务后台开发组", "https://www.glodon.com/", null)) .version("0.0.1") .build(); } }
在这里需要注意的是,需要扫描的位置:
有以下两种方式
//扫描指定的包 //.apis(RequestHandlerSelectors.basePackage("com.glodon.demo.mybatis")) //扫描的包 //扫描只包含Swagger的注解,这种方式灵活 .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
如何使用了第一种,那么就会扫描固定的包下的所有的Controller类,会全部自动生成相应的API示例,例如下图所示:
好处是只要你在Controller控制层的类中定义了某个接口,或者定义了多个接口,就会直接扫描出来。
简单
,方便
,快捷
。有好处的到来,就相当于要带来不好的问题,那么就是:
会造成一团乱麻
,全部生成的API,也会很乱。
只配置了这些还不够,还需要配置MVC模式来显示网页:
@Configuration public class SwaggerWebMvcConfigurer implements WebMvcConfigurer { // UI界面的配置 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //原始的swagger 如果没有用到原始的可以不用谢 // registry.addResourceHandler("swagger-ui.html") // .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("doc.html"). addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**"). addResourceLocations("classpath:/META-INF/resources/webjars/"); } }
(三)Swagger常用注解的使用简单归纳
- 1、@Api
- 2、@ApiOperation
- 3、@ApiOperation
- 4、@ApiImplicitParams、@ApiImplicitParam
- 5、@ApiResponses、@ApiResponse
- 6、@ApiModel、@ApiModelProperty
- 7、 @PathVariable
- 8、 @RequestParam
1、@APi
描述:
@Api 注解用于标注一个Controller(Class)。
主要属性:
属性 | 描述 |
value | url的路径值 |
tags | 如果设置这个值、value的值会被覆盖 |
description | 对api资源的描述 |
basePath | 基本路径可以不配置 |
position | 如果配置多个Api 想改变显示的顺序位置 |
produces | For example, “application/json, application/xml” |
consumes | For example, “application/json, application/xml” |
protocols | Possible values: http, https, ws, wss. |
authorizations | 高级特性认证时配置 |
hidden | 配置为true 将在文档中隐藏 |
例子:
@Controller @Api(tags = "请求测试API",position = 1) public class TestController { }
效果:
加上@Api注解就代表你这Controller允许被Swagger相关组件扫描到。
2、@ApiOperation
描述:
@ApiOperation 注解在用于对一个操作或HTTP方法进行描述。具有相同路径的不同操作会被归组为同一个操作对象。不同的HTTP请求方法及路径组合构成一个唯一操作。
主要属性:
属性 | 描述 |
value | url的路径值 |
tags | 如果设置这个值、value的值会被覆盖 |
description | 对api资源的描述 |
basePath | 基本路径可以不配置 |
position | 如果配置多个Api 想改变显示的顺序位置 |
produces | For example, “application/json, application/xml” |
consumes | For example, “application/json, application/xml” |
protocols | Possible values: http, https, ws, wss. |
authorizations | 高级特性认证时配置 |
hidden | 配置为true 将在文档中隐藏 |
response | 返回的对象 |
responseContainer | 这些对象是有效的 “List”, “Set” or “Map”.,其他无效 |
httpMethod | “GET”, “HEAD”, “POST”, “PUT”, “DELETE”, “OPTIONS” and “PATCH” |
code | http的状态码 默认 200 |
extensions | 扩展属性 |
例子:
@GetMapping("/get") @ResponseBody @ApiOperation(value = "get请求测试",notes = "get请求",position = 1) public String get(String name){ JSONObject json = new JSONObject(); json.put("requestType","getType"); json.put("name",name); return json.toString(); }
效果:
3、@ApiParam
描述:
@ApiParam作用于请求方法上,定义api参数的注解。
主要属性:
属性 | 描述 |
name | 属性名称 |
value | 属性值 |
defaultValue | 默认属性值 |
allowableValues | 可以不配置 |
required | 是否属性必填 |
access | 不过多描述 |
allowMultiple | 默认为false |
hidden | 隐藏该属性 |
example | 举例子 |
例子:
@GetMapping("/get") @ResponseBody @ApiOperation(value = "get请求测试",notes = "get请求",position = 1) public String get(@ApiParam(required = true,value = "name",example = "张三",name = "name") String name){ JSONObject json = new JSONObject(); json.put("requestType","getType"); json.put("name",name); return json.toString(); }
效果:
4、@ApiImplicitParams、@ApiImplicitParam
描述:
- @ApiImplicitParams:用在请求的方法上,包含一组参数说明
- @ApiImplicitParam:对单个参数的说明
主要属性:
属性 | 描述 |
name | 参数名 |
value | 参数的说明、描述 |
required | 参数是否必须必填 |
paramType | 参数放在哪个地方 query --> 请求参数的获取:@RequestParam header --> 请求参数的获取:@RequestHeader path(用于restful接口)–> 请求参数的获取:@PathVariable body(请求体)–> @RequestBody User user form(普通表单提交) |
dataType | 参数类型,默认String,其它值dataType=“Integer” |
defaultValue | 参数的默认值 |
例子:
@ApiImplicitParams({ @ApiImplicitParam(name="mobile",value="手机号",required=true,paramType="form"), @ApiImplicitParam(name="password",value="密码",required=true,paramType="form"), @ApiImplicitParam(name="age",value="年龄",required=true,paramType="form",dataType="Integer") }) @PostMapping("/login") public JsonResult login(@RequestParam String mobile, @RequestParam String password, @RequestParam Integer age){ //... return JsonResult.ok(map); }
效果:
和上一个一样,就是换了个位置来表达而已。
5、@ApiResponses、@ApiResponse
描述:
@ApiResponses、@ApiResponse进行方法返回对象的说明。
主要属性:
属性 | 描述 |
code | 数字,例如400 |
message | 信息,例如"请求参数没填好" |
response | 自定义的schema的实体类 |
例子:
@RequestMapping(value = "/ceshia", method = RequestMethod.POST) @ResponseBody @ApiOperation(value = "post请求测试",notes = "测试2",position = 2) @ApiResponses({ @ApiResponse(code = 200,message = "success",response = RequestCode200.class), @ApiResponse(code = 509,message = "服务器校验错误",response = RequestCode509.class), @ApiResponse(code = 410,message = "参数错误",response = RequestCode410.class), @ApiResponse(code = 510,message = "系统错误",response = RequestCode510.class) }) public String ceshia(@RequestBody String str){ JSONObject json = new JSONObject(); json.put("requestType","postType"); json.put("body",str); return json.toString(); }
效果:
6、@ApiModel、@ApiModelProperty
描述:
- @ApiModel用于描述一个Model的信息(这种一般用在post创建的时候,使用@RequestBody这样的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候)。
- @ApiModelProperty用来描述一个Model的属性。
主要属性:
例子:
package com.github.swaggerplugin.codes; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; /** * 200 请求成功 */ @ApiModel("RequestCode200") public class RequestCode200 { @ApiModelProperty(value = "响应码",name = "messageCode",example = "200") private Integer messageCode; @ApiModelProperty(value = "返回消息",name = "message",example = "success") private String message; public Integer getMessageCode() { return messageCode; } public void setMessageCode(Integer messageCode) { this.messageCode = messageCode; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
效果:
7、@PathVariable
描述:
@PathVariable用于获取get请求url路径上的参数,即参数绑定的作用,通俗的说是url中"?"前面绑定的参数。
例如:www.baidu.com/name=zhenghui
例子:
@GetMapping("/get") @ResponseBody @ApiOperation(value = "get请求测试",notes = "get请求",position = 1) public String get(@ApiParam(required = true,value = "name",example = "张三",name = "name") @PathVariable("name") String name){ JSONObject json = new JSONObject(); json.put("requestType","getType"); json.put("name",name); json.put("id","123"); return json.toString(); }
注:如果不加这个的话,默认就是body类型的,body类型也就是接收的是json格式的数据。
8、@RequestParam
描述:
@RequestParam用于获取前端传过来的参数,可以是get、post请求,通俗的说是url中"?"后面拼接的每个参数。
例子:
@GetMapping("/get") @ResponseBody @ApiOperation(value = "get请求测试",notes = "get请求",position = 1) public String get(@ApiParam(required = true,value = "name",example = "张三",name = "name") @RequestParam("name") String name){ JSONObject json = new JSONObject(); json.put("requestType","getType"); json.put("name",name); json.put("id","123"); return json.toString(); }
二、第二部分:自定义注解扩展Swagger的功能完成特定的需求
(一)注解是什么?如何自定义注解?
对于注解的讲解部分,我之前整理过一篇比较详细的文章,此处就不在过多的说明了,可以参考我的下列文章:
WX公众号:什么是注解?如何定义注解
CSDM:你说啥什么?注解你还不会?
(二)为什么要扩展Swagger功能以及扩展后的效果
答:
当然是Swagger当前的功能不能满足我们当前项目的现状了。
其实Swagger的已有的功能也能满足我们的需求,但是对代码的侵入性太大
了。
一句话了解侵入性:
当你的代码引入了一个组件,导致其它代码或者设计,要做相应的更改以适应新组件.这样的情况我们就认为这个新组件具有
侵入性
同时,这里又涉及到一个设计方面的概念,就是耦合性的问题.
我们代码设计的思路是"高内聚,低耦合",为了实现这个思路,就必须
降低
代码的侵入性
.
Swagger的优点有很多,有优点,必定有缺点,这是谁也改变不了的,我们能做到的就是减少缺点。
优点前面已经说了,我总结如下,当然了还有其他的:
- 1、可以跟据业务代码自动生成相关的api接口文档,尤其用于restful风格中的项目;
- 2、开发人员几乎可以不用专门去维护rest api,这个框架可以自动为你的业务代码生成restfut风格的api;
- 3、而且还提供相应的测试界面,自动显示json格式的响应。
- 4、大大方便了后台开发人员与前端的沟通与联调成本。
对于缺点:
- 1、不方便维护(当接口变动了,每次都需要去修改相应的参数配置);
- 2、关于Swagger的代码太多,严重的覆盖了原有的java逻辑代码(重点);
- 3、当一个接口有多个响应实例是,不能显示多个,只能显示一个(例如自定义的响应参数:401的响应码就包括:密码错误,参数错误,id错误等);
- 4、当接口接收的参数为json字符串的时候,在Swagger的UI中不能显示JSON字符串中具体的参数(与前端交接会出现问题,前端会不知道他要传递给你什么);
本文要解决的问题也是对于缺点的弥补,通过扩展Swagger的功能来解决这些问题。
反例:
天哪,这只是一个接口,就占了80多行。
@ResponseBody @RequestMapping(method = RequestMethod.GET) @ApiOperation(position = 2,value = "9.2.获取人工监管配置",notes = "<p><strong>请求的url:</strong></p>\n" + "<p>/api/background/config/robotMonitorConfig</p>\n" + "<p><strong>返回的成功数据:</strong></p>\n" + "<details> \n" + "<summary>点击展开查看</summary> \n" + "<pre><code class=\"language-json\">{\n" + "\n" + " message: "success",\n" + "\n" + " messageCode:"200",\n" + "\n" + " result: [\n" + "\n" + " {\n" + "\n" + " id: 1,\n" + "\n" + " robotId: 18,\n" + "\n" + " authStatus: 1,\n" + "\n" + " warnStatus: 1,\n" + "\n" + " warnRule: \n" + "\n" + " {\n" + "\n" + " "angry":1,\n" + "\n" + " "unknown":1,\n" + "\n" + " "down":1,\n" + "\n" + " "sameAnswer":1,\n" + "\n" + " "noClick":1,\n" + "\n" + " "keywords":1\n" + "\n" + " },\n" + "\n" + " warnKeywords: \n" + "\n" + " [\n" + "\n" + " "转人工",\n" + "\n" + " "人工回复"\n" + "\n" + " ],\n" + "\n" + " operatorId: "qubb",\n" + "\n" + " lastModifyTime: 231431341343123\n" + "\n" + " }\n" + "\n" + " ]\n" + "\n" + "}\n" + "\n" + "</code></pre>\n" + "</details>\n" + "<p><strong>返回的失败数据:</strong></p>\n" + "<details> \n" + "<summary>点击展开查看</summary> \n" + "<pre><code class=\"language-json\">{\n" + " message: "当前用户已退出登陆,请登陆后重试",\n" + " messageCode:"509"\n" + "}\n" + "\n" + "{\n" + " message: "param robotId error",\n" + " messageCode:"410"\n" + "}\n" + "\n" + "{\n" + " message: "system error",\n" + " messageCode:"510"\n" + "}\n" + "\n" + "</code></pre>\n" + "</details>\n" + "\n" + "\n" + "\n") public Object xxx(@ApiParam(value = "机器人ID",defaultValue = "7",required = true) @RequestParam(value = "robotId", required = false) String robotIdStr) { }
经过我对功能的扩展:
一行代码轻松搞定
@APiFileInfo("/xxx")
(三)前奏准备
1、必须要了解的Spring的三个注解
- (1)@Component(把普通pojo实例化到spring容器中,相当于配置文件中的)
- (2)@Order(1) (调整这个类被注入的顺序,也可以说是优先级)
- (3)@Configuration (用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。)
2、Swagger的可扩展组件
在源码中:可以看到下图所示的一些Plugin结尾的接口文件,我们就是要在这些上面做文章的。
关于具体的介绍,可以去本文最后,去查看另一篇文章,对于这些接口的详细分析,本文不再说明。
三、第三部分:实战
(一)实战一:针对传递json字符串的参数,使其具有相关参数的描述的功能
1、需求来源
有需求,就有需求来源或者说是需求的产生。首先要知道为什么会有这个需求呢?我们先来看为什么会有这个需求。
我们拿三个接口做示范吧:
@Controller("/studentController") @Api(tags = "学生Controller",position = 1) public class StudentController { /** * 根据学生ID获取学生信息 * @param id 学生id * @return 返回查询的结果 */ @GetMapping("/getStudentById") @ApiOperation(value = "根据学生ID获取学生信息",notes = "根据传入的学生ID获取学生信息",position = 1) public Object getStudentById(String id){ return "id="+id; } /** * 添加学生信息 * @param student 学生的信息 * @return 返回是否添加成功 */ @PostMapping("/addStudent") @ApiOperation(value = "添加学生信息",notes = "添加学生信息",position = 1) public Object addStudent(@RequestBody Student student){ return "student="+student.toString(); } /** * 含有特殊字段的,添加学生信息 * @param str 包含两种信息,1:学生信息,2:其他的特殊字段 * @return 返回是否添加成功 */ @PostMapping("/addStudent2") @ApiOperation(value = "含有特殊字段的,添加学生信息",notes = "含有特殊字段的,添加学生信息",position = 1) public Object addStudentStr(@RequestBody String str){ return "str="+str; } }
分析1:
getStudentById(String id)接口只传递一个id。
页面效果如下:
测试功能页面如下:
分析2:
addStudent(@RequestBody Student student)接口需要传递一个json数据类型的对象。
页面效果如下:
测试功能页面如下:
分析3:(问题就出在这了,注意看哦
)
addStudentStr(@RequestBody String str)接口需要传递一个json数据类型字符串。
页面效果如下:
测试功能页面如下:
2、需求分析
通过分析1
、分析2
和分析3
,三个实例可知,当传递参数为json字符串的时候,是不会显示具体的参数的。这就造成了前端人员根本就无法知道传递的是什么
。
我们的需求,简单,明了,直接。就是针对传递的参数为json字符串格式的参数时
,实现有相关参数的描述的功能
。
3、开发思路
(1)走的弯路
你首先可能想到的是:在自定义一个类呗,里面写上你需求的字段,这样不就有了吗。
首先这种办法是可以的;
但是存在的问题也是相当大的,如果我只有几个接口的话,还可以。但是一个项目有几百个接口就需要定义几百个类
。经过团队讨论,这种方法被kill了。
(2)正确的路
可以自定义一个注解,把注解加在需要加的参数上。然后通过注解的传值,去自动生成类。
这种方法是可行的。
有很多可以动态创建类的方法,经过我亲自实践,选择了:Javassist的ClassPool机制
。
如果对ClassPool有兴趣的话,可以自行查阅资料去了解下,此处就不在过多赘述了。
4、关键代码
关于自定义的注解,描述的很详细,就不多说了。
@Apicp注解的定义
:
package com.github.swaggerplugin.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author zhenghui * @date 2020年9月17日17:00:25 * @desc 需要的属性的值 */ @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Apicp { Class<?> classPath();//对象的原始class地址,必填 String modelName();//自定义 Model的名字,必填 String values()[]; //原始对象中已经存在的对象属性名字 ,必填 String noValues()[] default {} ;//原始对象中不存在的对象属性名字,非必填 String noValueTypes()[] default {};//原始对象中不存在的对象属性的类型,基本类型例如:String等,非必填 String noVlaueExplains()[] default {};//自定义变量的参数说明 非必填 }
@ApiIgp注解的定义
:
package com.github.swaggerplugin.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author zhenghui * @date 2020年9月17日17:00:49 * @desc 排除不需要的属性的值 */ @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiIgp { Class<?> classPath();//对象的原始class地址,必填 String modelName();//自定义 Model的名字,必填 String values()[]; //原始对象中已经存在的对象属性名字 ,必填 String noValues()[] default {} ;//原始对象中不存在的对象属性名字,非必填 String noValueTypes()[] default {};//原始对象中不存在的对象属性的类型,基本类型例如:String等,非必填 String noVlaueExplains()[] default {};//自定义变量的参数说明 非必填 }
自定义完了注解,但是怎么让他起作用呢?这是一步最关键的地方。
在Spring中的自动装配原理,可以去了解下。在本项目中,我们使用的是Spring的@Component
注解或者@Configuration
注解来实现的自动注入到pojo中。前面已经介绍了这俩注解的作用。
通过翻阅SpringFox(Swagger)
和knife4j-spring-boot
的源代码,我发现如果自定义扩展功能的话,只需要实现某个xxxPlugin
的接口中的apply
方法就可以。apply
方法中我们去手动扫描我们自定义的注解,然后加上相关实现的逻辑即可。
代码是没放全的,太长了,只选择了部分来放。感兴趣的话,可以去我的github上拉取,随后我还会说如何直接应用的办法。
/** * 针对传值的参数自定义注解 * @author zhenghui * @date 2020年9月13日13:25:18 * @desc 读取自定义的属性并动态生成model */ @Configuration @Order(-19999) //plugin加载顺序,默认是最后加载 public class SwaggerModelReader implements ParameterBuilderPlugin { @Autowired private TypeResolver typeResolver; static final Map<String,String> MAPS = new HashMap<>(); static { MAPS.put("byte","java.lang.Byte"); MAPS.put("short","java.lang.Short"); MAPS.put("integer","java.lang.Integer"); MAPS.put("long","java.lang.Long"); MAPS.put("float","java.lang.Float"); MAPS.put("double","java.lang.Double"); MAPS.put("char","java.lang.Character"); MAPS.put("string","java.lang.String"); MAPS.put("boolean","java.lang.Boolean"); } //根据用户自定义的类型拿到该类型所在的包的class位置 static public String getTypePath(String key){ return key==null || !MAPS.containsKey(key.toLowerCase()) ? null : MAPS.get(key.toLowerCase()); } @Override public void apply(ParameterContext context) { ResolvedMethodParameter methodParameter = context.resolvedMethodParameter(); //自定义的注解 Optional<ApiIgp> apiIgp = methodParameter.findAnnotation(ApiIgp.class); Optional<Apicp> apicp = methodParameter.findAnnotation(Apicp.class); if (apiIgp.isPresent() || apicp.isPresent()) { Class originClass = null; String[] properties = null; //注解传递的参数 Integer annoType = 0;//注解的类型 String name = null + "Model" + 1; //model 名称 //参数名称 String[] noValues = null; String[] noValueTypes = null; String[] noVlaueExplains = null; //拿到自定义注解传递的参数 if (apiIgp.isPresent()){ properties = apiIgp.get().values(); //排除的 originClass = apiIgp.get().classPath();//原始对象的class name = apiIgp.get().modelName() ; //model 名称 //参数名称 noValues = apiIgp.get().noValues(); noValueTypes = apiIgp.get().noValueTypes(); noVlaueExplains = apiIgp.get().noVlaueExplains(); }else { properties = apicp.get().values(); //需要的 annoType = 1; originClass = apicp.get().classPath();//原始对象的class name = apicp.get().modelName() ;//自定义类的名字 noValues = apicp.get().noValues(); noValueTypes = apicp.get().noValueTypes(); noVlaueExplains = apicp.get().noVlaueExplains(); } //生成一个新的类 Class newClass = createRefModelIgp(properties, noValues, noValueTypes, noVlaueExplains, name, originClass, annoType); context.getDocumentationContext() .getAdditionalModels() .add(typeResolver.resolve(newClass)); //向documentContext的Models中添加我们新生成的Class context.parameterBuilder() //修改model参数的ModelRef为我们动态生成的class .parameterType("body") .modelRef(new ModelRef(name)) .name(name); } } /** * * @param properties annoType=1:需要的 annoType=0:排除的 * @param noValues * @param noValueTypes * @param noVlaueExplains * @param name 创建的mode的名称 * @param origin * @param annoType 注解的类型 * @return */ private Class createRefModelIgp(String[] properties, String[] noValues, String[] noValueTypes, String[] noVlaueExplains, String name, Class origin, Integer annoType) { try { //获取原始实体类中所有的变量 Field[] fields = origin.getDeclaredFields(); //转换成List集合,方便使用stream流过滤 List<Field> fieldList = Arrays.asList(fields); //把传入的参数也转换成List List<String> dealProperties = Arrays.asList(properties);//去掉空格并用逗号分割 //过滤出来已经存在的 List<Field> dealFileds = fieldList .stream() .filter(s -> annoType==0 ? (!(dealProperties.contains(s.getName()))) //如果注解的类型是0,说明要取反 : dealProperties.contains(s.getName()) ).collect(Collectors.toList()); //存储不存在的变量 List<String> noDealFileds = Arrays.asList(noValues); List<String> noDealFiledTypes = Arrays.asList(noValueTypes); List<String> noDealFiledExplains = Arrays.asList(noVlaueExplains); //创建一个类 ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass(origin.getPackage().getName()+"."+name); //创建对象,并把已有的变量添加进去 createCtFileds(dealFileds,noDealFileds,noDealFiledTypes,noDealFiledExplains,ctClass,annoType); //返回最终的class return ctClass.toClass(); } catch (Exception e) { e.printStackTrace(); return null; } } @Override public boolean supports(DocumentationType delimiter) { return true; } /** * 根据propertys中的值动态生成含有Swagger注解的javaBeen * * @param dealFileds 原始对象中已经存在的对象属性名字 * @param noDealFileds 原始对象中不存在的对象属性名字 * @param noDealFiledTypes 原始对象中不存在的对象属性的类型,八大基本类型例如:dounle等,还有String * @param noDealFiledExplains 自定义变量的参数说明 * @param ctClass 源class * @throws CannotCompileException * @throws NotFoundException * @throws ClassNotFoundException */ public void createCtFileds(List<Field> dealFileds, List<String> noDealFileds, List<String> noDealFiledTypes,List<String> noDealFiledExplains, CtClass ctClass, Integer annoType) { //添加原实体类存在的的变量 // if(annoType==1) for (Field field : dealFileds) { CtField ctField = null; try { ctField = new CtField(ClassPool.getDefault().get(field.getType().getName()), field.getName(), ctClass); } catch (CannotCompileException e) { System.out.println("找不到了1:"+e.getMessage()); } catch (NotFoundException e) { System.out.println("找不到了2:"+e.getMessage()); } ctField.setModifiers(Modifier.PUBLIC); ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class); String apiModelPropertyValue = java.util.Optional.ofNullable(annotation).map(s -> s.value()).orElse(""); if (StringUtils.isNotBlank(apiModelPropertyValue)) { //添加model属性说明 ConstPool constPool = ctClass.getClassFile().getConstPool(); AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag); Annotation ann = new Annotation(ApiModelProperty.class.getName(), constPool); ann.addMemberValue("value", new StringMemberValue(apiModelPropertyValue,constPool)); attr.addAnnotation(ann); ctField.getFieldInfo().addAttribute(attr); } try { ctClass.addField(ctField); } catch (CannotCompileException e) { System.out.println("无法添加字段1:"+e.getMessage()); } } //添加原实体类中不存在的的变量 for (int i = 0; i < noDealFileds.size(); i++) { String valueName = noDealFileds.get(i);//变量名字 String valueType = noDealFiledTypes.get(i);//变量的类型 valueType=getTypePath(valueType); //根据变量的类型,变量的名字,变量将要在的类 创建一个变量 CtField ctField = null; try { ctField = new CtField(ClassPool.getDefault().get(valueType), valueName, ctClass); } catch (CannotCompileException e) { System.out.println("找不到了3:"+e.getMessage()); } catch (NotFoundException e) { System.out.println("找不到了4:"+e.getMessage()); } ctField.setModifiers(Modifier.PUBLIC);//设置权限范围是私有的,或者public等 if(noDealFiledExplains.size()!=0){ //参数设置描述 String apiModelPropertyValue = (apiModelPropertyValue=noDealFiledExplains.get(i))==null?"无描述":apiModelPropertyValue;//参数描述 System.out.println(apiModelPropertyValue); if (StringUtils.isNotBlank(apiModelPropertyValue)) { //添加model属性说明 ConstPool constPool = ctClass.getClassFile().getConstPool(); AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag); Annotation ann = new Annotation(ApiModelProperty.class.getName(), constPool); ann.addMemberValue("value", new StringMemberValue(apiModelPropertyValue,constPool)); attr.addAnnotation(ann); ctField.getFieldInfo().addAttribute(attr); } } //把此变量添加到类中 try { ctClass.addField(ctField); } catch (CannotCompileException e) { System.out.println("无法添加字段2:"+e.getMessage()); } } } }
5、实战成果
我们修改接口如下:
/** * 含有特殊字段的,添加学生信息 * @param str 包含两种信息,1:学生信息,2:其他的特殊字段 * @return 返回是否添加成功 */ @PostMapping("/addStudent2") @ApiOperation(value = "含有特殊字段的,添加学生信息",notes = "含有特殊字段的,添加学生信息",position = 1) public Object addStudentStr(@Apicp(values = {"Id","name"}, //Student类中已经存在的 modelName = "addStudent2", //自定义Model的名字,也是要生成的类名 classPath = Student.class, //原始的类 noValues = {"lala","haha","xixi"}, //原始的类中没有的参数 noValueTypes = {"string","integer","double"},//原始的类中没有的参数的类型 noVlaueExplains = {"啦啦","哈哈","嘻嘻"})//原始的类中没有的参数的描述 @RequestBody String str){ return "str="+str; }
效果:
可以看到,我们自定义的注解起作用了,而且参数名称,参数说明,数据类型都有了。
注:是否必须这个选项,下次升级,就会有了。
在调试的时候,也可以看到,也有了:
这样就不用我们再去手动为几百个接口创建几百个类了。
(二)实战二:减少在Controller中Swagger的代码,使其可以从某些文件中读取信息,自动配置Swagge的功能
1、需求来源
我们需要对接口的返回值进行描述,例如:
code为200的返回值:(来源:我从简书上的api返回的结果拷贝过来的)
[ { "id":62564697, "slug":"09c7db472fa6", "title":"Java的SPI机制", "view_count":42, "user":{ "id":12724216, "nickname":"bdqfork", "slug":"a2329f464833", "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg" } }, { "id":62564140, "slug":"ec3bd614dcb0", "title":"SLF4J日志级别以及使用场景", "view_count":381, "user":{ "id":12724216, "nickname":"bdqfork", "slug":"a2329f464833", "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg" } } ]
code为410的返回值:
{ "messageCode": 410, "message": "姓名不能为空" }
code为509的返回值:
{ "messageCode": 509, "message": "姓名长度不超过15" } { "messageCode": 509, "message": "姓名数量不能超过15个" } { "messageCode": 509, "message": "姓名已存在" } { "messageCode": 509, "message": "存在姓名,暂不可新增" }
code为510的返回值:
{ "messageCode": 510, "message": "system error" }
我们可以这样加:
/** * 根据学生ID获取学生信息 * @param id 学生id * @return 返回查询的结果 */ @GetMapping("/getStudentById") @ApiOperation(value = "根据学生ID获取学生信息",notes = "" + "[\n" + " {\n" + " \"id\":62564697,\n" + " \"slug\":\"09c7db472fa6\",\n" + " \"title\":\"Java的SPI机制\",\n" + " \"view_count\":42,\n" + " \"user\":{\n" + " \"id\":12724216,\n" + " \"nickname\":\"bdqfork\",\n" + " \"slug\":\"a2329f464833\",\n" + " \"avatar\":\"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg\"\n" + " }\n" + " },\n" + " {\n" + " \"id\":62564140,\n" + " \"slug\":\"ec3bd614dcb0\",\n" + " \"title\":\"SLF4J日志级别以及使用场景\",\n" + " \"view_count\":381,\n" + " \"user\":{\n" + " \"id\":12724216,\n" + " \"nickname\":\"bdqfork\",\n" + " \"slug\":\"a2329f464833\",\n" + " \"avatar\":\"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg\"\n" + " }\n" + " }\n" + "]",position = 1) public Object getStudentById(String id){ return "id="+id; }
页面效果:
我们要做的就是来纠正这个。
而且这一个200的接口描述就占了一个屏幕:
当一个controller有好多个接口该如何是好的,一定会被接口的描述所覆盖的。这就是我们需求的来源。
2、需求分析
看到页面效果
你可能会有疑惑为什么加了\n也不能回车显示,我去查阅了Swagger的UI源码是如何展现出来的。原理是通过makdown的方式,通过渲染得到的。所以我们可以把makdown的语法转换成html语法进行实现,经过我编写的转换小工具之后,发现是可以的。
3、开发思路
先去网上查查是否有相应的转换工具。
我们先引入一下,就是通过这个来做转换的:
<!--makdown to html--> <dependency> <groupId>com.vladsch.flexmark</groupId> <artifactId>flexmark-all</artifactId> <version>0.50.42</version> </dependency>
实现的代码很简单:
//makdown语法转换成html语法的工具 MutableDataSet options = new MutableDataSet(); Parser parser = Parser.builder(options).build(); HtmlRenderer renderer = HtmlRenderer.builder(options).build(); // You can re-use parser and renderer instances Node document = parser.parse(mdBody.toString()); //转换成html String html = renderer.render(document); // "<p>This is <em>Sparta</em></p>\n"
运行之后就会得到转换后的html语法:
我们把转换后的html代码复制到接口描述中:
/** * 根据学生ID获取学生信息 * @param id 学生id * @return 返回查询的结果 */ @GetMapping("/getStudentById") @ApiOperation(value = "根据学生ID获取学生信息",notes = "" + "<pre><code class=\"language-json\">[\n" + " {\n" + " "id":62564697,\n" + " "slug":"09c7db472fa6",\n" + " "title":"Java的SPI机制",\n" + " "view_count":42,\n" + " "user":{\n" + " "id":12724216,\n" + " "nickname":"bdqfork",\n" + " "slug":"a2329f464833",\n" + " "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg"\n" + " }\n" + " },\n" + " {\n" + " "id":62564140,\n" + " "slug":"ec3bd614dcb0",\n" + " "title":"SLF4J日志级别以及使用场景",\n" + " "view_count":381,\n" + " "user":{\n" + " "id":12724216,\n" + " "nickname":"bdqfork",\n" + " "slug":"a2329f464833",\n" + " "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg"\n" + " }\n" + " }\n" + "]\n" + "</code></pre>\n" + "\n" + "\n" + "",position = 1) public Object getStudentById(String id){ return "id="+id; }
再次看一下效果:
果然是可以的。
但是如果有几百个接口的话,你还去一个一个去复制粘贴吗?
下面就是来解决当有大量接口的时候如何办的问题。
4、关键代码
关于makdown转换成html语法的代码如下:
我是做了升级的,当遇到代码块的时候会变成折叠的。
public class MdToHtml { /** * @param md makdown语法 * @return html语法 */ public final static String makdownToHtml(String md) { StringBuilder mdBody = new StringBuilder(); //遍历传递过来的md,查找到```xxx开头的 一直到```结尾的 for (int i = 0; i < md.length() ; i++) { //重新构造一个```json ```代码块 StringBuilder newCodeBody = new StringBuilder(); newCodeBody.append("\n\n"); newCodeBody.append("<details> \n"); newCodeBody.append("\n"); newCodeBody.append("<summary>点击展开查看</summary> \n"); //code body start ```开始 if(md.charAt(i)=='`' && md.charAt(i+1)=='`' && md.charAt(i+2)=='`'){ String temp = md.substring(i+3);//开始分割``` int strIndex = findStrIndex(temp);//查找```之后的代码类型例如:```java,```json等,查找结束的位置 String codeType = md.substring(i + 3, i + 3 + strIndex);//保留代码的类型 newCodeBody.append("\n```"+codeType+"\n");//重新合并类型 //从代码块的类型开始例如:```json之后开始 +1:代码块类型之后的回车 ```结束 for (int j = i+3+strIndex+1; j < md.length() ; j++) { //code body end 遇到```代码块结束了 if(md.charAt(j)=='`' && md.charAt(j+1)=='`' && md.charAt(j+2)=='`'){ i=j+3; //让i跳过去```json 到```中的代码 //追加上```代码块的结尾 newCodeBody.append("\n```\n"); //追加上折叠代码块的结尾 newCodeBody.append("\n"); newCodeBody.append("</details>"); newCodeBody.append("\n"); break;//跳出本次循环 代表```json 此代码块 ```结束了 }else{ //组合code body内的代码 newCodeBody.append(md.charAt(j)); } } //代码整合结束后,把整合后的代码追加到makdown的Body中 mdBody.append(newCodeBody.toString()); }else{ //其他的代码一律直接追加到makdown body中 mdBody.append(md.charAt(i)); } } //makdown语法转换成html语法的工具 MutableDataSet options = new MutableDataSet(); Parser parser = Parser.builder(options).build(); HtmlRenderer renderer = HtmlRenderer.builder(options).build(); // You can re-use parser and renderer instances Node document = parser.parse(mdBody.toString()); //转换成html String html = renderer.render(document); // "<p>This is <em>Sparta</em></p>\n" return html; } static private int findStrIndex(String str){ int sum = 0; for (int i = 0; i <str.length() ; i++) { if(str.charAt(i)=='\n') return sum++; else sum++; } return -1; } }
如果用了,效果会是这样:
可以展开,可以合上。
纯粹是利用了makdown的语法来实现的。
依然首先自定义一个注解:
package com.github.swaggerplugin.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface APiFileInfo { String value() default "";//url,flag 在某文档中存放的一个标志 }
该注解的实现:
实现是非常简单的
,难的是如何解析。
/** * 针对方法自定义注解 * @author zhenghui * @date 2020年9月13日13:25:18 * @desc 读取自定义的属性并动态生成model * OperationBuilderPlugin:对方法起作用 */ @Component @Order(1) public class OperationPositionBulderPlugin implements OperationBuilderPlugin { @Autowired private TypeResolver typeResolver; static Map<String,Map<Integer, APiFileInfoBean>> apiFileInfoMaps = null; public static String swaggerMdPaths = "src/main/resources/md"; private static boolean flag = false; @Value("${swagger.md.paths}") private void setSwaggerMdPaths(String swaggerMdPaths){ OperationPositionBulderPlugin.swaggerMdPaths = swaggerMdPaths; } // 解析文件 public OperationPositionBulderPlugin() { String[] paths = swaggerMdPaths.split(","); System.out.println("开始解析文件了------------>>>>"); System.out.println("文件地址:"+Arrays.toString(paths)); // Map<String,Map<Integer,APiFileInfoBean>> apiFileInfoMaps = initFile(new String[]{"src/main/resources/md/md.md"}); if(apiFileInfoMaps==null) { apiFileInfoMaps = ReadFromFile.initFileOrDirectory(paths); flag=apiFileInfoMaps==null?false:true; } } @Override public void apply(OperationContext context) { if(flag){ System.out.println("有文件,加载:"+flag); //1、查找是否定义了说明文件的所在位置 // Optional<ApiFIleURI> apiFIleURIOptional = context.findAnnotation(ApiFIleURI.class); // if(apiFIleURIOptional.isPresent()){ // String mdFileURIs[] = apiFIleURIOptional.get().vlaue();//拿到文件的所在位置 //2、查找APiFileInfo注解, Optional<APiFileInfo> apiFileInfoOptional = context.findAnnotation(APiFileInfo.class); if (apiFileInfoOptional.isPresent()) { String flag = null;//获取URL(URL作用是定位到) System.out.println("apiFileInfoOptional--->"+apiFileInfoOptional.get().value()); flag = apiFileInfoOptional.get().value();//获取标志,标志:在文件中所在的位置 //构建消息 context.operationBuilder() .responseMessages(buildResponseMessage(flag, apiFileInfoMaps)); } // } }else { System.out.println("没有文件,不加载"); } } /** * 构造ResponseMessage * @param flag 该消息说明描述的文本所在的位置 * @param apiFileInfoMaps * @return */ private Set<ResponseMessage> buildResponseMessage(String flag, Map<String, Map<Integer,APiFileInfoBean>> apiFileInfoMaps) { Map<Integer,APiFileInfoBean> aPiFileInfoBean = apiFileInfoMaps.get(flag); Set<ResponseMessage> set = new HashSet<>(); ResponseMessage responseMessage = null; if(aPiFileInfoBean!=null) for (Integer code : aPiFileInfoBean.keySet()) { APiFileInfoBean fileInfoBean = aPiFileInfoBean.get(code); responseMessage = new ResponseMessageBuilder() .code(code) .message(MdToHtml.makdownToHtml(fileInfoBean.getMessage())) .responseModel(new ModelRef("UpdateRobotModel")) .build(); // responseMessage = new ResponseMessage(code, MdToHtml.makdownToHtml(fileInfoBean.getMessage()), null,new HashMap<>(),new LinkedList<>()); set.add(responseMessage); } return set; } @Override public boolean supports(DocumentationType delimiter) { return true; } }
解析文件内容:
代码中有很多遗留的debug的打印语句,可以忽略。
package com.github.swaggerplugin.util; import com.github.swaggerplugin.bean.APiFileInfoBean; import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; //文件解析工具 public class ReadFromFile { /** * 解析文件 * @param mdFileURIs * @return */ public static Map<String, Map<Integer, APiFileInfoBean>> initFileOrDirectory(String[] mdFileURIs) { Map<String, Map<Integer,APiFileInfoBean>> map = new HashMap<>(); for (String fileURIs : mdFileURIs) { File file = new File(fileURIs); //是文件夹 if(file!=null && !file.isFile()){ //解析目录中的文件 initDirectory(map,file); //是文件 }else if(file!=null && file.isFile()){ //解析单个文件 initFile(map,file); } } return map.size()<=0?null:map; } /** * 解析文件 * @param map * @param file */ private static void initFile(Map<String, Map<Integer,APiFileInfoBean>> map, File file) { List<String> list = ReadFileDataToList(file);//文件内容读取到List中,方便之后解析和操作 int start = 0;//一个api的描述的开始 int end = 0;//一个api的描述的结束 //操作每一行数据 for (int i = 0; i < list.size(); i++) { String dataLine = list.get(i); // System.out.println("dataLine-->"+dataLine); //开头是# URL:号的 代表是一个URL的开始 if(dataLine.startsWith("# URL:")){ start = i; // System.out.println("#-->"+dataLine); //找到当前这个API描述的 结束的位置 for (int j = i+1;j<list.size();j++){ String dataLine_start = list.get(j); if(dataLine_start.startsWith("# URL:")) { System.out.println("当前API的起始位置:"+i+"-->"+(j-1)); end = j-1;//记录结束的位置 i=j-1;//i位置跳过整个流程 break; } } //针对这个区间进行处理 disposeInterval(start,end,list,map); } } //处理最后一组API的数据 // System.out.println(start+"-->"+(list.size())); System.out.println("当前API的起始位置:"+start+"-->"+(list.size())); //针对这个区间进行处理 disposeInterval(start,list.size(),list,map); } /** * 处理这个区间的数据 * @param start * @param end * @param list * @param map */ private static void disposeInterval(int start, int end, List<String> list, Map<String,Map<Integer,APiFileInfoBean>> map) { // System.out.println("正在处理--------------------------"); int code_start = 0; int code_end = 0; String flag = null; for (int index = start; index < end ; index++) { String s = list.get(index); if(s.startsWith("# URL:")){ flag = s.substring(6, s.length()).replace(" ",""); } //找出每个code的数据的开始和结束 if(s.replace(" ","").endsWith("---") && s.replace(" ","").equals("---")){ //记录开始的位置 code_start = index+1+1; for (int i = index+1; i < end; i++) { s=list.get(i); //结束 if(s.replace(" ","").endsWith("---") && s.replace(" ","").equals("---")) { //记录结束位置 code_end = i-1; //跳过这个区间的代码 index = i-1; System.out.println("code起始位置:"+code_start+"--->"+code_end); //处理这个code的区间的数据 disposeCodeInterval(flag,code_start,code_end,list,map); break; } } } } } /** * 处理code区间的数据 * @param flag * @param code_start * @param code_end * @param list * @param map */ private static void disposeCodeInterval(String flag, int code_start, int code_end, List<String> list, Map<String, Map<Integer, APiFileInfoBean>> map) { APiFileInfoBean aPiFileInfoBean = new APiFileInfoBean(); Map<Integer,APiFileInfoBean> fileInfoBeanMap = new HashMap<>(); StringBuilder sb = new StringBuilder(); // System.out.println("正在处理--------------------------"); for (int index = code_start; index <= code_end ; index++) { String s = list.get(index); if(s.startsWith("code:")){ String code = s.substring(5, s.length()); // System.out.println(code); try {//防止code不是数字 aPiFileInfoBean.setCode(Integer.valueOf(code.replace(" ",""))); }catch (Exception e){ e.fillInStackTrace(); } }else if(s!=null && !(s.replace(" ","")).equals("") ){ // String md = list.get(index); //code body start ```开始 if(s.charAt(0)=='`' && s.charAt(1)=='`' && s.charAt(2)=='`'){ // System.out.println("哈哈,找到了:"+md); sb.append(s+"\n"); for (int i = index+1; i < list.size() ; i++) { s = list.get(i); // System.out.println("内容:"+md); sb.append(list.get(i)+"\n"); if(s!=null && !(s.replace(" ","").equals("")) &&s.charAt(0)=='`' && s.charAt(1)=='`' && s.charAt(2)=='`'){ index=i; break; } } }else{ // System.out.println(list.get(index)); sb.append(s+"\n"); } }else{ // System.out.println(list.get(index)); sb.append(list.get(index)+"\n"); } } // System.out.println("code起始位置:"+code_end+"--->"+end); aPiFileInfoBean.setMessage(sb.toString()); // System.out.println("处理结束--------------------------"); // System.out.println(aPiFileInfoBean); if(flag!=null && !flag.equals("") && aPiFileInfoBean!=null){ fileInfoBeanMap.put(aPiFileInfoBean.getCode(),aPiFileInfoBean); Map<Integer, APiFileInfoBean> fileInfoBeanMap1 = map.get(flag); if(fileInfoBeanMap1!=null) fileInfoBeanMap.putAll(fileInfoBeanMap1); map.put(flag,fileInfoBeanMap); } } /** * 解析目录中所有的文件 * @param map * @param file */ private static void initDirectory(Map<String, Map<Integer,APiFileInfoBean>> map, File file) { //解析目录 //1、查询 File[] files = file.listFiles(); for (File fi : files) { if(!fi.isFile()){ System.out.println(fi+"-->是目录"); initDirectory(map,fi);//处理目录 }else{ System.out.println(fi+"-->是文件"); initFile(map,fi);//处理文件 } } System.out.println("---"); //解析单个文件 // initFile(map,file); } /** * 读取单个文件中的内容 * @param file * @return */ public static List<String> ReadFileDataToList( File file) { FileInputStream fis = null; InputStreamReader isr = null; BufferedReader br = null; List<String> list = new ArrayList<>(); try { fis = new FileInputStream(file); isr = new InputStreamReader(fis); br = new BufferedReader(isr); String dataLine = null; // int i = 0; while((dataLine = br.readLine()) != null) { // System.out.println(i+" --->"+dataLine); list.add(dataLine); // i++; } } catch (FileNotFoundException e) { System.out.println("解析文件不存在"+e.getMessage()); } catch (IOException e) { System.out.println("解析文件出错了:"+e.getMessage()); } finally { try { if(br != null) br.close(); if(isr != null) isr.close(); if(fis != null) fis.close(); } catch (IOException e) { e.printStackTrace(); } } return list; } }
5、实战成果
我们的成果就是完成了一个注解。
注解:APiFileInfo("flag")
然后该注解就会从相应的文件中按规则进行解析出来。
xxxx.md文件存放在src/main/resources/md
下,内容如下:
# URL:/getStudentById --- code:200 **这是200响应码的描述** ```json [ { "id":62564697, "slug":"09c7db472fa6", "title":"Java的SPI机制", "view_count":42, "user":{ "id":12724216, "nickname":"bdqfork", "slug":"a2329f464833", "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg" } }, { "id":62564140, "slug":"ec3bd614dcb0", "title":"SLF4J日志级别以及使用场景", "view_count":381, "user":{ "id":12724216, "nickname":"bdqfork", "slug":"a2329f464833", "avatar":"https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg" } } ] ``` --- code:410 ### 517状态码的描述 ```json { "messageCode": 410, "message": "姓名不能为空" } ``` --- code:509 #### 510测试描述 ```json { "messageCode": 509, "message": "姓名长度不超过15" } { "messageCode": 509, "message": "姓名数量不能超过15个" } { "messageCode": 509, "message": "姓名已存在" } { "messageCode": 509, "message": "存在姓名,暂不可新增" } ``` ---
接口代码修改如下:
这样是不是就很方便了,不在有大批量的代码,也不会显得特别的乱了。
/** * 根据学生ID获取学生信息 * @param id 学生id * @return 返回查询的结果 */ @GetMapping("/getStudentById") @ApiOperation(value = "根据学生ID获取学生信息",notes = "",position = 1) @APiFileInfo("/getStudentById") public Object getStudentById(String id){ return "id="+id; }
效果如下:
是不是很方便了,看着还有多余的状态码,401,403这些事系统默认的,我们可以关闭:
return new Docket(DocumentationType.SWAGGER_2) .useDefaultResponseMessages(false)
再来看就比较简洁了:
四、第四部分:如何直接在项目中应用
1、持续关注此GitHub仓库:https://github.com/8042965/swagger-plugin
2、拉取该仓库代码;
3、想办法引入到你的项目中;
4、使用步骤很简单和前面第三部分实战环节的一样,通过注解就可以了。
也可以加我的微信进行交流:weiyi3700,QQ也行:8042965
也可以关注我的微信公众号:TrueDei,回复swagger-plugin也可以拿到。
五、第五部分:应该注意的地方
1、自定义注解时,@Order()注解如何有效的使用?
如何你想调整这个类被注入的顺序,也可以说是优先级。
那么我们可以通过调整@Order的值来调整类执行顺序的优先级,即执行的先后。
这就是@Order注解的作用。
该注解默认的优先级:
如果不指定,那么就会使用默认的这个优先级级别。
可想而知,如果你有个东西需要先加载的话,如果不指定,或者指定的优先级级别很低,那么很
有可能加载不出来
。我就遇到了这个问题。
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Documented public @interface Order { int value() default 2147483647; }
再来看一下Ordered
这个接口:
public interface Ordered { int HIGHEST_PRECEDENCE = -2147483648; int LOWEST_PRECEDENCE = 2147483647; int getOrder(); }
这个类可把我害惨了,具体怎么参的,请看:
当我自定义一个注解,并想使用Spring注入到bean中:
我从网上查的是使用@Order(Ordered.HIGHEST_PRECEDENCE)这个注解来指定顺序,由于指定好之后并没有去看一下具体是做什么的,就导致有些参数是无法被加载到的。
@Component @Order(Ordered.HIGHEST_PRECEDENCE) //@Order(1) public class OperationPositionBulderPlugin implements OperationBuilderPlugin { .........忽略代码 }
可以看到我已经指定好了200的相关数据,但是并没有起到效果。
解决办法:切换个高优先级:
@Component //@Order(Ordered.HIGHEST_PRECEDENCE) @Order(999999) public class OperationPositionBulderPlugin implements OperationBuilderPlugin { ........ }
再来看一下:
@Order实验,来源:
https://blog.csdn.net/yaomingyang/article/details/86649072
@Component @Order(1) public class BlackPersion implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("----BlackPersion----"); } } @Component @Order(0) public class YellowPersion implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("----YellowPersion----"); } }
打印结果:
----YellowPersion---- ----BlackPersion----
六、第六部分:相关链接、资料等
所有的代码均放在: