0.引言
之前寫過一篇關于swagger(實際上(shang)是springfox)的使用(yong)指南(//www.daliqc.cn/developer/article/371704742199365),涵蓋(gai)了本人在(zai)開發與學習(xi)的時候碰(peng)到的各種大(da)坑。但(dan)由于springfox已(yi)經(jing)不更新(xin)了,很多(duo)項目都在(zai)往springdoc遷移
筆者也是花了一些(xie)時間試了一下這個(ge)號稱“把springfox按(an)在地(di)下摩擦(ca)”的springdoc究(jiu)竟好(hao)不(bu)好(hao)使,本文就來簡單介紹下springdoc的使用以及優(you)劣勢
1.引入maven依賴
這里有個(ge)大坑一定要(yao)注意!!!
如果你跟我一樣,現在使用的是springfox,但是想往springdoc遷移,結果試了一下發現還是springfox好用/懶得改那么多注解,還是想換回springfox,一定要把springdoc的maven依賴刪掉!!!不然springboot會默認你用的是springdoc,導致swagger界面出不來
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.11</version>
</dependency>
2.springdoc配置類
實際上springdoc的配置非(fei)常簡單,使用的是OpenAPI類與(yu)GroupedOpenApi來(lai)配置
/**
* SpringDoc API文檔相關配置
* Created by macro on 2023/02/02.
*/
@Configuration
public class SpringDocConfig {
@Bean
public OpenAPI mallTinyOpenAPI() {
return new OpenAPI()
.info(new Info().title("CTYUN API")
.description("SpringDoc API 演示")
.version("v1.0.0")
.license(new License().name("Apache 2.0").url("//github.com/")))
.externalDocs(new ExternalDocumentation()
.description("SpringBoot項目")
.url("//www.ctyun.com"));
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/**")
.build();
}
//可以創建不同的GroupedOpenApi來判斷不同的controller
@Bean
public GroupedOpenApi userApi() {
return GroupedOpenApi.builder()
.group("user")
.pathsToMatch("/user/**")
.build();
}
}
3.啟動
默認配(pei)置之后直接進入://localhost:8080/swagger-ui/index.html 即可
注(zhu)意這里與springfox略有不同(//localhost:8080/swagger-ui.html)

4.與SpringFox的注解對照

5.SpringDoc基本注解用法
這(zhe)里(li)轉載一個寫的非常全面的示例接口,原文可以去看://blog.csdn.net/zhenghongcs/article/details/123812583
/**
* 品牌管理Controller
* Created by macro on 2019/4/19.
*/
@Tag(name = "PmsBrandController", description = "商品品牌管理")
@Controller
@RequestMapping("/brand")
public class PmsBrandController {
@Autowired
private PmsBrandService brandService;
private static final Logger LOGGER = LoggerFactory.getLogger(PmsBrandController.class);
@Operation(summary = "獲取所有品牌列表",description = "需要登錄后訪問")
@RequestMapping(value = "listAll", method = RequestMethod.GET)
@ResponseBody
public CommonResult<List<PmsBrand>> getBrandList() {
return CommonResult.success(brandService.listAllBrand());
}
@Operation(summary = "添加品牌")
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
@PreAuthorize("hasRole('ADMIN')")
public CommonResult createBrand(@RequestBody PmsBrand pmsBrand) {
CommonResult commonResult;
int count = brandService.createBrand(pmsBrand);
if (count == 1) {
commonResult = CommonResult.success(pmsBrand);
LOGGER.debug("createBrand success:{}", pmsBrand);
} else {
commonResult = CommonResult.failed("操作失敗");
LOGGER.debug("createBrand failed:{}", pmsBrand);
}
return commonResult;
}
@Operation(summary = "更新指定id品牌信息")
@RequestMapping(value = "/update/{id}", method = RequestMethod.POST)
@ResponseBody
@PreAuthorize("hasRole('ADMIN')")
public CommonResult updateBrand(@PathVariable("id") Long id, @RequestBody PmsBrand pmsBrandDto, BindingResult result) {
CommonResult commonResult;
int count = brandService.updateBrand(id, pmsBrandDto);
if (count == 1) {
commonResult = CommonResult.success(pmsBrandDto);
LOGGER.debug("updateBrand success:{}", pmsBrandDto);
} else {
commonResult = CommonResult.failed("操作失敗");
LOGGER.debug("updateBrand failed:{}", pmsBrandDto);
}
return commonResult;
}
@Operation(summary = "刪除指定id的品牌")
@RequestMapping(value = "/delete/{id}", method = RequestMethod.GET)
@ResponseBody
@PreAuthorize("hasRole('ADMIN')")
public CommonResult deleteBrand(@PathVariable("id") Long id) {
int count = brandService.deleteBrand(id);
if (count == 1) {
LOGGER.debug("deleteBrand success :id={}", id);
return CommonResult.success(null);
} else {
LOGGER.debug("deleteBrand failed :id={}", id);
return CommonResult.failed("操作失敗");
}
}
@Operation(summary = "分頁查詢品牌列表")
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
@PreAuthorize("hasRole('ADMIN')")
public CommonResult<CommonPage<PmsBrand>> listBrand(@RequestParam(value = "pageNum", defaultValue = "1")
@Parameter(description = "頁碼") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "3")
@Parameter(description = "每頁數量") Integer pageSize) {
List<PmsBrand> brandList = brandService.listBrand(pageNum, pageSize);
return CommonResult.success(CommonPage.restPage(brandList));
}
@Operation(summary = "獲取指定id的品牌詳情")
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
@ResponseBody
@PreAuthorize("hasRole('ADMIN')")
public CommonResult<PmsBrand> brand(@PathVariable("id") Long id) {
return CommonResult.success(brandService.getBrand(id));
}
}
6.與SpringSecurity的結合
如果項目中使用了SpringSecurity,需要做(zuo)兩個配置來讓springdoc正常使用:
1.在SpringSecurity配(pei)置類中放(fang)行白名單:"/v3/api-docs/**", "/swagger-ui/**"
2.在SpringDoc配置中增加對應內容,如下(xia):
@Configuration
public class SpringDocConfig {
private static final String SECURITY_SCHEME_NAME = "BearerAuth";
@Bean
public OpenAPI managerOpenAPI() {
return new OpenAPI()
.info(new Info().title("Galaxy-Cluster-Manager后端接口文檔")
.description("提供給前端界面(portal)的接口文檔")
.version("v1.0.0")
.license(new License().name("galaxy 1.2.0").url("//gitlab.daliqc.cn/hpc/galaxy-parent/-/tree/v1.2.0")))
.externalDocs(new ExternalDocumentation()
.description("彈性高性能計算(CTHPC)")
.url("//www.daliqc.cn"))
//以下是針對SpringSecurity的設置,同時還有設置白名單
.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
.components(new Components()
.addSecuritySchemes(SECURITY_SCHEME_NAME,
new SecurityScheme()
.name(SECURITY_SCHEME_NAME)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("portal")
.pathsToMatch("/api/**")
.build();
}
}
7.SpringDoc使用對象作為Query參數的問題
實際上springfox也會有(you)這個問題,使(shi)用(yong)對象作為query傳參的時候,頁面通常(chang)是這樣(yang)的:

就(jiu)沒有辦法逐個描述參數(shu),也(ye)不能逐個調(diao)試(shi)(只能用(yong)json調(diao)試(shi)),非常(chang)的麻煩(fan);springdoc有一個解(jie)決這個問(wen)題非常(chang)方便的注(zhu)解(jie):@ParameterObject
比如(ru)某一個控制器(qi)的入參是User user,我們只需要在這前面加上注(zhu)解變為:@ParameterObject User user 即可,結果(guo)如(ru)下;

這里也有一個大坑:參數的類型可能會不正確,比如這里我們的id參數實際上是int,但顯示出來是string,這個時候就需要我們在User類的屬性中加上對應的注解,比如:
@Parameter(description = "id傳參(can)",example = "6")
再(zai)重(zhong)啟UI就會發現(xian)參數類(lei)型正確(que)了(le)
8.SpringDoc配置掃包范圍
有的時候(hou)僅(jin)僅(jin)使用(yong)@Hidden并不(bu)(bu)能(neng)滿足我們的需(xu)要(yao),因為可能(neng)需(xu)要(yao)配(pei)置不(bu)(bu)同(tong)group的controller類(lei),這個(ge)時候(hou)就需(xu)要(yao)在(zai)配(pei)置類(lei)中取設置掃(sao)包范圍(wei)代碼如下:

9.SpringDoc的優劣勢
優勢:SpringDoc有著非常好看(kan)的UI,以及比Springfox更加完善(shan)的參數注解體系,看(kan)起來(lai)非常舒服,并且還在不(bu)斷更新與維護中(zhong)
劣勢(shi):一些冷門功(gong)能還不完善,比如:
a.有(you)十個(ge)(ge)接口(kou),他們的(de)url是一樣的(de)但是可(ke)以通(tong)過query參數來分別(bie)(如:@PostMapping(params = "action=QueryUsers"))這個(ge)(ge)時候(hou)springdoc只(zhi)能通(tong)過掃包范(fan)圍配(pei)置,來寫(xie)多個(ge)(ge)GroupOpenApi來解(jie)決,非常的(de)麻煩;springfox可(ke)以在docket創(chuang)建的(de)時候(hou)使用(yong):docket.enableUrlTemplating(true); 這個(ge)(ge)方法即可(ke)解(jie)決
b.springdoc的(de)網絡配(pei)(pei)(pei)置(zhi)可(ke)能會與springfox沖突,如果(guo)遷移,需(xu)要逐(zhu)個嘗試網絡配(pei)(pei)(pei)置(zhi)是(shi)(shi)否合適(主要是(shi)(shi)GsonHttpMessageConverter的(de)配(pei)(pei)(pei)置(zhi))
c.兼容(rong)性(xing)問題仍需要觀望(wang),相對于springfox,springdoc的(de)兼容(rong)性(xing)并沒有那么好,在許多時候可能(neng)會(hui)出現序列化的(de)亂碼問題
總結:如果當前項目/工程已經集成了完備的springfox,建議不要輕易嘗試遷移到springdoc,尤其是接口類型比較復雜、springfox配置docket比較多的項目;但如果是從頭開始的項目,由于接口相對比較簡單,可以采用springdoc,畢竟可以獲得更加清晰明了的顯示界面與參數解釋。