SpringMvc集成Springfox使用Swagger写文档和测试

SpringMvc集成Springfox使用Swagger写文档和测试

前言

swagger简介

swagger确实是个好东西,可以跟据业务代码自动生成相关的api接口文档,尤其用于restful风格中的>>项目,开发人员几乎可以不用专门去维护rest >api,这个框架可以自动为你的业务代码生成restfut风格的api,而且还提供相应的测试界面,自动显>>示json格式的响应。大大方便了后台开发人员与前端的沟通与联调成本。

springfox-swagger简介

Swagger 是一系列对 RESTful 接口进行规范描述和页面展示的工具. 通过 springfox-swagger 将
Swagger 与 Spring-MVC 整合, 可从代码中的注解获取信息,
并生成相应的文档。springfox本身只是利用自身的aop的特点,通过plug的方式把swagger集成了进来>>,它本身对业务api的生成,还是依靠swagger来实现。

springfox大致原理

springfox的大致原理就是,在项目启动的过种中,spring上下文在初始化的过程,框架自动跟据配置>加载一些swagger相关的bean到当前的上下文中,并自动扫描系统中可能需要生成api文档那些类,并
生成相应的信息缓存起来。如果项目MVC控制层用的是springMvc那么会自动扫描所有Controller类,跟>据这些Controller类中的方法生成相应的api文档。

注意要点

一:SpringMVC与Spring分别是两个容器,把Swagger注入到根容器(Spring)中,可能会导致异常

Spring是根容器,SpringMvc是子容器,为了能自动扫描出所有Controller类,以制定数据缓存供页>> 面api使用,有些bean需要依赖于SpringMvc中的一些bean,但这时候如果把这个注入到spring容器>>中了,因为rootcontext中没有spring mvc的context中的那些配置类时就会报错。

所以,我们解决方案是,1:保证这个类所在的路径刚好在springmvc的component-scan的配置的b ase-package范围内, 2:直接在Spring-mvc.xml配置文件中使用标签,把这个类直接进行注入,所以我们可以不使用@Configuration注解让Spring进行注入,而采用SpringMVC的配置文件的方式注入,当我们在上正式的时候,不进行注入就可以了。也不用在编辑代码去掉配置@Configuration,而重新编译一下了。


二:api分组相关,Docket实例不能延迟加载

springfox默认会把所有api分成一组,这样通过类似于http://127.0.0.1:8080/jadDemo/swagger-ui.html这样的地址访问时,会在同一个页面里加载所有api列表。这样,如果系统稍大一点,api稍微多一点,页面就会出现假死的情况,所以很有必要对api进行分组。api分组,是通过在ApiConf这个配置文件中,通过@Bean注解定义一些Docket实例

然而,同使用@Configuration一样,我并不赞成使用@Bean来配置Docket实例给api分组。因为这样,同样会把代码写死。所以,我推荐在xml文件中自己配置Docket实例实现这些类似的功能。当然,考虑到Docket中的众多属性,直接配置bean比较麻烦,可以自己为Docket写一个FactoryBean,然后在xml文件中配置FactoryBean就行了。然而将Docket配置到xml中时。又会遇到一个大坑,就那是,spring对bean的加载方式默认是延迟加载的,在xml中直接配置这些Docket实例Bean后。你会发现,没有一点效果,页面左上角的下拉列表中跟本没有你的分组项。
这个问题曾困扰过我好几个小时,后来凭经验推测出可能是因为sping bean默认延迟加载,这个Docket实例还没加载到spring context中。实事证明,我的猜测是对的。我不知道这算是springfox的一个bug,还是因为我跟本不该把对Docket的配置从原来的java代码中搬到xml配置文件中来。


三:Controller类的参数,注意防止出现无限递归的情况

Spring mvc有强大的参数绑定机制,可以自动把请求参数绑定为一个自定义的命令对像。所以,很多开发人员在写Controller时,为了偷懒,直接把一个实体对像作为Controller方法的一个参数。@RequestMapping(value = “update”)
public String update(MenuVomenuVo, Model model){}
这是大部分程序员喜欢在Controller中写的修改某个实体的代码。在跟swagger集成的时候,这里有一个大坑。如果MenuVo这个类中所有的属性都是基本类型,那还好,不会出什么问题。但如果这个类里面有一些其它的自定义类型的属性,而且这个属性又直接或间接的存在它自身类型的属性,那就会出问题。例如:假如MenuVo这个类是菜单类,在这个类时又含有MenuVo类型的一个属性parent代表它的父级菜单。这样的话,系统启动时swagger模块就因无法加载这个api而直接报错。报错的原因就是,在加载这个方法的过程中会解析这个update方法的参数,发现参数MenuVo不是简单类型,则会自动以递归的方式解释它所有的类属性。这样就很容易陷入无限递归的死循环。

为了解决这个问题,我目前只是自己写了一个OperationParameterReader插件实现类以及它依赖的ModelAttributeParameterExpander工具类,通过配置的方式替换掉到srpingfox原来的那两个类,偷梁换柱般的把参数解析这个逻辑替换掉,并避开无限递归。当然,这相当于是一种修改源码级别的方式。我目前还没有找到解决这个问题的更完美的方法,所以,只能建议大家在用spring-fox Swagger的时候尽量避免这种无限递归的情况。毕竟,这不符合springmvc命令对像的规范,springmvc参数的命令对像中最好只含有简单的基本类型属性。

SpringMVC+SpringFox的集成

1. 增加依赖

<!--springfox-swagger需要的最小依赖 start-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.5.0</version>
        </dependency>

        <!--jackson用于将springfox返回的文档对象转换成JSON字符串-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${version.jackson}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${version.jackson}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${version.jackson}</version>
        </dependency>

        <!--petStore是官方提供的一个代码参考, 可用于后期写文档时进行参考, 可不加-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-petstore</artifactId>
            <version>2.5.0</version>
        </dependency>
<!--最小依赖 end-->

2. spring-fox 提供了配置信息

说明:下面例子是配置里两个分组,分组是什么呢?
分组其实就是按照一定的规则显示接口的文档,相当于文档的指定文档的目录。
- 通过.paths(PathSelectors.ant(“/user/**”)).build()配置匹配请求地址进行分组。
- 下面是定义了两个分组,userDocket,shopDocket,注意分组名称不可相同。
- 当分组没有效果时,可以参考此配置,还可以检查配置的顺序有可能导致分组的配置失效。

package com.pkk.ssms1.config;


import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * @author peikunkun
 * @version V1.0
 * @Title: mybatisproject
 * @Package com.pkk.ssms1.config
 * @Description: <Swagger配置类>
 * @date 2018/4/12 20:02
 */

/*spring框架中本身就有的
* 有了这个注解后,spring会自动把这个类实例化成一个bean注册到spring上下文中
* 但是Spring和SpringMVC是俩个不同的容器,在使用的时候我们需要把其注入到SpringMVC容器器中,而此配置在上正式的时候,
* 需要取消掉,因为一般此api只是用于测试的时候用,我们可以采用xml配置的方式注册此bean*/
/*@Configuration*/
/*用来集成swagger 2的*/
@EnableSwagger2
/*启用srpingmvc了*/
@EnableWebMvc
public class MySwaggerConfig {

    private static final String INDEX_PAGE   = "http://localhost:8099/index.jsp";
    private static final String BASE_PACKAGE = "com.pkk.ssms1";


    @Bean
    public Docket userDocket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("用户信息操作")
                .description("用于操作用户相关接口的文档")
                .contact(new Contact("kunzai", INDEX_PAGE, "youmail"))
                .version("1.0")
                .build();
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("userDocket")
                //配置pathMapping之后,会显示为/useruser/**/user/getInfo
                /*docket.pathMapping("/user*//**");*/
                .pathMapping("/")
                //设置只生成被Api这个注解注解过的Ctrl类中有ApiOperation注解的api接口的文档
                 /*docket.select().apis(RequestHandlerSelectors.withClassAnnotation(Api.class)).apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)).build();*/
                //指定要扫描的包
                .select().apis(RequestHandlerSelectors.basePackage(BASE_PACKAGE))
                /*设置此组只匹配user/**的请求*/
                .paths(PathSelectors.ant("/user/**")).build()
                .apiInfo(apiInfo);
    }

    @Bean
    public Docket shopDocket() {

        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("商品信息操作")
                .description("用于操作商品相关接口的文档")
                .contact(new Contact("kunzai", INDEX_PAGE, "youmail"))
                .version("1.0")
                .build();
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("shopDocket")
                //配置pathMapping之后,会显示为/shop/**/shop/getInfo
                 /*docket.pathMapping("/shop*//**");*/
                .pathMapping("/")
                //设置只生成被Api这个注解注解过的Ctrl类中有ApiOperation注解的api接口的文档
                /*docket.select().apis(RequestHandlerSelectors.withClassAnnotation(Api.class)).apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)).build();*/
                //指定要扫描的包
                .select().apis(RequestHandlerSelectors.basePackage(BASE_PACKAGE))
                /*设置此组只匹配user/**的请求,需要放到最后否则无效*/
                .paths(PathSelectors.ant("/shop/*")).build()
                /*最后设置并返回Docket*/
                .apiInfo(apiInfo);
    }
}

3:接口定义Controller

userController接口定义(属于userDocket)

package com.pkk.ssms1.controller;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.pkk.ssms1.entity.User;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;

/**
 * @author peikunkun
 * @version V1.0
 * @Title: mybatisproject
 * @Package com.pkk.ssms1.controller
 * @Description: <>
 * @date 2018/4/12 20:13
 */
@RequestMapping("/user")
@RestController
@Api(tags = "UserController", description = "用户信息相关")
public class UserController {

    /*https://github.com/catinred2/swagger_test/blob/master/src*/

    @RequestMapping("/getInfo")
    @ApiOperation(value = "获取用户信息", httpMethod = "GET", notes = "显示用户信息,不显示密码")
    public Object getInfo() {
        return "哈哈,我是信息";
    }

    @RequestMapping("/login")
    @ApiOperation(value = "登录", httpMethod = "POST", notes = "用户登录文档说明", consumes = "application/x-www-form-urlencoded")
    /*@ApiImplicitParams({
            @ApiImplicitParam(name = "username", value = "用户名", required = true, defaultValue = "kunzai", paramType = "query", dataType = "String"),
            @ApiImplicitParam(name = "password", value = "密码(MD5)", required = true, defaultValue = "admin", paramType = "query", dataType = "String"),
            @ApiImplicitParam(name = "deptid", value = "部门id", required = true, defaultValue = "1", paramType = "query", dataType = "Long"),
            @ApiImplicitParam(name = "cardid", value = "卡号", required = true, defaultValue = "1", paramType = "query", dataType = "Long"),})*/
    public Object login(@ApiParam User user, HttpServletRequest request) throws Exception {
        return "哈哈可以啊";
    }
}

shopController接口定义(属于shopDocket)

package com.pkk.ssms1.controller;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.pkk.ssms1.entity.User;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;

/**
 * @author peikunkun
 * @version V1.0
 * @Title: mybatisproject
 * @Package com.pkk.ssms1.controller
 * @Description: <>
 * @date 2018/4/12 20:13
 */
@RestController
@RequestMapping("/shop")
@Api(tags = "ShopController", description = "商户信息相关")
public class ShopController {

    /*https://github.com/catinred2/swagger_test/blob/master/src*/

    @RequestMapping("/getInfo")
    @ApiOperation(value = "获取商品信息", httpMethod = "GET", notes = "显示商品信息")
    public Object getInfo() {
        return "哈哈,我是信息";
    }

    @RequestMapping("/login")
    @ApiOperation(value = "商品信息登录", httpMethod = "POST", notes = "商品信息文档说明", consumes = "application/x-www-form-urlencoded")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "username", value = "用户名", required = true, defaultValue = "kunzai", paramType = "query", dataType = "String"),
            @ApiImplicitParam(name = "password", value = "密码(MD5)", required = true, defaultValue = "admin", paramType = "query", dataType = "String"),
            @ApiImplicitParam(name = "deptid", value = "部门id", required = true, defaultValue = "1", paramType = "query", dataType = "Long"),
            @ApiImplicitParam(name = "cardid", value = "卡号", required = true, defaultValue = "1", paramType = "query", dataType = "Long"),})
    public Object login(@Valid User user, BindingResult bindingResult, HttpServletRequest request) throws Exception {
        if (bindingResult.hasErrors()) {
            System.out.println("使用注解@Valid,检验参数时,出现以下错误:" + bindingResult.getFieldError().getDefaultMessage());
            return "使用注解@Valid,检验参数时,出现以下错误:" + bindingResult.getFieldError().getDefaultMessage();
        }
        return "哈哈可以啊";
    }
}

实体类

package com.pkk.ssms1.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * @param
 * @author peikunkun
 * @version V1.0
 * @return
 * @Description: <用户实体类>
 * @date 2018/4/11 20:16
 */

/*data相当于注解在类上,相当于同时使用了@ToString、@EqualsAndHashCode
、@Getter、@Setter和@RequiredArgsConstrutor这些注解,对于POJO类十分有用
@RequiredArgsConstructor: 会生成一个包含常量,和标识了NotNull的变量
的构造方法。生成的构造方法是private,如何想要对外提供使用可以使用staticName选项生成一个static方法。*/
@Data
@ApiModel(value = "User对象", description = "用户对象信息")
public class User implements BaseEntity {

    @ApiModelProperty(required = false, dataType = "Long", hidden = true)
    private Long   id;
    @ApiModelProperty(name = "username", example = "昆仔", dataType = "String", value = "用户名称")
    private String username;
    @ApiModelProperty(name = "password", example = "admin", dataType = "String", value = "用户密码")
    private String password;
    @ApiModelProperty(required = false, name = "deptid", hidden = true, example = "1", dataType = "Long", value = "部门id")
    private Long   deptid;
    @ApiModelProperty(name = "cardid", hidden = true, example = "1", dataType = "Long", value = "身份id")
    private Long   cardid;
}

文档常用的注解

- @Api 表示该类是一个 SwaggerResource, 是对 Controller 进行注解的,表示标识这个类是swagger的资源(tags–表示说明 value–也是说明,可以使用tags替代 )
- @ApiOperation 表示对应一个 RESTful 接口, 对方法进行注解,表示一个http请求的操作(value用于方法描述 notes用于提示内容,tags可以重新分组(视情况而用) )
- @ApiResponse 表示对不同 HTTP 状态码的意义进行描述 
- @ApiParam 表示对传入参数进行注解用于方法,参数,字段说明;表示对参数的添加元数据(说明或是否必填等)(name–参数名 value–参数说明 required–是否必填)
- @ApiModel()用于类 表示对类进行说明,用于参数用实体类接收(value–表示对象名description–描述 )
- @ApiModelProperty()用于方法,字段 表示对model属性的说明或者数据操作更改(value–字段说明 name–重写属性名字dataType–重写属性类型 required–是否必填 example–举例说明 hidden–隐藏) 
- @ApiIgnore()用于类,方法,方法参数 表示这个方法或者类被忽略 
- @ApiImplicitParam() 用于方法 表示单独的请求参数 
- @ApiImplicitParams() 用于方法,包含多个 @ApiImplicitParam(name–参数名 value–参数说明dataType–数据类型 paramType–参数类型 example–举例说明)

传送门

注解说明传送门

springfox-swagger原理解析与使用过程中遇到的坑(原理分析的还是不错的)【精】

猜你喜欢

转载自blog.csdn.net/a656678879/article/details/79931477
今日推荐