项目环境
- springBoot 2.4.0
- jdk1.8
- swagger3
- knife4j(Swagger生成Api文档的增强解决方案)
引入的包
io.springfox springfox-boot-starter 3.0.0 com.github.xiaoymin knife4j-spring-boot-starter 3.0.2 swagger-annotations io.swagger swagger-models io.swagger
编写一个可在方法和类上添加的注解
实现版本动态分组,核心在于自定义注解。定义一个 @ApiVersion 注解,里面放一个枚举 Version,默认分组和具体版本号都可以枚举出来。这样以后加新版本,直接在枚举里新增常量就行,扩展性很好。
import ja va.lang.annotation.ElementType;
import ja va.lang.annotation.Retention;
import ja va.lang.annotation.RetentionPolicy;
import ja va.lang.annotation.Target;
/**
* ApiVersion
* 自定义swagger接口上的版本分组注解
* 需要新分组时在下面的Version枚举类中新增一个常量即可
*
* @author 七濑武
* @date 2021/4/16 16:50
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE_USE})
public @interface ApiVersion {
Version[] value();
enum Version {
/**
* 分组名称
*/
DEFAULT("default"),
v_1_1_0("1.1.0");
private final String display;
Version(String display) {
this.display = display;
}
public String getDisplay() {
return display;
}
}
}
配置Swagger3
配置部分需要覆盖 Swagger 原生的 DocumentationPluginsManager,通过自定义的 SwaggerPluginRegistry 来管理多个 Docket。思路是这样的:遍历枚举中的所有版本,为每个版本创建一个 Docket 实例,然后在 .apis() 选择器中判断当前接口属于哪个分组——默认分组扫描所有带 @Api 注解的 controller,其他分组则匹配带有对应版本值的 @ApiVersion 注解。同时还要注入 Knife4j 的扩展配置,设置授权信息(token 鉴权),并通过 operationSelector 实现“白名单”效果——加了 @PassToken 注解的接口不强制携带 token。
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import com.google.common.collect.ImmutableList;
import com.nanase.takeshi.annotion.ApiVersion;
import com.nanase.takeshi.annotion.PassToken;
import com.nanase.takeshi.constants.JwtConstant;
import com.nanase.takeshi.util.enums.SysCodeEnum;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.HttpMethod;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.ResponseBuilder;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.DocumentationPlugin;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import ja va.util.*;
import ja va.util.stream.Collectors;
import ja va.util.stream.Stream;
import ja va.util.stream.StreamSupport;
/**
* Swagger3Config
*
* @author 七濑武
* @date 2021/4/16 16:50
*/
@Primary //自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常。(只对接口的多个实现生效)覆盖swagger自己的配置
@Configuration //定义配置类
@EnableKnife4j //开启Knife4j
public class Swagger3Config extends DocumentationPluginsManager {
//yml文件中配置的name
@Value("${spring.application.name}")
private String applicationName;
//注入Knife4j
private final OpenApiExtensionResolver openApiExtensionResolver;
//注入Knife4j
@Autowired
public Swagger3Config(OpenApiExtensionResolver openApiExtensionResolver) {
this.openApiExtensionResolver = openApiExtensionResolver;
}
@Override
public Collection documentationPlugins() throws IllegalStateException {
List plugins = registry().getPlugins();
ensureNoDuplicateGroups(plugins);
return plugins.isEmpty() ? Collections.singleton(this.defaultDocumentationPlugin()) : plugins;
}
private void ensureNoDuplicateGroups(List allPlugins) throws IllegalStateException {
Map> plugins = allPlugins.stream().collect(Collectors.groupingBy((input) -> {
return Optional.ofNullable(input.getGroupName()).orElse("default");
}, LinkedHashMap::new, Collectors.toList()));
Iterable duplicateGroups = plugins.entrySet().stream().filter((input) -> {
return (input.getValue()).size() > 1;
}).map(Map.Entry::getKey).collect(Collectors.toList());
if (StreamSupport.stream(duplicateGroups.spliterator(), false).count() > 0L) {
throw new IllegalStateException(String.format("Multiple Dockets with the same group name are not supported. The following duplicate groups were discovered. %s", String.join(",", duplicateGroups)));
}
}
private DocumentationPlugin defaultDocumentationPlugin() {
return new Docket(DocumentationType.OAS_30);
}
private SwaggerPluginRegistry registry() {
List list = new ArrayList<>();
for (ApiVersion.Version version : ApiVersion.Version.values()) {
Docket docket = new Docket(DocumentationType.OAS_30)
// 指定构建api文档的详细信息的方法:apiInfo()
.apiInfo(apiInfo())
.groupName(version.getDisplay())
.select()
.apis(input -> {
if (ApiVersion.Version.DEFAULT.equals(version)) {
//指定扫描有Api注解的类
return input.findControllerAnnotation(Api.class).isPresent();
}
//指定扫描有此版本的ApiVersion注解的方法
return input.findAnnotation(ApiVersion.class).filter(item -> Arrays.asList(item.value()).contains(version)).isPresent();
})
.paths(PathSelectors.any())
.build()
//使Knife4j的增强配置生效
.extensions(openApiExtensionResolver.buildSettingExtensions())
// 支持的通讯协议集合
.protocols(Stream.of("https", "http").collect(Collectors.toSet()))
// 授权信息设置,必要的header token等认证信息
.securitySchemes(securitySchemes())
// 授权信息全局应用
.securityContexts(securityContexts());
list.add(docket);
}
return new SwaggerPluginRegistry(list, new AnnotationAwareOrderComparator());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 设置页面标题
.title(applicationName)
// 设置接口描述
.description(applicationName + "通用框架接口")
// 设置联系方式
.contact(new Contact("七濑武", null, null))
.build();
}
/**
* 设置授权信息
*/
private List securitySchemes() {
//swagger3此处有个小坑(ApiKey中的参数name和keyname有问题,只有第一个参数name会生效,第二个参数keyname无效,此处就配置一样的字符串)
return Collections.singletonList(new ApiKey("token", "token", In.HEADER.toValue()));
}
/**
* 授权信息全局应用
*/
private List securityContexts() {
return ImmutableList.of(SecurityContext.builder()
.securityReferences(
Collections.singletonList(
SecurityReference.builder()
.scopes(new AuthorizationScope[0])
//此处需要配置的与ApiKey中的name一致才可以全局应用上
.reference("token")
.build()
)
)
//声明作用域,@PassToken注解的方法不在header中添加token,全局应用时不应用在有@PassToken注解上面
.operationSelector(o -> !o.findAnnotation(PassToken.class).isPresent())
.build());
}
}
/**
* SwaggerPluginRegistry
*
* @author 七濑武
* @date 2021/4/16 16:55
*/
class SwaggerPluginRegistry extends OrderAwarePluginRegistry implements PluginRegistry {
protected SwaggerPluginRegistry(List plugins, Comparator super DocumentationPlugin> comparator) {
super(plugins, comparator);
}
@Override
public List getPlugins() {
return super.getPlugins();
}
}
下面是我用到的PassToken注解类
这个注解很简单,就是一个标志位,加到不需要 token 校验的接口上。
import ja va.lang.annotation.ElementType;
import ja va.lang.annotation.Retention;
import ja va.lang.annotation.RetentionPolicy;
import ja va.lang.annotation.Target;
/**
* 过滤token校验注解
* controller层方法上加上注解可不校验token
*
* @author 七濑武
* @date 2020/11/27 16:50
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
下面是用到的yml配置
Knife4j 的增强配置里,注意开启了 basic 认证,访问文档时需要输入用户名密码;同时关闭了 OpenAPI 原生的 swagger 页面(我们只用 Knife4j 的 UI),也关闭了 footer 中的版权信息。
server:
port: 8080
spring:
application:
name: NanaseTakeshi
# knife4j配置
knife4j:
basic:
username: admin
password: admin
enable: true #开启认证,访问接口文档时需要用户名密码访问
enable: true # 开启增强配置
setting: # 增强的配置信息
enableOpenApi: false
enableFooter: false
接口版本动态分组例子
使用起来很直观:在方法上标注 @ApiVersion(ApiVersion.Version.v_1_2_0),该接口就会自动归入“1.2.0”这个文档分组里。
//传入对应的版本即可
@ApiVersion(ApiVersion.Version.v_1_2_0)
@ApiOperation("测试ApiVersion注解的方法")
@GetMapping("/test")
public String test(){
return "Hello World";
}
总结
以上完整实现了一套基于 Swagger3 + Knife4j 的接口版本动态分组方案。核心思路是利用自定义注解配合多 Docket 注册器,让不同版本的接口各自归组,在文档页面上清晰区分。搭配 Knife4j 的增强界面,体验比原生的 Swagger UI 好不少。如果你也在做微服务或前后端分离项目,需要管理多版本接口文档,这套模式可以直接复用。

