从零新建一个 Springboot 2.7.1 项目搭配 Swagger 3.0 Knife4j MyBatisPlus 等
2022-08-19
阅读 {{counts.readCount}}
评论 {{counts.commentCount}}
<br><br>
## 前言
<br>
本身应该是一个很简单的事情,新建项目,引入依赖,启动,结束。理论上不值得写笔记。
BUT!!!
<br>
最近发现,才大半年没新建项目,变化太快了,尤其是`springboot 2.7.1`,和一些框架依赖会有冲突,感觉需要重新学一学,
顺便把从零配置的过程全部详细记录一下,防止以后忘记。
<br>
开发环境如下
- Windows 11
- Intellij Idea 2020.3
- Maven 3.6.3
- Oracle Java 1.8.0_251
- Tomcat 8.5.56
<br><br>
## 折腾
<br>
#### 新建项目
<br>
开头部分都是基础中的基础了,而且新版本和老版本也没什么区别,就用截图快速介绍下,不需要的可以跳到下一个部分
<br>
![](/api/file/getImage?fileId=62d62a86da740500130121dc)
![](/api/file/getImage?fileId=62d62b72da740500130121df)
![](/api/file/getImage?fileId=62d62b11da740500130121de)
![](/api/file/getImage?fileId=62d62bf2da740500130121e1)
![](/api/file/getImage?fileId=62d62cb9da740500130121e2)
<br>
#### 配置 Maven
<br>
先配置maven环境,改为开发环境本地的maven,否则会出现maven依赖引不进来的问题。
![](/api/file/getImage?fileId=62d6468eda740500130121ec)
![](/api/file/getImage?fileId=62d6468eda740500130121eb)
![](/api/file/getImage?fileId=62d64706da740500130121ed)
<br>
#### 配置 Swagger 3.0
<br>
然后添加 `swagger 3.0` 相关依赖
在 `pom.xml` 中添加这段依赖,并刷新 `maven`
```xml
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
```
<br>
之后直接启动项目,就会看到这段报错
```java
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-07-19 13:59:01.772 ERROR 1236 --- [ 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.21.jar:5.3.21]
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.21.jar:5.3.21]
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.21.jar:5.3.21]
at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_251]
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.21.jar:5.3.21]
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.21.jar:5.3.21]
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.21.jar:5.3.21]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.21.jar:5.3.21]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.1.jar:2.7.1]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734) [spring-boot-2.7.1.jar:2.7.1]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.1.jar:2.7.1]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) [spring-boot-2.7.1.jar:2.7.1]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) [spring-boot-2.7.1.jar:2.7.1]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) [spring-boot-2.7.1.jar:2.7.1]
at com.example.demo.DemoApplication.main(DemoApplication.java:10) [classes/:na]
...
```
<br>
参考网上文章
https://blog.csdn.net/Java__EE/article/details/124808044
<br>
得知 `Springboot 2.6.0` 之后 `SpringMVC` 默认路径匹配策略从 `AntPathMatcher` 更改为 `PathPatternParser` ,导致出错,解决办法是切换为原先的 `AntPathMatcher` ,或者降低 `Springboot` 版本到 `2.6.0` 以下。
<br>
我这里直接选择最简单的改配置文件方法解决
配置文件中添加配置解决
```yml
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
```
<br>
删除默认的 `application.properties`
<br>
新建3个文件分别是
`application.yml`
```yml
# 通用配置
spring:
profiles:
active: dev
jackson:
time-zone: GMT+8
serialization:
write-dates-as-timestamps: true
mvc:
pathmatch:
matching-strategy: ant_path_matcher
```
<br>
`application-dev.yml`
```yml
# 开发环境
server:
port: 8080
servlet:
context-path: /demo
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
```
<br>
`application-prod.yml`
```yml
# 生产环境
server:
port: 8000
servlet:
context-path: /demo
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
```
再次启动项目就不报错了 (如果还报刚才的错可以刷新下项目 刷新下 `maven` 等)
<br>
这时候访问 http://localhost:8080/demo/swagger-ui/ 可以看到 `swagger 3.0` 的默认画面
![](/api/file/getImage?fileId=62d64ad5da740500130121ee)
<br>
接下来写一个最简单的接口模拟开发场景
![](/api/file/getImage?fileId=62d6639fda74050013012205)
<br>
`LoginController.java`
```java
package com.example.demo.controller;
import com.example.demo.form.LoginForm;
import com.fasterxml.jackson.databind.util.JSONPObject;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/login")
@Api(tags = "登录接口")
public class LoginController {
private Logger log = LoggerFactory.getLogger(getClass());
@PostMapping("login")
@ApiOperation("登录测试")
public Map login(@RequestBody LoginForm form) {
// 模拟传入数据
System.out.println(form.toString());
// 模拟返回数据
return new HashMap() {{
put("code", 200);
put("message", "success");
}};
}
}
```
<br>
`LoginForm.java`
```java
package com.example.demo.form;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(value = "登录表单")
public class LoginForm {
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "手机号")
private Integer mobile;
@ApiModelProperty(value = "验证码")
private Integer verifyCode;
}
```
<br>
使用swagger调用效果如图
![](/api/file/getImage?fileId=62d66338da74050013012204)
<br>
再加个简单的配置文件
```java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@EnableOpenApi
@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30)
.enable(true)
.apiInfo(new ApiInfoBuilder()
.title("DEMO测试系统")
.description("DEMO测试系统 API接口文档")
.version("1.0.0")
.contact(new Contact("admin", "httsp://www.xxx.com", "admin@xxx.com"))
.build())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}
}
```
<br>
对应页面效果图
![](/api/file/getImage?fileId=62d66653da74050013012213)
<br>
#### 配置 knife4j
<br>
这个可以看做 `swagger 3.0` web界面简单美化
<br>
添加 `maven` 依赖
`pom.xml`
```xml
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
```
<br>
刷新 `maven` 以后直接重启项目即可
访问地址改为
http://localhost:8080/demo/doc.html
<br>
基础效果如下图
<br>
![](/api/file/getImage?fileId=62d6681cda74050013012214)
<br>
![](/api/file/getImage?fileId=62d66835da74050013012215)
<br>
#### 配置 `Mybatis Plus` 相关
<br>
官网
https://baomidou.com/
<br>
maven相关
`pom.xml`
```xml
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
```
<br>
idea插件 MybatisX (用于对着数据库表逆向生成代码)
![](/api/file/getImage?fileId=62d670cfda74050013012227)
<br>
配置数据库连接
`application-dev.yml`
```yml
# 开发环境
server:
port: 8080
servlet:
context-path: /demo
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&allowPublicKeyRetrieval=true
username: root
password:
mybatis-plus:
mapper-locations: classpath*:/mapper/*.xml
type-aliases-package: com.example.demo.service
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
banner: false
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
call-setters-on-nulls: true
```
<br>
随便开个库创建个简单的表
![](/api/file/getImage?fileId=62d67580da7405001301222d)
<br>
在 idea 中配置数据库连接
<br>
![](/api/file/getImage?fileId=62d673b0da7405001301222a)
<br>
![](/api/file/getImage?fileId=62d67400da7405001301222b)
<br>
![](/api/file/getImage?fileId=62d67471da7405001301222c)
<br>
![](/api/file/getImage?fileId=62d6773fda7405001301222f)
<br>
![](/api/file/getImage?fileId=62d6775eda74050013012230)
<br>
![](/api/file/getImage?fileId=62d6778cda74050013012231)
<br>
到这里已经生成完毕了
简单贴几个生成出来的文件代码
<br>
`DbUser`
```java
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/**
* 用户表
* @TableName db_user
*/
@TableName(value ="db_user")
@Data
public class DbUser implements Serializable {
/**
* 自增ID
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 手机号
*/
private Integer mobile;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 软删除 0 正常 1 删除
*/
private Integer delFlag;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
```
`DbUserMapper.xml`
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.DbUserMapper">
<resultMap id="BaseResultMap" type="com.example.demo.entity.DbUser">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="mobile" column="mobile" jdbcType="INTEGER"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="delFlag" column="del_flag" jdbcType="TINYINT"/>
</resultMap>
<sql id="Base_Column_List">
id,username,password,
mobile,create_time,update_time,
del_flag
</sql>
</mapper>
```
<br>
之后需要在程序中指定 `service` 和 `mapper` 的位置才可以启动项目
`service` 在刚才的配置文件 `application-dev.yml` 中的 `type-aliases-package` 一栏中配置例如 `com.example.demo.service`
![](/api/file/getImage?fileId=62d67b36da74050013012233)
<br>
`mapper` 在启动类 `DemoApplication` 上面加一个注解 `@MapperScan(basePackages = "")`
```java
@SpringBootApplication
@MapperScan(basePackages = "com.example.demo.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
```
<br><br>
随便改造下刚才的示例
`LoginController`
```java
@RestController
@RequestMapping("/login")
@Api(tags = "登录接口")
public class LoginController {
private Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private DbUserService userService;
@PostMapping("login")
@ApiOperation(value = "登录测试")
public Result login(@RequestBody LoginForm form) {
DbUser user = new DbUser();
// 复制同名参数 从form到entity
BeanUtils.copyProperties(form, user);
// 保存到数据库 (MybatisPlus自带方法)
boolean save = userService.save(user);
if(!save){
throw new RuntimeException("系统错误: 保存到数据库发生异常!");
}
// 模拟返回数据
return Result.success(user);
}
}
```
<br><br>
swagger在线调试效果
![](/api/file/getImage?fileId=62d67c55da74050013012234)
<br>
mysql中数据查看
![](/api/file/getImage?fileId=62d67c55da74050013012235)
<br>
(这里我发现mobile用int类型会超出范围,后全部改成string/varchar(20),与前文代码略有出入)
## END
基本就是这样,可以快速实现增删改查,前端联调,只要建表生成,写接口逻辑即可
末尾再附上一些常用工具类
<br><br>
**JSON工具类**
`JsonUtils.java`
```java
package com.skmagic.common.utils;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* JSON 工具类
* 基于Springboot自带的 Jackson
*
* @author zzzmh
* @version 1.0.0
* @email admin@zzzmh.cn
* @date 2020/4/21 16:23
*/
public class JsonUtils {
private static Logger logger = LoggerFactory.getLogger(JsonUtils.class);
public static ObjectMapper mapper = new ObjectMapper();
static {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.registerModule(new JavaTimeModule());
}
public static String toJSONString(Object object) {
String result = "";
try {
result = mapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return result;
}
public static <T> T toObject(String json, Class<T> clazz) {
T result = null;
try {
result = mapper.readValue(json, clazz);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return result;
}
public static <T> List toArray(String json, Class<T> clazz) {
try {
return (List) mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (JsonParseException e) {
logger.error("decode(String, JsonTypeReference<T>)", e);
e.printStackTrace();
} catch (JsonMappingException e) {
logger.error("decode(String, JsonTypeReference<T>)", e);
e.printStackTrace();
} catch (IOException e) {
logger.error("decode(String, JsonTypeReference<T>)", e);
e.printStackTrace();
}
return null;
}
public static Map<String, Object> toObject(String json) {
return toObject(json, Map.class);
}
public static List<Map<String, Object>> toArray(String json) {
return toArray(json, Map.class);
}
public static void main(String[] args) {
String test = "[{\"key\":1,\"value\":1},{\"key\":1,\"value\":2},{\"key\":2,\"value\":1},{\"key\":2,\"value\":2}]";
List<Map<String, Object>> array = toArray(test);
System.out.println(array);
}
}
```
<br><br>
**Redis相关**
`pom.xml`
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
<br>
`RedisConfig.java`
```java
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericToStringSerializer(Object.class));
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
```
<br>
`RedisUtils.java`
```java
@Component
public class RedisUtils {
@Autowired
private RedisTemplate redisTemplate;
@Resource(name = "redisTemplate")
private ValueOperations<String, String> valueOperations;
@Resource(name = "redisTemplate")
private HashOperations<String, String, Object> hashOperations;
@Resource(name = "redisTemplate")
private ListOperations<String, Object> listOperations;
@Resource(name = "redisTemplate")
private SetOperations<String, Object> setOperations;
@Resource(name = "redisTemplate")
private ZSetOperations<String, Object> zSetOperations;
/**
* 默认过期时长,单位:秒
*/
public final static long DEFAULT_EXPIRE = 60 * 60 * 12;
/**
* 不设置过期时长
*/
public final static long NOT_EXPIRE = -1;
/**
* 计数器
*/
public Long increment(String key, long expire) {
Long increment = valueOperations.increment(key);
// 单位时间内首次计数才设置过期时间
if (increment == 1) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return increment;
}
/**
* redis计数器 默认每次加一
*/
public Long increment(String key) {
return valueOperations.increment(key, 1);
}
public Long getIncrement(final String key) {
return (Long) redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
byte[] rowkey = serializer.serialize(key);
byte[] rowval = connection.get(rowkey);
try {
String val = serializer.deserialize(rowval);
return Long.parseLong(val);
} catch (Exception e) {
return 0L;
}
}
});
}
public void set(String key, Object value, long expire) {
valueOperations.set(key, toJson(value));
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
}
public void set(String key, Object value) {
set(key, value, DEFAULT_EXPIRE);
}
public <T> T get(String key, Class<T> clazz, long expire) {
String value = valueOperations.get(key);
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return value == null ? null : fromJson(value, clazz);
}
public <T> T get(String key, Class<T> clazz) {
return get(key, clazz, NOT_EXPIRE);
}
public String get(String key, long expire) {
String value = valueOperations.get(key);
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return value;
}
public String get(String key) {
return get(key, NOT_EXPIRE);
}
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* Object转成JSON数据
*/
private String toJson(Object object) {
if (object instanceof Integer || object instanceof Long || object instanceof Float ||
object instanceof Double || object instanceof Boolean || object instanceof String) {
return String.valueOf(object);
}
return JsonUtils.toJSONString(object);
}
/**
* JSON数据,转成Object
*/
private <T> T fromJson(String json, Class<T> clazz) {
return JsonUtils.toObject(json, clazz);
}
public Map<String, String> getAllKV() {
Map<String, String> result = new HashMap<>();
Set<String> set = redisTemplate.keys("*");
for (String k : set) {
result.put(k, get(k));
}
return result;
}
}
```