概述

由于Springfox在2017年的时候就停更了,现在公司也正好想升级一下RestAPI,目前我们用的是Springfox的swagger2,本来想偷懒直接升级成Springfox的swagger3,但是最终失败了,页面一直加载不出接口,不知道什么原因,google了一圈都没找到答案,无奈放弃Springfox,转用Springdoc了。

快速开始

1、添加依赖

项目pom.xml中添加springdoc-openapi-ui依赖

<dependency>
   <groupId>org.springdoc</groupId>
   <artifactId>springdoc-openapi-ui</artifactId>
   <version>1.5.0</version>
</dependency>

2、启动项目,查看OpenAPI3.0的json文件

http://localhost:8080/v3/api-docs/

3、修改默认/v3/api-docs的访问路径

修改application.properties

springdoc.api-docs.path=/api-docs

修改后,可以通过下面的地址访问:

http://localhost:8080/api-docs/

效果跟第2步是一样的

4、集成SwaggerUI

通过下面的地址访问

http://localhost:8080/swagger-ui.html

5、修改默认访问地址

修改application.properties

springdoc.swagger-ui.path=doc.html

修改后,可以通过下面的地址访问:

http://localhost:8080/doc.html

效果跟第4步是一样的,这个地址并不是真正被修改,最终地址还是被重定向到http://localhost:8080/swagger-ui/index.html
但是你不能直接访问这个地址,否则会展示OpenAPI3.0的演示UI页面,如下图所示:
在这里插入图片描述

6、启用分组配置

对于Springboot微服务来说,目前可以通过分组方式显示各个微服务的API文档【目前我还没找到如何合并多个微服务的api-docs.json的办法,等我找到后再补充】
修改application.properties

springdoc.api-docs.groups.enabled=true

访问

http://localhost:8080/doc.html

在这里插入图片描述

示例代码

1、配置类OpenApiConfig.java

package com.kevin.demo.config;

import org.springdoc.core.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.annotations.tags.Tag;


/**
 * @author kevin
 *
 */
@OpenAPIDefinition(
		// openapi定义描述
        info = @Info(
                title = "${spring.application.name}",
                version = "1.0.0",
                description = "OpenApi3.0",
                license = @License(name = "Apache 2.0", url = "http://www.apache.org/licenses/LICENSE-2.0.html")
        		),
        // 请求服务地址配置,可以按不同的环境配置		
        servers = {
        		@Server(
		        		url = "http://localhost:8181",
		        		description = "本地地址"
		        		),
        		@Server(
		        		url = "http://dev1-api.kevin.com",
		        		description = "dev1环境地址"
		        		),
        		@Server(
                		url = "http://test1-api.kevin.com",
                		description = "test1环境地址"
                		),
        		@Server(
                		url = "http://api.kevin.com",
                		description = "生产环境地址"
                		)
        },
        // 这个tags可以用来定义一些公共参数说明,比如:token或者其他自定义key
        tags = {
        		@Tag(name = "Header:" + HttpHeaders.AUTHORIZATION, description = "登录之后获取的JWT Token,类型是bearer"),
        		@Tag(name = "Header:Accept-Language", description = "国际化语言:zh-CN(中文),en-US(英文)")
        }
)
// 安全配置:JWT Token。也可以配置其他类型的鉴权,比如:basic
@SecurityScheme(
	    name = HttpHeaders.AUTHORIZATION,
	    type = SecuritySchemeType.HTTP,
	    bearerFormat = "JWT",
	    scheme = "bearer"
	)
@Configuration
public class OpenApiConfig {
	
	// 分组配置:Books
	@Bean
	public GroupedOpenApi bookOpenApi() {
		String packagesToscan[] = {"com.kevin.demo.book"};
		return GroupedOpenApi.builder().group("Books").packagesToScan(packagesToscan)
				.build();
	}
	// 分组配置:Stores
	@Bean
	public GroupedOpenApi storeOpenApi() {
		String packagesToscan[] = {"com.kevin.demo.store"};
		return GroupedOpenApi.builder().group("Stores").packagesToScan(packagesToscan)
				.build();
	}
}

2、创建2个不同的包,用来模拟多个微服务

一个book包,一个store包
在这里插入图片描述

3、application.properties配置

server.port=8080
spring.application.name=springdoc-openapi3-demo
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=doc.html

# 开启分组
springdoc.api-docs.groups.enabled=true

4、定义一个Controller的公共Response对象R.java

package com.kevin.demo.common.response;

import java.io.Serializable;

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Schema(description = "Response对象")
public class R<T> implements Serializable {
	private static final long serialVersionUID = 1L;
	
    @Schema(description = "消息code", required = true)
    private int code;
    @Schema(description = "消息提示内容")
    private String message;
    @Schema(description = "消息实体")
    private T data;
    
    public R<T> code(int code) {
    	this.code = code;
        return this;
    }
    
    public R<T> code(HttpStatus status) {
    	this.code = status.value();
        return this;
    }

    public R<T> message(String message) {
    	this.message = message;
        return this;
    }

    public R<T> data(T data) {
    	this.data = data;
        return this;
    }

    public R<T> success() {
        this.code(0);
        this.message(HttpStatus.OK.getReasonPhrase());
        return this;
    }


    /**
     * 未授权返回结果
     */
    public R<T> forbidden(String message) {
    	this.code(HttpStatus.FORBIDDEN);
    	if(StringUtils.isNotBlank(message)) {
    		this.message(message);
    	} else {
    		this.message(HttpStatus.FORBIDDEN.getReasonPhrase());
    	}
    	return this;
    }
    
    /**
     * 未登录返回结果
     */
    public R<T> unauthorized(String message) {
    	this.code(HttpStatus.UNAUTHORIZED);
    	this.message(HttpStatus.UNAUTHORIZED.getReasonPhrase());
    	return this;
    }
    
    /**
     * 校验失败
     * 
     * @param message
     * @return
     */
    public R<T> validateFailed(String message) {
    	this.code(HttpStatus.NOT_FOUND);
    	this.message(message);
    	return this;
    }

}

5、自定义异常类CustomException.java

package com.kevin.demo.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * @author kevin
 *
 */
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class CustomException extends Exception {
	private static final long serialVersionUID = 1L;
	
	/**
	 * 异常消息
	 */
	private String message;
}

6、统一异常处理类GlobalControllerExceptionHandler.java

package com.kevin.demo.handler;

import org.springframework.core.convert.ConversionFailedException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.kevin.demo.common.response.R;
import com.kevin.demo.exception.CustomException;

/**
 * 
 * OpenApi会解析@RestControllerAdvice注解的类,在response中会显示这些异常定义:400/404/500
 * 
 * @author kevin
 *
 */
@RestControllerAdvice
public class GlobalControllerExceptionHandler {

    @ExceptionHandler(ConversionFailedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public R handleConnversion(RuntimeException ex) {
    	R resp = new R()
    			.code(HttpStatus.BAD_REQUEST)
    			.message(ex.getMessage());
        return resp;
    }
    
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public R handleException(Exception ex) {
    	R resp = new R()
    			.code(HttpStatus.INTERNAL_SERVER_ERROR)
    			.message(ex.getMessage());
    	return resp;
    }
   
    @ExceptionHandler(CustomException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public R handleBookNotFound(RuntimeException ex) {
    	R resp = new R()
    			.code(HttpStatus.NOT_FOUND)
    			.message(ex.getMessage());
        return resp;
    }
}

7、Book包Controller类BookController.java

package com.kevin.demo.book.web.controller;

import java.util.ArrayList;
import java.util.List;

import javax.validation.Valid;

import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.kevin.demo.book.web.dto.BookDTO;
import com.kevin.demo.book.web.vo.BookVO;
import com.kevin.demo.common.response.R;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;

/**
 * @author kevin
 *
 */
@RestController
@RequestMapping("/api/book")
@Tag(name = "书籍管理")
public class BookController {
	
	//security属性会要求在swagger-ui页面请求调试的时候,需要带上指定的header -> HttpHeaders.AUTHORIZATION,安全配置请参考OpenApiConfig.java的@SecurityScheme
	@Operation(summary ="查询某一本书籍", security = @SecurityRequirement(name = HttpHeaders.AUTHORIZATION))
    @GetMapping("/{id}")
    public R<BookVO> findById(@Parameter(description="书籍的id")@PathVariable long id) {
    	BookVO book = new BookVO();
    	book.setId(1L);
    	book.setTitle("java");
    	book.setAuthor("kevin1");
        return new R<BookVO>().success().data(book);
    }

	@Operation(summary ="查询书籍列表", security = @SecurityRequirement(name = HttpHeaders.AUTHORIZATION))
    @GetMapping
    public R<List<BookVO>> findBooks() {
    	BookVO b1 = new BookVO();
    	b1.setId(1L);
    	b1.setTitle("java");
    	b1.setAuthor("kevin1");
    	BookVO b2 = new BookVO();
    	b2.setId(2L);
    	b2.setTitle("c");
    	b2.setAuthor("kevin2");
    	BookVO b3 = new BookVO();
    	b3.setId(3L);
    	b3.setTitle("c++");
    	b3.setAuthor("kevin3");
    	
    	List<BookVO> list = new ArrayList<>();
    	list.add(b1);
    	list.add(b2);
    	list.add(b3);
        return new R<List<BookVO>>().success().data(list);
    }

	@Operation(summary ="修改某一本书籍", security = @SecurityRequirement(name = HttpHeaders.AUTHORIZATION))
    @PutMapping("/{id}")
    public R<BookVO> updateBook(@Parameter(description="书籍的id")@PathVariable("id") final String id, 
    		@Valid @RequestBody BookDTO book) {
        return new R<BookVO>().success();
    }
}

8、Book包DTO类BookDTO.java

package com.kevin.demo.book.web.dto;

import javax.validation.constraints.Size;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

/**
 * @author kevin
 *
 */
@Getter
@Setter
@Schema(description = "书传输对象")
public class BookDTO {

//    @NotBlank
    @Size(min = 0, max = 20)
//    @Schema(description = "标题", required = true, hidden = true)
    @Schema(description = "标题", required = true)
    private String title;

//    @NotBlank
    @Size(min = 0, max = 30)
    @Schema(description = "作者", required = true)
    private String author;
}

9、Book包VO类BookVO.java

package com.kevin.demo.book.web.vo;


import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

/**
 * @author kevin
 *
 */
@Getter
@Setter
@Schema(description = "书对象")
public class BookVO {
	@Schema(description = "书id")
	private long id;

    @Schema(description = "标题", required = true)
    private String title;

    @Schema(description = "作者", required = true)
    private String author;
    
    //对前端隐藏不可见
    @Hidden
    private String secret;
}

10、Store包Controller类StoreController.java

package com.kevin.demo.store.web.controller;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.kevin.demo.common.response.R;
import com.kevin.demo.store.web.dto.StoreDTO;
import com.kevin.demo.store.web.vo.StoreVO;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;

/**
 * @author kevin
 *
 */
@RestController
@RequestMapping("/api/store")
@Tag(name = "仓库管理")
public class StoreController {

	@Operation(summary ="查询某个仓库", security = @SecurityRequirement(name = HttpHeaders.AUTHORIZATION))
    @GetMapping("/{id}")
    public R<StoreVO> findById(@Parameter(description="仓库的id")@PathVariable long id, HttpServletRequest request) {
		String token = request.getHeader(HttpHeaders.AUTHORIZATION);
		System.out.println(token);
    	StoreVO store = new StoreVO();
    	store.setId(1L);
    	store.setName("1号库");
    	store.setSize(100);
        return new R<StoreVO>().success().data(store);
    }

	@Operation(summary ="查询仓库列表", security = @SecurityRequirement(name = HttpHeaders.AUTHORIZATION))
    @GetMapping
    public R<List<StoreVO>> findStores() {
    	StoreVO b1 = new StoreVO();
    	b1.setId(1L);
    	b1.setName("1号库");
    	b1.setSize(100);
    	StoreVO b2 = new StoreVO();
    	b2.setId(2L);
    	b2.setName("2号库");
    	b2.setSize(200);
    	StoreVO b3 = new StoreVO();
    	b3.setId(3L);
    	b3.setName("3号库");
    	b3.setSize(300);
    	
    	List<StoreVO> list = new ArrayList<>();
    	list.add(b1);
    	list.add(b2);
    	list.add(b3);
        return new R<List<StoreVO>>().success().data(list);
    }

	@Operation(summary ="修改某一本仓库", security = @SecurityRequirement(name = HttpHeaders.AUTHORIZATION))
    @PutMapping("/{id}")
    public R<StoreVO> updateStore(@Parameter(description="仓库的id")@PathVariable("id") final String id, 
    		@Valid @RequestBody StoreDTO Store) {
        return new R<StoreVO>().success();
    }
}

11、Store包DTO类StoreDTO.java

package com.kevin.demo.store.web.dto;

import javax.validation.constraints.Max;
import javax.validation.constraints.Size;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

/**
 * @author kevin
 *
 */
@Getter
@Setter
@Schema(description = "仓库DTO对象")
public class StoreDTO {

//    @NotBlank
    @Size(min = 0, max = 20)
//    @Schema(description = "标题", required = true, hidden = true)
    @Schema(description = "仓库名称", required = true)
    private String name;

//    @NotBlank
    @Max(Integer.MAX_VALUE)
    @Schema(description = "仓库大小", required = true)
    private Integer size;
}

12、Store包VO类StoreVO.java

package com.kevin.demo.store.web.vo;


import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

/**
 * @author kevin
 *
 */
@Getter
@Setter
@Schema(description = "仓库VO对象")
public class StoreVO {
	@Schema(description = "仓库id")
	private long id;

	@Schema(description = "仓库名称", required = true)
    private String name;

    @Schema(description = "仓库大小", required = true)
    private Integer size;
    
}

附录:Swagger2转Swagger3注解说明

@ApiParam -> @Parameter(description="参数描述")
@ApiOperation -> @Operation(summary ="方法描述")
@Api -> @Tag(name = "类描述")
@ApiImplicitParams -> @Parameters
@ApiImplicitParam -> @Parameter
@ApiIgnore -> @Parameter(hidden = true) or @Operation(hidden = true) or @Hidden
@ApiModel -> @Schema(description = "类的描述信息")
@ApiModelProperty -> @Schema(description = "字段描述信息", 
                                    required = true, // 是否必填,默认false。在字段上加上:@NotBlank/@NotEmpty/@NotNull一样的效果
                                    hidden = true) // 是否隐藏此字段,默认false

项目Github地址

https://github.com/0xkevin/springdoc-openapi3-demo
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐