小言_互联网的博客

历经14天自定义3个注解解决项目的3个Swagger难题

413人阅读  评论(0)

文章目录

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: &quot;success&quot;,\n" +
              "\n" +
              "    messageCode:&quot;200&quot;,\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" +
              "                &quot;angry&quot;:1,\n" +
              "\n" +
              "                &quot;unknown&quot;:1,\n" +
              "\n" +
              "                &quot;down&quot;:1,\n" +
              "\n" +
              "                &quot;sameAnswer&quot;:1,\n" +
              "\n" +
              "                &quot;noClick&quot;:1,\n" +
              "\n" +
              "                &quot;keywords&quot;:1\n" +
              "\n" +
              "            },\n" +
              "\n" +
              "            warnKeywords: \n" +
              "\n" +
              "            [\n" +
              "\n" +
              "                &quot;转人工&quot;,\n" +
              "\n" +
              "                &quot;人工回复&quot;\n" +
              "\n" +
              "            ],\n" +
              "\n" +
              "            operatorId: &quot;qubb&quot;,\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: &quot;当前用户已退出登陆,请登陆后重试&quot;,\n" +
              "    messageCode:&quot;509&quot;\n" +
              "}\n" +
              "\n" +
              "{\n" +
              "    message: &quot;param robotId error&quot;,\n" +
              "    messageCode:&quot;410&quot;\n" +
              "}\n" +
              "\n" +
              "{\n" +
              "    message: &quot;system error&quot;,\n" +
              "    messageCode:&quot;510&quot;\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" +
              "        &quot;id&quot;:62564697,\n" +
              "        &quot;slug&quot;:&quot;09c7db472fa6&quot;,\n" +
              "        &quot;title&quot;:&quot;Java的SPI机制&quot;,\n" +
              "        &quot;view_count&quot;:42,\n" +
              "        &quot;user&quot;:{\n" +
              "            &quot;id&quot;:12724216,\n" +
              "            &quot;nickname&quot;:&quot;bdqfork&quot;,\n" +
              "            &quot;slug&quot;:&quot;a2329f464833&quot;,\n" +
              "            &quot;avatar&quot;:&quot;https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg&quot;\n" +
              "        }\n" +
              "    },\n" +
              "    {\n" +
              "        &quot;id&quot;:62564140,\n" +
              "        &quot;slug&quot;:&quot;ec3bd614dcb0&quot;,\n" +
              "        &quot;title&quot;:&quot;SLF4J日志级别以及使用场景&quot;,\n" +
              "        &quot;view_count&quot;:381,\n" +
              "        &quot;user&quot;:{\n" +
              "            &quot;id&quot;:12724216,\n" +
              "            &quot;nickname&quot;:&quot;bdqfork&quot;,\n" +
              "            &quot;slug&quot;:&quot;a2329f464833&quot;,\n" +
              "            &quot;avatar&quot;:&quot;https://upload.jianshu.io/users/upload_avatars/12724216/6f2b07cc-e9bf-440d-a6ad-49fbaa3b49ce.jpg&quot;\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----

六、第六部分:相关链接、资料等

1、八一菜刀springfox源码解读

2、Swagger常用注解

3、一句话让你明白代码的侵入性

所有的代码均放在:

GitHub仓库:https://github.com/8042965/swagger-plugin


转载:https://blog.csdn.net/qq_17623363/article/details/109259315
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场