从零新建一个 Springboot 2.7.1 项目搭配 Swagger 3.0 Knife4j MyBatisPlus 等 home 编辑时间 2022/07/19 ![](/api/file/getImage?fileId=62d625f4da740500130121d9) <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; } } ``` 送人玫瑰,手留余香 赞赏 Wechat Pay Alipay 纯 JavaScript Canvas 实现图片放大预览、多张切换、动画效果等 Linux 与 Windows 双系统 时间慢8小时问题