首先创建一个项目 test0618
,添加 Spring Web
依赖。
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";
}
}
运行起来测试一下效果:
现在我们来集成Swagger。
首先要添加依赖。从浏览器打开 https://mvnrepository.com
,搜索 springfox
:
在过去Swagger 2.X的时候,需要添加 Springfox Swagger2
和 Springfox Swagger UI
这两个依赖包。当然,现在对于Swagger 3.0,也可以添加这两个依赖包的3.0版本,只不过Swagger 3.0已经直接提供了 Springfox Boot Starter
,集成更加方便。
打开 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
,如下:
如果想尝试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
:
注意:Swagger2.X和Swagger 3.0的访问地址不同:
http://localhost:8080/swagger-ui.html
http://localhost:8080/swagger-ui/index.html
Swagger 2.X与Swagger 3.0类似,下面只以Swagger 3.0为例。
打开 http://localhost:8080/swagger-ui/index.html
:
可见 /hello
具有各种method,这是因为我们在 @RequestMapping
里没有配置method。修改 Test0618_1.java
文件如下:
........
@RequestMapping(value = "/hello", method = RequestMethod.GET)
........
这次显式配置method为 GET
。再次运行程序,如下:
可见,现在只有 /hello
的 GET
请求了。
可以直接通过Swagger页面发送API请求。点击 GET
按钮展开,然后点击 Try it out
按钮:
本例中, /hello
API非常简单,不需要认证,也不需要参数,所以可以直接点击 Execute
按钮运行:
可见,API返回的正是期望结果 hello
。
在 Test0618_1
类中添加如下方法:
@RequestMapping(value = "/foo", method = RequestMethod.GET)
public String foo(String param1) {
return "hello " + param1;
}
效果如下:
可见,Swagger里标明了需要一个String类型的参数。
点击 Try it out
,输入参数比如 world
,然后点击 Execute
按钮,如下:
添加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;
}
效果如下:
展开页面下方的 Models
:
注意:Models
里出现 MyClass1
,是因为 bar()
方法返回了 MyClass1
实体对象。如果API不返回 MyClass1
实体对象,则 MyClass1
不会出现在 Models
里。
点击 Try it out
按钮,然后点击 Execute
按钮:
添加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<>()));
}
}
运行效果如下:
创建另一个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<>()));
}
效果如下:
可见,只有 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<>()));
}
效果如下:
注: .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();
}
效果如下:
使用 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<>()));
}
效果如下:
注:我发现 Controller2
也被列出来了(尴尬)……是因为 RequestHandlerSelectors.basePackage("com.example.test0618.controller")
这个过滤条件把 com.example.test0618.controller2
也扫描进来了。所以最好把 controller
重命名为 controller1
。前面已经写了这么多东西,就不一一改动了。