迁移到SpringBoot 06 - Swagger

随着技术的发展,现在的Web系统架构基本都由原来的后端渲染(例如JSP),变成了:前端渲染、前后端分离的形态。前端和后端的唯一联系,变成了API接口。API文档变成了前后端开发人员联系的纽带,变得越来越重要,swagger就是一款让你更好的书写API文档的框架。没有API文档工具之前,大家都是手写API文档的,在什么地方书写的都有,有在Wiki上写的,有在Word里面写的,也有在对应的项目目录下readme.md上写的,每个公司都有每个公司的玩法,无所谓好坏。另外还有专门针对API文档开发的应用,例如showdoc之类的。

书写API文档的工具有很多,但是能称之为“框架”的,估计也只有swagger了。使用Swagger有个好处就是文档与代码结合紧密,更新代码的同时也会更新文档,不用担心同步的问题。而且在Java中使用Swagger也比较方便。

云认证平台提供的Restful接口是基于CXF实现的。网上现有的关于Swagger的文档多数是基于Spring MVC的,并不适用于CXF。下面我将针对CXF+SpringBoot的组合如何使用Swagger进行介绍。

1. 集成方法

分成后端、前端两部分来介绍。Swagger框架包含前端和后端两部分。前端指的是Swagger中的网页和JS部分,它从后台获取一个json或yaml文件,文件中有接口相关的定义信息;后端则负责扫描代码,根据注解相关信息生成对应的json或yaml文件。

1.1 后端

后端指的是如何扫描代码中的swagger标记,生成对应的json格式元数据。这部分使用了CXF库的swagger2Feature这个特性。具体可以参考CXF库的帮助页面。
其原理是开启Swagger2Feature,由CXF库生成这个json元数据。方法就是JAXRSServerFactoryBean调用setFeatures方法,添加一个Swagger2Feature对象。代码如下(@Configuration类):

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
@Bean
public ServletRegistrationBean cxfServlet() {
final ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new CXFServlet(), "/api/*");
servletRegistrationBean.setLoadOnStartup(1);
return servletRegistrationBean;
}

@Bean(destroyMethod = "shutdown")
public SpringBus cxf() {
return new SpringBus();
}

// 新增代码:定义一个SwaggerFeature
public Swagger2Feature createSwaggerFeature() {
Swagger2Feature swagger2Feature = new Swagger2Feature();
swagger2Feature.setPrettyPrint(true);
swagger2Feature.setTitle("UID API");
swagger2Feature.setContact("limin.zhang");
swagger2Feature.setDescription("UID & Admin & CAP API");
swagger2Feature.setVersion("1.0.0");
swagger2Feature.setBasePath("/api");
swagger2Feature.setResourcePackage("com.eveus.cloudauth.api");
return swagger2Feature;
}


@Bean(destroyMethod = "destroy")
@DependsOn("cxf")
public Server jaxRsServer() throws Exception{
JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();
factory.setAddress("/");
factory.setServiceBeans(Arrays.asList(uidApiController, userIdentityApiService, uidAdminApiService));
ArrayList providers = new ArrayList();
providers.add(corsFilter());
providers.add(fastJsonProvider());
factory.setProviders(providers);
factory.setFeatures(Arrays.asList(createSwaggerFeature())); // 注入SwaggerFeature
return factory.create();

}

以上功能需要一个库支持,需要添加到pom.xml中:

1
2
3
4
5
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-jaxrs</artifactId>
<version>1.5.7</version>
</dependency>

1.2 前端

前端依然使用springfox-swagger-ui。但是单纯的springfox-swagger-ui这个库只是一个前端文件包。其要工作,还需要后台接口配合(从中获取接口定义json文件):

  • /swagger-resources/configuration/security Configuring swagger-ui security
  • /swagger-resources/configuration/ui Configuring swagger-ui options
    这些接口依赖于springfox其他库。

因此最终的项目依赖情况如下:

1
2
3
4
5
6
7
8
9
<!-- Springfox Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>

基本上配置完这些依赖项之后,Swagger就可以工作了。这里不需要再像Spring MVC使用Swagger一样,定义接口之类的。因为Swagger定义文件已经通过Swagger2Feature生成了。

最后一个问题是json文件的路径问题:springfox-swagger-ui默认访问的后台json地址是 /v2/api-docs,使用swagger2Feature生成的地址为/api/swagger.json(该地址基于CXF监听的地址,上面配置的监听地址是/api/*)。这个路径可以通过 springfox.documentation.swagger.v2.path参数进行配置。例如可以按照下面的方法修改成swagger2Feature生成的地址:

1
springfox.documentation.swagger.v2.path: /api/swagger.json

至此可以工作了。访问:http://localhost:8080/swagger-ui.html即可打开swagger文档。

2. 多个JAXRSServerFactoryBean

2.1 更多思考

首先总结一下CXF发布Restful接口的流程:
要使用CXF发布Restful接口,在SpringBoot中首先要定义CXFServlet,这是一个标准的Servlet,一般通过定义:ServletRegistrationBean实现。这一步和之前在Spring中web.xml中的如下配置是一样的:

1
2
3
4
5
6
7
8
9
<servlet>
<servlet-name>CXFService</servlet-name>
<servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>CXFService</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>

然后定义一个或多个JAXRSServerFactoryBean,它会创建JaxRsServer,以发布Restful接口。这和如下的xml配置功能类似:

1
2
3
4
5
6
7
8
9
<jaxrs:server id="didApiServiceJsonServer" address="/rest">
<jaxrs:serviceBeans>
<ref bean="uidApiService" />
</jaxrs:serviceBeans>
<jaxrs:providers>
<ref bean="jsonProvider" />
<ref bean="jsonExceptionMapper" />
</jaxrs:providers>
</jaxrs:server>

2.2 剩余问题

最初寻找用于CXF的Swagger方案的时候,一直无法解决的一个问题是多个JAXRSServerFactoryBean存在的时候如何管理管理API文档。上面的解决方案有投机取巧的地方:将所有需要发布的内容整合到一个JAXRSServerFactoryBean,通过一个JAXRSServerFactoryBean进行发布,这样就可以只生成一个json文件包含所有接口定义了。

但是这个方案是有缺陷的:

首先上述改造方案有个问题就是 /api/这个路径无法查看到CXF已经发布的api列表,会报错。按照传统的Spring中CXF的配置方法,/api/*这个地址是由CXFServlet监听的,这个Servlet可以列出所有使用CXF发布的接口列表,包括SOAP和Restful两种类型的API。但是使用前面的简化配置方案后,将JaxRSServer发布在/api/上。导致不是CXF本身拦截这个路径,而是 JaxRsServer拦截了。所以还是要改成每个Bean单独发布到不同的路径上,例如:/api/rest, /api/admin, /api/v1

尝试着创建三个SwaggerFeature,配置到三个JAXRSServerFactoryBean中,却发现生成的三个json文件内容是相同的,都对应最后一个设置的JAXRSServerFactoryBean对应的接口定义。经过查找,原来是因为CXF的Swagger2Feature缓存特性导致的。具体看附录中的链接。

Right, those two approaches are very similar. The ‘basePath’-based one does implicit Swagger IDs manipulation (scanner / config / …) by making base path a part of identifier (it triggers when usePathBasedConfig is set). So I think in this regards, we could make this property true by default and cover quite a large number of collisions (there is no need to set Swagger IDs). However, if I understand Łukasz Dywicki correctly, Swagger caching mechanism causing the issues when endpoints are loaded / unloaded dynamically (like in OSGi), in this case Swagger still exposes the API specs even if there are no running endpoints, the fix for this ticket does not address the issue yet but Łukasz Dywicki has a PR.

也就是说说,如果没有配置usePathBasedConfig参数,会以最后basePath为基准检查是否已经生成过,如果生成过则不再重新生成,直接采用缓存。解决办法是使用usePathBasedConfig这个参数,禁用这种错误的缓存策略。

2.3 最终修改方案

2.3.1 ApiConfig

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
   @Bean(destroyMethod = "shutdown")
public SpringBus cxf() {
return new SpringBus();
}

// 创建SwaggerFeature,指定usePathBasedConfig为true
public Swagger2Feature createCapSwaggerFeature() {
Swagger2Feature swagger2Feature = new Swagger2Feature();
swagger2Feature.setPrettyPrint(true);
swagger2Feature.setTitle("CAP API");
swagger2Feature.setContact("limin.zhang");
swagger2Feature.setDescription("CAP API");
swagger2Feature.setVersion("1.0.0");
swagger2Feature.setBasePath("/api");
// 关键代码:否则swagger2Feature会缓存,不认为这是创建了三个不同的对象
swagger2Feature.setUsePathBasedConfig(true); // 设置usePathBasedConfig参数
swagger2Feature.setResourcePackage("com.eveus.cloudauth.api.cap");
return swagger2Feature;
}


public Swagger2Feature createUidSwaggerFeature() {
Swagger2Feature swagger2Feature = new Swagger2Feature();
swagger2Feature.setPrettyPrint(true);
swagger2Feature.setTitle("UID API");
swagger2Feature.setContact("limin.zhang");
swagger2Feature.setDescription("UID API");
swagger2Feature.setVersion("1.0.0");
swagger2Feature.setBasePath("/api");
swagger2Feature.setUsePathBasedConfig(true); // 设置usePathBasedConfig参数
swagger2Feature.setResourcePackage("com.eveus.cloudauth.api.uid");
return swagger2Feature;
}

public Swagger2Feature createAdminSwaggerFeature() {
Swagger2Feature swagger2Feature = new Swagger2Feature();
swagger2Feature.setPrettyPrint(true);
swagger2Feature.setTitle("Admin API");
swagger2Feature.setContact("limin.zhang");
swagger2Feature.setDescription("Admin API");
swagger2Feature.setVersion("1.0.0");
swagger2Feature.setBasePath("/api");
swagger2Feature.setUsePathBasedConfig(true); // 设置usePathBasedConfig参数
swagger2Feature.setResourcePackage("com.eveus.cloudauth.api.admin");
return swagger2Feature;
}


@Bean(destroyMethod = "destroy")
@DependsOn("cxf")
public Server capApiJaxRsServer() throws Exception{
JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();
factory.setAddress("/v1");
factory.setServiceBean(uidApiController);
ArrayList providers = new ArrayList();
providers.add(corsFilter());
providers.add(fastJsonProvider());
factory.setProviders(providers);
factory.setFeatures(Arrays.asList(createUidSwaggerFeature())); // 指定一个SwaggerFeature
return factory.create();
}

@Bean(destroyMethod = "destroy")
@DependsOn("cxf")
public Server uidApiJaxRsServer() throws Exception{
JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();
factory.setAddress("/rest");
factory.setServiceBean(userIdentityApiService);
ArrayList providers = new ArrayList();
providers.add(corsFilter());
providers.add(fastJsonProvider());
factory.setProviders(providers);
factory.setFeatures(Arrays.asList(createUidSwaggerFeature())); // 指定一个SwaggerFeature
return factory.create();
}

@Bean(destroyMethod = "destroy")
@DependsOn("cxf")
public Server adminApiJaxRsServer() throws Exception{
JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();
factory.setAddress("/admin");
factory.setServiceBean(uidAdminApiService);
ArrayList providers = new ArrayList();
providers.add(corsFilter());
providers.add(fastJsonProvider());
factory.setProviders(providers);
factory.setFeatures(Arrays.asList(createAdminSwaggerFeature())); // 指定一个SwaggerFeature
return factory.create();
}

2.3.2 SwaggerConfig

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
@Primary    // 关键,否则报错
@Bean
public SwaggerResourcesProvider swaggerResourcesProvider(InMemorySwaggerResourcesProvider defaultResourcesProvider) {
return () -> {
// 定义不同的API文档及路径
SwaggerResource uidResource = new SwaggerResource();
uidResource.setName("UID Apis");
uidResource.setSwaggerVersion("2.0");
uidResource.setLocation("/api/rest/swagger.json");

SwaggerResource adminResource = new SwaggerResource();
adminResource.setName("Admin Apis");
adminResource.setSwaggerVersion("2.0");
adminResource.setLocation("/api/admin/swagger.json");

SwaggerResource capResource = new SwaggerResource();
capResource.setName("CAP Apis");
capResource.setSwaggerVersion("2.0");
capResource.setLocation("/api/v1/swagger.json");
// 返回一个List
List<SwaggerResource> resources = new ArrayList<>();
resources.add(uidResource);
resources.add(adminResource);
resources.add(capResource);
return resources;
};
}

因为现在生成了多个接口定义json文件,通过springfox.documentation.swagger.v2.path参数无法配置多个路径让swagger-ui感知。这时就需要定义SwaggerResourcesProvider这个Bean,springfox-swagger-ui这个库中的js封装了对上述配置的逻辑,会自动获取多个接口定义。

至此,基于SpringBoot+CXF的Swagger使用方案完美了。

附录、参考资料

热评文章