文章目录
前后端分离之swagger复习
前言
说起Swagger就不得不说前后端分离
当前最主流的前后端分离技术栈:Vue+Springboot
后端时代:
前端只用管理静态页面:html
最后统一交给后端,后端将其修改为jsp,在整个过程中,后端充当主力
前后端分离时代:
后端:后端控制层,服务层,数据访问层【后端团队】
前端:前端控制层,视图层【前端团队】
前端可以自定义一些伪后端数据:json,在写的时候就存在,不需要后端,前端工程依旧能够跑起来
那么前端后端如何交互?
此时我们可以考虑API
前后端分离好处:
前后端相对独立,松耦合
前后端设置可以部署在不同的服务器上
产生的问题:
前后弹集成联调,前端人员和后端人员无法做到及时协商,尽早解决,最终导致问题爆发;
解决方式:
首先指定schema[计划的提纲],实时更新最新的API,降低集成风险
早期:指定word计划文档
前后端分离时期:
前端测试后端接口:postman
后端提供接口,需要实时更新最新的消息及改动
1、Swagger
号称世界上最流行的Api框架
RestFul Api 文档在线自动生成工具,Api文档与Api定义同步更新
直接运行,可以在线测试API接口
支持多种语言:(Java,php等)
官网:http://swagger.io/
2、SpringBoot集成Swagger
1.引入库
2.导入依赖
swagger2
ui
2.9以上的需要2.6x以上版本的springboot
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
3.配置Swagger
@EnableSwagger2 用来开启Swagger2
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
}
配置文件记得写加入
spring.mvc.pathmatch.matching-strategy=ant_path_matcher #避免null空指针
4.进入测试
http://localhost:8080/swagger-ui.html
5. 配置Swagger基本信息
Swagger的Bean实例Docket:
@Configuration
//开启Swagger2
@EnableSwagger2
public class SwaagerConfig {
@Bean
public Docket docket(){
return new Docket()
//未写入参数
}
}
源码
public class Docket implements DocumentationPlugin {
public Docket(DocumentationType documentationType) {
this.apiInfo = ApiInfo.DEFAULT;
this.groupName = "default";
this.enabled = true;
this.genericsNamingStrategy = new DefaultGenericTypeNamingStrategy();
this.applyDefaultResponseMessages = true;
this.host = "";
this.pathMapping = Optional.absent();
this.apiSelector = ApiSelector.DEFAULT;
this.enableUrlTemplating = false;
this.vendorExtensions = Lists.newArrayList();
this.documentationType = documentationType;
}
public Docket apiInfo(ApiInfo apiInfo) {
this.apiInfo = (ApiInfo)BuilderDefaults.defaultIfAbsent(apiInfo, apiInfo);
return this;
}
}
然后apiinfo源码
其中内部类
static {
DEFAULT = new ApiInfo("Api Documentation", "Api Documentation", "1.0", "urn:tos", DEFAULT_CONTACT, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList());
}
在 public Docket(DocumentationType documentationType),所以要传入一个type,看源码有三种
public static final DocumentationType SWAGGER_12 = new DocumentationType("swagger", "1.2");
public static final DocumentationType SWAGGER_2 = new DocumentationType("swagger", "2.0");
public static final DocumentationType SPRING_WEB = new DocumentationType("spring-web", "1.0");
写入自己的信息
@Configuration
//开启Swagger2
@EnableSwagger2
public class SwaagerConfig {
@Bean
public Docket docket(){
//配置了swaggerbean的Docket的bean实例
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo());
}
//配置swagger的信息apiinfo
private ApiInfo apiInfo(){
//作者信息
Contact contact=new Contact("nameyang","https://yangxingyue0623.github.io/","1573478674@qq.com");
return new ApiInfo("titleyang的Swaggerapi文档",
"描述加油加油",
"1.0",
"urn:tos",
contact,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());
}
}
http://localhost:8080/swagger-ui.html 配置完后访问就变成自己信息
可以查看下面的接口信息。为什么访问swagger-ui.html,是因为extend libnary里面swagger依赖包的META-INFO里面的
6.Swagger配置扫描接口
一般用Docket.select()方法,所以继续配置
RequestHandlerSelectors()配置要扫描接口的方式
basePackage()指定扫描的包
any()扫描全部的包
none()都不扫描
withClassAnnotation()扫描类上的注解,参数是注解的反射对象
withMethodAnnotation()扫描方法上的注解
@Bean
public Docket docket(){
//配置了Swagger的Docket的Bean实例
// DocumentationType documentationType = new DocumentationType();
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//RequestHandlerSelectors,配置要扫描接口的方式
//basePackage指定扫描的包
//any扫描全部的包
//none()都不扫描
//withClassAnnotation()扫描类上的注解,参数是注解的反射对象
//withClassAnnotation(RestController.class)扫描类上带rescontroller的注解
//withMethodAnnotation()扫描方法上的注解
//比如apis(RequestHandlerSelectors.basePackage())
.apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
//paths(),过滤路径,只让/kuang开头的请求通过
.paths(PathSelectors.ant("/kuang/**"))
//ant只扫描/kuang/**
.build();
}
7.配置是否启动Swagger
enable是否启动swagger,如果为false,则swagger不能在浏览器中访问
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(false) //其实是源码里面的
那么如果我们希望Swagger在某一个环境中使用,在其他时候不使用怎么办
首先判断环境
一般 application.properties spring.profiles.active=dev 生产环境
application-dev.yml 生产环境
application-pro.yml 正式环境
@EnableSwagger2
public class SwaagerConfig {
@Bean
public Docket docket(Environment environment){
//是springformworker的
//获取项目的生产环境
//设置要显示的swagger环境
Profiles profiles = Profiles.of("dev");
//这个包下的org.springframework.core.env
//通过environment.acceptsProfiles判断是否处在自己设定的环境当中
boolean flag = environment.acceptsProfiles(profiles);
//配置了swaggerbean的Docket的bean实例
return new Docket(DocumentationType.SWAGGER_2)
.enable(flag)
运行出现密码问题,在启动类上加入@EnableSwagger2
8. 配置API文档的分组
设置组名就是
return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .groupName("hello")
如果我们想配置多个分组怎么办呢?
创建多个docket对象,设置不同的组名@Bean public Docket docket1(){ return new Docket(DocumentationType.SWAGGER_2).groupName("A"); } @Bean public Docket docket2(){ return new Docket(DocumentationType.SWAGGER_2).groupName("B"); } @Bean public Docket docket3(){ return new Docket(DocumentationType.SWAGGER_2).groupName("C"); }
9.实体类的配置
User类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
public String username;
public String password;
HelloController中:
//只要返回值中存在实体类,他就会被扫描到Swagger中
@PostMapping("/user")
public User user(){
return new User("zs","123");
}
查看Swagger:
10.文档注解
为了方便交互,方便文档阅读,我们可以为文档添加说明信息
@ApiModel(“用户实体类”)为实体类添加说明信息
@ApiModelProperty(“用户名”)为实体类中的属性添加说明信息
@Api(tags = “控制器-hello”)为接口添加说明信息
@ApiOperation(“hello请求”)为请求增加说明信息
@ApiParam(“hello请求中的name”)为请求中的参数增加说明信息
HelloController中:
//只要返回值中存在实体类,他就会被扫描到Swagger中
@PostMapping("/user")
public User user(){
return new User("zs","123");
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户名")
public String username;
@ApiModelProperty("密码")
public String password;
}
@ApiOperation("Hello控制类")
@GetMapping(value="/hello2")
public String hello2(@ApiParam("用户名") String username){
return "Hello"+username;
}
总结
- 我们可以通过Swagger给一些比较难理解的属性或者接口,增加注释信息
- 接口文档实时更新
- 可以在线测试
注意点:
在正式发布的时候,需要关闭Swagger,一是出于安全考虑,,二是节省内存
之前项目用到的Swagger
官网:API Documentation & Design Tools for Teams | Swagger
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。功能主要包含以下几点:
A. 使得前后端分离开发更加方便,有利于团队协作
B. 接口文档在线自动生成,降低后端开发人员编写接口文档的负担
C. 接口功能测试
使用Swagger只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。
直接使用Swagger, 需要按照Swagger的规范定义接口, 实际上就是编写Json文件,编写起来比较繁琐、并不方便, 。而在项目中使用,我们一般会选择一些现成的框架来简化文档的编写,而这些框架是基于Swagger的,如knife4j。
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。而我们要使用kinfe4j,需要在pom.xml中引入如下依赖即可:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
3.0.3 Cannot resolve org.mapstruct:mapstruct:1.3.1.Final
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.2</version> </dependency> 3.0.3 Cannot resolve org.mapstruct:mapstruct:1.3.1.Final
3.2 使用方式
接下来,我们就将我们的项目集成Knife4j,来自动生成接口文档。这里我们还是需要再创建一个新的分支v1.2,在该分支中进行knife4j的集成,集成测试完毕之后,没有问题,我们再将v1.2分支合并到master。
使用knife4j,主要需要操作以下几步:
1). 导入knife4j的maven坐标
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
(我用的版本优点问题,我调低了2.04)
2). 导入knife4j相关配置类
这里我们就不需要再创建一个新的配置类了,我们直接在WebMvcConfig配置类中声明即可。
A. 在该配置类中加上两个注解 @EnableSwagger2 @EnableKnife4j ,开启Swagger和Knife4j的功能。
B. 在配置类中声明一个Docket类型的bean, 通过该bean来指定生成文档的信息。
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
@Bean
public Docket createRestApi() {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
注意: Docket声明时,指定的有一个包扫描的路径,该路径指定的是Controller所在包的路径。因为Swagger在生成接口文档时,就是根据这里指定的包路径,自动的扫描该包下的@Controller, @RestController, @RequestMapping等SpringMVC的注解,依据这些注解来生成对应的接口文档。
————————————————
3). 设置静态资源映射
由于Swagger生成的在线文档中,涉及到很多静态资源,这些静态资源需要添加静态资源映射,否则接口文档页面无法访问。因此需要在 WebMvcConfig类中的addResourceHandlers方法中增加如下配置。
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
4). 在LoginCheckFilter中设置不需要处理的请求路径
需要将Swagger及Knife4j相关的静态资源直接放行,无需登录即可访问,否则我们就需要登录之后,才可以访问接口文档的页面。
在原有的不需要处理的请求路径中,再增加如下链接:
"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"
3.3 查看接口文档
经过上面的集成配置之后,我们的项目集成Swagger及Knife4j就已经完成了,接下来我们可以重新启动项目,访问接口文档,访问链接为: http://localhost:8080/doc.html
我们可以看到,在所有的Controller中提供的所有的业务增删改查的接口,全部都已经自动生成了,我们通过接口文档可以看到请求的url、请求方式、请求参数、请求实例、响应的参数,响应的示例。 并且呢,我们也可以通过这份在线的接口文档,对接口进行测试。
注意: 由于我们服务端的Controller中的业务增删改查的方法,都是必须登录之后才可以访问的,所以,我们在测试时候,也是需要先访问登录接口。登录完成之后,我们可以再访问其他接口进行测试。
我们不仅可以在浏览器浏览生成的接口文档,Knife4j还支持离线文档,对接口文档进行下载,支持下载的格式有:markdown、html、word、openApi。
3.4 常用注解
3.4.1 问题说明
在上面我们直接访问Knife4j的接口文档页面,可以查看到所有的接口文档信息,但是我们发现,这些接口文档分类及接口描述都是Controller的类名(驼峰命名转换而来)及方法名,而且在接口文档中,所有的请求参数,响应数据,都没有中文的描述,并不知道里面参数的含义,接口文档的可读性很差。
外卖注解swaager
3.4.2 注解介绍
为了解决上述的问题,Swagger提供了很多的注解,通过这些注解,我们可以更好更清晰的描述我们的接口,包含接口的请求参数、响应数据、数据模型等。核心的注解,主要包含以下几个:
注解 位置 说明
@Api 类 加载Controller类上,表示对类的说明
@ApiModel 类(通常是实体类) 描述实体类的作用
@ApiModelProperty 属性 描述实体类的属性
@ApiOperation 方法 说明方法的用途、作用
@ApiImplicitParams 方法 表示一组参数说明
@ApiImplicitParam 方法 用在@ApiImplicitParams注解中,指定一个请求参数的各个方面的属性
3.4.3 注解测试
1). 实体类
可以通过 @ApiModel , @ApiModelProperty 来描述实体类及属性
@Data
@ApiModel("套餐")
public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键")
private Long id;
//分类id
@ApiModelProperty("分类id")
private Long categoryId;
//套餐名称
@ApiModelProperty("套餐名称")
private String name;
//套餐价格
@ApiModelProperty("套餐价格")
private BigDecimal price;
//状态 0:停用 1:启用
@ApiModelProperty("状态")
private Integer status;
//编码
@ApiModelProperty("套餐编号")
private String code;
//描述信息
@ApiModelProperty("描述信息")
private String description;
//图片
@ApiModelProperty("图片")
private String image;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
2). 响应实体R
@Data
@ApiModel("返回结果")
public class R<T> implements Serializable{
@ApiModelProperty("编码")
private Integer code; //编码:1成功,0和其它数字为失败
@ApiModelProperty("错误信息")
private String msg; //错误信息
@ApiModelProperty("数据")
private T data; //数据
@ApiModelProperty("动态数据")
private Map map = new HashMap(); //动态数据
//省略静态方法 ....
}
3). Controller类及其中的方法
描述Controller、方法及其方法参数,可以通过注解: @Api, @APIOperation, @ApiImplicitParams, @ApiImplicitParam
@RestController
@RequestMapping("/setmeal")
@Slf4j
@Api(tags = "套餐相关接口")
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private CategoryService categoryService;
@Autowired
private SetmealDishService setmealDishService;
/**
* 新增套餐
* @param setmealDto
* @return
*/
@PostMapping
@CacheEvict(value = "setmealCache",allEntries = true)
@ApiOperation(value = "新增套餐接口")
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}",setmealDto);
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
@ApiOperation(value = "套餐分页查询接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "page",value = "页码",required = true),
@ApiImplicitParam(name = "pageSize",value = "每页记录数",required = true),
@ApiImplicitParam(name = "name",value = "套餐名称",required = false)
})
public R<Page> page(int page,int pageSize,String name){
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>();
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
//对象拷贝
BeanUtils.copyProperties(item,setmealDto);
//分类id
Long categoryId = item.getCategoryId();
//根据分类id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
//分类名称
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
/**
* 删除套餐
* @param ids
* @return
*/
@DeleteMapping
@CacheEvict(value = "setmealCache",allEntries = true)
@ApiOperation(value = "套餐删除接口")
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids:{}",ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@GetMapping("/list")
@Cacheable(value = "setmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status")
@ApiOperation(value = "套餐条件查询接口")
public R<List<Setmeal>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}
log.info("ids:{}",ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@GetMapping("/list")
@Cacheable(value = "setmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status")
@ApiOperation(value = "套餐条件查询接口")
public R<List<Setmeal>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}
}