大家好,我是小富~
前言
說個挺奇葩的事,有個老鐵給我發私信吐槽了一下它的面試經歷,他去了個國企單位面試,然後面試官跟他就Swagger的問題聊了半個多小時。額~ 面試嘛這些都不稀奇,總能遇到是千奇百怪的人,千奇百怪的問題。不過,我分析這個面試官是不太好意思直接讓他走,哈哈哈!
什麼是Swagger?
Swagger目前是比較主流的RESTful
風格的API文檔工具,做過開發的人應該都用過它吧!
它提供了一套工具和規範,讓開發人員能夠更輕鬆地創建和維護可讀性強、易於使用和交互的API文檔(官方口吻)。
title: Swagger
desc: Swagger 官方網站
logo: https://static1.smartbear.co/swagger/media/assets/images/swagger_logo.svg
link: https://swagger.io/
爲什麼用Swagger?
以往在沒有這樣的API文檔工具,開發人員需要手動編寫和維護功能API的文檔。而且,由於API變更往往難以及時更新到文檔中,這可能會給依賴文檔的開發者帶來困惑。
說幾個Swagger
的特點:
-
最重要的一點可以根據代碼註解自動生成API文檔,
能生成的絕對不手寫
,而且API文檔與API定義會同步更新。 -
它提供了一個可執行的Web界面,支持API在線測試,可以直接在界面上直接設置參數測試,不用額外的測試工具或插件。
-
支持多種編程語言,
Java
、PHP
、Python
等語言都支持,喜歡什麼語言構建API都行。
總的來說,Swagger可以讓我們更多時間在專注於編寫代碼(摸魚),而不是花費額外精力來維護文檔,實踐出真知先跑個demo試試。
Swagger搭建
maven 依賴
目前使用的版本是Swagger3.0、Springboot 2.7.6,Swagger2.0與3.0依賴包名稱的變化有些大,需要特別注意一下。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.6</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
配置類
首先我們創建一個控制器TestController
類,裏邊只有一個最簡單的請求 /test
。
@RestController
public class TestController {
@RequestMapping("/test")
public String test(String name) {
return name;
}
}
接下來創建配置類SwaggerConfig
,類標註@EnableSwagger2
註解是關鍵,到這最簡單的Swagger文檔環境就搭建好了。
import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
}
啓動報錯
啓動時可能會報如下的錯誤,這是由於高版本的Springboot
與Swagger
版本使用的路徑匹配策略衝突導致的。
Springfox
使用的路徑匹配規則爲AntPathMatcher
的,而SpringBoot2.7.6
使用的是PathPatternMatcher
,兩者衝突了。
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.24.jar:5.3.24]
at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_341]
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) [spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) [spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) [spring-boot-2.7.6.jar:2.7.6]
at com.springboot101.SwaggerApplication.main(SwaggerApplication.java:10) [classes/:na]
解決方案
這個錯誤的解決辦法比較多,我整理了四種解決此問題的方案,你看哪個更合適你。
1、降低版本
SpringBoot版本降低到2.5.X 、springfox降到3.X 以下可以解決問題,不過不推薦這麼做,畢竟降配置做兼容顯得有點傻。
2、統一路徑匹配策略
將SpringMVC
的匹配URL路徑的策略改爲ant_path_matcher
,application.yml
文件增加如下的配置:
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
3、@EnableWebMvc註解
在配置類SwaggerConfig
上標註@EnableWebMvc
註解也可以解決。
Swagger
框架需要通過解析和掃描帶有註解的Controller
類和方法來生成API文檔。@EnableWebMvc
註解會註冊一個RequestMappingHandlerMapping
的Bean,並將其作爲默認的請求映射處理器,以確保這些Controller類和方法能夠被正確處理,可以與Swagger的路徑配置規則相匹配,從而使得Swagger能夠成功生成API文檔。
@EnableWebMvc
@Configuration
@EnableSwagger2
public class SwaggerConfig {
}
4、註冊 BeanPostProcessor
也可以自行實現兼容邏輯來解決這個問題,我們可以在Spring
容器中註冊一個BeanPostProcessor
,在該處理器中對 HandlerMappings 進行定製。
通過過濾掉已存在PatternParser的映射,意味着我們可以將Swagger特定的HandlerMappings添加到HandlerMappings
列表中,從而使用自定義的設置來替代原有的HandlerMappings。
這樣修復了可能導致Swagger無法正常使用的問題。
@Bean
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return bean;
}
private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
List<T> copy = mappings.stream()
.filter(mapping -> mapping.getPatternParser() == null)
.collect(Collectors.toList());
mappings.clear();
mappings.addAll(copy);
}
@SuppressWarnings("unchecked")
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
try {
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
field.setAccessible(true);
return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
} catch (IllegalArgumentException | IllegalAccessException e) {
log.warn("修改WebMvcRequestHandlerProvider的屬性:handlerMappings出錯,可能導致swagger不可用", e);
throw new IllegalStateException(e);
}
}
};
}
訪問 swagger-ui
到這,問題解決!我們訪問Swagger文檔路徑 http://127.0.0.1:9002/swagger-ui/index.html
,能夠看到我們寫的 API 信息以及一些Swagger 文檔的默認配置信息。
注意到我們只寫了一個 /test
接口,但這裏確把這個方法的所有請求方式都列了出來,因爲我們在 controller
方法中使用了@RequestMapping
註解,並沒有具體的指定接口的請求方式,所以避免文檔冗餘,儘量指定請求方式或者使用指定請求方式的 @XXXMapping
註解。
指定請求方式後:
API文檔配置
上邊我們訪問的文檔中展示的數據都是默認的配置,現在咱們來定製化一下文檔。
Springfox
提供了一個Docket
對象,供我們靈活的配置Swagger
的各項屬性。Docket
對象內提供了很多的方法來配置文檔,下邊介紹下常用的配置項。
select
select()
返回一個ApiSelectorBuilder
對象,是使用apis()
、paths()
兩個方法的前提,用於指定Swagger要掃描的接口和路徑。
apis
默認情況下,Swagger會掃描整個項目中的接口,通過 apis()
方法,你可以傳入一個RequestHandlerSelector
對象實例來指定要包含的接口所在的包路徑。
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.springboot101.controller"))
.build();
}
paths
僅將某些特定請求路徑的API
展示在Swagger文檔中,例如路徑中包含/test
。可以使用 apis()
和 paths()
方法一起來過濾接口。
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.paths(PathSelectors.ant("/test/**"))
.build();
}
groupName
爲生成的Swagger
文檔指定分組的名稱,用來區分不同的文檔組。
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("用戶分組")
.build();
}
實現文檔的多個分組,則需創建多個 Docket
實例,設置不同的組名,和組內過濾 API 的條件。
@Bean
public Docket docket1(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("商家分組")
.select()
.paths(PathSelectors.ant("/test1/**"))
.build();
}
apiInfo
設置API文檔的基本信息,例如標題、描述、版本等。你可以使用ApiInfo
對象自定義信息。
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()); // 文檔基礎配置
}
private ApiInfo apiInfo() {
Contact contact = new Contact("小富", "http://fire100.top", "[email protected]");
return new ApiInfoBuilder()
.title("Swagger學習")
.description("程序員小富-帶你一起學習 Swagger")
.version("v1.0.1")
.termsOfServiceUrl("http://fire100.top")
.contact(contact)
.license("許可證")
.licenseUrl("許可鏈接")
.extensions(Arrays.asList(
new StringVendorExtension("我是", "小富"),
new StringVendorExtension("你是", "誰")
))
.build();
}
對應的Swagger文檔頁面上展示的位置
enable
啓用或禁用Swagger文檔的生成,有時測試環境會開放API文檔,但在生產環境則要禁用,可以根據環境變量控制是否顯示。
@Bean
public Docket docket(Environment environment) {
// 可顯示 swagger 文檔的環境
Profiles of = Profiles.of("dev", "test","pre");
boolean enable = environment.acceptsProfiles(of);
return new Docket(DocumentationType.SWAGGER_2)
.enable(enable)
.apiInfo(apiInfo()); // 文檔基礎配置
}
host
API文檔顯示的主機名稱或IP地址,即在測試執行接口時使用的IP或域名。
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2)
.host("http://test.com") // 請求地址
.apiInfo(apiInfo()); // 文檔基礎配置
}
securitySchemes
配置API安全認證方式,比如常見的在header
中設置如Bearer
、Authorization
、Basic
等鑑權字段,ApiKey
對象中字段含義分別是別名、鑑權字段key、鑑權字段添加的位置。
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2)
.securitySchemes(
Arrays.asList(
new ApiKey("Bearer鑑權", "Bearer", "header"),
new ApiKey("Authorization鑑權", "Authorization", "header"),
new ApiKey("Basic鑑權", "Basic", "header")
)
);
}
這樣配置後,Swagger文檔將會包含一個Authorize
按鈕,供用戶輸入我們設定的Bearer
、Authorization
、Basic
進行身份驗證。
securityContexts
securitySchemes
方法中雖然設置了鑑權字段,但此時在測試接口的時候不會自動在 header
中加上鑑權字段和值,還要配置API的安全上下文,指定哪些接口需要進行安全認證。
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2)
.securitySchemes(
Arrays.asList(
new ApiKey("Bearer鑑權", "Bearer", "header"),
new ApiKey("Authorization鑑權", "Authorization", "header"),
new ApiKey("Basic鑑權", "Basic", "header")
)
)
.securityContexts(Collections.singletonList(securityContext()));
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(
Arrays.asList(
new SecurityReference("Authorization", new AuthorizationScope[0]),
new SecurityReference("Bearer", new AuthorizationScope[0]),
new SecurityReference("Basic", new AuthorizationScope[0])))
.build();
}
現在在測試調用API接口時,header
中可以正常加上鑑權字段和值了。
tags
爲API文檔中的接口添加標籤,標籤可以用來對API進行分類或分組,並提供更好的組織和導航功能。
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2)
.tags(new Tag("小富接口", "小富相關的測試接口"))
}
授權登錄
出於對系統安全性的考慮,通常我們還會爲API文檔增加登錄功能。
引入maven依賴
swagger的安全登錄是基於security
實現的,引入相關的maven
依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
登錄配置
application.yml
文件中配置登錄swagger的用戶名和密碼。
spring:
security:
user:
name: admin
password: 123456
再次訪問文檔就會出現如下的登錄頁
文檔註解
當我們希望在Swagger文檔中提供詳細和完整的內容時,還可以使用其他許多Swagger內置註解來進一步豐富和定製API文檔。
@ApiIgnore
上邊我們提到可以根據指定路徑或者包路徑來提供API,也可以使用粒度更細的@ApiIgnore
註解,來實現某個API在文檔中忽略。
@ApiIgnore
@GetMapping("/user2/{id}")
public User test2(@PathVariable Integer id, @RequestBody User user) {
return user;
}
@ApiModel
在我們的接口中,只要使用實體作爲參數或響應體,Swagger就會自動掃描到它們,但你會發現目前這些實體缺乏詳細的描述信息。爲了讓使用者通俗易懂,需要使用swagger提供的註解爲這些實體添加詳細的描述。
@ApiModel
註解的使用在實體類上,提供對Swagger Model額外信息的描述。
@ApiModelProperty
@ApiModelProperty
註解爲實體類中的屬性添加描述,提供了字段名稱、是否必填、字段示例等描述信息。
@ApiModel(value = "用戶實體類", description = "用於存放用戶登錄信息")
@Data
public class User {
@ApiModelProperty(value = "用戶名字段", required = true, example = "#公衆號:程序員小富")
private String name;
@ApiModelProperty(value = "年齡", required = true, example = "19")
private Integer age;
@ApiModelProperty(value = "郵箱", required = true, example = "#公衆號:程序員小富")
private String email;
}
@Api
@Api
註解用於標記一個控制器(controller
)類,並提供接口的詳細信息和配置項。
value
:API 接口的描述信息,由於版本swagger版本原因,value
可能會不生效可以使用description
hidden
:該 API 是否在 Swagger 文檔中隱藏tags
:API 的標籤,如果此處與new Docket().tags
中設置的標籤一致,則會將該 API 放入到這個標籤組內authorizations
:鑑權配置,配合@AuthorizationScope
註解控制權限範圍或者特定密鑰才能訪問該API。produces
:API的響應內容類型,例如 application/json。consumes
:API的請求內容類型,例如 application/json。protocols
:API支持的通信協議。
@Api(value = "用戶管理接口描述",
description = "用戶管理接口描述",
hidden = false,
produces = "application/json",
consumes = "application/json",
protocols = "https",
tags = {"用戶管理"},
authorizations = {
@Authorization(value = "apiKey", scopes = {
@AuthorizationScope(scope = "read:user", description = "讀權限"),
@AuthorizationScope(scope = "write:user", description = "寫權限")
}),
@Authorization(value = "basicAuth")
})
@RestController
public class TestController {
}
@ApiOperation
@ApiOperation
該註解作用在接口方法上,用來對一個操作或HTTP方法進行描述。
value
:對接口方法的簡單說明notes
:對操作的詳細說明。httpMethod
:請求方式code
:狀態碼,默認爲 200。可以傳入符合標準的HTTP Status Code Definitions。hidden
:在文檔中隱藏該接口response
:返回的對象tags
:使用該註解後,該接口方法會單獨進行分組produces
:API的響應內容類型,例如 application/json。consumes
:API的請求內容類型,例如 application/json。protocols
:API支持的通信協議。authorizations
:鑑權配置,配合@AuthorizationScope
註解控制權限範圍或者特定密鑰才能訪問該API。responseHeaders
:響應的header內容
@ApiOperation(
value = "獲取用戶信息",
notes = "通過用戶ID獲取用戶的詳細信息",
hidden = false,
response = UserDto.class,
tags = {"用戶管理"},
produces = "application/json",
consumes = "application/json",
protocols = "https",
authorizations = {
@Authorization(value = "apiKey", scopes = {@AuthorizationScope(scope = "read:user", description = "讀權限")}),
@Authorization(value = "Basic")
},
responseHeaders = {@ResponseHeader(name = "X-Custom-Header", description = "Custom header", response = String.class)},
code = 200,
httpMethod = "GET"
)
@GetMapping("/user1")
public UserDto user1(@RequestBody User user) {
return new UserDto();
}
@ApiImplicitParams
@ApiImplicitParams
註解用在方法上,以數組方式存儲,配合@ApiImplicitParam
註解使用。
@ApiImplicitParam
@ApiImplicitParam
註解對API方法中的單一參數進行註解。
注意這個註解
@ApiImplicitParam
必須被包含在註解@ApiImplicitParams
之內。
name
:參數名稱value
:參數的簡短描述required
:是否爲必傳參數dataType
:參數類型,可以爲類名,也可以爲基本類型(String,int、boolean等)paramType
:參數的傳入(請求)類型,可選的值有 path、query、body、header、form。
@ApiImplicitParams({
@ApiImplicitParam(name = "用戶名", value = "用戶名稱信息", required = true, dataType = "String", paramType = "query")
})
@GetMapping("/user")
public String user(String name) {
return name;
}
@ApiParam()
@ApiParam()
也是對API方法中的單一參數進行註解,其內部屬性和@ApiImplicitParam
註解相似。
@GetMapping("/user4")
public String user4(@ApiParam(name = "主鍵ID", value = "@ApiParam註解測試", required = true) String id) {
return id;
}
@ApiResponses
@ApiResponses
註解可用於描述請求的狀態碼,作用在方法上,以數組方式存儲,配合 @ApiResponse
註解使用。
@ApiResponse
@ApiResponse
註解描述一種請求的狀態信息。
code
:HTTP請求響應碼。message
:響應的文本消息response
:返回類型信息。responseContainer
:如果返回類型爲容器類型,可以設置相應的值。有效值爲 "List"、 "Set"、"Map"其他任何無效的值都會被忽略。
@ApiResponses(value = {
@ApiResponse(code = 200, message = "@ApiResponse註解測試通過", response = String.class),
@ApiResponse(code = 401, message = "可能參數填的有問題", response = String.class),
@ApiResponse(code = 404, message = "可能請求路徑寫的有問題", response = String.class)
})
@GetMapping("/user4")
public String user4(@ApiParam(name = "主鍵ID", value = "@ApiParam註解測試", required = true) String id) {
return id;
}
總結
儘管在面試中不會過多考察Swagger這類工具,但作爲開發者,養成良好的文檔規範習慣是非常重要的,無論使用Swagger還是其他文檔工具,編寫清晰、詳盡的API文檔都應該是我們的素養之一。
代碼示例
https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot101/通用功能/springboot-swagger