springboot2集成knife4j(swagger3)
- springboot2集成knife4j(swagger3)
- 环境说明
- 集成knife4j
- 第一步:引入依赖
- 第二步:编写配置类
- 第三步:放行相关资源 & 保证启动了knife4j
- 第四步:测试一下
- 第一小步:编写controller
- 第二小步:启动项目,访问api文档
- 相关资料
环境说明
- springboot:2.6.4
- knife4j-spring-boot-starter:3.0.3
集成knife4j
第一步:引入依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
第二步:编写配置类
提示:可以借助配置文件,进一步改造
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.github.xiaoymin.knife4j.core.model.MarkdownProperty;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import com.ideaaedi.demo.support.EnumDescriptor;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ModelSpecificationBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.PropertySpecificationBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.RequestParameterBuilder;
import springfox.documentation.schema.ScalarType;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.ModelPropertyBuilderPlugin;
import springfox.documentation.spi.schema.contexts.ModelPropertyContext;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.spi.service.contexts.ParameterContext;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Knife4j配置
*
* @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img
* src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
* @since 1.0.0
*/
@Slf4j
@Configuration
@EnableSwagger2
public class Knife4jConfig implements WebMvcConfigurer {
/** 对于管控了权限的应用,应放行以下资源 */
public static String[] RESOURCE_URLS = new String[]{"/webjars/**", "/swagger**", "/v3/api-docs", "/doc.html"};
@Value("${spring.application.name:default}")
private String applicationName;
@Resource
private OpenApiExtensionResolver openApiExtensionResolver;
@Bean
public Docket docket() {
// 指定使用Swagger2规范
Docket docket = new Docket(DocumentationType.OAS_30)
.apiInfo(new ApiInfoBuilder()
// 简介(支持Markdown语法)
.description("# 我是API简介")
// 服务地址
.termsOfServiceUrl("http://local.idea-aedi.com/")
// 作者及联系信息
.contact(new Contact("JustryDeng", "https://gitee.com/JustryDeng", "13548417409@163.com"))
// api版本
.version("1.0.0")
.build())
.extensions(openApiExtensionResolver.buildExtensions(applicationName))
//分组名称(微服务项目可以用微服务名分组)
.groupName(applicationName)
.select()
// 定位api
.apis(
RequestHandlerSelectors.basePackage(getProjectBasePackage())
.and(RequestHandlerSelectors.withClassAnnotation(RestController.class)
.or(RequestHandlerSelectors.withClassAnnotation(Controller.class))
)
)
.paths(PathSelectors.any())
.build();
docket.securitySchemes(securitySchemes()).securityContexts(securityContexts());
// 自定义文档解析
try {
Field markdownPropertiesField = FieldUtils.getDeclaredField(OpenApiExtensionResolver.class,
"markdownProperties", true);
List<MarkdownProperty> markdownProperties = (List<MarkdownProperty>)markdownPropertiesField.get(openApiExtensionResolver);
if (!CollectionUtils.isEmpty(markdownProperties)) {
for (MarkdownProperty markdownProperty : markdownProperties) {
docket.extensions(openApiExtensionResolver.buildExtensions(markdownProperty.getGroup()));
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return docket;
}
private List<SecurityScheme> securitySchemes() {
// 设置请求头信息
List<SecurityScheme> result = new ArrayList<>();
// 第一个参数name,自定义即可。 在配置securityContexts时,通过此name对应到apiKey即可
// 第二个参数,header name自定义即可。 如:JWT_TOKEN_KEY=Auth-Token,然后在代码里request.getHeader(JWT_TOKEN_KEY)取值
String JWT_TOKEN_KEY="Auth-Token";
ApiKey apiKey = new ApiKey("JustryDengApiKey", JWT_TOKEN_KEY, "header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContexts() {
// 设置需要登录认证的路径
List<SecurityContext> result = new ArrayList<>();
List<SecurityReference> securityReferences = defaultAuth();
result.add(
SecurityContext.builder().securityReferences(securityReferences).forPaths(
// 当直接使用swagger文档发送请求时,这些api需要满足securityReferences认证要求
PathSelectors.regex("/.*")
.and(
// 当直接使用swagger文档发送请求时,这些api不需要满足securityReferences认证要求. '.*'表示匹配所有
PathSelectors.regex("/hello.*").or(PathSelectors.regex("/hi.*"))
.negate()
)
).build()
);
return result;
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
// 这里指定使用哪个apiKey进行认证鉴权. 这里指定使用上面名为JustryDengApiKey的apiKey
result.add(new SecurityReference("JustryDengApiKey", authorizationScopes));
return result;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* 获取项目包前缀
*/
private String getProjectBasePackage() {
String projectBasePackage;
String currPackageName = this.getClass().getPackage().getName();
String[] packageItemArr = currPackageName.split("\\.");
if (packageItemArr.length > 3) {
projectBasePackage = String.join(".", packageItemArr[0], packageItemArr[1], packageItemArr[2]);
} else {
projectBasePackage = currPackageName;
}
log.info("Base package to scan api is -> {}", projectBasePackage);
return projectBasePackage;
}
/**
* 显示自定义枚举类型注释
* <p>
* <br/> 参考<a
* href="https://blog.gelu.me/2021/Knife4j-Swagger%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9E%9A%E4%B8%BE%E7%B1%BB%E5%9E%8B
* /">here</a>
*/
@Component
@SuppressWarnings("unchecked")
public static class Knife4jSwaggerEnumPlugin implements ModelPropertyBuilderPlugin, ParameterBuilderPlugin {
private static final Field parameterDescriptionField;
private static final Field modelPropertyBuilderDescriptionField;
static {
parameterDescriptionField = ReflectionUtils.findField(RequestParameterBuilder.class, "description");
Objects.requireNonNull(parameterDescriptionField, "parameterDescriptionField should noe be null.");
ReflectionUtils.makeAccessible(parameterDescriptionField);
modelPropertyBuilderDescriptionField = ReflectionUtils.findField(PropertySpecificationBuilder.class, "description");
Objects.requireNonNull(modelPropertyBuilderDescriptionField, "ModelPropertyBuilder_descriptionField should noe be null.");
ReflectionUtils.makeAccessible(modelPropertyBuilderDescriptionField);
}
/**
* {@link ApiModelProperty}相关
* <p>
* 主要处理枚举对象直接作为方法参数的内部字段的情况. 如:
* <pre>
* @Data
* public class LoginTokenRespVO {
*
* @ApiModelProperty("用户类型")
* private UserTypeEnum userType;
* }
* </pre>
*/
@Override
public void apply(ModelPropertyContext context) {
Optional<BeanPropertyDefinition> optional = context.getBeanPropertyDefinition();
if (!optional.isPresent()) {
return;
}
// 对应被@ApiModelProperty标注的字段
BeanPropertyDefinition beanPropertyDefinition = optional.get();
Class<?> fieldType = beanPropertyDefinition.getField().getRawType();
if (!Enum.class.isAssignableFrom(fieldType)) {
return;
}
Class<Enum<?>> enumType = (Class<Enum<?>>) fieldType;
Enum<?>[] enumConstants = enumType.getEnumConstants();
PropertySpecificationBuilder modelPropertyBuilder = context.getSpecificationBuilder();
Object oldValue = ReflectionUtils.getField(modelPropertyBuilderDescriptionField, modelPropertyBuilder);
// 解析枚举
List<String> enumDescList =
Arrays.stream(enumConstants).map(this::obtainEnumDescription).collect(Collectors.toList());
modelPropertyBuilder.description((oldValue == null ? "" : oldValue) + buildHtmlUnOrderList(enumDescList))
.type(new ModelSpecificationBuilder().scalarModel(ScalarType.UUID).build());
}
/**
* {@link ApiParam}、{@link io.swagger.v3.oas.annotations.Parameter}相关.
* <p> 主要处理:枚举对象直接作为方法参数的情况. 如:
* <pre>
* @PostMapping("/test1")
* @ApiOperation(value = "测试1")
* public void test1(@ApiParam(value = "用户类型", required = true) UserTypeEnum userTypeEnum)
* </pre>
*/
@Override
public void apply(ParameterContext context) {
Class<?> type = context.resolvedMethodParameter().getParameterType().getErasedType();
RequestParameterBuilder parameterBuilder = context.requestParameterBuilder();
if (!Enum.class.isAssignableFrom(type)) {
return;
}
Class<Enum<?>> enumType = (Class<Enum<?>>) type;
Enum<?>[] enumConstants = enumType.getEnumConstants();
// 解析枚举
List<String> enumDescList = Arrays.stream(enumConstants).map(this::obtainEnumDescription).collect(Collectors.toList());
Object oldValue = ReflectionUtils.getField(parameterDescriptionField, parameterBuilder);
parameterBuilder.description((oldValue == null ? "" : oldValue) + buildHtmlUnOrderList(enumDescList));
}
/**
* 此插件是否支持处理该DocumentationType
*/
@Override
public boolean supports(@NonNull DocumentationType documentationType) {
return true;
}
/**
* 获取枚举描述
*
* @param enumObj 枚举对象
*
* @return 枚举描述
*/
private String obtainEnumDescription(@NonNull Enum<?> enumObj) {
String name = enumObj.name();
/*
* 枚举说明器示例:
*
* public interface EnumDescriptor {
* // 获取枚举项说明
* String obtainDescription();
* }
*/
if (enumObj instanceof EnumDescriptor) {
return name + ":" + ((EnumDescriptor) enumObj).obtainDescription();
}
return name;
}
/**
* 构建无序列表html
*
* @param itemList 列表元素
*
* @return 无序列表html
*/
private String buildHtmlUnOrderList(@Nullable List<String> itemList) {
if (CollectionUtils.isEmpty(itemList)) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append("<ul>");
for (String item : itemList) {
sb.append("<li>");
sb.append(item);
sb.append("</li>");
}
sb.append("</ul>");
return sb.toString();
}
}
}
第三步:放行相关资源 & 保证启动了knife4j
-
放行相关资源
对于管控了权限的应用,应放行以下资源
/** 对于管控了权限的应用,应放行以下资源 */ public static String[] RESOURCE_URLS = new String[]{"/webjars/**", "/swagger**", "/v3/api-docs", "/doc.html"};
-
保证启动了knife4j
knife4j: # 启动knife4j(注:有时,如果我们不进行此配置,knife4j不会开启) # 其实核心是:保证com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration生效即可。如果knife4j怎么也没启动,请debug此类,确保其被加载 enable: true # 添加自定义文档 documents: - group: my-group # 这里的group是处理自定义文档时用到的标识,自定义唯一即可 # 效果等价使用 @Api(tags = "自定义文档") name: 自定义文档 # 指定.md文档位置 (更多详见官网:https://doc.xiaominfo.com/docs/features/selfdocument) # 每个md文档的标题,则等价于 @ApiOperation(value = "md文档的标题") locations: classpath:api-doc/*.md # 兼容swagger3 spring: mvc: pathmatch: matching-strategy: ant_path_matcher
第四步:测试一下
第一小步:编写controller
import com.ideaaedi.demo.controller.model.UserAddReqVO;
import com.ideaaedi.demo.controller.model.UserDetailRespVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 用于测试knife4j的controller
*
* @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img
* src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
* @since 1.0.0
*/
@RestController
@Api(tags = "我是DemoController")
public class TestController {
@GetMapping("/hello")
@ApiOperation(value = "哈喽")
public String hello(@ApiParam(name = "name", value = "姓名", required = true) @RequestParam String name) {
return "hello " + name;
}
@GetMapping("/hi")
@ApiOperation(value = "嘿")
public String hei(@ApiParam(name = "name", value = "姓名", required = true) @RequestParam String name) {
return "hello " + name;
}
@PostMapping("/user/add")
@ApiOperation(value = "新增用户")
public UserDetailRespVO addUser(@RequestBody UserAddReqVO req) {
UserDetailRespVO resp = new UserDetailRespVO();
resp.setId(9527L);
resp.setName(req.getName());
resp.setAge(req.getAge());
return resp;
}
@DeleteMapping("/user/delete/{id}")
@ApiOperation(value = "删除用户")
public Boolean addUser(@ApiParam(name = "id", value = "数据id", required = true) @PathVariable Long id) {
return true;
}
/**
* 测试 @RequestBody、@RequestParam、@PathVariable并存
*/
@PostMapping("/multi-anno/{id}")
@ApiOperation(value = "组合使用测试")
public UserDetailRespVO testMultiAnno(@RequestBody UserAddReqVO req, @ApiParam(name = "name", value = "姓名",
required = true) @RequestParam String name,
@ApiParam(name = "id", value = "数据id", required = true) @PathVariable Long id) {
UserDetailRespVO resp = new UserDetailRespVO();
resp.setId(9527L);
resp.setName(req.getName());
resp.setAge(req.getAge());
return resp;
}
}
此controller中用到的相关模型
-
UserAddReqVO
import io.swagger.annotations.ApiModelProperty; import lombok.Data; import javax.validation.constraints.NotBlank; /** * 用户新增req模型 */ @Data public class UserAddReqVO { @ApiModelProperty(value = "姓名",required = true) @NotBlank(message = "姓名不能为空") private String name; @ApiModelProperty("年龄") private Integer age; }
-
UserDetailRespVO
import io.swagger.annotations.ApiModelProperty; import lombok.Data; /** * 用户详情resp模型 */ @Data public class UserDetailRespVO { @ApiModelProperty("id") private Long id; @ApiModelProperty("姓名") private String name; @ApiModelProperty("年龄") private Integer age; }
第二小步:启动项目,访问api文档
启动项目后,直接访问
http://{ip}:{端口}/doc.html
即可
说明:
- 文档分组(不设置则默认分组名称为default):可切换观察其余分组下的api
- 主页:概览
- Swagger Models:可以查看所有请求模型的信息
- 文档管理:可以导出文档、进行高级设置(如设置后处理脚本等)、进行全局参数设置、查看api信息
- 自定义文档:可以添加一些自定义的md格式的文档
- 点击进入文档后,会展示api的详细信息,也可以进行调试,还可以打开api json数据等等
相关资料
- demo代码下载(knife4j-swagger3-springboot2分支)
- knife4j官网
- 本文已被收录进《程序员成长笔记》 ,笔者JustryDeng