前言


本身应该是一个很简单的事情,新建项目,引入依赖,启动,结束。理论上不值得写笔记。
BUT!!!

最近发现,才大半年没新建项目,变化太快了,尤其是springboot 2.7.1,和一些框架依赖会有冲突,感觉需要重新学一学,
顺便把从零配置的过程全部详细记录一下,防止以后忘记。


开发环境如下
- Windows 11
- Intellij Idea 2020.3
- Maven 3.6.3
- Oracle Java 1.8.0_251
- Tomcat 8.5.56



折腾


新建项目


开头部分都是基础中的基础了,而且新版本和老版本也没什么区别,就用截图快速介绍下,不需要的可以跳到下一个部分



配置 Maven


先配置maven环境,改为开发环境本地的maven,否则会出现maven依赖引不进来的问题。


配置 Swagger 3.0


然后添加 swagger 3.0 相关依赖
pom.xml 中添加这段依赖,并刷新 maven

  1. <dependency>
  2. <groupId>io.springfox</groupId>
  3. <artifactId>springfox-boot-starter</artifactId>
  4. <version>3.0.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>io.springfox</groupId>
  8. <artifactId>springfox-swagger-ui</artifactId>
  9. <version>3.0.0</version>
  10. </dependency>


之后直接启动项目,就会看到这段报错

  1. Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
  2. 2022-07-19 13:59:01.772 ERROR 1236 --- [ main] o.s.boot.SpringApplication : Application run failed
  3. org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
  4. at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.21.jar:5.3.21]
  5. at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.21.jar:5.3.21]
  6. at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.21.jar:5.3.21]
  7. at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_251]
  8. at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.21.jar:5.3.21]
  9. at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.21.jar:5.3.21]
  10. at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.21.jar:5.3.21]
  11. at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.21.jar:5.3.21]
  12. at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.1.jar:2.7.1]
  13. at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734) [spring-boot-2.7.1.jar:2.7.1]
  14. at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.1.jar:2.7.1]
  15. at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) [spring-boot-2.7.1.jar:2.7.1]
  16. at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) [spring-boot-2.7.1.jar:2.7.1]
  17. at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) [spring-boot-2.7.1.jar:2.7.1]
  18. at com.example.demo.DemoApplication.main(DemoApplication.java:10) [classes/:na]
  19. ...


参考网上文章
https://blog.csdn.net/Java__EE/article/details/124808044


得知 Springboot 2.6.0 之后 SpringMVC 默认路径匹配策略从 AntPathMatcher 更改为 PathPatternParser ,导致出错,解决办法是切换为原先的 AntPathMatcher ,或者降低 Springboot 版本到 2.6.0 以下。


我这里直接选择最简单的改配置文件方法解决
配置文件中添加配置解决

  1. spring:
  2. mvc:
  3. pathmatch:
  4. matching-strategy: ant_path_matcher


删除默认的 application.properties


新建3个文件分别是
application.yml

  1. # 通用配置
  2. spring:
  3. profiles:
  4. active: dev
  5. jackson:
  6. time-zone: GMT+8
  7. serialization:
  8. write-dates-as-timestamps: true
  9. mvc:
  10. pathmatch:
  11. matching-strategy: ant_path_matcher


application-dev.yml

  1. # 开发环境
  2. server:
  3. port: 8080
  4. servlet:
  5. context-path: /demo
  6. spring:
  7. servlet:
  8. multipart:
  9. max-file-size: 100MB
  10. max-request-size: 100MB


application-prod.yml

  1. # 生产环境
  2. server:
  3. port: 8000
  4. servlet:
  5. context-path: /demo
  6. spring:
  7. servlet:
  8. multipart:
  9. max-file-size: 20MB
  10. max-request-size: 20MB

再次启动项目就不报错了 (如果还报刚才的错可以刷新下项目 刷新下 maven 等)


这时候访问 http://localhost:8080/demo/swagger-ui/ 可以看到 swagger 3.0 的默认画面


接下来写一个最简单的接口模拟开发场景


LoginController.java

  1. package com.example.demo.controller;
  2. import com.example.demo.form.LoginForm;
  3. import com.fasterxml.jackson.databind.util.JSONPObject;
  4. import io.swagger.annotations.Api;
  5. import io.swagger.annotations.ApiOperation;
  6. import org.slf4j.Logger;
  7. import org.slf4j.LoggerFactory;
  8. import org.springframework.web.bind.annotation.PostMapping;
  9. import org.springframework.web.bind.annotation.RequestBody;
  10. import org.springframework.web.bind.annotation.RequestMapping;
  11. import org.springframework.web.bind.annotation.RestController;
  12. import java.util.HashMap;
  13. import java.util.Map;
  14. @RestController
  15. @RequestMapping("/login")
  16. @Api(tags = "登录接口")
  17. public class LoginController {
  18. private Logger log = LoggerFactory.getLogger(getClass());
  19. @PostMapping("login")
  20. @ApiOperation("登录测试")
  21. public Map login(@RequestBody LoginForm form) {
  22. // 模拟传入数据
  23. System.out.println(form.toString());
  24. // 模拟返回数据
  25. return new HashMap() {{
  26. put("code", 200);
  27. put("message", "success");
  28. }};
  29. }
  30. }


LoginForm.java

  1. package com.example.demo.form;
  2. import io.swagger.annotations.ApiModel;
  3. import io.swagger.annotations.ApiModelProperty;
  4. import lombok.Data;
  5. @Data
  6. @ApiModel(value = "登录表单")
  7. public class LoginForm {
  8. @ApiModelProperty(value = "用户名")
  9. private String username;
  10. @ApiModelProperty(value = "密码")
  11. private String password;
  12. @ApiModelProperty(value = "手机号")
  13. private Integer mobile;
  14. @ApiModelProperty(value = "验证码")
  15. private Integer verifyCode;
  16. }


使用swagger调用效果如图


再加个简单的配置文件

  1. package com.example.demo.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.bind.annotation.RestController;
  5. import springfox.documentation.builders.ApiInfoBuilder;
  6. import springfox.documentation.builders.PathSelectors;
  7. import springfox.documentation.builders.RequestHandlerSelectors;
  8. import springfox.documentation.oas.annotations.EnableOpenApi;
  9. import springfox.documentation.service.Contact;
  10. import springfox.documentation.spi.DocumentationType;
  11. import springfox.documentation.spring.web.plugins.Docket;
  12. @EnableOpenApi
  13. @Configuration
  14. public class SwaggerConfig {
  15. @Bean
  16. public Docket createRestApi() {
  17. return new Docket(DocumentationType.OAS_30)
  18. .enable(true)
  19. .apiInfo(new ApiInfoBuilder()
  20. .title("DEMO测试系统")
  21. .description("DEMO测试系统 API接口文档")
  22. .version("1.0.0")
  23. .contact(new Contact("admin", "httsp://www.xxx.com", "admin@xxx.com"))
  24. .build())
  25. .select()
  26. .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
  27. .paths(PathSelectors.any())
  28. .build();
  29. }
  30. }


对应页面效果图


配置 knife4j


这个可以看做 swagger 3.0 web界面简单美化


添加 maven 依赖
pom.xml

  1. <dependency>
  2. <groupId>com.github.xiaoymin</groupId>
  3. <artifactId>knife4j-spring-boot-starter</artifactId>
  4. <version>3.0.3</version>
  5. </dependency>


刷新 maven 以后直接重启项目即可
访问地址改为
http://localhost:8080/demo/doc.html


基础效果如下图




配置 Mybatis Plus 相关


官网
https://baomidou.com/


maven相关
pom.xml

  1. <dependency>
  2. <groupId>mysql</groupId>
  3. <artifactId>mysql-connector-java</artifactId>
  4. <version>8.0.29</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.baomidou</groupId>
  8. <artifactId>mybatis-plus-boot-starter</artifactId>
  9. <version>3.5.2</version>
  10. </dependency>


idea插件 MybatisX (用于对着数据库表逆向生成代码)


配置数据库连接
application-dev.yml

  1. # 开发环境
  2. server:
  3. port: 8080
  4. servlet:
  5. context-path: /demo
  6. spring:
  7. servlet:
  8. multipart:
  9. max-file-size: 100MB
  10. max-request-size: 100MB
  11. datasource:
  12. driver-class-name: com.mysql.cj.jdbc.Driver
  13. url: jdbc:mysql://localhost:3306/demo?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&allowPublicKeyRetrieval=true
  14. username: root
  15. password:
  16. mybatis-plus:
  17. mapper-locations: classpath*:/mapper/*.xml
  18. type-aliases-package: com.example.demo.service
  19. global-config:
  20. db-config:
  21. id-type: auto
  22. logic-delete-value: 1
  23. logic-not-delete-value: 0
  24. banner: false
  25. configuration:
  26. map-underscore-to-camel-case: true
  27. cache-enabled: true
  28. call-setters-on-nulls: true


随便开个库创建个简单的表


在 idea 中配置数据库连接













到这里已经生成完毕了
简单贴几个生成出来的文件代码


DbUser

  1. package com.example.demo.entity;
  2. import com.baomidou.mybatisplus.annotation.IdType;
  3. import com.baomidou.mybatisplus.annotation.TableField;
  4. import com.baomidou.mybatisplus.annotation.TableId;
  5. import com.baomidou.mybatisplus.annotation.TableName;
  6. import java.io.Serializable;
  7. import java.util.Date;
  8. import lombok.Data;
  9. /**
  10. * 用户表
  11. * @TableName db_user
  12. */
  13. @TableName(value ="db_user")
  14. @Data
  15. public class DbUser implements Serializable {
  16. /**
  17. * 自增ID
  18. */
  19. @TableId(type = IdType.AUTO)
  20. private Integer id;
  21. /**
  22. * 用户名
  23. */
  24. private String username;
  25. /**
  26. * 密码
  27. */
  28. private String password;
  29. /**
  30. * 手机号
  31. */
  32. private Integer mobile;
  33. /**
  34. * 创建时间
  35. */
  36. private Date createTime;
  37. /**
  38. * 更新时间
  39. */
  40. private Date updateTime;
  41. /**
  42. * 软删除 0 正常 1 删除
  43. */
  44. private Integer delFlag;
  45. @TableField(exist = false)
  46. private static final long serialVersionUID = 1L;
  47. }

DbUserMapper.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper
  3. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5. <mapper namespace="com.example.demo.mapper.DbUserMapper">
  6. <resultMap id="BaseResultMap" type="com.example.demo.entity.DbUser">
  7. <id property="id" column="id" jdbcType="INTEGER"/>
  8. <result property="username" column="username" jdbcType="VARCHAR"/>
  9. <result property="password" column="password" jdbcType="VARCHAR"/>
  10. <result property="mobile" column="mobile" jdbcType="INTEGER"/>
  11. <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
  12. <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
  13. <result property="delFlag" column="del_flag" jdbcType="TINYINT"/>
  14. </resultMap>
  15. <sql id="Base_Column_List">
  16. id,username,password,
  17. mobile,create_time,update_time,
  18. del_flag
  19. </sql>
  20. </mapper>


之后需要在程序中指定 servicemapper 的位置才可以启动项目
service 在刚才的配置文件 application-dev.yml 中的 type-aliases-package 一栏中配置例如 com.example.demo.service


mapper 在启动类 DemoApplication 上面加一个注解 @MapperScan(basePackages = "")

  1. @SpringBootApplication
  2. @MapperScan(basePackages = "com.example.demo.mapper")
  3. public class DemoApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(DemoApplication.class, args);
  6. }
  7. }



随便改造下刚才的示例
LoginController

  1. @RestController
  2. @RequestMapping("/login")
  3. @Api(tags = "登录接口")
  4. public class LoginController {
  5. private Logger log = LoggerFactory.getLogger(getClass());
  6. @Autowired
  7. private DbUserService userService;
  8. @PostMapping("login")
  9. @ApiOperation(value = "登录测试")
  10. public Result login(@RequestBody LoginForm form) {
  11. DbUser user = new DbUser();
  12. // 复制同名参数 从form到entity
  13. BeanUtils.copyProperties(form, user);
  14. // 保存到数据库 (MybatisPlus自带方法)
  15. boolean save = userService.save(user);
  16. if(!save){
  17. throw new RuntimeException("系统错误: 保存到数据库发生异常!");
  18. }
  19. // 模拟返回数据
  20. return Result.success(user);
  21. }
  22. }



swagger在线调试效果


mysql中数据查看


(这里我发现mobile用int类型会超出范围,后全部改成string/varchar(20),与前文代码略有出入)

END

基本就是这样,可以快速实现增删改查,前端联调,只要建表生成,写接口逻辑即可

末尾再附上一些常用工具类



JSON工具类
JsonUtils.java

  1. package com.skmagic.common.utils;
  2. import com.fasterxml.jackson.core.JsonParseException;
  3. import com.fasterxml.jackson.core.JsonProcessingException;
  4. import com.fasterxml.jackson.databind.DeserializationFeature;
  5. import com.fasterxml.jackson.databind.JsonMappingException;
  6. import com.fasterxml.jackson.databind.ObjectMapper;
  7. import com.fasterxml.jackson.databind.SerializationFeature;
  8. import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
  9. import org.slf4j.Logger;
  10. import org.slf4j.LoggerFactory;
  11. import java.io.IOException;
  12. import java.util.List;
  13. import java.util.Map;
  14. /**
  15. * JSON 工具类
  16. * 基于Springboot自带的 Jackson
  17. *
  18. * @author zzzmh
  19. * @version 1.0.0
  20. * @email admin@zzzmh.cn
  21. * @date 2020/4/21 16:23
  22. */
  23. public class JsonUtils {
  24. private static Logger logger = LoggerFactory.getLogger(JsonUtils.class);
  25. public static ObjectMapper mapper = new ObjectMapper();
  26. static {
  27. mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  28. mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
  29. mapper.registerModule(new JavaTimeModule());
  30. }
  31. public static String toJSONString(Object object) {
  32. String result = "";
  33. try {
  34. result = mapper.writeValueAsString(object);
  35. } catch (JsonProcessingException e) {
  36. e.printStackTrace();
  37. }
  38. return result;
  39. }
  40. public static <T> T toObject(String json, Class<T> clazz) {
  41. T result = null;
  42. try {
  43. result = mapper.readValue(json, clazz);
  44. } catch (JsonProcessingException e) {
  45. e.printStackTrace();
  46. }
  47. return result;
  48. }
  49. public static <T> List toArray(String json, Class<T> clazz) {
  50. try {
  51. return (List) mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, clazz));
  52. } catch (JsonParseException e) {
  53. logger.error("decode(String, JsonTypeReference<T>)", e);
  54. e.printStackTrace();
  55. } catch (JsonMappingException e) {
  56. logger.error("decode(String, JsonTypeReference<T>)", e);
  57. e.printStackTrace();
  58. } catch (IOException e) {
  59. logger.error("decode(String, JsonTypeReference<T>)", e);
  60. e.printStackTrace();
  61. }
  62. return null;
  63. }
  64. public static Map<String, Object> toObject(String json) {
  65. return toObject(json, Map.class);
  66. }
  67. public static List<Map<String, Object>> toArray(String json) {
  68. return toArray(json, Map.class);
  69. }
  70. public static void main(String[] args) {
  71. String test = "[{\"key\":1,\"value\":1},{\"key\":1,\"value\":2},{\"key\":2,\"value\":1},{\"key\":2,\"value\":2}]";
  72. List<Map<String, Object>> array = toArray(test);
  73. System.out.println(array);
  74. }
  75. }



Redis相关
pom.xml

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>


RedisConfig.java

  1. @Configuration
  2. public class RedisConfig {
  3. @Autowired
  4. private RedisConnectionFactory factory;
  5. @Bean
  6. public RedisTemplate<String, Object> redisTemplate() {
  7. RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
  8. redisTemplate.setKeySerializer(new StringRedisSerializer());
  9. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
  10. redisTemplate.setHashValueSerializer(new StringRedisSerializer());
  11. redisTemplate.setValueSerializer(new GenericToStringSerializer(Object.class));
  12. redisTemplate.setConnectionFactory(factory);
  13. return redisTemplate;
  14. }
  15. @Bean
  16. public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
  17. return redisTemplate.opsForHash();
  18. }
  19. @Bean
  20. public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
  21. return redisTemplate.opsForValue();
  22. }
  23. @Bean
  24. public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
  25. return redisTemplate.opsForList();
  26. }
  27. @Bean
  28. public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
  29. return redisTemplate.opsForSet();
  30. }
  31. @Bean
  32. public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
  33. return redisTemplate.opsForZSet();
  34. }
  35. }


RedisUtils.java

  1. @Component
  2. public class RedisUtils {
  3. @Autowired
  4. private RedisTemplate redisTemplate;
  5. @Resource(name = "redisTemplate")
  6. private ValueOperations<String, String> valueOperations;
  7. @Resource(name = "redisTemplate")
  8. private HashOperations<String, String, Object> hashOperations;
  9. @Resource(name = "redisTemplate")
  10. private ListOperations<String, Object> listOperations;
  11. @Resource(name = "redisTemplate")
  12. private SetOperations<String, Object> setOperations;
  13. @Resource(name = "redisTemplate")
  14. private ZSetOperations<String, Object> zSetOperations;
  15. /**
  16. * 默认过期时长,单位:秒
  17. */
  18. public final static long DEFAULT_EXPIRE = 60 * 60 * 12;
  19. /**
  20. * 不设置过期时长
  21. */
  22. public final static long NOT_EXPIRE = -1;
  23. /**
  24. * 计数器
  25. */
  26. public Long increment(String key, long expire) {
  27. Long increment = valueOperations.increment(key);
  28. // 单位时间内首次计数才设置过期时间
  29. if (increment == 1) {
  30. redisTemplate.expire(key, expire, TimeUnit.SECONDS);
  31. }
  32. return increment;
  33. }
  34. /**
  35. * redis计数器 默认每次加一
  36. */
  37. public Long increment(String key) {
  38. return valueOperations.increment(key, 1);
  39. }
  40. public Long getIncrement(final String key) {
  41. return (Long) redisTemplate.execute(new RedisCallback<Long>() {
  42. @Override
  43. public Long doInRedis(RedisConnection connection) throws DataAccessException {
  44. RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
  45. byte[] rowkey = serializer.serialize(key);
  46. byte[] rowval = connection.get(rowkey);
  47. try {
  48. String val = serializer.deserialize(rowval);
  49. return Long.parseLong(val);
  50. } catch (Exception e) {
  51. return 0L;
  52. }
  53. }
  54. });
  55. }
  56. public void set(String key, Object value, long expire) {
  57. valueOperations.set(key, toJson(value));
  58. if (expire != NOT_EXPIRE) {
  59. redisTemplate.expire(key, expire, TimeUnit.SECONDS);
  60. }
  61. }
  62. public void set(String key, Object value) {
  63. set(key, value, DEFAULT_EXPIRE);
  64. }
  65. public <T> T get(String key, Class<T> clazz, long expire) {
  66. String value = valueOperations.get(key);
  67. if (expire != NOT_EXPIRE) {
  68. redisTemplate.expire(key, expire, TimeUnit.SECONDS);
  69. }
  70. return value == null ? null : fromJson(value, clazz);
  71. }
  72. public <T> T get(String key, Class<T> clazz) {
  73. return get(key, clazz, NOT_EXPIRE);
  74. }
  75. public String get(String key, long expire) {
  76. String value = valueOperations.get(key);
  77. if (expire != NOT_EXPIRE) {
  78. redisTemplate.expire(key, expire, TimeUnit.SECONDS);
  79. }
  80. return value;
  81. }
  82. public String get(String key) {
  83. return get(key, NOT_EXPIRE);
  84. }
  85. public void delete(String key) {
  86. redisTemplate.delete(key);
  87. }
  88. /**
  89. * Object转成JSON数据
  90. */
  91. private String toJson(Object object) {
  92. if (object instanceof Integer || object instanceof Long || object instanceof Float ||
  93. object instanceof Double || object instanceof Boolean || object instanceof String) {
  94. return String.valueOf(object);
  95. }
  96. return JsonUtils.toJSONString(object);
  97. }
  98. /**
  99. * JSON数据,转成Object
  100. */
  101. private <T> T fromJson(String json, Class<T> clazz) {
  102. return JsonUtils.toObject(json, clazz);
  103. }
  104. public Map<String, String> getAllKV() {
  105. Map<String, String> result = new HashMap<>();
  106. Set<String> set = redisTemplate.keys("*");
  107. for (String k : set) {
  108. result.put(k, get(k));
  109. }
  110. return result;
  111. }
  112. }