30.SpringBoot集成swagger

 

1.为什么要使用swagger

随着互联网技术的发展,现在的网站架构基本都由原来的后端渲染,变成了:前端渲染、前后端分离的形态,而且前端技术和后端技术在各自的道路上越走越远。前端和后端的唯一联系,变成了API接口;API文档变成了前后端开发人员联系的纽带,变得越来越重要,swagger就是一款让你更好的书写API文档的框架,而且swagger可以完全模拟http请求,入参出参和实际情况差别几乎为零。

没有API文档工具之前,大家都是手写API文档的(维护起来相当困难),在什么地方书写的都有,有在confluence上写的,有在对应的项目目录下readme.md上写的,每个公司都有每个公司的玩法,无所谓好坏。但是能称之为“框架”的,估计也只有swagger了

2.Demo

项目结构图:

(1) pom.xml

有两种swagger-ui:

1.springfox-swagger-ui

1
2
3
4
5
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>

2.swagger-bootstrap-ui

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>2.0.2.RELEASE</version>

<exclusions>
<exclusion>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>19_springboot_swagger</artifactId>
<version>1.0</version>
<name>19_springboot_swagger</name>
<description>Spring Boot swagger</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- swagger-spring-boot-starter -->
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

(2) entity: DataFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package com.example.demo.entity;

import java.util.Date;

import org.springframework.format.annotation.DateTimeFormat;

import com.fasterxml.jackson.annotation.JsonFormat;

public class DataFile {
private Integer id;

private String fileName;

private String contentType;

private Integer size;

private String relativePath;

private String absolutePath;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date uploadTime;

public DataFile() {
super();
}

public DataFile(Integer id, String fileName, String contentType, Integer size, String relativePath,
String absolutePath, Date uploadTime) {
super();
this.id = id;
this.fileName = fileName;
this.contentType = contentType;
this.size = size;
this.relativePath = relativePath;
this.absolutePath = absolutePath;
this.uploadTime = uploadTime;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getFileName() {
return fileName;
}

public void setFileName(String fileName) {
this.fileName = fileName == null ? null : fileName.trim();
}

public String getContentType() {
return contentType;
}

public void setContentType(String contentType) {
this.contentType = contentType == null ? null : contentType.trim();
}

public Integer getSize() {
return size;
}

public void setSize(Integer size) {
this.size = size;
}

public String getRelativePath() {
return relativePath;
}

public void setRelativePath(String relativePath) {
this.relativePath = relativePath == null ? null : relativePath.trim();
}

public String getAbsolutePath() {
return absolutePath;
}

public void setAbsolutePath(String absolutePath) {
this.absolutePath = absolutePath == null ? null : absolutePath.trim();
}

public Date getUploadTime() {
return uploadTime;
}

public void setUploadTime(Date uploadTime) {
this.uploadTime = uploadTime;
}

@Override
public String toString() {
return "DataFile [id=" + id + ", fileName=" + fileName + ", contentType=" + contentType + ", size=" + size
+ ", relativePath=" + relativePath + ", absolutePath=" + absolutePath + ", uploadTime=" + uploadTime
+ "]";
}

}

(3) vo: ResultObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.example.demo.vo;

public class ResultObject {

private String code;

private String message;

private Object body;

public ResultObject() {
super();
}

public ResultObject(String code, String message) {
super();
this.code = code;
this.message = message;
}

public ResultObject(String code, String message, Object body) {
super();
this.code = code;
this.message = message;
this.body = body;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public Object getBody() {
return body;
}

public void setBody(Object body) {
this.body = body;
}

@Override
public String toString() {
return "ResultObject [code=" + code + ", message=" + message + ", body=" + body + "]";
}

}

(4) DataFileController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.example.demo.controller;

import java.util.Date;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.entity.DataFile;
import com.example.demo.vo.ResultObject;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.parameters.RequestBody;

@RestController
@RequestMapping("dataFile")
@Api(tags = "文件管理")
public class DataFileController {

@GetMapping("sayHello")
@ApiOperation(value = "测试接口", notes = "测试连通性")
public ResultObject sayHello() {
ResultObject resultObject = new ResultObject("0", "hello, swagger!");
return resultObject;
}

@RequestMapping("testRequestType")
public String testRequestType(String requestType) {
return requestType + ", success!";
}

@ApiOperation(value = "添加文件", notes = "新增文件")
@PostMapping("addFile")
public ResultObject addFile(DataFile dataFile) {
System.out.println("添加文件:" + dataFile);
ResultObject resultObject = new ResultObject("200", "添加文件成功!");
return resultObject;
}

@ApiOperation(value = "编辑文件", notes = "修改文件")
@RequestMapping(value = "updateFile", method = RequestMethod.POST)
public ResultObject updateFile(@RequestBody DataFile dataFile) {
System.out.println("编辑文件:" + dataFile);
ResultObject resultObject = new ResultObject("200", "编辑文件成功!");
return resultObject;
}

@ApiOperation(value = "删除文件", notes = "清除文件")
@ApiImplicitParams(value = {@ApiImplicitParam(name = "fileId", value = "文件Id", required = true, paramType = "query", dataType = "String")})
@RequestMapping(value = "deleteFile", method = RequestMethod.DELETE)
public ResultObject deleteFile(@RequestParam("fileId") String id) {
System.out.println("删除文件,fileId = " + id);
ResultObject resultObject = new ResultObject("200", "删除文件成功!");
return resultObject;
}

@ApiOperation(value = "查询文件", notes = "搜索文件")
@ApiImplicitParams(value = {@ApiImplicitParam(name = "fileName", value = "文件名", required = true, paramType = "query", dataType = "String")})
@RequestMapping(value = "queryFile", method = RequestMethod.POST)
public ResultObject queryFile(@RequestParam("fileName") String fileName) {
System.out.println("查询文件,fileName = :" + fileName);
DataFile dataFile = new DataFile(1, fileName, "txt", 100, "file/", "/home/admin/file/", new Date());
ResultObject resultObject = new ResultObject("200", "查询文件成功!", dataFile);
return resultObject;
}

}

@ApiImplicitParam的属性paramTypedataType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiImplicitParam {

/**
* The parameter type of the parameter.
* <p>
* Valid values are {@code path}, {@code query}, {@code body},
* {@code header} or {@code form}.
*/
String paramType() default "";

/**
* The data type of the parameter.
* <p>
* This can be the class name or a primitive.
*/
String dataType() default "";

}

(5) application.yml

1
2
3
4
spring:
mvc:
pathmatch:
matching-strategy: ant-path-matcher

解决以下报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-06-24 23:00:02.078 ERROR 3604 --- [ main] o.s.boot.SpringApplication : Application run failed

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.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.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_271]
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.demo.Application.main(Application.java:10) [classes/:na]
Caused by: java.lang.NullPointerException: 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.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:469) ~[na:1.8.0_271]
at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:1.8.0_271]
at java.util.TimSort.sort(TimSort.java:220) ~[na:1.8.0_271]
at java.util.Arrays.sort(Arrays.java:1512) ~[na:1.8.0_271]
at java.util.ArrayList.sort(ArrayList.java:1464) ~[na:1.8.0_271]
at java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:387) ~[na:1.8.0_271]
at java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:1.8.0_271]
at java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:1.8.0_271]
at java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:1.8.0_271]
at java.util.stream.Sink$ChainedReference.end(Sink.java:258) ~[na:1.8.0_271]
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:483) ~[na:1.8.0_271]
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472) ~[na:1.8.0_271]
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) ~[na:1.8.0_271]
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_271]
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) ~[na:1.8.0_271]
at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:81) ~[springfox-spring-webmvc-3.0.0.jar:3.0.0]
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) ~[na:1.8.0_271]
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1384) ~[na:1.8.0_271]
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482) ~[na:1.8.0_271]
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472) ~[na:1.8.0_271]
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) ~[na:1.8.0_271]
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_271]
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) ~[na:1.8.0_271]
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

(6) 启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

1.springfox-swagger-ui

浏览器访问: http://localhost:8080/swagger-ui/

sayHello:

testRequestType:

因为没有指明requestType,所以支持所有的requestType

addFile(), updateFile(), deleteFile(), queryFile():

2.swagger-bootstrap-ui

浏览器访问: http://localhost:8080/doc.html