SpringBoot集成Swagger

环境

  • 操作系统:Ubuntu 20.04
  • IntelliJ IDEA 2022.1.2 (Community Edition)
  • JDK 17.0.1
  • SpringBoot 2.5.8
  • Swagger 3.0

准备

首先创建一个项目 test0618 ,添加 Spring Web 依赖。

SpringBoot集成Swagger_第1张图片
添加一个Controller:

package com.example.test0618.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Test0618_1 {
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}

运行起来测试一下效果:

SpringBoot集成Swagger_第2张图片

集成Swagger

现在我们来集成Swagger。

首先要添加依赖。从浏览器打开 https://mvnrepository.com ,搜索 springfox

SpringBoot集成Swagger_第3张图片

在过去Swagger 2.X的时候,需要添加 Springfox Swagger2Springfox Swagger UI 这两个依赖包。当然,现在对于Swagger 3.0,也可以添加这两个依赖包的3.0版本,只不过Swagger 3.0已经直接提供了 Springfox Boot Starter ,集成更加方便。

Swagger 3.0

打开 pom.xml 文件,添加依赖:

		<dependency>
			<groupId>io.springfoxgroupId>
			<artifactId>springfox-boot-starterartifactId>
			<version>3.0.0version>
		dependency>

此时,如果再次运行程序(先刷新一下Maven),会报错:

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-06-18 15:30:50.846 ERROR 5336 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null
	at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.20.jar:5.3.20]
	at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.20.jar:5.3.20]
	at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.20.jar:5.3.20]
	at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
	at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.20.jar:5.3.20]
	at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.20.jar:5.3.20]
	at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.20.jar:5.3.20]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.20.jar:5.3.20]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.0.jar:2.7.0]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734) ~[spring-boot-2.7.0.jar:2.7.0]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) ~[spring-boot-2.7.0.jar:2.7.0]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-2.7.0.jar:2.7.0]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-2.7.0.jar:2.7.0]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-2.7.0.jar:2.7.0]
	at com.example.test0618.Test0618Application.main(Test0618Application.java:10) ~[classes/:na]
Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null
	at springfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns(WebMvcPatternsRequestConditionWrapper.java:56) ~[springfox-spring-webmvc-3.0.0.jar:3.0.0]
	at springfox.documentation.RequestHandler.sortedPaths(RequestHandler.java:113) ~[springfox-core-3.0.0.jar:3.0.0]
	at springfox.documentation.spi.service.contexts.Orderings.lambda$byPatternsCondition$3(Orderings.java:89) ~[springfox-spi-3.0.0.jar:3.0.0]
	at java.base/java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:473) ~[na:na]
	at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:na]
	at java.base/java.util.TimSort.sort(TimSort.java:220) ~[na:na]
	at java.base/java.util.Arrays.sort(Arrays.java:1307) ~[na:na]
	at java.base/java.util.ArrayList.sort(ArrayList.java:1721) ~[na:na]
	at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:392) ~[na:na]
	at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
	at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
	at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
	at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) ~[na:na]
	at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:81) ~[springfox-spring-webmvc-3.0.0.jar:3.0.0]
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) ~[na:na]
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[na:na]
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) ~[na:na]
	at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.withDefaults(AbstractDocumentationPluginsBootstrapper.java:107) ~[springfox-spring-web-3.0.0.jar:3.0.0]
	at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.buildContext(AbstractDocumentationPluginsBootstrapper.java:91) ~[springfox-spring-web-3.0.0.jar:3.0.0]
	at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.bootstrapDocumentationPlugins(AbstractDocumentationPluginsBootstrapper.java:82) ~[springfox-spring-web-3.0.0.jar:3.0.0]
	at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:100) ~[springfox-spring-web-3.0.0.jar:3.0.0]
	at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ~[spring-context-5.3.20.jar:5.3.20]
	... 14 common frames omitted

由于 NullPointerException ,Spring没有启起来。在网上搜了一下错误消息 Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException ,说是高版本的SpringBoot和Swagger 3.0不兼容(参见前面的图片,我使用的是SpringBoot 2.7.0 )。

最简单粗暴的解决办法是改用低版本的SpringBoot。例如改成 2.5.8

	<parent>
		<groupId>org.springframework.bootgroupId>
		<artifactId>spring-boot-starter-parentartifactId>
		
		<version>2.5.8version>
		<relativePath/> 
	parent>

注:没有具体研究过有哪些版本兼容,反正 2.5.8 OK。

刷新Maven,运行程序,这回不报错了。

打开浏览器,访问 http://localhost:8080/swagger-ui/index.html ,如下:

SpringBoot集成Swagger_第4张图片

Swagger 2.x

如果想尝试Swagge 2.X,删掉刚才添加的依赖,添加如下依赖:

		
		<dependency>
			<groupId>io.springfoxgroupId>
			<artifactId>springfox-swagger2artifactId>
			<version>2.9.2version>
		dependency>

		
		<dependency>
			<groupId>io.springfoxgroupId>
			<artifactId>springfox-swagger-uiartifactId>
			<version>2.9.2version>
		dependency>

注:SrpingBoot 2.7同样和Swagger 2.9.2不兼容,所以仍然要改为 2.5.8

注:Swagger 2.X的最高版本是2.10.X,但是2.10.X貌似跟2.9.X的注解不同了,没仔细研究。

不像 xxx-boot-starter 会自动配置,这时就需要手工配置。添加 SwaggerConfig.java 如下:

package com.example.test0618.config;

import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
}

注:

  • @Configuration :表示这是一个Configuration类;
  • @EnableSwagger2 :表示enable Swagger;

运行程序,打开浏览器,访问 http://localhost:8080/swagger-ui.html

SpringBoot集成Swagger_第5张图片

注意:Swagger2.X和Swagger 3.0的访问地址不同:

  • Swagger 2.X:http://localhost:8080/swagger-ui.html
  • Swagger 3.0:http://localhost:8080/swagger-ui/index.html

总览Swagger

Swagger 2.X与Swagger 3.0类似,下面只以Swagger 3.0为例。

打开 http://localhost:8080/swagger-ui/index.html

SpringBoot集成Swagger_第6张图片

可见 /hello 具有各种method,这是因为我们在 @RequestMapping 里没有配置method。修改 Test0618_1.java 文件如下:

    ........
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    ........

这次显式配置method为 GET 。再次运行程序,如下:

SpringBoot集成Swagger_第7张图片

可见,现在只有 /helloGET 请求了。

通过Swagger发送API请求

可以直接通过Swagger页面发送API请求。点击 GET 按钮展开,然后点击 Try it out 按钮:

SpringBoot集成Swagger_第8张图片
本例中, /hello API非常简单,不需要认证,也不需要参数,所以可以直接点击 Execute 按钮运行:

SpringBoot集成Swagger_第9张图片

可见,API返回的正是期望结果 hello

带参数的API

Test0618_1 类中添加如下方法:

    @RequestMapping(value = "/foo", method = RequestMethod.GET)
    public String foo(String param1) {
        return "hello " + param1;
    }

效果如下:

SpringBoot集成Swagger_第10张图片
可见,Swagger里标明了需要一个String类型的参数。

点击 Try it out ,输入参数比如 world ,然后点击 Execute 按钮,如下:

SpringBoot集成Swagger_第11张图片

返回POJO的API

添加POJO类 MyClass1 如下:

package com.example.test0618.pojo;

public class MyClass1 {
    private int length;
    private int weight;

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }
}

Test0618_1 类中添加如下方法:

    @RequestMapping(value = "/bar", method = RequestMethod.GET)
    public MyClass1 bar() {
        MyClass1 obj1 = new MyClass1();
        obj1.setLength(100);
        obj1.setWeight(200);
        return obj1;
    }

效果如下:

SpringBoot集成Swagger_第12张图片

展开页面下方的 Models

SpringBoot集成Swagger_第13张图片

注意:Models 里出现 MyClass1 ,是因为 bar() 方法返回了 MyClass1 实体对象。如果API不返回 MyClass1 实体对象,则 MyClass1 不会出现在 Models 里。

点击 Try it out 按钮,然后点击 Execute 按钮:

SpringBoot集成Swagger_第14张图片

配置Swagger

自定义描述信息

添加Configuration类 SwaggerConfig 如下:

package com.example.test0618.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfo("我是标题", "我是描述", "我是版本",
                        "http://example.com", ApiInfo.DEFAULT_CONTACT, "Apache 2.0",
                        "http://www.apache.org/license/LICENSE-2.0", new ArrayList<>()));
    }
}

运行效果如下:

SpringBoot集成Swagger_第15张图片

过滤API

创建另一个Controller(在另一个package下) Controller2 如下:

package com.example.test0618.controller2;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Controller2 {
    @RequestMapping(value = "/test2", method = RequestMethod.GET)
    public String test2() {
        return "OK";
    }
}

现在我们来指定要扫描的API。

通过 apis() 过滤

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.test0618.controller2"))
                .build()
                .apiInfo(new ApiInfo("我是标题", "我是描述", "我是版本",
                        "http://example.com", ApiInfo.DEFAULT_CONTACT, "Apache 2.0",
                        "http://www.apache.org/license/LICENSE-2.0", new ArrayList<>()));
    }

效果如下:

SpringBoot集成Swagger_第16张图片

可见,只有 Controller2 包下的API被扫描到了。

RequestHandlerSelectors 提供的方法有:

  • basePackage() :指定package;
  • any() :所有API;
  • none() :所有API都不处理;
  • withClassAnnotation() :指定注解的类,比如 RestController.class
  • withMethodAnnotation() :指定注解的方法,比如 GetMapping.class

通过 paths() 过滤

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.test0618.controller"))
                .paths(PathSelectors.regex(".*foo.*"))
                .build()
                .apiInfo(new ApiInfo("我是标题", "我是描述", "我是版本",
                        "http://example.com", ApiInfo.DEFAULT_CONTACT, "Apache 2.0",
                        "http://www.apache.org/license/LICENSE-2.0", new ArrayList<>()));
    }

效果如下:

SpringBoot集成Swagger_第17张图片
可见,只有指定正则 .*foo.* 的API。

注: .apis() 方法也在,二者都起作用。

PathSelectors 提供的方法有:

  • regex() :正则;
  • ant() :ant pattern;
  • any() :所有API;
  • none() :所有API都不处理;

注:对于ant pattern,请参考:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/AntPathMatcher.html

分组

通过 groupName() 方法来指定组名,实现分组。

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("Group1")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.test0618.controller"))
                .paths(PathSelectors.regex(".*foo.*"))
                .build()
                .apiInfo(new ApiInfo("我是标题", "我是描述", "我是版本",
                        "http://example.com", ApiInfo.DEFAULT_CONTACT, "Apache 2.0",
                        "http://www.apache.org/license/LICENSE-2.0", new ArrayList<>()));
    }

    @Bean
    public Docket docket2() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("Group2")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.test0618.controller2"))
                .build();
    }

效果如下:

SpringBoot集成Swagger_第18张图片
比如切换到 Group2

SpringBoot集成Swagger_第19张图片
Group1Group2 互相独立,互不干扰。

启用/禁用Swagger

使用 enable() 方法,传入 true 或者 false 。如果没有显式调用 enable() ,则默认是启用。

禁用后,在分组里就看不到了。

注:对于内部API,可以单独分一个组,在开发环境启用,在生产环境禁用。当然也可以在生产环境禁用所有分组,通过其它渠道提供Swagger文档给用户。

通过注解添加描述信息

  • @Api
  • @ApiOperation
  • @ApiParam
  • @ApiModel
  • @ApiModelProperty

具体用法参见下面示例。

修改 Test0618_1 类,添加注解如下:

........
@Api(tags = "我是一个Controller")
public class Test0618_1 {
........
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    @ApiOperation("这是hello()方法")
    public String hello() {
........
    @RequestMapping(value = "/foo", method = RequestMethod.GET)
    @ApiOperation("这是foo()方法")
    public String foo(@ApiParam("这是param1参数") String param1) {
........
    @RequestMapping(value = "/bar", method = RequestMethod.GET)
    @ApiOperation("这是bar()方法")
    public MyClass1 bar() {
........

修改 MyClass1 POJO类,添加注解如下:

........
@ApiModel("这是MyClass1")
public class MyClass1 {
    @ApiModelProperty("这是length属性")
    private int length;
    @ApiModelProperty("这是weight属性")
    private int weight;
........

为了方便查看效果,把 Group1 的path过滤去掉:

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("Group1")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.test0618.controller"))
                .build()
                .apiInfo(new ApiInfo("我是标题", "我是描述", "我是版本",
                        "http://example.com", ApiInfo.DEFAULT_CONTACT, "Apache 2.0",
                        "http://www.apache.org/license/LICENSE-2.0", new ArrayList<>()));
    }

效果如下:

SpringBoot集成Swagger_第20张图片

注:我发现 Controller2 也被列出来了(尴尬)……是因为 RequestHandlerSelectors.basePackage("com.example.test0618.controller") 这个过滤条件把 com.example.test0618.controller2 也扫描进来了。所以最好把 controller 重命名为 controller1 。前面已经写了这么多东西,就不一一改动了。

参考

  • https://www.bilibili.com/video/BV1Y441197Lw?p=1
  • https://springfox.github.io/springfox/docs/current

你可能感兴趣的:(swagger,spring,boot,spring,boot,java,Swagger)