封面来源:本文封面来源于网络,如有侵权,请联系删除。

0.前言

拥有臭虫(BUG)聚集体质的我,总会遇到各种各样的臭虫,这些臭虫还很顽固,有时候驱散半天它们都不走,为了以后的美好生活,我将部分臭虫的驱散(DEBUG)方式总结下来,也方便其他小伙伴遇到这些臭虫时可以“对症下药”,并“药到病除”。

1. 问题与解决方法

1.1 Assert.assertEquals()

在使用 Junit 的 Assert.assertEquals() 对预期值和实际值进行比较时:

1
Assert.assertEquals(6L, borrowInfo.getBorrowId());

编译器出现了一下的错误信息:

1
2
Ambiguous method call. Both assertEquals(Object, Object) in Assert and assertEquals(long, 
long)in Assert match

产生这样的原因是:borrowInfo.getBorrowId() 返回的是 Long,而不是 longAssert 类中有很多 assertEquals() 方法的重载,因此编译器就蒙了,是该将参数类型都转换成 Object 呢?还是只将 Long 转换成 long 呢?

解决方式:明确指定参数是基本数据类型,还是 Object 类型。比如:

1
Assert.assertEquals(6L, borrowInfo.getBorrowId().longValue());

或者

1
Assert.assertEquals(6L, (long)borrowInfo.getBorrowId());

参考链接:Ambiguous method call Both assertEquals(Object, Object) in Assert and assertEquals(double, double) in Assert match:

1.2 日期校验注解 @Past

在 DTO 中对某个日期类型的属性使用了 @Past 校验注解,这个注解可以帮助我们校验这个日期属性值是否是过去的日期。

当我在编写单元测试时,我是这样这样设置这个属性值的:

1
dto.setBirth(new Date());

这样编写对单个测试方法进行测试时不会出现问题,如果对整个测试类进行 All in 的单元测试时,有一定概率校验不通过,提示该属性值必须是过去的日期。

注意: 是一定概率校验不通过,就是有时候是可以通过的,但是有时候又不能通过。(薛定谔的测试)

解决方式很简单,设计单元测试时传入固定的日期,且这个日期是过去的日期,比如:

1
2
// 2020-12-01 00:00:00 ==> 1606752000000
dto.setBirth(new Date(1606752000000L));

1606752000000L 是一个时间戳,表示:2020-12-01 00:00:00

1.3 无法创建 SpringBoot 项目

使用 IDEA 创建 SpringBoot 项目时,可能由于 https://start.spring.io/ 网址的原因(比如,无法访问)导致无法常见项目,或者创建项目失败,项目结构无法创建,我们可以更改默认的创建网址来拉取 SpringBoot 项目:

更改拉取SpringBoot项目地址

网址为:https://start.springboot.io/

1.4 Redis BGSAVE方法失败

参考链接:MISCONF Redis is configured to save RDB snapshots, but it is currently not a

快速应急方案

修改 Redis 的配置文件 redis.conf,将 stop-writes-on-bgsave-error yes 修改为 stop-writes-on-bgsave-error no,或者在 Redis 客户端中执行以下命令:

1
config set stop-writes-on-bgsave-error no

解决方案

在 Linux 系统中,切换至 / 目录下,执行以下命令:

1
vi /etc/sysctl.conf

修改 /etc/sysctl.conf 文件,添加配置:

vm.overcommit_memory=1

如:

修改sysctl.conf文件

添加完毕后,在 管理员模式 下,执行以下命令,使其生效:

1
sysctl -p /etc/sysctl.conf

1.5 Command line is too long

使用 IDEA 启动测试类进行测试时,出现以下错误:

Error running 'Test1.test': Command line is too long. Shorten command line for Test1.test or also for JUnit default configuration.

解决方法一

最简单粗暴的方法:

创建一个新的测试类,然后在这个测试类中将报错的测试类中的代码复制过来,就完事了。

解决方式二

1、打开当前项目的 .idea 文件夹,找到文件夹中的 workspace.xml文件。

2、搜索 PropertiesComponent

PropertiesComponent

3、然后在这个父级结构中添加:

1
<property name="dynamic.classpath" value="true" />

添加dynamic.classpath的property

4、如果这样配置后还是有问题,可以在以下界面中,将 Shorten command line 修改为 JAR manifest

修改Shorten-command-line

1.6 整合 Redis 时出现 Unable to connect to

Springboot 配置 Redis 时出现 Unable to connect to 和 ERR invalid password

检查配置文件后发现是密码字段没有使用引号,只需要加上引号之后就可以了。

但一般来说,即使不使用引号应该也是可以连接上的。

1.7 IDEA Git 出现 Line Separators Warning

当我们使用 IDEA 的 Git Commit 代码时,可能会出现以下窗口:

LineSeparatorsWarning窗口

出现这种情况主要是因为不同的操作系统采用的行分隔符的方式不同。

在 Windows 下采用的是 CRLF 方式,即回车并换行。CR 是老版本 MAC 的做法,即回车,后来 MAC 系统统一成了 LF,LF 也是 Linux 下的做法,即回车。

关于这三者的关系,有兴趣的朋友可以参考这篇文章:趣谈、浅析CRLF和LF

一般操作系统上的运行库会自动决定文本文件的换行格式。 如一个程序在 Windows 上运行就生成 CRLF 换行格式的文本文件,而在 Linux 上运行就生成 LF 格式换行的文本文件。

需要注意的是 在一个平台上使用另一种换行符的文件文件可能会带来意想不到的问题。

那怎么搞?🤪

通常来说,IDE 或文本编辑器都带有换行符转换功能,使用这个功能可以将文本文件中的换行符在不同格式单互换。

场景复现

我是在 Windows 操作系统下新建了一个文件,在对这个文件编写好代码后,需要 Commit 到本地仓库,这个时候就出现了开头的窗口。

这是因为 Windows 下使用的是 CRLF 换行方式,我创建的文件的换行方式也是 CRLF,与仓库中其他文件文件的换行方式不同,所以 IDEA 就弹出了这个窗口。

解决方式

解决方式很简单,前往我们新建的文件,将 IDEA 右下角的行分隔符设置由 CRLF 修改为 LF 即可,如:

设置文件的行分隔符

注意: Windows 下是从 CRLF 改成 LF,而 MAC 下相反。

这样再 Commit 代码就不会出现这个提示框了,问题暂时解决。🤨

为什么说暂时解决,因为我们下次再创建文件时,右下角行分隔符的设置还是 CRLF,虽然也可以点一下修改,那有没有什么一劳永逸的方法?🤔

诶!有的!🧐

这主要是因为 CRLF 是跟随系统来设置的,因为当前系统是 Windows,所以采取的方式就是 CRLF 了。

主需要打开 Settings,切换至 Editor,点击 Code Style,然后将 Line separator 由 System-Dependent 修改为 Unix and MacOS(\n) 即可:

修改IDEA的默认LineSeparator

1.8 Sourcetree 提示 SSH 密钥认证失败

使用 Sourcetree 在推送代码时,可能会出现 启用 SSH 代理? 的提示框,并告诉你:

通过SSH密钥认证失败, 你想要运行SSH密钥代理( Pageant )并重试吗?

一般来说,在安装 Git 后都会绑定 SSH 公钥(不知道如何操作可在本站搜索【Git理论与使用 - Gitee的使用】进行查看),既然已经绑定了,为什么还会出现这个提示框呢?

只需要在 Sourcetree 进行如下设置即可:

点击菜单栏的 工具 - 选项 - 一般,对 SSH 客户端进行配置:

对Sourcetree的SSH客户端进行配置

将 SSH 客户端配置为 OpenSSH 即可,SSH 秘钥会自动填写(如果没有,自行选择)。

1.9 npm ERR! Cannot read property ‘match’ of undefined

当我们对前端项目执行 npm install 命令时,出现了以下错误:

npm ERR! Cannot read property 'match' of undefined

解决方法:把项目中 package-lock.json 文件删除,再次运行 npm 命令即可。

1.10 VS Code 自带终端 无法执行 yarn

参考链接:vscode 自带终端无法执行yarn

当我们安装并配置好 Yarn 后,使用 VS Code 自带的终端执行 yarn start 命令启动项目时,出现了以下错误:

找不到或无法加载主类

原因: VS Code 中的集成终端使用的是 PowerShell,所以我们要设置一下 PowerShell 的执行权限或者说策略。

其默认执行策略为 Restricted(默认设置),该不允许任何脚本运行。我们可以将其设置为 RemoteSigned,该执行策略可防止 Windows PowerShell 运行没有数字签名的脚本。

有关 Windows PowerShell 执行策略的详细信息,可以查考:关于执行策略

结局方式一

这种方式最简单,在 VS Code 内部中安装名为 yarn 的插件即可。它长这样:

VSCode中yarn插件

解决方式二

1、以 管理员身份 运行 VS Code。

2、打开 VS Code 终端,执行以下命令:

1
2
get-ExecutionPolicy # 查看执行策略
set-ExecutionPolicy RemoteSigned # 设置执行策略

3、重启 VS Code,再打开终端,执行 yarn start 试试? 😉

1.11 SpringBoot 项目运行后无法打开 Swagger 界面

运行 SpringBoot 项目后,能够访问到 Swagger 的 api-docs 页面,数据能够正常显示,但是无法加载 Swagger 界面,界面出现 404 错误码。

除了网上能够找到的解决方法外,还可能是由于 IDEA 的配置造成的。

修改项目的启动配置:

SwaggerUI无法访问进行的配置

Shorten command line 修改为 classpath file 即可,当然使用默认的 user-local default 也行,但是使用 JAR manifest 是不行的,将无法访问 Swagger UI 界面。

1.12 LF will be replaced by CRLF

参考链接:

关于 LF will be replaced by CRLF 问题出现的原因以及解决方式

关于git提示“warning: LF will be replaced by CRLF”终极解答

具体分析与解决

在 Windows 操作系统上使用 Git 命令行提交代码时,有时候会出现这样的警告:

warning: LF will be replaced by CRLF in xxxxx

原因很简单:Windows 中的换行符为 CRLF,而在 Linux 下的换行符为 LF,所以在将修改的文件添加至暂存区时就会出现提示。

如果你在 Windows 下想要 开启 换行符的转化,可以执行以下命令:

1
2
# 提交时转换为LF,检出时转换为CRLF
git config --global core.autocrlf true

在 Linux 或 Mac 中,行结束符是 LF,但是当 CRLF 引入文件时,我们应当使用 Git 进行修正,可以执行以下命令:

1
2
# 提交时转换为LF,检出时不转换
git config --global core.autocrlf input

如果仅在 Windows 上进行开发,且开发仅运行在 Windows 上的项目,可以设置 false 取消此功能,把回车保留在版本库中,关闭自动转换的功能即可:

1
2
# 提交检出均不转换
git config --global core.autocrlf false

还可以在文件提交时进行 safecrlf 检查:

1
2
3
4
5
6
7
8
# 拒绝提交包含混合换行符的文件
git config --global core.safecrlf true

# 允许提交包含混合换行符的文件
git config --global core.safecrlf false

# 提交包含混合换行符的文件时给出警告
git config --global core.safecrlf warn

总结

一般来说,不建议关闭这个功能,因为实际项目开发时是协同合作,两个开发者的操作系统不一致时常发生,所以建议开启这个功能。

如果是使用在 Windows 上编写自己的项目,关闭这个功能也是可以的。

而且这只是一个警告,可以直接忽略,对我们整体工作不会造成影响!

1.13 Error:java: Compilation failed: internal java compiler error

在 IDEA 中运行项目时,在编译期控制台可能会出现这个错误:

Error:java: Compilation failed: internal java compiler error

出现这个问题主要是因为 JDK 的版本问题。一个是编译版本不匹配,一个是当前项目 JDK 版本不支持。

主要需要在 IDEA 查看以下三个地方的 JDK 版本,看它们是否一致且符合要求:

首先进行以下步骤查看项目的 JDK 版本:

1、 File -> Project Structure -> Project Settings -> Project

2、或者使用快捷键 Ctrl + Alt + Shift + S

查看项目JDK版本

然后点击上图中 Modules 查看当前 Module 对应 JDK 版本:

当前Module对应JDK版本

最后查看 Java 编译器版本:

查看Java编译器版本

1.14 MySQL 查询条件不区分大小写?

场景重现

今天在对数据库进行如下条件查询时出现了意想不到的结果:

1
SELECT * FROM tb_user WHERE username = 'mofan';

很显然,我这里是想要查询 usernamemofan 的数据,结果在运行上述 SQL 语句时,查询得到了 usernameMofan 的数据。

这是怎么个情况,数据库不区分大小写?

不对啊,我记得区分啊,那咋回事?难不成是我使用的 MyBatis-Plus 的问题?

解决思路

在使用数据库可视化工具(比如 Navicat)创建 MySQL 数据库时,需要我们指定数据库名称、字符集(CHARSET)和排序规则(COLLATE)。

指定之后,当我们在这个数据库下创建表时、在表中新添字段时,都会 默认 采用数据库的字符集和排序规则。

小贴士: 我们一般将数据库、表、表中字段的字符集设置为 utf8mb4。MySQL 中 utf8 字符集不是真正的 UTF-8utf8mb4 才是真正的 UTF-8

如果在创建数据表时,没有指定 字符集和排序规则,那么这张数据表将会默认采用所在数据库的字符集和排序规则;如果在表中新增字段时 没有指定 字符集和排序规则,那么这个字段将会默认采用所在数据表的字符集和排序规则。

回归正题,在进行条件查询时没有区分查询条件的大小写是 由字段的排序规则造成的

MySQL 的各个排序规则结尾由以下三种组成,它们也代表了不同的含义:

结尾 含义
_bin 将字符串中的每一个字符用二进制数据存储,区分大小写。
_ci 不区分大小写,ci 为 case insensitive 的缩写,即大小写不敏感。
_cs 区分大小写,cs为 case sensitive 的缩写,即大小写敏感。

因此,MySQL 查询条件不区分大小写的解决方案就是:将字段的排序规则设置为以 _cs 或 _bin 结尾

当然为了后续操作方便,建议一并将数据库和数据表的排序规则也设置为以 _cs_bin 结尾。

1.15 移除 Goovy 导致编译报错

近期公司移除了项目中的 Goovy,但由于 IDEA 会默认对其进行编译,所以 IDEA 出现了编译报错,我们只需要在设置中删除 !?*.groovy 即可:

删除goovy的编译设置

1.16 Chrome 打开开发者工具缓慢

在有次更新 Chrome 后,不知道点到了那,后续在打开开发者工具时等半天才打开。可以对 Chrome 的设置进行重置就好了:

重置Chrome设置

1.17 git reset --hard HEAD^ 后显示 more?

在 Windows 的 CMD(或 IDEA 的 Terminal)中执行 git reset --hard HEAD^ 命令后,显示 more?,自觉告诉我按下回车即可,结果多次按下回车后出现以下错误:

fatal: ambiguous argument 'HEAD
': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

这是因为 CMD 中默认的换行符恰好是 ^,出现的 more? 就是在询问是否要再输入,而 ^ 被当成换行符后就被 Git 命令忽略了,因此多次回车后就报错了。

处理方法:

1、加引号:git reset --hard "HEAD^"

2、多加个 ^git reset --hard HEAD^^

3、换成 ~git reset --hard HEAD~ 或者 git reset --hard HEAD~1,这表示回退 1 次提交,单独一个 ~ 默认是一次

4、不使用 CMD(或 IDEA 的 Terminal)执行 Git 命令,换用 PowerShell 或者 Git bash

2. 经验之谈

2.1 Spring 的 @Scope 注解

@Scope 可以用来设置 Spring 容器中 Bean 的作用域,默认作用域是单例的,使用这个注解后可以将其设置成其他作用域。

在此不介绍 @Scope 注解的使用方式,但需要明白的是:

默认的单例作用域适用 90% 的场景。如果需要更改作用域,请在更改之前确认是不是由于自己的代码设计缺陷而导致需要更改作用域。

2.2 单元测试心得

在进行单元测试时:

对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。

保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。

单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 或 JUnit 的 Assert 等来验证。

注意: 注意在测试类中的 成员变量静态变量 的区别。如果需要所有测试方法共享那个变量,需要使用静态变量,反之使用成员变量,并且各测试方法每次使用成员变量时,都会初始化成员变量。

【强烈建议】

1、运行单元测试前后,数据库表中都不能存在数据。如果在运行测试代码时,出现数据无法保存导致的异常或错误,请检查数据库中数据是否符合该条件。

2、为了测试覆盖率能够达标,有时会对同一个方法进行多次调用测试以便能够覆盖到各个分支,但 在每次调用时不要使用同一份入参 进行测试,因此在被测试的方法内部很可能会对入参进行修改,这时再使用这份数据进行测试产生的结果就很有可能不符合我们的预期结果。

2.3 @Accessors 与 链式

在 Lombok 中有这样一个注解 @Accessors

1
2
3
4
5
6
7
8
9
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Accessors {
boolean fluent() default false;

boolean chain() default false;

String[] prefix() default {};
}

其中有一个默认值为 false,类型为 boolean 名为 chain 的属性,如果在使用 Accessors 注解时将 chain 设置为 true,即:

1
@Accessors(chain = true)

表示当前类下所有属性或当前字段支持链式编程,即:Setter 方法不再返回 void,而是 this

但需要注意的是,链式编程用起来很爽,但不要随意使用,尤其是在涉及到 Bean 映射的场景,比如:BeanCopier、MapStruct 等都不支持链式,同时 EasyExcel 也不支持链式。

主要原因是:它们都存在从源中 get 出数据,然后 set 到目标中,而它们所使用的 Setter 都是返回 void

因此:不推荐随意使用链式编程。

2.4 toMap() 的使用

在 Java8 的 Stream 流中提供了集合转 Map 的方法,即:toMap() 方法,但此方法的使用有以下两点注意事项:

1、转换后的 Map 的 value 不能为 null,否则会抛出 NullPointException

2、转换后的 Map 的 key 不能重复,否则会抛出 IllegalStateException

转换后的 Map 的 value 为什么不能为 null

Collectors 中的 toMap() 方法有很多重载,我们一般会选择使用两个参数的 toMap() 方法,即:

1
2
3
4
5
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

这个方法调用了 4 个参数的 toMap() 方法:

1
2
3
4
5
6
7
8
9
10
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}

该方法的函数含义如下:

1、keyMapper:key 的映射函数;
2、valueMapper:value 的映射函数;
3、mergeFunction:key 冲突时,调用的合并方法;
4、mapSupplier:Map 构造器,在需要返回特定的 Map 时使用;

并且我们可以看到此方法中调用了 Map 接口中的 merge() 方法,merge() 方法也是 Java8 中新增的。

再回到两个参数的 toMap() 方法中,这个方法使用的默认 Map 构造器是 HashMap 的,因此查看 HashMap 的 merge() 方法:

1
2
3
4
5
6
7
8
9
10
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
// ...
}

可以看到一开始就对 value 进行了判断,如果为 null,就抛出 NullPointException

转换后的 Map 的 key 为什么不能重复

merge() 方法进行简单的分析,可以知道 merge() 方法的作用如下:当 Map 中不存在指定的 key 时,便将传入的 value 设置为 key 对应的值,相当于 put(key, value);当 key 存在值时执行 remappingFunction.apply() 方法,方法接收 key 对应的旧值和 merge()方法传入的 value,apply() 方法的返回值为 key 新对应的值。

通过前面的分析,merge() 方法是由 4 个参数的 toMap() 方法调用的,merge() 方法的第三个参数是由 toMap() 方法的第三个参数指定的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

throwingMerger() 如下:

1
2
3
private static <T> BinaryOperator<T> throwingMerger() {
return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
}

这下就不难明白为什么会抛出 IllegalStateException 异常了。

除此之外,也可以自定义 Key 冲突合并方法,比如将 List<Person> 转换成 Map,当 Key 冲突时,以新值代替旧值:

1
Map<String, Person> map = list.stream().collect(Collectors.toMap(Person::getId, Person::getName, (oldValue, newValue) -> newValue));