凤凰涅槃:Spring Boot 开发之路上的坎坷与成长

本文最后更新于:1 个月前

生命的旅程就像一场冒险,每一步都充满了未知与惊喜。

破冰

写作目标

今天是开发壁纸网站 memory-icon 的第一天,本来没打算写什么开发文档的,太耗时间,整体实现思路也简单,就不写了

但是有必要记录一下:从零开发一个SpringBoot项目,至少应该要做些什么

这就算给定制自己的 SpringBoot 模板开个好头了(2023/08/01午)

开发思路

从零开发一个SpringBoot要做什么?一定是做到以下几点:(2023/08/01午)
  • 新建模块

  • 导入必要的依赖:MySQL、Mybatis、MybatisPlus、commons-lang3、lombok、Gson等

  • 新增必要yaml配置:项目名、启动端口、上下文路径、数据库地址

  • 新增banner.txt

  • 新增数据库表,连接数据库,保存建表语句

  • 新增controller层、service层

  • 利用MybatisX-Generator插件,快速生成domain、mapper、service、XXXMaper.xml

  • 新增config层:CorsConfig、MybatisPlusConfig、Knife4jConfig、JsonConfig等

思维碰撞

着手开发

快速编写Controller层

  • 什么是Controller层?(2023/08/14早)
1
2
3
4
5
6
7
在Web开发中,Controller层是MVC(模型-视图-控制器)架构的一部分,用于接收并处理前端请求。
具体来说,Controller层负责以下几个任务:
1.接收HTTP请求:Controller通过定义特定的路由(URL映射)来接收前端发送的HTTP请求。
2.处理请求:Controller中的方法将根据请求的类型、路径以及请求参数等信息,执行相应的逻辑操作。
3.调用服务层:在Controller中,可以调用服务层(Service层、业务层)的方法来实现相关业务逻辑。
4.返回响应:在完成对请求的处理后,Controller负责将数据或者视图响应返回给前端。
Controller层的目标是实现请求-响应的流程控制,将请求按照业务逻辑分配给对应的服务层处理,并将执行结果返回给前端。同时,Controller层还可以处理一些与业务逻辑相关的验证或者数据转换等操作。
  • 引入依赖:
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.14</version>
</dependency>
  • 这里贴个小技巧,alt + insert 快速添加依赖模板:

image-20230814130719252

  • 新增controller层,编写响应请求的逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author 邓哈哈
* 2023/8/14 12:06
* Function:
* Version 1.0
*/

@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/name")
public String getName(String name) {
return String.format("我的名字是: %s", name);
}
}
  • 编辑 application.yaml 配置文件:
1
2
3
4
server:
port: 8088
servlet:
context-path: /api
  • 发送请求,测试接口
  • 这一步的操作方法有很多:

    • Postman工具
    • Swagger + Knif4j 接口文档
    • 浏览器
    • IDEA模拟浏览器
  • 有关Postman工具的使用,这里不再详述
  • 有关快速生成 Swagger + Knif4j 接口文档的教程,可以在下面👇的《快速生成 Swagger + Knif4j接口文档》栏目中了解到
  • 有关IDEA如何模拟浏览器,发送HTTP请求,可以在《掌握-JetBrains-IntelliJ-IDEA:使用心得与技巧》一文中的《模拟浏览器发送请求》栏目中找到答案
  • 有关SpringMVC处理HTTP请求参数的方式,可以在《SpringBoot配置》一文中的 《SpringMVC 请求参数的处理》栏目中找到答案
  • 浏览器发送请求,结果如下图所示:(2023/08/14午)

image-20230814132007439

  • IDEA模拟浏览器发送请求,结果如下图所示:
1
GET http://localhost:8088/api/test/name?name=邓啊呀

image-20230814132051083

全局异常处理

MybatisPlus

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
  • 配置:
1
2
3
4
5
6
7
8
9
10
11
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志
global-config:
db-config:
id-type: ASSIGN_ID # 主键自增长策略
logic-delete-field: isDelete # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
  • 以上是最常用的配置,其中,按照驼峰命名法映射是这样处理的:
    • 建库建表后,如果表字段是下划线命名的,而实体类是驼峰命名,如下:

    • image-20230821115235186

    • 当我们操作数据库时,MybatisPlus会将驼峰命名的实体类属性,映射为下划线命名的表字段了:
    • ```java
      boolean updateById = setMealService.updateById(setmeal);

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      - ##### 开启SQL日志不用多说,方便代码调试和分析报错信息

      - ##### 主键自增长策略,也可以在主键字段上添加注解:

      ```java
      /**
      * 主键
      */
      @TableId(value = "id", type = IdType.AUTO)
      private Long id;
  • 逻辑删除,被删除的数据记录不会直接从表中删除,而是在指定字段上标注为已删除,需要在字段上添加以下注解:

1
2
3
4
5
6
/**
* 是否删除(0-未删, 1-已删)
*/
@TableField(value = "isDelete")
@TableLogic
private Integer isDelete;
  • 最后在WebConfig下配置分页插件:(2023/08/22早)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Web配置类
*/
@Configuration
@Slf4j
public class WebConfig implements WebMvcConfigurer {
/**
* 分页插件(官网最新)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}

}

对象映射器(消息转换器)

  • 编写JacksonObjectMapper:(2023/08/22早)
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
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {

public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
  • 在WebConfig下作如下配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Web配置类
*/
@Configuration
@Slf4j
public class WebConfig implements WebMvcConfigurer {
/**
* 扩展消息转换器
*
* @param converters converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置具体的对象映射器
messageConverter.setObjectMapper(new JacksonObjectMapper());
//通过索引设置,让自己的转换器放在最前面,否则默认的jackson转换器会在最前面,用不上我们设置的转换器
converters.add(0, messageConverter);
}
}

全局跨域处理

  • 在WebConfig下作如下配置:(2023/08/22早)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Web配置类
*/
@Configuration
@Slf4j
public class WebConfig implements WebMvcConfigurer {
/**
* 允许跨域请求
*
* @param registry registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedOrigins("http://localhost:7070", "http://localhost:3000","http://120.55.62.195:7071")
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}

  • 只要是前端浏览器页面,调用后端接口,都应该在前后端做好跨域处理,不然就会遇到这种鸟问题,都不知道怎么解决:(2023/09/09晚)

image-20230909174834655

Filter全局过滤器

  • 年后,我学习了第一个完整的SpringBoot项目:瑞吉外卖,下面给出编写过滤器的一段代码:(2023/08/22早)
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
/**
* @author 邓哈哈
* 2023/1/13 10:19
* Function:登录拦截
* Version 1.0
*/

/**
* 检查用户/员工是否完成登录
*/
@WebFilter(filterName = "LoginCheckFilter")
@Slf4j
public class LoginCheckFilter implements Filter {//验证是否登录
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1.获取本次请求的URI
String requestURI = request.getRequestURI();

//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login", //登录时
"/employee/logout", //登出时
"/backend/**",
"/front/**",
"/user/sendMsg",
"/user/login"
};

//2.判断本次请求是否需要处理
boolean check = check(urls, requestURI);

//3.如果不需要处理,则直接放行
if (check) {
log.info("本次请求不需要处理...");
filterChain.doFilter(request, response);//放行请求
return;
}

// 4.1.需要处理的员工的请求,则判断登录状态,如果已经登录,则直接放行
Long empId;
if ((empId = (Long) request.getSession().getAttribute("employee")) != null) {//Session中存储着员工id(登录成功)
log.info("该员工已登录,id为{}", empId);

BaseContext.setCurrentId(empId);//ThreadLocal
filterChain.doFilter(request, response);//放行请求
return;
}

//4.2.需要处理的用户的请求,则判断登录状态,如果已经登录,则直接放行
Long userId;
if ((userId = (Long) request.getSession().getAttribute("user")) != null) {//Session中存储着用户id(登录成功)
log.info("该用户已登录,id为{}", userId);

BaseContext.setCurrentId(userId);//ThreadLocal

filterChain.doFilter(request, response);//放行请求
return;
}

//5.如果未登录,则返回登录结果,通过输出流方式向客户端页面响应数据
log.info("该用户未登录...");

response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
}

/**
* 路径匹配,检查本次请求是否需要放行
*
* @param urls :已定义的不需要处理的请求
* @param requestURI :接收检查的请求
* @return
*/
public boolean check(String[] urls, String requestURI) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if (match) {//该请求不需要处理
return true;
}
}
return false;//该请求得处理
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 路径匹配,检查本次请求是否需要放行
*
* @param urls :已定义的不需要处理的请求
* @param requestURI :接收检查的请求
* @return
*/
public boolean check(String[] urls, String requestURI) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if (match) {//该请求不需要处理
return true;
}
}
return false;//该请求得处理
}
  • 这段代码的亮点包括以下几点:
    • 使用了@Log4j注解,简化了日志记录的操作。通过这个注解,可以直接使用log变量来记录日志。
    • 使用了AntPathMatcher来匹配请求路径,支持通配符。这样可以减少对每个请求进行完全匹配的操作,提高了效率。
    • 定义了不需要处理的请求路径,使用数组来存储。这样可以在数组中添加需要排除的路径,只对数组中未包含的路径进行处理。
    • 使用check方法来判断当前请求是否需要处理。check方法会遍历请求路径数组,通过AntPathMatcher进行匹配,如果匹配成功则返回true,否则返回false。
    • 如果当前请求需要处理,将继续调用FilterChain的doFilter方法,执行后续的过滤器或Servlet逻辑。如果不需要处理,则直接放行,不进行后续处理。
  • 这样的设计可以在过滤器中对请求进行过滤,在需要处理的请求中执行相应的逻辑,而不需要处理的请求可以直接放行,提高了性能和效率。(2023/08/22早)

ThreadLocal

  • 从上面的代码中可以看到这样的业务流程:
1
2
3
4
5
6
7
8
9
// 4.1.需要处理的员工的请求,则判断登录状态,如果已经登录,则直接放行
Long empId;
if ((empId = (Long) request.getSession().getAttribute("employee")) != null) {//Session中存储着员工id(登录成功)
log.info("该员工已登录,id为{}", empId);

BaseContext.setCurrentId(empId);//ThreadLocal
filterChain.doFilter(request, response);//放行请求
return;
}
  • 这是将已登录员工的id封装在ThreadLocal中,为每一个使用到这个变量的线程创建了一个属于自己的副本
  • ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal();

/**
* 设置当前线程的局部变量的值
*
* @param id
*/
public static void setCurrentId(Long id) {
threadLocal.set(id);
}

/**
* 返回当前线程对应的局部变量的值
*
* @return
*/
public static Long getCurrentId() {
return threadLocal.get();
}
}

公共字段自动填充

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
/**
* @author 邓哈哈
* 2023/1/16 11:12
* Function:
* Version 1.0
*/
@Configuration
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
*
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充...");

metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}

/**
* 更新操作,自动填充
*
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}

  • 这段代码是一个实现了MyBatis的MetaObjectHandler接口的类。它的主要作用是实现自动填充功能,用于在插入和更新操作时自动填充某些公共字段的值

  • 具体来说,这段代码通过重写insertFill和updateFill方法实现了自动填充功能

    • insertFill方法会在插入操作时被调用,自动填充一些字段的值。在这段代码中,它会设置createTime、updateTime、createUser和updateUser字段的值。其中,createTime和updateTime字段会被设置为当前时间,createUser和updateUser字段会被设置为当前用户的ID

    • updateFill方法会在更新操作时被调用,同样实现自动填充功能。在这段代码中,它会设置updateTime和updateUser字段的值,分别设置为当前时间和当前用户的ID

  • 通过使用这个自定义的MyMetaObjectHandler类,可以实现在插入和更新操作时自动填充公共字段的值,避免了手动在每次操作中设置这些字段的麻烦,提高了开发效率(2023/08/22早)

Lombok

commons-lang3

Gson

基于基础类,开发一个VO类

  • 在开发过程,我们经常中使用基础类来开发VO类,是因为:(2023/09/14晚)
1
2
基础类可以作为扩展点,用于定义VO类的共同特性和行为。
你可以在基础类中定义一些通用的属性和方法,然后在VO类中添加自定义属性和方法,以实现更具体的功能
  • 最近在开发 MemoryChat 项目的过程中,遇到了这样的业务问题:

  • 查询用户已加入的队伍 / 已创建的队伍,我想要展示队伍的 username(队长昵称),在team表中只有 userId字段(队长Id)

  • 这就需要多表联查,封装携带队伍信息以及队长username的对象,并返回给前端

  • 那我们就开发一个teamVO类,用username字段代替userId字段,作为封装返回的对象:
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
/**
* @author 邓哈哈
* 2023/9/12 23:28
* Function:
* Version 1.0
*/
@Data
public class TeamVO {
/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;

/**
* 队伍名称
*/
private String name;

/**
* 描述
*/
private String description;

/**
* 最大人数
*/
private Integer maxNum;

/**
* 队长昵称
*/
private String userName;

/**
* 队伍图片
*/
private String imgUrl;

/**
* 已加人数
*/
private Integer joinNum;

............................
}
  • 根据相关查询条件,正确执行业务逻辑获取到teamList后,转换为teamVOList
1
2
// 转换teamList为teamVOList
List<TeamVO> teamVOList = getTeamVOByTeam(teamList);
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
/**
* 转换teamList为teamVOList
*
* @param teamList teamList
* @return teamVOList
*/
public List<TeamVO> getTeamVOByTeam(List<Team> teamList) {
List<TeamVO> teamVOList = teamList.stream().map(team -> {
Long userId = team.getUserId();
String userName = userService.getById(userId).getUsername();
TeamVO teamVO = new TeamVO();

teamVO.setId(team.getId());
teamVO.setName(team.getName());
teamVO.setDescription(team.getDescription());
teamVO.setMaxNum(team.getMaxNum());
teamVO.setUserName(userName);
teamVO.setImgUrl(team.getImgUrl());
teamVO.setJoinNum(team.getJoinNum());
teamVO.setStatus(team.getStatus());
teamVO.setExpireTime(team.getExpireTime());
teamVO.setCreateTime(team.getCreateTime());
teamVO.setUpdateTime(team.getUpdateTime());
teamVO.setIsDelete(team.getIsDelete());

return teamVO;
}).collect(Collectors.toList());

return teamVOList;
}
  • 就这样,我们将队伍信息和队长 username 封装在了 teamVOList 对象中,成功返回到了前端
  • 现在,我们可以看到队伍的队长信息了:(2023/09/14晚)

image-20230914222548059

踩坑记录

  • 每当我兴致勃勃地启动项目,准备大展身手的时候,项目启动窗口总会弹出冰冷的报错信息
  • 这里列举了:我遇到过的所有启动项目报错的解决办法

XXXMapper包扫描不到

  • 当你看到这样的报错,你会怎么解决呢:
1
Unsatisfied dependency expressed through field 'baseMapper'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.memory.memoryiconbackend.mapper.WallpaperMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
  • 这个报错信息大致意思是,未扫描到你的XXXMapper包,项目启动失败
  • 这个问题可谓最常见了,刚刚我就又被这个问题恶心到了,网上查了半天,感觉他们都是一知半解
  • 那么我是怎么解决这个问题的呢?思路如下:

XXXMapper.xml配置错误

  • 检查resource下的XXXMapper.xml配置,检查实体类扫描和mapper扫描路径是否正确:

image-20230801180028233

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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.memory.memoryiconbackend.mapper.WallpaperMapper">

<resultMap id="BaseResultMap" type="com.memory.memoryiconbackend.model.Wallpaper">
<result property="id" column="id" jdbcType="VARCHAR"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="url" column="url" jdbcType="VARCHAR"/>
<result property="type" column="type" jdbcType="VARCHAR"/>
<result property="tags" column="tags" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="VARCHAR"/>
<result property="updateTime" column="update_time" jdbcType="VARCHAR"/>
<result property="isDelete" column="is_delete" jdbcType="VARCHAR"/>
<result property="userId" column="user_id" jdbcType="VARCHAR"/>
</resultMap>

<sql id="Base_Column_List">
id,name,url,
type,tags,create_time,
update_time,is_delete,user_id
</sql>
</mapper>
  • 确保XXXMapper包的扫描路径正确后,再继续排查:

XXXMapper上添加@Mapper注解

  • 检查XXXMapper上是否添加了@Mapper注解:

image-20230801180147709

1
2
3
4
@Mapper
public interface WallpaperMapper extends BaseMapper<Wallpaper> {

}
  • 如果这两部还没有解决你的问题,请一定继续往下看:

开启@MapperScan注解

  • @MapperScan注解是干嘛的呢?它是用来在项目启动后,扫描你的XXXMapper所在路径,用法如下:
1
2
3
4
5
6
7
8

@SpringBootApplication
@MapperScan("com.memory.memoryiconbackend.mapper.WallpaperMapper")
public class MemoryIconBackendApplication {
public static void main(String[] args) {
SpringApplication.run(MemoryIconBackendApplication.class, args);
}
}
  • 那这个注解跟上面提到的@Mapper注解,功能不是一样的吗?都是将XXXMapper标注为一个Bean,交给Spring管理
  • 没错,这两个注解的作用是可以说是一摸一样的,无非就是写的位置不一样
  • 正是因为这两个注解作用是一样的,所以在开发过程中,这两个注解写一个就行,而且只能写一个,否则会报错
  • 网上总会有蠢蛋,说在XXXMapper上,添加了@Mapper注解之后,一定不要忘了在启动类上添加@MapperScan注解
  • 这种方法肯定解决不了问题,是大错特错的
  • 所以,如果你已经在XXXMapper上添加了@Mapper注解,一定记得删除启动类上的@MapperScan注解
  • 如果到这里,你已经按照上面的方法解决了问题,成功启动了项目,恭喜你;如果仍旧报错,请继续往下看:

MybatisPlusConfig配置

  • 我们在项目中,导入了MybatisPlus依赖之后,总会写一个MybatisPlusConfig的分页配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* mybatis-plus 分页配置类
*/
@Configuration
@MapperScan("com.memory.memoryiconbackend.mapper.WallpaperMapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
  • 如果你的问题没有解决,一定是因为在这个配置类上边,写上了@MapperScan注解:

image-20230801182811187

  • 而这个注解的作用,跟启动类上的@MapperScan注解的作用是一模一样的,删除它就好了

总结

  • 如果你已经在XXXMapper上添加了@Mapper注解,请把启动类和MybatisPlusConfig配置类上的@MapperScan注解删除
  • 如果你已经在启动类和MybatisPlusConfig配置类上添加了@MapperScan注解,请把XXXMapper上的@Mapper注解删除
  • 希望这篇文章对你有帮助,感谢您的支持!😁

类文件版本不匹配

  • 如果在启动项目时,出现了这样的报错,你会怎么解决呢:

image-20230802082050802

  • 引起这样报错的原因只有一种:类文件版本不匹配,即项目里导入的依赖版本不兼容

Mybatis版本 和 SpringBoot版本不兼容

  • 查看上方报错信息,很显然是XXXMapper包扫描失败了,这就是Mybatis版本 和 SpringBoot版本不兼容的问题
  • 如此配置:(2023/08/02早)
1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

Jdk版本 和 SpringBoot版本不兼容

  • 当你启动项目时,看到了这样的报错:

image-20230802182601591

  • 这是由于Jdk版本 和 SpringBoot版本不兼容导致的
  • JDK版本不兼容,还并且可能导致其他配置构建失败:

image-20230802182701312

  • 这是我的SpringBoot坐标依赖:
1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
  • 按照提示,升级JDK版本到1.8以上即可解决问题

相关依赖未导入

MySQL相关依赖未导入

  • 今天在做 PicMemories 项目过程中,启动项目时,出现了以下报错:
1
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'downloadServiceImpl': Unsatisfied dependency expressed through field 'baseMapper'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'downloadMapper' defined in file [D:\Project\星球项目\PicMemories\target\classes\com\memory\picmemories\mapper\DownloadMapper.class]: Cannot resolve reference to bean 'sqlSessionTemplate' while setting bean property 'sqlSessionTemplate'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]: Unsatisfied dependency expressed through method 'dataSourceScriptDatabaseInitializer' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception; nested exception is java.lang.IllegalStateException: Cannot load driver class: com.mysql.cj.jdbc.Driver
  • 经排查,发现是MySQL相关依赖导入错了,我导入的错误依赖如下:
1
2
3
4
5
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
  • 正确的依赖应该为:
1
2
3
4
5
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
  • 为什么会出现这样的错误呢?是因为我在构建项目时,选择导入以下依赖中,发生了错误:

image-20230802183420381

  • 这里一定要选择MySQL Driver,而不是MySQL Server Driver,否则就会导入错误的依赖而引发报错(2023/08/02午)

快速生成 Swagger + Knif4j 接口文档

  • 昨天遇到的问题,今天总算解决了,废话少说,上案例:

  • 在开发 Memory-icon 和 PicMemories 项目时,都遇到了这个问题:明明Knif4j配置无误,接口文档访问却报404错误:

image-20230802184955677

  • Knif4j官方文档:快速开始 | Knife4j (xiaominfo.com)
  • 按照官方文档,我们可以清楚地看到不同版本的 SpringBoot 导入Swagger + Knif4j 接口文档的方式是不一样的

  • Spring Boot 版本在 2.4.0 ~ 3.0.0之间,以我为例,我的Spring Boot 版本是2.7.9

  • 那么使用 Swagger + Kni4j 自动生成接口文档的步骤如下:

    • 导入依赖坐标
    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
    <version>4.0.0</version>
    </dependency>
    • 在appilcation.yaml中导入配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    knife4j:
    enable: true
    openapi:
    title: PicMemories 接口文档
    description: PicMemories 壁纸分享小程序
    concat: 3348407547@qq.com
    url: https://deng-2022.gitee.io/blog/
    version: 1.0
    license: Apache 2.0
    group:
    test1:
    group-name: 壁纸分享
  • 完毕,已经能够正常访问到接口文档了:http://localhost:8084/api/doc.html(2023/08/02晚)

执行SQL找不到字段

  • 如果在执行SQL时,出现了以下问题:
1
Unknown column 'user_id' in 'field list'

image-20230803082954240

  • 报错信息显示,找不到字段user_id,解决思路如下:
    • 首先,确定这个对应数据库中有这个字段user_id
    • 其次,确保实体类属性和表中字段映射无误
    • 最后,检查数据库配置是否正确
  • 我就栽在最后一步上了,数据库连接到别的表了,怪不得找不到相应字段,折磨了我一晚上(2023/08/03早)

表记录/实体类ID自增长

  • 如何实现:往数据库表中插入数据记录时,id自增长?
    • 建表语句:
    1
    `user_id`   bigint auto_increment primary key comment '用户id',
    • 实体类映射:
    1
    2
    3
    4
    5
    /**
    * 用户id
    */
    @TableId(type = IdType.ASSIGN_ID)
    private Long userId;
    • 插入数据:
    1
    2
    3
    4
    5
    6
    7
    8
    // 3.向数据库中插入用户数据
    User user = new User();
    user.setUsername(username);
    user.setPassword(encryptPassword);
    user.setPassword(phone);
    user.setAvatar("http://ry2s7czdf.hd-bkt.clouddn.com/imgs/avatar/winter_nature_6-wallpaper-2560x1600.jpg");

    boolean save = this.save(user);
    • 表记录:(2023/08/04早)
    1
    id = 1687297781782978562

测试类添加@SpringBootTest注解

  • 如果不加这个注解,导入的对象值为null:(2023/08/05午)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MemoryClientTest {
@Resource
private MemoryClient memoryClient;

@Test
public void testMemorySdk() {
if(memoryClient != null){
System.out.println("成功了");
memoryClient.getNameByGet("邓哈哈");
memoryClient.getNameByPost("邓嘻嘻");

// User user = new User();
// user.setName("邓尼玛");
User user = new User("邓尼玛");
memoryClient.getUserByPost(user);
}else {
System.out.println("失败!");
}
}
}
  • 还有一点要注意,测试类返回值必须为void,返回其他值会报错:(2023/08/07早)
1
no test were found

image-20230807102218377

Enum类不能使用@DATA注解

  • 手写 getXXX() / setXXX():
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
public enum WallpaperStatusEnum {
/**
* 0 - 公开, 在队伍大厅中可以直接加入
*/
REVIEW(0, "审核中"),

/**
* 1 - 私有, 在队伍大厅中不可以直接加入
*/
PASS(1, "已发布"),

/**
* 2 - 公开且加密, 加入队伍需要密码
*/
NOPASS(2, "不通过");

/**
* 状态码
*/
private int value;

/**
* 状态描述
*/
private String text;

/**
* 判断用户状态
*
* @param value 用户状态
* @return 存在与否
*/
public static WallpaperStatusEnum getEnumByValue(Integer value) {
if (value == null) {
return null;
}
WallpaperStatusEnum[] values = WallpaperStatusEnum.values();
for (WallpaperStatusEnum teamStatusEnum : values) {
if (teamStatusEnum.getValue() == value) {
return teamStatusEnum;
}
}
return null;
}

WallpaperStatusEnum(int value, String text) {
this.value = value;
this.text = text;
}

public int getValue() {
return value;
}

public void setValue(int value) {
this.value = value;
}

public String getText() {
return text;
}

public void setText(String text) {
this.text = text;
}
}

循环依赖

  • 我在项目中如此写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class WallpaperServiceImpl extends ServiceImpl<WallpaperMapper, Wallpaper>
implements WallpaperService {
@Resource
private WallpaperService wallpaperService;

/**
* 分页查询
* 分类查询壁纸
*
* @return 分类壁纸
*/
@Override
public List<Wallpaper> getPageByType(Integer searchType) {
if (WallpaperTypeEnum.getEnumByValue(searchType) == null) {
throw new BusinessException(ErrorCode.PARMS_ERROR, "没有这样的壁纸类型");
}

QueryWrapper<Wallpaper> type_wqw = new QueryWrapper<>();
type_wqw.eq("type", searchType);

return wallpaperService.list(type_wqw);
}
}
  • 产生了循环依赖,会报错:
1
2
3
4
5
6
7
8
Description:

The dependencies of some of the beans in the application context form a cycle:

wallpaperController
┌─────┐
| wallpaperServiceImpl
└─────┘
  • 应该写成这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class WallpaperServiceImpl extends ServiceImpl<WallpaperMapper, Wallpaper>
implements WallpaperService {
/**
* 分页查询
* 分类查询壁纸
*
* @return 分类壁纸
*/
@Override
public List<Wallpaper> getPageByType(Integer searchType) {
if (WallpaperTypeEnum.getEnumByValue(searchType) == null) {
throw new BusinessException(ErrorCode.PARMS_ERROR, "没有这样的壁纸类型");
}

QueryWrapper<Wallpaper> type_wqw = new QueryWrapper<>();
type_wqw.eq("type", searchType);

return this.list(type_wqw);
}
}

时间戳格式问题

1
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")

MyBatis逻辑删除

1
2
3
4
5
/**
* 是否删除
*/
@TableLogic
private Integer isDelete;
1
2
3
4
5
6
7
mybatis-plus:
global-config:
# 逻辑删除
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

Redis的引入和测试

  • 快速实现Redis的引入,主要做到以下几点:(2023/08/07早)
    • 导入相关依赖坐标
    • 作相关yaml配置
    • 作测试
    • 项目引入
  • 导入依赖坐标:
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>
  • 写配置:
1
2
3
4
5
# redis 配置
redis:
port: 6379
host: localhost
database: 0
  • 作测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
@Slf4j
class UserServiceImplTest {
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();

@Test
void loginTest() {

String redisKey = "pic_memories:user:login:session_key";


stringRedisTemplate.opsForValue().set("pic", "memory");

stringRedisTemplate.opsForValue().set(redisKey, "memory", 20, TimeUnit.HOURS);
redisTemplate.opsForValue().set(redisKey + "2", "memory2");
}
}
  • 这里注意:
  • RedisTemplate 底层的序列化方式,会导致存入的序列化后的value值成为乱码
  • StringRedisTemplate 继承了 RedisTemplate 有效解决了这个问题,但只能存放<String,String>
  • 综上,我们在使用Redis缓存技术时,可以自己自定义(封装一个)RedisTemplate
  • 自定义 RedisTemplate<String, Object> (config/RedisTemplateConfig)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 1.创建RedisTemplate对象
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 2.设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 3.设置Key的序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
// 4.创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 5.设置value的序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
  • 解决 RedisTemplate 存入的序列化后的value值成为乱码的问题(2023/08/07早)

接入阿里云对象存储服务

自定义Banner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
启动成功!
${AnsiColor.BRIGHT_GREEN}
*
( `
)\))( ( ) ( (
((_)()\ ))\ ( ( )( )\ )
(_()((_)/((_) )\ ' )\(()\(()/(
| \/ (_)) _((_)) ((_)((_))(_))
| |\/| / -_) ' \() _ \ '_| || |
|_| |_\___|_|_|_|\___/_| \_, |
|__/
${AnsiColor.BRIGHT_WHITE}
欢迎使用~
spring boot 版本为 ${spring-boot.version}
作者:@Memory
项目名:PicMemories
线上访问地址: 未完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LaunchApplication {

public static void main(String[] args) {
SpringApplication app = new SpringApplication(LaunchApplication.class);
app.setBannerMode(Banner.Mode.OFF); // 设置 Banner 模式为关闭
app.run(args);
}

}

主键自增长

  • 今天使用MybatisPlus执行插入数据时,发现了这样的报错:(2023/08/15午)
1
2
3
4
5
6
7
8
org.springframework.jdbc.BadSqlGrammarException: 
### Error updating database. Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'like ( id,
user_id,
wallpaper_id ) VALUES ( 1691349539878477825,
168799521293' at line 1
### The error may exist in com/memory/picmemories/mapper/LikeMapper.java (best guess)
### The error may involve com.memory.picmemories.mapper.LikeMapper.insert-Inline
### The error occurred while setting parameters
  • 在上面的栏目《表记录/实体类ID自增长》中,已经实现了如何使记录中的id自增长,这次我就犯了这个错误:
1
2
3
4
5
6
7
8
9
10
create table `like`
(
id bigint primary key comment '点赞id',
user_id bigint not null comment '点赞用户id',
wallpaper_id bigint not null comment '被点赞的壁纸id',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null comment '更新时间',
is_delete varchar(256) default '0' not null comment '是否删除'
)
comment '点赞信息';
1
2
3
4
5
/**
* 点赞id
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
1
2
3
4
5
6
// 4.新增记录
Like like = new Like();
like.setUserId(userId);
like.setWallpaperId(wallpaperId);

boolean save = likeService.save(like);
  • 如上,很显然,我没有将id字段设置为自增长,所以才会出现这样的报错(2023/08/15午)

修改表字段后要做的那些事

  • 修改表字段后,应该做哪些事?

    • 修改对应实体属性
    • 修改 XXXmapper.xml 文件(mapper路径、domain路径)
  • 使用插件 MybatisX-Generator 快速生成 mapper、domain、service(2023/08/16午)

YAML配置重复键

  • 启动项目报了这样的错误:(2023/08/23晚)

image-20230823164029471

  • 这个错误通常是由于在YAML配置文件中多次定义了相同的键,检查yaml配置即可

git合并提交

  • 在上午,我使用git进行合并提交项目代码后,下午运行项目,代码变成了这样:

image-20230826182411757

  • 没什么大问题,这是因为:
  • 你使用git合并推送项目代码后遇到了冲突(conflict),冲突的部分被Git标记为<<<<<<< HEAD, =======和>>>>>>>。
  • 这是因为在合并时,Git无法确定如何自动合并这些不同版本的代码。2023/08/26晚)
  • 为解决冲突,你需要手动编辑冲突的文件,然后再次提交。以下是我采取的一些步骤:

    • 打开标记有冲突的文件,找到<<<<<<< HEAD, =======和>>>>>>>之间的冲突代码段。
    • 理解每个版本的更改,并决定要保留哪些部分。可以选择保留某个版本的代码,或者进行适当的修改以使两个版本的更改合并。
    • 对冲突的代码段进行适当修改,解决冲突。
    • 删除冲突标记(<<<<<<< HEAD, =======和>>>>>>>)。
    • 完成修改后,保存文件。
  • 完成(2023/08/26午)

Spring项目起不来

  • 这个栏目标题起的有点怪啊,不过确实是这么回事:(2023/08/29早)
  • 今早计划测试一把Spring的定时任务功能,结果构建完成一个SpringBoot项目之后,项目却启动不起来:

image-20230829113246414

  • 准确的说,项目没有启动成为一个Web服务器后台,这是为什么呢?
  • 妈的,原来是构建项目时,忘记导入相关依赖了:

image-20230829112442434

  • 即:
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 呐,导入以上依赖后,SpringBoot成功运行起来了:(2023/08/29早)

image-20230829112724037

  • 真是学傻了,这么基础的问题,平时竟然没注意到,呵呵呵

  • 顺带提一下,SpringBoot项目构建完成后,默认导入以下两个依赖:
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

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

Spring项目起不来(2)

  • 这次是什么原因呢?看报错:(2023/09/06)

image-20230906172719883

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class

Action:

Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

Process finished with exit code 1
  • 这个就很清晰了,因为引入了数据库相关依赖,却没有作相关配置:
1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>
1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/memory_api
username: root
password: Dw990831

mapper注入为null

  • 从来没有踩过这个坑,这次让我记忆深刻了(2023/09/07晚)
  • 如图,我注入了interfaceInfoMapper

image-20230907214553078

  • 但项目运行后,通过debug发现,该interfaceInfoMapper为null,为什么注入不成功呢?我犯了一个错误:

image-20230907214839239

  • 如上,我new了一个interfaceIdSource,这个问题被我忽视了:
1
类的实例化问题:确保你在使用 InterfaceIdSource 类时是通过 Spring 容器来获取实例,而不是通过 new 关键字手动创建对象。只有通过 Spring 容器管理的对象才会进行自动注入。
  • 直接注入即 interfaceIdSource 可解决问题:(2023/09/07晚)

image-20230907215126113

MybatisPlus踩坑记录

  • 我在根据id查询用户时,写了如下代码:
1
2
3
4
5
6
for (Friends friends : friendsList) {
Long friendId = friends.getFriendId();
uqw.eq("id", friendId);
User one = userService.getOne(uqw);
userList.add(one);
}
  • 结果,查出来的 one 对象均为 null,可能是因为 id 设置为了主键

  • 我们改写为MybatisPlus提供的的根据id查询方法,成功解决问题(2023/09/12午)
1
2
3
4
5
6
// 2.根据id查询道好友信息
for (Friends friends : friendsList) {
Long friendId = friends.getFriendId();
User one = userService.getById(friendId);
userList.add(one);
}

日常犯傻

  • 使用 Vue 的 ref() 语法时,容易忘记取.value
1
const currentUserId = currentUser.value.id;
  • 访问后端服务器路径,容易忘记写/api
1
const socketUrl = `ws://localhost:8081/api/websocket/${currentUserId}`;

导入 Excel 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
File file = null;
try {
file = ResourceUtils.getFile("classpath:test_excel.xlsx");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 读取数据
List<Map<Integer, String>> list = null;

list = EasyExcel.read(file)
.excelType(ExcelTypeEnum.XLSX)
.sheet()
.headRowNumber(0)
.doReadSync();

if (CollUtil.isEmpty(list)) {
return "";
}
  • 这部分代码,简单地读取了 resourse 目录下test_excel.xlsx 文件,并成功获取表格数据
  • 附上原表格数据和解析效果:

image-20231005162100413


image-20231005162143753

  • 修改代码,接收上传的文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 智能分析
*
* @param multipartFile Excel文件
* @param genChartByAiRequest 指定图表信息
* @param request request
* @return 生成的图表信息
*/
@PostMapping("/generate")
public BaseResponse<String> uploadFile(@RequestPart("file") MultipartFile multipartFile,
GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
String name = genChartByAiRequest.getName();
String goal = genChartByAiRequest.getGoal();
String chartType = genChartByAiRequest.getChartType();

// 校验
ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空");
ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长");

String result = ExcelUtils.excelToCsv(multipartFile);
return ResultUtils.success(result);
  • 逐行解析 Excle 表格,获取数据:
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
/**
* excel 转 csv
*
* @param multipartFile
* @return
*/
public static String excelToCsv(MultipartFile multipartFile) {
File file = null;
try {
file = ResourceUtils.getFile("classpath:test_excel.xlsx");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 读取数据
List<Map<Integer, String>> list = null;
list = EasyExcel.read(file)
.excelType(ExcelTypeEnum.XLSX)
.sheet()
.headRowNumber(0)
.doReadSync();
if (CollUtil.isEmpty(list)) {
return "";
}
// 转换为 csv
StringBuilder stringBuilder = new StringBuilder();
// 读取表头
LinkedHashMap<Integer, String> headerMap = (LinkedHashMap) list.get(0);
List<String> headerList = headerMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList());
stringBuilder.append(StringUtils.join(headerList, ",")).append("\n");
// 读取数据
for (int i = 1; i < list.size(); i++) {
LinkedHashMap<Integer, String> dataMap = (LinkedHashMap) list.get(i);
List<String> dataList = dataMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList());
stringBuilder.append(StringUtils.join(dataList, ",")).append("\n");
}
return stringBuilder.toString();
}
  • ,我们在接口文档中,上传 Excel 文件:

image-20231005161702354

  • 成功解析获取数据并返回:

image-20231005161948245

校验文件

  • 只要涉及到用户自主上传操作,一定要校验文件(图像)

  • 校验:

    • 文件大小
    • 文件后缀
    • 文件内容(成本略高)
    • 文件合规(敏感内容)
  • 校验文件大小和文件后缀
1
2
3
4
5
6
7
8
9
10
/**
* 允许上传的文件大小
*/
long ONE_MB = 1024 * 1024L;


/**
* 合法的文件后缀
*/
List<String> VALID_FILE_SUFFIX_LIST = Arrays.asList("xlsx", "xls");
1
2
3
4
5
6
7
8
9
// 3.1.校验文件
// 3.1.1.校验文件大小
long size = multipartFile.getSize();
ThrowUtils.throwIf(size > ONE_MB, ErrorCode.PARAMS_ERROR, "文件超过 1M");

// 3.1.2.校验文件后缀
String originalFilename = multipartFile.getOriginalFilename();
String suffix = FileUtil.getSuffix(originalFilename);
ThrowUtils.throwIf(!VALID_FILE_SUFFIX_LIST.contains(suffix), ErrorCode.PARAMS_ERROR, "文件后缀非法");

限流

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.21.0</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
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissionConfig {
private String host;

private String port;

private String password;

private Integer database;

@Bean
public RedissonClient redissonClient() {
// 1. 创建配置
Config config = new Config();
String redisAddress = String.format("redis://%s:%s", host, port);
// 使用单个Redis,没有开集群 useClusterServers() 设置地址和使用库
config.useSingleServer().setAddress(redisAddress).setDatabase(database).setPassword(password);
// 2. 创建实例
return Redisson.create(config);
}
}
1
2
3
4
redis:
port: 6379
host: localhost
database: 1
  • 限流实现(区别不同的限流器,每个用户都分别拥有对应的限流器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 限流实现
*
* @param key 识别用户的key
*/
public void doRateLimit(String key) {
// 创建限流器
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
// 每当一个操作来了之后,请求一个令牌
boolean canOp = rateLimiter.tryAcquire(1);
// 超出发放令牌数目,请求过于频繁
ThrowUtils.throwIf(!canOp, ErrorCode.TOO_MANY_REQUEST);
}
  • 简单测试,效果良好,测试结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void doRateLimit() throws InterruptedException {
String userId = "1";
for (int i = 0; i < 2; i++) {
redisLimiterManager.doRateLimit(userId);
System.out.println("成功");
}
Thread.sleep(1000);
for (int i = 0; i < 5; i++) {
redisLimiterManager.doRateLimit(userId);
System.out.println("成功");
}
}

image-20231011180516219

  • 业务中使用限流器,调用智能分析接口时,限制用户在单位时间内的调用次数 (2023/10/11晚)
1
2
// 限流(限制用户的调用次数,以用户id为key,区分各个限流器)
redisLimiterManager.doRateLimit("genCharByAi_" + loginUser.getId());

Slf4j 导入依赖错误

  • 这是个很有意思的问题,起因是我计划实现一个延迟队列 Demo,结果在运行时出现了这样的报错:

image-20231019123726686

  • 这些报错是什么意思呢?简单解释一下:

这些警告信息是由 SLF4J(Simple Logging Facade for Java)引发的,它是一个为 Java 程序提供日志功能的简单抽象层。

警告 No SLF4J providers were found 表示在类路径上未找到任何 SLF4J 提供者。这意味着没有配置合适的日志库实现,导致程序在运行时无法正常输出日志。

警告 Defaulting to no-operation (NOP) logger implementation 表示在缺少真正的日志库实现时,SLF4J 默认使用了一个空操作的日志记录器实现。这意味着代码在运行时不会输出任何日志。

警告 Class path contains SLF4J bindings targeting slf4j-api versions 1.7.x or earlier 表示在类路径上发现了与 SLF4J 绑定,但它们是针对 1.7.x 版本或更早版本的 slf4j-api。这可能导致不兼容或出现日志记录问题。

警告 Ignoring binding found at [jar:file:/D:/softWare/Maven/apache-maven-3.6.1/mvn_resp/ch/qos/logback/logback-classic/1.2.12/logback-classic-1.2.12.jar!/org/slf4j/impl/StaticLoggerBinder.class] 表示 SLF4J 发现了一个冲突的绑定,将被忽略。这意味着在类路径上有多个不同版本的 SLF4J 绑定或日志库实现。

解决这些警告的方法是确保正确配置了 SLF4J 绑定和日志库实现,以及它们的版本兼容性。

  • 同时我也受到了 AI 的点拨,可以从以下几个方面排查问题:
1
2
3
4
5
1、确保在项目的依赖管理中添加了正确的 SLF4J 绑定和日志库实现。
2、确保依赖的版本与所使用的 SLF4J 版本兼容。推荐使用 SLF4J 版本 1.7.x 或更高版本。
3、检查项目的构建路径,确保没有冲突的 SLF4J 绑定存在于类路径上。
4、如果问题仍然存在,可以尝试在项目中排除掉冲突的 SLF4J 绑定,或通过 Maven 或 Gradle 等构建工具来管理依赖关系。
5、如果仍然无法解决问题,可以参考 SLF4J 的文档和常见问题页面(https://www.slf4j.org/codes.html)寻找更详细的解决方案。
  • 我检查了引入的相关依赖,发现除了 lombok 依赖外,还引入了一个 slf4j-api 依赖:
1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.5</version>
</dependency>
  • 这就是问题所在了,删除这个依赖即可解决问题,程序成功运行:(2023/10/19早)

image-20231019124205016

实现 updateTime 字段自动更新

  • 什么意思呢?我们希望在修改完数据库表中的记录后,该条记录对应的 uodateTime 字段实现自动更新
  • 实现方法很简单,在 IDEA 中,直接修改表的 updateTime 字段属性,如下:

image-20231107224250849

  • 对应的 DDL 语句是这样的:
1
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
  • 这样,我们更新记录后,该记录 updateTime 字段会自动更新为最近修改时间(2023/11/07晚)

image-20231107224550821

实体类未序列化

  • 我们在后端写接口时,会这样编写接收的参数:(2023/11/20晚)
1
2
3
4
5
6
7
8
@PostMapping("/list/page/vo")
public BaseResponse<Page<Picture>> listPictureByPage(@RequestBody Picture picture) throws IOException {
// controller层对参数的校验
String category = picture.getCategory();

Page<Picture> picturePage = pictureService.listPictureVOByPage(category);
return ResultUtils.success(picturePage);
}
  • 当然,要理解这种接收参数的编写语法,需要系统学习 SpringMVC 的相关内容,这里只谈一点:

    Picture 要支持序列化才能实现网络传输
  • 妈的,所以才会出现这样的报错:

1
2
om.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `memory.cloud.memoryclient.model.domain.Picture` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
  • 排了一个小时的错,可算发现了:我编写的 Picture 不支持序列化,改写成如下这样即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class Picture implements Serializable {
public Picture() {
}

/**
* 所属分类
*/
private String category;

/**
* 图片名
*/
private String title;

/**
* 图片路径
*/
private String url;

private static final long serialVersionUID = 1L;
}
  • 平时这种实体类都是使用 MybatisX-Generator 快速生成的,没注意到实体类要支持序列化 (2023/11/20晚)

存储数据库编码错误

  • 终于解决了如何正确保存含 emoji 表情数据到数据库中的问题了
  • 直接保存
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
 // 1. 获取数据
String url = "https://juejin.cn/post/7313418992310976549";
try {
Document doc = Jsoup.connect(url)
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.81")
.get();


Elements title = doc.select(".article-area .article-title");
System.out.println("----------------博文标题----------------");
System.out.println(title.text());

Elements content = doc.select(".article-viewer p");
// for (Element p : content) {
// p.select("img").remove();
// p.select("a").remove();
// }

System.out.println("---------------博文正文------------------");
// System.out.println(content);

byte[] contentBytes = content.toString().getBytes(StandardCharsets.UTF_8);

System.out.println(contentBytes);
// 获取博文
Article article = new Article();
article.setId(Long.valueOf("7313418992310976549"));
article.setTitle(title.text());

// 直接保存
article.setContent(content.toString());

// article.setContent(Arrays.toString(contentBytes));
// article.setContent(content.toString());
article.setAuthorId(0L);
article.setView(0);
article.setLikes(0);
article.setComments("");
article.setCollects(0);
article.setTags("");

// articleService.save(article);

String decodedContent = new String(contentBytes, StandardCharsets.UTF_8);
System.out.println("-------------解码后--------------");
System.out.println(decodedContent);

} catch (IOException e) {
throw new RuntimeException(e);
}
}
article.setContent(contentBytes);

  • 这里直接保存,会出现字符编码无法识别而转换错误,就是因为保存的数据记录中有 emoji 这样的小图标
  • 这里我也查询了相关文章,解决这个问题,虽然最后没有解决,但仍可做参考:

🔥 推荐阅读:

1
Error updating database.  Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x8D\x84 \xE5...' for column 'content' at row 1
  • 经过测试,转码保存解决报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Document doc = Jsoup.connect(url)
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.81")
.get();


Elements title = doc.select(".article-area .article-title");
System.out.println("----------------博文标题----------------");
System.out.println(title.text());

Elements content = doc.select(".article-viewer p");
// for (Element p : content) {
// p.select("img").remove();
// p.select("a").remove();
// }

System.out.println("---------------博文正文------------------");
// System.out.println(content);

byte[] contentBytes = content.toString().getBytes(StandardCharsets.UTF_8);
1
2
3
String decodedContent = new String(contentBytes, StandardCharsets.UTF_8);
System.out.println("-------------解码后--------------");
System.out.println(decodedContent);

image-20231224104703715

  • 保存到数据库中的问题解决了,接下来就是保证正确从数据库中拿到数据并解码出原数据:
1
2
String contentStr = article.getContent();
byte[] contentBytes = contentStr.getBytes(StandardCharsets.UTF_8);
1
2
3
4
5
6
7
8
9
// 移除两端的方括号  
String contentStrWithoutBrackets = contentStr.substring(1, contentStr.length() - 1);
// 使用逗号作为分隔符分割字符串
String[] byteStrs = contentStrWithoutBrackets.split(",");
// 将字符串数组转换为字节数组
byte[] contentBytes = new byte[byteStrs.length];
for (int i = 0; i < byteStrs.length; i++) {
contentBytes[i] = Byte.parseByte(byteStrs[i]);
}
  • 经过诸多尝试,仍无法正确解码
  • 经过前面的测试发现,转码后保存 byte [] 可以解决编码错误,问题是出在保存数据库时
  • 由于字段 content 为 text(varchar 也可以,可能会出现要保存的数据记录过长而导致溢出,就选择 text 了),所以我们在保存 byte [] 到数据库中时,是先转换成字符串再保存的
1
article.setContent(Arrays.toString(contentBytes));
  • 而后才引发了解码失败的问题,因为对 byte [] 直接解码是可以获取原文内容 content 的,但是先转字符串存入,取出时就不好处理了
  • 那就干脆直接保存 byte [] 到数据库中了,改变字段 content 属性为 blob:

image-20231224104321929

  • 改变对应实体类的字段数据类型为 byte []:
1
2
3
4
/**
* 文章内容
*/
private byte[] content;
  • 接下来,我们选择直接保存 byte [] 到数据库中即可:
1
article.setContent(contentBytes);
  • 这里也可以看出,将 byte [] 转字符串数组后保存和直接保存 byte [] 到数据库中的形式是很不一样的(如下图所示):

image-20231224104532681

  • 接下来,就可以直接从数据库中取出数据并解码了:
1
2
3
4
5
6
7
8
9
10
11
12
13
Article article = articleService.getById(7313418992310976549L);

Long id = article.getId();
String title = article.getTitle();
byte[] content = article.getContent();

String decodedContent = new String(content, StandardCharsets.UTF_8);
Integer type = article.getType();

System.out.println(id);
System.out.println(title);
System.out.println(decodedContent);
System.out.println(type);
  • 解码结果如下:

image-20231224104725599

  • 至此,我们成功解决了如何正确保存数据记录到数据库中的问题,并成功解决了编码问题

汉字转拼音

1
2
3
4
Random random = new Random();
int randomPage = random.nextInt(5) + 1;
String url = String.format("https://www.vcg.com/creative-image/%s/?page=%d", category, randomPage);
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.81";
  • 拼接 category ,只要是中文字符就不定时出现报错,尝试将中文转为拼音:
  • 导入依赖:(2024/01/18晚)
1
2
3
4
5
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version> <!-- 使用时检查是否有更新的版本 -->
</dependency>
  • 开始转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 带声调
StringBuilder output = new StringBuilder();
for (char c : name.toCharArray()) {
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c);
if (pinyinArray != null && pinyinArray.length > 0) {
output.append(pinyinArray[0]).append(" ");
} else {
output.append(c).append(" ");
}
} catch (Exception e) {
output.append(c).append(" ");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 不带声调
net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat format = new net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat();
format.setToneType(net.sourceforge.pinyin4j.format.HanyuPinyinToneType.WITHOUT_TONE);

StringBuilder output = new StringBuilder();
for (char c : name.toCharArray()) {
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format);
if (pinyinArray != null && pinyinArray.length > 0) {
output.append(pinyinArray[0]).append("");
} else {
output.append(c);
}
} catch (Exception e) {
output.append(c).append("");
}
}

无法加载Spring的配置文件

启动测试类,发现这样的报错:(2024/02/13早)

image-20240213094455214

在SpringBoot中报错java.lang.IllegalStateException: Failed to load ApplicationContext,无法加载Spring的配置文件

出现这种问题,无外于这几个方面:jar 包过于老旧、@Bean 注入有分歧、配置文件中的数据库连接失败、未扫描到 Mapper

1、jar包有问题或者过老,换最新的试试。
2、 内部Bean配置有歧义,Spring自身无法分辨
3、缺少某个依赖、或属性的配置
4、引用 外部属性文件的情况下,属性文件内有错误,无法加载。比如属性文件配置的数据库连接 有问题
5、在使用到applicationContext的 地方引用的路径不正确。

🍖 推荐阅读: java.lang.IllegalStateException: Failed to load ApplicationContext-CSDN博客

我的问题解决了,确实是配置文件中数据库连接有问题。我这个项目配置了本地 Elasticsearch 的,启动 本地 ES 就好了

1
2
3
4
5
# Elasticsearch 配置
elasticsearch:
uris: http://localhost:9200
username: root
password: ******

@ControllerAdvice 拦截

2024年4月25日

🍖 推荐阅读:@ControllerAdvice 的介绍及三种用法(转载)-CSDN博客

在看单位的后端开发代码模板,看到了这个类:

这段代码是一个名为ApiResultHandler的类,它实现了Spring框架中的ResponseBodyAdvice接口。这个类的主要作用是对RestController的接口方法进行拦截,对返回的结果进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
@ControllerAdvice(annotations = {RestController.class})
public class ApiResultHandler implements ResponseBodyAdvice {

private static final Class[] annos = {
RequestMapping.class,
GetMapping.class,
PostMapping.class,
DeleteMapping.class,
PutMapping.class
};

...............................
}

首先,定义了一个静态数组annos,包含了RequestMapping、GetMapping、PostMapping、DeleteMapping和PutMapping这五个注解,用于判断一个方法是否使用了这些注解。

1
2
3
4
5
6
7
8
/**
* 对所有RestController的接口方法进行拦截
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
AnnotatedElement element = returnType.getAnnotatedElement();
return Arrays.stream(annos).anyMatch(anno -> anno.isAnnotation() && element.isAnnotationPresent(anno));
}

重写了supports方法,用于判断当前拦截的方法是否使用了上述五个注解之一。如果使用了,返回true,表示需要拦截;否则返回false,表示不需要拦截。

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
@Override
public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

if(body instanceof RestfulResponse){
return body;
}

//feign内部请求,不转格式
HttpHeaders headers = request.getHeaders();
List<String> list = headers.get(FeginClientConfig.KEEP_ORIGINAL);
if(list != null && list.contains(FeginClientConfig.KEEP_ORIGINAL)) {
return body;
}

if(body instanceof ResultCode) {
return new RestfulResponse(((ResultCode) body).getCode(), ((ResultCode) body).getDesc());
}

AnnotatedElement element = returnType.getAnnotatedElement();
if(element.isAnnotationPresent(SuccessfulMessage.class)) {
boolean ignore = element.getAnnotation(SuccessfulMessage.class).ignore();
if(ignore) {
return body;
}
}

String msg = null;

//1. 配置文件优先级最高
//2. 方法上的 SuccessfulMessage
//3. 默认
// RequestMapping 成功
// GetMapping 查询成功
// PostMapping 添加成功
// DeleteMapping 删除成功
// PutMapping 修改成功

if(element.isAnnotationPresent(SuccessfulMessage.class)) {
/*String value = element.getAnnotation(SuccessfulMessage.class).value();
//解析EL ${}
if(value != null) {
value = value.trim();
if(value.startsWith("${") && value.endsWith("}")) {
value = value.substring(2, value.length()-1);
String[] separator = StringUtils.splitByWholeSeparator(value, ":");
String key = separator[0];
String defaultValue = separator.length > 1 ? separator[1] : null;
//查询配置文件
msg = env.getProperty(key, defaultValue);
} else {
msg = value;
}
}*/
msg = element.getAnnotation(SuccessfulMessage.class).value();
}

if(msg == null) {
if(element.isAnnotationPresent(GetMapping.class)) {
msg = "操作成功";
} else if(element.isAnnotationPresent(PostMapping.class)) {
msg = "操作成功";
} else if(element.isAnnotationPresent(PutMapping.class)) {
msg = "修改成功";
} else if(element.isAnnotationPresent(DeleteMapping.class)) {
msg = "删除成功";
} else {
msg = "请求成功";
}
}
return RestfulResponse.success(msg, body);
}

重写了beforeBodyWrite方法,这个方法会在Controller方法执行后,返回结果之前被调用。在这个方法中,对返回的结果进行了处理:

  • 如果返回的结果已经是RestfulResponse类型,直接返回;
  • 如果请求头中包含FeginClientConfig.KEEP_ORIGINAL,表示是内部请求,不进行格式转换,直接返回;
  • 如果返回的结果是ResultCode类型,将其转换为RestfulResponse类型并返回;
  • 如果方法上有SuccessfulMessage注解,根据注解的属性决定是否忽略该次拦截;
  • 根据方法上的注解(如GetMapping、PostMapping等)设置默认的成功消息;
  • 最后,将成功消息和原始结果封装成RestfulResponse对象并返回。

定时任务实现

纯手写单线程循环

1
2
3
4
5
6
7
8
9
10
11
12
public static void timer1() {
new Thread(() -> {
while (true) {
System.out.println("定时任务A 当前时间: " + LocalDateTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}

Timer和他的小伙伴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void timer2() {
// 单线程
Timer timer = new Timer();
System.out.println("1秒后执行任务A,A完成后,等待1秒开始定时执行任务B,当前时间: " + LocalDateTime.now());
// 1秒后执行
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时任务A 当前时间: " + LocalDateTime.now());
}
}, 1000); // 这里 1000,就是代表延迟 1000 毫秒后再执行

// 每隔2秒执行一次这个任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时任务B 当前时间: " + LocalDateTime.now());
}
}, 1000, 2000); // 1000 同理,2000 即执行完本次任务后,隔 2000 毫秒后再一次执行,达到定时任务的效果
}

ScheduledExecutorService

  • 定时执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void timer4() {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
System.out.println("2秒后开始执行任务,此刻时间---" + LocalDateTime.now());
// 固定频率(每隔5秒)开始执行一个任务
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("任务开始---" + LocalDateTime.now());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务结束---" + LocalDateTime.now());
}, 2000, 5000, TimeUnit.MILLISECONDS);
}
  • 延时执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void timer5() {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
System.out.println("2秒后开始执行任务,此刻时间---" + LocalDateTime.now());
// 任务完成后间隔4秒开始执行下一次任务
scheduledExecutorService.scheduleWithFixedDelay(() -> {
System.out.println("任务开始---" + LocalDateTime.now());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务结束---" + LocalDateTime.now());
}, 2000, 4000, TimeUnit.MILLISECONDS);
}

DelayQueue 延迟任务

  • DelayQueue是JDK提供的api,是一个延迟队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
public class DelayQueueDemo {

public static void main(String[] args) {
DelayQueue<SanYouTask> sanYouTaskDelayQueue = new DelayQueue<>();

new Thread(() -> {
while (true) {
try {
SanYouTask sanYouTask = sanYouTaskDelayQueue.take();
log.info("获取到延迟任务:{}", sanYouTask.getTaskContent());
} catch (Exception e) {
}
}
}).start();

log.info("提交延迟任务");
sanYouTaskDelayQueue.offer(new SanYouTask("三友的java日记5s", 5L));
sanYouTaskDelayQueue.offer(new SanYouTask("三友的java日记3s", 3L));
sanYouTaskDelayQueue.offer(new SanYouTask("三友的java日记8s", 8L));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class SanYouTask implements Delayed {

private final String taskContent;

private final Long triggerTime;

public SanYouTask(String taskContent, Long delayTime) {
this.taskContent = taskContent;
this.triggerTime = System.currentTimeMillis() + delayTime * 1000;
}

@Override
public long getDelay(TimeUnit unit) {
return unit.convert(triggerTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}

@Override
public int compareTo(Delayed o) {
return this.triggerTime.compareTo(((SanYouTask) o).triggerTime);
}
}
  • getDelay方法返回这个任务还剩多久时间可以执行,小于0的时候说明可以这个延迟任务到了执行的时间了。
  • compareTo这个是对任务排序的,保证最先到延迟时间的任务排到队列的头。
    • taskContent:延迟任务的具体的内容
    • delayTime:延迟时间,秒为单位
实现原理

🍻 offer方法在提交任务的时候,会通过根据compareTo的实现对任务进行排序,将最先需要被执行的任务放到队列头。

🍛take方法获取任务的时候,会拿到队列头部的元素,也就是队列中最早需要被执行的任务,通过getDelay返回值判断任务是否需要被立刻执行,如果需要的话,就返回任务,如果不需要就会等待这个任务到延迟时间的剩余时间,当时间到了就会将任务返回。

  • 效果如下:

Spring提供定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author 邓哈哈
* 2023/8/29 11:50
* Function:
* Version 1.0
*/

@EnableScheduling
@Component
public class Timer {
@Scheduled(cron = "*/2 * * * * *")
public void timer() {
System.out.println("哈哈哈哈");
}
}
  • 如果有多个定时任务类,可以考虑把@EnableScheduling注解添加在启动类上

Cron表达式

image-20230829122937976

image-20230829123041111

image-20230829123201856

总结


凤凰涅槃:Spring Boot 开发之路上的坎坷与成长
http://example.com/2023/07/06/凤凰涅槃:Spring Boot 开发之路上的坎坷与成长/
作者
Memory
发布于
2023年7月6日
更新于
2024年4月11日
许可协议