Swagger在php和java项目中的应用
- Swagger简介
- Swagger在java项目中的应用
- 步骤
- 常用注解
- Swagger在php项目中的应用
Swagger简介
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。
总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法、参数和模型紧密集成到服务器端的代码,允许 API 来始终保持同步。Swagger 让部署管理和使用功能强大的 API 从未如此简单。
- 对于后端开发人员来说
- 不用再手写WiKi接口拼大量的参数,避免手写错误
- 对代码侵入性低,采用全注解的方式,开发简单
- 方法参数名修改、增加、减少参数都可以直接生效,不用手动维护
- 增加了开发成本,写接口还得再写一套参数配置
- 对于前端开发来说
- 后端只需要定义好接口,会自动生成文档,接口功能、参数一目了然
- 联调方便,如果出问题,直接测试接口,实时检查参数和返回值,就可以快速定位是前端还是后端的问题
- 对于测试来说
- 对于某些没有前端界面UI的功能,可以用它来测试接口
- 操作简单,不用了解具体代码就可以操作
Swagger在java项目中的应用
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案
步骤
- 导入knife4j的maven坐标
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
- 导入knife4j相关配置类(WebMvcConfig)
- 设置静态资源,否则接口文档页面无法访问
package com.demo.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers (ResourceHandlerRegistry registry) {
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Bean
public Docket createRestApi () {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.demo.controller")) // swagger扫描的包
.paths(PathSelectors.any())
.build();
}
/**
* 介绍接口信息
*/
private ApiInfo apiInfo () {
return new ApiInfoBuilder()
.title("Robin's Swagger documents")
.version("1.0")
.description("Robin's Swagger documents")
.build();
}
}
- 在LoginCheckFilter中设置不需要处理的请求路径
package com.demo.filter;
import com.alibaba.fastjson.JSON;
import com.demo.common.BaseContext;
import com.demo.common.R;
import com.demo.entity.Employee;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 检查用户是否已经完成登录
*/
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
// 路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
// 定义不需要处理的请求路径
public static final String[] urls = new String[]{
"/employee/login", // 登录接口
"/employee/logout", // 退出接口
"/backend/**", // 放行后台静态资源
"/front/**", // 放行前台静态资源
"/common/download", // 文件下载接口
"/user/login", // 移动端登录
"/user/sendMsg", // 获取验证码接口
"/doc.html", // swagger文档路径
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs",
};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1、获取本次请求的URI
String requestUri = request.getRequestURI();
// 2、判断本次请求是否需要处理,如果不需要处理,则直接放行
if (check(requestUri)) {
filterChain.doFilter(request, response);
return;
}
// 3、判断登录状态,如果已登录,则直接放行
Long empId = (Long) request.getSession().getAttribute("employee");
Long userId = (Long) request.getSession().getAttribute("user");
if (empId != null || userId != null) {
// 将当前登录的用户id保存到ThreadLocal中
if (empId != null) BaseContext.setCurrentId(empId);
if (userId != null) BaseContext.setCurrentId(userId);
filterChain.doFilter(request, response);
return;
}
// 4、如果未登录,则返回未登录结果
log.info("拦截到请求:{}", request.getRequestURI());
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
}
/**
* 路径匹配,检测本次请求是否需要放行
* @param requestUri 本次请求资源
* @return 路径匹配结果,true-放行,false-不放行
*/
public boolean check (String requestUri) {
for (String url:urls) {
if (PATH_MATCHER.match(url, requestUri)) {
return true;
}
}
return false;
}
}
访问域名/doc.html,
常用注解
注解 | 说明 |
---|---|
@Api | 用在请求的类上,例如Controller,表示对类的说明 |
@ApiModel | 用在类上,通常是实体类,表示一个返回响应数据的信息 |
@ApiModelProperty | 用在属性上,描述响应类的属性 |
@ApiOperation | 用在请求的方法上,说明方法的用途、作用 |
@ApiImplicitParams | 用在请求的方法上,表示一组参数说明 |
@ApiImplicitParam | 用在@ApilmplicitParams 注解中,指定一个请示参数的各个方面 |
R实体类补充注解:
package com.demo.common;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 通用返回结果,服务端响应的数据最终都会封装成此对象
* @param <T>
*/
@Data
@ApiModel("返回结果")
public class R<T> implements Serializable {
@ApiModelProperty("编码:1成功,0和其它数字为失败")
private Integer code; //编码:1成功,0和其它数字为失败
@ApiModelProperty("错误信息")
private String msg; //错误信息
@ApiModelProperty("数据")
private T data; //数据
@ApiModelProperty("动态数据")
private Map map = new HashMap(); //动态数据
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
套餐实体类补充注解:
package com.demo.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 套餐
*/
@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("状态 0:停用 1:启用")
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;
//是否删除
@ApiModelProperty("是否删除")
private Integer isDeleted;
}
套餐接口补充注解:
package com.demo.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.demo.common.R;
import com.demo.dto.SetmealDto;
import com.demo.entity.Category;
import com.demo.entity.Setmeal;
import com.demo.service.CategoryService;
import com.demo.service.SetmealDishService;
import com.demo.service.SetmealService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 套餐管理
*/
@Slf4j
@RestController
@RequestMapping("/setmeal")
@Api(tags = "套餐相关接口")
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishService setmealDishService;
@Autowired
private CategoryService categoryService;
/**
* 新增套餐
* @param setmealDto 套餐实体
* @return 返回信息
*/
@CacheEvict(value = "setmealCache", allEntries = true)
@PostMapping
@ApiOperation(value = "新增套餐接口")
public R<String> save(@RequestBody SetmealDto 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<SetmealDto>> page (int page, int pageSize, String name) {
// 构建分页构造器
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
LambdaQueryWrapper<Setmeal> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件,根据name进行模糊查询
lambdaQueryWrapper.like(name != null, Setmeal::getName, name);
// 添加排序条件,根据更新时间降序排列
lambdaQueryWrapper.orderByAsc(Setmeal::getStatus).orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo, lambdaQueryWrapper);
// 对象拷贝,补充分类信息
Page<SetmealDto> dtoPage = new Page<>(page, pageSize);
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);
// 设置分类名称
Category category = categoryService.getById(item.getCategoryId());
if (category != null) {
setmealDto.setCategoryName(category.getName());
}
return setmealDto;
}
).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
/**
* 批量删除套餐
* @param ids 套餐id
* @return 返回信息
*/
@CacheEvict(value = "setmealCache", allEntries = true)
@DeleteMapping
@ApiOperation(value = "批量删除套餐")
public R<String> delete(@RequestParam List<Long> ids) {
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
/**
* 批量上下架
* @param ids 套餐id
* @param status 上下架状态
* @return 返回信息
*/
@PostMapping("/status/{status}")
@ApiOperation(value = "批量上下架")
public R<String> updateStatus(@RequestParam List<Long> ids, @PathVariable int status) {
List<Setmeal> setmealList = ids.stream().map(
(item) -> {
Setmeal setmeal = new Setmeal();
setmeal.setId(item);
setmeal.setStatus(status);
return setmeal;
}
).collect(Collectors.toList());
setmealService.updateBatchById(setmealList);
return R.success("更新成功");
}
/**
* 获取套餐详情数据
* @param id 套餐id
* @return 套餐实体信息
*/
@GetMapping("/{id}")
@ApiOperation(value = "获取套餐详情数据")
public R<SetmealDto> detail(@PathVariable Long id) {
SetmealDto setmealDto = setmealService.getSetmealWithDish(id);
return R.success(setmealDto);
}
/**
* 修改套餐
* @param setmealDto 套餐实体
* @return 舞台信息
*/
@PutMapping
@ApiOperation(value = "修改套餐")
public R<String> update(@RequestBody SetmealDto setmealDto) {
setmealService.updateWithDish(setmealDto);
return R.success("编辑成功");
}
/**
* 根据条件查询套餐数据
* @param setmeal 套餐过滤条件
* @return 套餐列表
*/
@GetMapping("/list")
@Cacheable(value = "setmealCache", key="#setmeal.categoryId + '_' + #setmeal.status", unless = "#result.data.size() == 0")
@ApiOperation(value = "根据条件查询套餐数据")
public R<List<Setmeal>> list(Setmeal setmeal) {
log.info("未命中缓存查询");
LambdaQueryWrapper<Setmeal> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// 分类条件
lambdaQueryWrapper.eq(setmeal.getCategoryId() != null, Setmeal::getCategoryId, setmeal.getCategoryId());
// 上下架状态
lambdaQueryWrapper.eq(setmeal.getStatus() != null, Setmeal::getStatus, setmeal.getStatus());
// 排序
lambdaQueryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(lambdaQueryWrapper);
return R.success(list);
}
}
重新启动服务,查看实体和接口文档
Swagger在php项目中的应用
- 安装swagger包
zircote/swagger-php是swagger的php版,是一个扩展库,可以通过composer来进行安装
composer require zircote/swagger-php
- 安装Swagger ui
将swagger-ui下载下来以后,把dist目录下的文件复制到项目public/docs目录下, 这个目录可以自定义, 通俗来讲就是放到项目能访问到的目录下, 然后再将dist目录下的swagger-initializer.js文件中的url改成./openapi.yaml, 这里的openapi.yaml是生成文档后的文件名,意思是访问本地的openapi.yaml文件
<?php
namespace app\controller;
use app\BaseController;
use think\App;
use think\facade\Request;
/**
* @OA\Info (
* title = "Robins's API documetns",
* version = "1.0",
* description="本文档仅限于测试"
* )
*/
class Index extends BaseController
{
public function getDoc() {
$openapi = \OpenApi\Generator::scan([__ROOT__ . '/../app']);
// 生成yaml文件
file_put_contents(__ROOT__ . '\doc\dist\openapi.yaml', $openapi->toYaml());
}
/**
* @OA\Get (
* path="/",
* tags = {"用户订单列表接口"},
* summary = "用户订单列表接口",
* description = "获取用户订单信息",
* @OA\Parameter (name = "order_id", in = "query", description = "订单号", required = true),
* @OA\Parameter (name = "account_id", in = "query", description = "账户id", required = true),
* @OA\Response(response = "200",description = "The data"),
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(ref="#/components/schemas/Info"),
* )
* )
* @return \think\response\Json
*/
public function index()
{
$accountId = Request::post('account_id', 0);
$orderId = Request::post('order_id', '');
$data = Order::where(['account_id' => $accountId, 'order_id' => $orderId])->select()->toArray();
return json($data);
}
}
swagger在php项目中的注解还需要一点点摸索熟练