怎么使用swagger,这里就不说了,本站已经跟各大搜索引擎达成合作,只要你在各大搜索引擎中输入关键词springboot swagger,就会在第一页返回给你集成教程。

背景

swagger确实很不错,可以自动生成接口文档,省去另外写文档的工作量,但是毕竟自动生成,肯定有不适合我们自己需求的地方。比如所有的接口文档没有分类,放在一起,前端很难找到所需的接口。还有接口文档有更新,没有任何地方提现处理。需要口头通知前端修改,如果前端忘了,后续还会怪后端没有通知到,以及发生各种扯皮。

我这里通过swagger提供的group功能进行增强,对接口文档进行分类、和版本管理。原生提供的group功能需要硬编码,生成Docket,使用起来极其不友好。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
public Docket app_api() {
return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.any())
.paths(PathSelectors.ant("/api/**")).build().groupName("APP接口文档V4.4").pathMapping("/")
.apiInfo(apiInfo("APP接口文档V4.4及之前","文档中可以查询及测试接口调用参数和结果","4.4"));
}

@Bean
public Docket wap_api() {
return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.any())
.paths(PathSelectors.ant("/web/**")).build().groupName("WEB接口文档V4.4").pathMapping("/")
.apiInfo(apiInfo("WEB接口文档V4.4及之前","文档中可以查询及测试接口调用参数和结果","4.4"));
}

解决方案

本篇记录的是,swagger自动生成group,实现对接口版本管理。这里我们公司习惯使用git分支进行管理,所有接口文档也跟着git分支做为版本管理。

定义注解

定义注解,用于在标注接口所属哪个版本。内部枚举,用来定义分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ApiVersion {

Version[] value();

enum Version {
MASTER("master"),
INTERESTING("20190701intersting");

private String display;

Version(String display) {
this.display = display;
}

public String getDisplay() {
return display;
}
}
}

重写group生成规则

这里代码看似又臭又长,其实不然,就是找到group生成的入口,然后遍历我们自定义的注解,生成多个group。

1
2
3
4
5
6
7
8
9
10
11
public class SwaggerPluginRegistry extends OrderAwarePluginRegistry<DocumentationPlugin, DocumentationType> implements PluginRegistry<DocumentationPlugin, DocumentationType> {

protected SwaggerPluginRegistry(List<Docket> plugins, Comparator<? super DocumentationPlugin> comparator) {
super(plugins, comparator);
}

@Override
public List<DocumentationPlugin> getPlugins() {
return super.getPlugins();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
@Component
@Primary
@ConditionalOnProperty(prefix = "swagger", value = {"enable"}, havingValue = "true")
@EnableSwagger2
public class SwaggerDocumentationPluginsManager extends DocumentationPluginsManager {
@Override
public Iterable<DocumentationPlugin> documentationPlugins() throws IllegalStateException {
List<DocumentationPlugin> plugins = registry().getPlugins();
ensureNoDuplicateGroups(plugins);
if (plugins.isEmpty()) {
return newArrayList(defaultDocumentationPlugin());
}
return plugins;
}

private void ensureNoDuplicateGroups(List<DocumentationPlugin> allPlugins) throws IllegalStateException {
Multimap<String, DocumentationPlugin> plugins = Multimaps.index(allPlugins, byGroupName());
Iterable<String> duplicateGroups = from(plugins.asMap().entrySet()).filter(duplicates()).transform(toGroupNames());
if (Iterables.size(duplicateGroups) > 0) {
throw new IllegalStateException(String.format("Multiple Dockets with the same group name are not supported. "
+ "The following duplicate groups were discovered. %s", Joiner.on(',').join(duplicateGroups)));
}
}

private Function<? super DocumentationPlugin, String> byGroupName() {
return new Function<DocumentationPlugin, String>() {
@Override
public String apply(DocumentationPlugin input) {
return Optional.fromNullable(input.getGroupName()).or("default");
}
};
}

private Function<? super Map.Entry<String, Collection<DocumentationPlugin>>, String> toGroupNames() {
return new Function<Map.Entry<String, Collection<DocumentationPlugin>>, String>() {
@Override
public String apply(Map.Entry<String, Collection<DocumentationPlugin>> input) {
return input.getKey();
}
};
}

private static Predicate<? super Map.Entry<String, Collection<DocumentationPlugin>>> duplicates() {
return new Predicate<Map.Entry<String, Collection<DocumentationPlugin>>>() {
@Override
public boolean apply(Map.Entry<String, Collection<DocumentationPlugin>> input) {
return input.getValue().size() > 1;
}
};
}

private DocumentationPlugin defaultDocumentationPlugin() {
return new Docket(DocumentationType.SWAGGER_2);
}

private SwaggerPluginRegistry registry() {
List<Docket> list = new ArrayList<>();
for (ApiVersion.Version version : ApiVersion.Version.values()) {
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName(version.getDisplay())
.select()
.apis(input -> {
if (ApiVersion.Version.MASTER.equals(version)) {
return true;
}
ApiVersion apiVersion = input.getHandlerMethod().getMethodAnnotation(ApiVersion.class);
if (apiVersion != null && Arrays.asList(apiVersion.value()).contains(version)) {
return true;
}
return false;
})
.paths(PathSelectors.any())
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());

list.add(docket);
}

SwaggerPluginRegistry registry = new SwaggerPluginRegistry(list, new AnnotationAwareOrderComparator());
return registry;
}

private ApiInfo apiInfo() {
Contact contact = new Contact("技术部", "", "");
return new ApiInfoBuilder()
.title("ofcoder接口文档")
.description("ofcoder接口文档")
.version("1.0.0")
.contact(contact)
.build();
}

private List<ApiKey> securitySchemes() {
List<ApiKey> arrayList = new ArrayList<>();
arrayList.add(new ApiKey("Authorization", "token", "header"));
return arrayList;
}

private List<SecurityContext> securityContexts() {
List<SecurityContext> arrayList = new ArrayList<>();
arrayList.add(SecurityContext.builder()
.securityReferences(defaultAuth())
.build());
return arrayList;
}

private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List<SecurityReference> arrayList = new ArrayList<>();
arrayList.add(new SecurityReference("Authorization", authorizationScopes));
return arrayList;
}

}

使用

只需要对所要进行管理的接口上,增加该注解,value的值支持多个,也就是说你可以同时标注多个分支。我觉某个接口每修改一次,value的值则增加一个分支,方便后续追述在那些分支上做了修改,就可以定位到git的哪一次提交了。

1
2
3
4
5
@ApiVersion(ApiVersion.Version.INTERESTING)
@RequestMapping(value = "ofcoder.com")
public void hello(){
...
}