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 表示该类是一个 Swagger 的 Resource, 是对 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–举例说明)