修复:启用评论组件
This commit is contained in:
104
docs/categories/fragments/2022/03/25/合并两个Git仓库的历史提交记录.md
Normal file
104
docs/categories/fragments/2022/03/25/合并两个Git仓库的历史提交记录.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: 合并两个Git仓库的历史提交记录
|
||||
author: 查尔斯
|
||||
date: 2022/03/25 21:30
|
||||
categories:
|
||||
- 杂碎逆袭史
|
||||
tags:
|
||||
- Git
|
||||
---
|
||||
|
||||
# 合并两个Git仓库的历史提交记录
|
||||
|
||||
## 前言
|
||||
|
||||
**C:** 最近在下班的时间一直在维护一个基于 EL-Admin 这个开源后台管理系统的衍生开源项目。EL-Admin 这个项目是采用前后端分离架构开发的,所以在开源平台上是分为了两个项目库,一个前端的,一个后端的。
|
||||
|
||||
这本无可厚非,分成两个项目库,在开发时还是挺友好的,公司内部基本也是这个模式,但对于一个开源项目来说,分散为两个库还是有一些不利的方面。
|
||||
|
||||
1. 在管理 issue 上不太方便,项目作者要兼顾查看两个项目,而且有些小伙伴在提出 issue 时并不会管你这是前端库还是后端库,提就完事了。在这方面,EL-Admin 项目的作者应该也发现了这个问题,所以直接干脆的关闭了前端库的 issue 功能,集中在后端库一起管理。
|
||||
2. 在 star 方面会造成分流,前段时间看了看微博,在不知什么时候,竟然加入了一个明星超话,意外就看到置顶帖里标注了一点禁止创建其他小号超话,现在想想这不是一个意思吗?
|
||||
3. ...
|
||||
|
||||
本来笔者最开始也是按原项目形式创建了两个 Git 仓库,但最近更换设备开发时单独要拉两次仓库,觉得很麻烦,思索了下突然意识到上述问题,干脆趁着这热乎劲儿,以后端仓库为主,把两个仓库合并一下。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 将前端项目提交到后端库
|
||||
|
||||
这是笔者首先想到的方法,即将前端项目拉下来,然后将前端项目的源码放到后端库里,提交一下。很简单粗暴,但是这种方法会造成之前前端项目历史提交记录的丢失。
|
||||
|
||||
### 不影响提交记录,合并仓库
|
||||
|
||||
笔者当然不希望将前端项目的历史提交记录丢失了,所以最终采用了下方的方案,完整步骤如下:
|
||||
|
||||
::: tip 笔者说
|
||||
|
||||
提示说明一下,后端仓库名叫:eladminx,前端仓库名叫:eladminx-web
|
||||
|
||||
:::
|
||||
|
||||
1. 克隆后端项目仓库到本地(笔者没有在 git bash 中操作,而是在 cmd 中进行的)
|
||||
|
||||
```bash
|
||||
git clone https://gitee.com/Charles7c/eladminx.git
|
||||
cd eladminx
|
||||
```
|
||||
|
||||

|
||||
|
||||
2. 将前端仓库作为后端仓库的远程仓库,起别名为 frontend(这个随便起)
|
||||
|
||||
```bash
|
||||
git remote add -f frontend https://gitee.com/Charles7c/eladminx-web.git
|
||||
```
|
||||
|
||||

|
||||
|
||||
3. 将前端仓库的 master 分支(自己选择哪个分支)合并到后端仓库
|
||||
|
||||
```bash
|
||||
git merge --strategy ours --no-commit frontend/master
|
||||
```
|
||||
|
||||

|
||||
|
||||
想法很美,但是报错了:
|
||||
|
||||
```
|
||||
fatal: refusing to merge unrelated histories
|
||||
```
|
||||
|
||||
这是因为后端仓库的本地分支历史记录和前端仓库的历史记录不匹配,人家 Git 怀疑你是不是合并错了,但咱们知道就是要合并,写个声明 “表明出事儿与人家无关”就可以了。
|
||||
|
||||
```bash
|
||||
git merge --strategy ours --allow-unrelated-histories --no-commit frontend/master
|
||||
```
|
||||
|
||||

|
||||
|
||||
4. 将前端仓库的 master 分支内容放到在后端仓库内刚建好的 eladminx-web 文件夹中
|
||||
|
||||
```bash
|
||||
mkdir eladminx-web
|
||||
git read-tree --prefix=eladminx-web/ -u frontend/master
|
||||
```
|
||||
|
||||
5. 提交刚才的修改(毕竟你刚才又合并又创建文件夹的,肯定要提交修改啊)
|
||||
|
||||
```bash
|
||||
git commit -m "迁移前端项目仓库,与后端项目仓库合并"
|
||||
```
|
||||
|
||||
6. 最后将本地的修改强制推送到远程仓库即可
|
||||
|
||||
```bash
|
||||
git push --force
|
||||
```
|
||||
|
||||
到此为止,笔者这两个项目的 master 分支就合并完了,如果你想合并其他分支,例如:dev,那就首先把后端仓库的分支切换到 dev,然后将上述中的 master 这个分支名换为 dev 就可以了。
|
||||
|
||||
## 后记
|
||||
|
||||
**C:** 关于这个合并,你以哪个仓库为主都可以。最后合并的提交记录是以历史提交时间进行降序排列的。
|
||||
|
43
docs/categories/fragments/2022/03/26/修改Git最后一次提交记录的作者和邮箱.md
Normal file
43
docs/categories/fragments/2022/03/26/修改Git最后一次提交记录的作者和邮箱.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: 修改Git最后一次提交记录的作者和邮箱
|
||||
author: 查尔斯
|
||||
date: 2022/03/26 10:30
|
||||
categories:
|
||||
- 杂碎逆袭史
|
||||
tags:
|
||||
- Git
|
||||
---
|
||||
|
||||
# 修改Git最后一次提交记录的作者和邮箱
|
||||
|
||||
## 前言
|
||||
|
||||
**C:** 今天周末了,抽出了一点时间继续维护下之前提到过的衍生开源项目。修了一个 bug 后,提交了一下。但是突然想起来,今天开发用的是工作本,工作本中的 Git author、email 是配的真实姓名和公司邮箱,提交前忘了修改,现在已经推送到开源平台了,这肯定不行啊。
|
||||
|
||||
但是现在即使修改了本地的 Git author、email 配置,历史提交记录也改变不了啊。别着急,看看怎么解决的。
|
||||
|
||||
## 问题解决
|
||||
|
||||
如果你确定是和笔者一样的情况,即确保是要修改最后一次提交记录,无论有没有推送到远程仓库都没问题。解决方法就两步,是不是很简单?
|
||||
|
||||
1. 修改最后一次提交的作者和邮箱信息
|
||||
|
||||
```bash
|
||||
git commit --amend --author="Charles7c <charles7c@126.com>"
|
||||
```
|
||||
|
||||
2. 最后将本地的修改强制推送到远程仓库即可(如果你没推送到远程仓库,这步就不需要执行了)
|
||||
|
||||
```bash
|
||||
git push --force
|
||||
```
|
||||
|
||||
## 后记
|
||||
|
||||
**C:** 另外说一下,如果你要修改最后一次提交记录的 commit message,执行下面的命令就可以了。
|
||||
|
||||
```bash
|
||||
git commit --amend -m "要修改为的提交信息"
|
||||
```
|
||||
|
||||
上方修改作者和邮箱信息的命令,还可以继续加参数 `--date` 来修改提交时间,有需要的话去试试去吧。
|
52
docs/categories/fragments/2022/03/27/修改Git所有提交记录中的作者和邮箱.md
Normal file
52
docs/categories/fragments/2022/03/27/修改Git所有提交记录中的作者和邮箱.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: 修改Git所有提交记录中的作者和邮箱
|
||||
author: 查尔斯
|
||||
date: 2022/03/27 13:00
|
||||
categories:
|
||||
- 杂碎逆袭史
|
||||
tags:
|
||||
- Git
|
||||
---
|
||||
|
||||
# 修改Git所有提交记录中的作者和邮箱
|
||||
|
||||
## 前言
|
||||
|
||||
**C:** 上一篇,笔者介绍了怎么修改 Git 最后一次提交的作者和邮箱信息。那如果你是已经提交了很多次的记录,难道一个个的回退过去修改吗?显然不可能,所以本篇笔者带着大家认识一下如何批量修改 Git 提交记录中的作者和邮箱信息。
|
||||
|
||||
## 问题解决
|
||||
|
||||
解决方法其实就是编写一个脚本来进行批量替换。
|
||||
|
||||
1. 新建一个 sh / bat 格式的脚本文件(如果你是在 cmd 中执行,那就用 bat 格式,如果是在 git bash 中执行就用 sh)
|
||||
|
||||
2. 复制下方脚本内容到脚本文件中,然后编辑替换好错误邮箱、正确作者和邮箱(如果是在 cmd 中执行,#!/bin/sh 就替换为 #!/bin/bat)
|
||||
|
||||
```shell
|
||||
#!/bin/sh
|
||||
|
||||
git filter-branch --env-filter '
|
||||
WRONG_EMAIL="错误的邮箱"
|
||||
NEW_NAME="正确的作者名"
|
||||
NEW_EMAIL="正确的邮箱"
|
||||
|
||||
if [ "$GIT_COMMITTER_EMAIL" = "$WRONG_EMAIL" ]
|
||||
then
|
||||
export GIT_COMMITTER_NAME="$NEW_NAME"
|
||||
export GIT_COMMITTER_EMAIL="$NEW_EMAIL"
|
||||
fi
|
||||
if [ "$GIT_AUTHOR_EMAIL" = "$WRONG_EMAIL" ]
|
||||
then
|
||||
export GIT_AUTHOR_NAME="$NEW_NAME"
|
||||
export GIT_AUTHOR_EMAIL="$NEW_EMAIL"
|
||||
fi
|
||||
' --tag-name-filter cat -- --branches --tags
|
||||
```
|
||||
|
||||
3. 保存脚本
|
||||
|
||||
4. 将脚本文件放到要批量修改提交记录的 Git 仓库中(根目录就行)
|
||||
|
||||
1. 执行脚本
|
||||
|
||||
随后你就会看到,先是提示一个 warn 警告,然后它会一条条的修改以往提交记录,如果错误的提交比较多,那就耐心等一会儿吧。
|
12
docs/categories/fragments/index.md
Normal file
12
docs/categories/fragments/index.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
editLink: false
|
||||
lastUpdated: false
|
||||
aside: false
|
||||
showComment: false
|
||||
---
|
||||
|
||||
# "杂碎"逆袭史
|
||||
|
||||
::: tip 笔者说
|
||||
碎片化知识的时代,纵然知晓不成体系的知识仅仅只能积累经验,却无法有效提升,但身在其中还是要如此,记录碎片知识,留作备忘。说不定后面还有机会 “把点连成线”。
|
||||
:::
|
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: F:\ 上的回收站已损坏。是否清空该驱动器上的"回收站"?
|
||||
author: 查尔斯
|
||||
date: 2021/12/01 22:36
|
||||
categories:
|
||||
- Bug万象集
|
||||
tags:
|
||||
- Windows
|
||||
---
|
||||
|
||||
# F:\ 上的回收站已损坏。是否清空该驱动器上的"回收站"?
|
||||
|
||||
## 问题描述
|
||||
|
||||
近期打开移动硬盘时,经常弹出一个提示框,提示内容是:**F:\ 上的"回收站"已损坏。是否清空该驱动器上的"回收站"?**
|
||||
|
||||

|
||||
|
||||
这个提示框出现过好多回了,除了第一次见到时担心数据丢失,所以比较重视,后来次数多了,点过 “是”,也点过 “否”,只要当下不再影响我就先忽略了。
|
||||
|
||||
但今天在公司打开时,它还没完没了的弹,我决定给它 "上一课"。
|
||||
|
||||
::: tip 笔者说
|
||||
|
||||
先说明一下我的这块移动硬盘情况,我把它分了两个区,在我的工作电脑上分别占据 E盘、F盘,E盘 打开没问题,打开 F盘就弹出这个提示。
|
||||
|
||||
:::
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 尝试1:重新插拔移动硬盘
|
||||
|
||||
我首先考虑的是不是接触不良类的问题,于是我在电脑上弹出了移动硬盘,然后把硬盘接线也重新插拔了一下,再插到电脑上。
|
||||
|
||||
再次打开该盘,依然如此,问题未解决。
|
||||
|
||||
### 尝试2:重启电脑
|
||||
|
||||
我又考虑是不是电脑本次出了点故障,于是我重启了电脑。
|
||||
|
||||

|
||||
|
||||
果然,问题还是没解决了。
|
||||
|
||||
::: tip 笔者说
|
||||
|
||||
不过,重启的确应该成为解决问题的优先方法,毕竟有这么个说法:"重启可以解决世间 90% 的问题"。巨石强森的电影《摩天营救》里不也是这么演的嘛。很明显,我遇到了剩下那 10% 的问题。
|
||||
|
||||
:::
|
||||
|
||||
### 尝试3:删除回收站
|
||||
|
||||
行吧,再次回归问题本身,它一直提示是回收站损坏,而且,此时我突然想到周末在使用 FreeFileSync 比较 NAS 和硬盘数据时,我看到过这块硬盘中有多一个 recylce 名词的目录(图标也是回收站图标)。
|
||||
|
||||
而除了系统回收站,其他硬盘回收站其实对我都无所谓,那就想法找到并干掉它。
|
||||
|
||||
然而,即使我开启了隐藏的项目显示,窗口下肉眼仍无法找到它,那就使用 CMD 吧。
|
||||
|
||||
以管理员身份打开 CMD,复制下方命令,改成你那提示的错误盘符,回车,再输入 y 确定后删除。
|
||||
|
||||
```shell
|
||||
# $RECYCLE.BIN 是回收站名称
|
||||
# rd 是删除命令
|
||||
# /s 代表除目录本身外,还将删除指定目录下的所有子目录和文件,适用于删除目录树。
|
||||
# f:\ 根据你自己提示的是哪个盘报错,你就将 f 改成哪个盘
|
||||
rd /s f:\$RECYCLE.BIN
|
||||
```
|
||||
|
||||

|
||||
|
||||
问题解决了。
|
395
docs/categories/issues/2021/12/08/for循环中删除集合元素隐藏的陷阱.md
Normal file
395
docs/categories/issues/2021/12/08/for循环中删除集合元素隐藏的陷阱.md
Normal file
@@ -0,0 +1,395 @@
|
||||
---
|
||||
title: for循环中删除集合元素隐藏的陷阱
|
||||
author: 查尔斯
|
||||
date: 2021/12/08 20:00
|
||||
categories:
|
||||
- Bug万象集
|
||||
tags:
|
||||
- Java集合
|
||||
---
|
||||
|
||||
# for循环中删除集合元素隐藏的陷阱
|
||||
|
||||
## 前言
|
||||
|
||||
**C:** 今天在审查代码时,发现某位同事提交的代码中有一个比较基础性的错误。
|
||||
|
||||
这部分需求的主要目的是将集合中指定的元素删除掉,而这位同事采用的方法是用 for 循环来循环集合索引,然后通过索引从集合中取出每一个元素,判断是否是要删除的元素,如果是就直接删除掉。
|
||||
|
||||
**大概意思的代码,如下:**
|
||||
|
||||
```java {11,15}
|
||||
// 创建集合,并初始化数据
|
||||
List<Integer> list = new ArrayList<>(4);
|
||||
list.add(1);
|
||||
list.add(2);
|
||||
list.add(3);
|
||||
list.add(4);
|
||||
|
||||
// 删除元素值为 2 的元素
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (Objects.equals(list.get(i), 2)) {
|
||||
list.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(list); // [1, 3, 4]
|
||||
```
|
||||
|
||||
笔者知道,肯定有同学会好奇,这结果是正确的啊,哪里有什么问题?的确,这个思路没问题,问题的关键是这位同事采用的循环方式存在问题。
|
||||
|
||||
别着急,接下来,笔者就带各位同学好好测试一下。
|
||||
|
||||
## 测试代码
|
||||
|
||||
### 基础for循环中删除
|
||||
|
||||
直接放代码吧,下方是使用基础的 for 循环(循环索引)来实现的集合元素删除,比之 前言 中的代码,无非是要删除的元素 2 有重复,变成了两个。
|
||||
|
||||
```java {30,33}
|
||||
/**
|
||||
* List集合-循环中删除元素-测试
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/12/8 20:59
|
||||
*/
|
||||
@DisplayName("List集合-循环中删除元素-测试")
|
||||
public class ListRemoveEleInForLoopTest {
|
||||
|
||||
private List<Integer> list;
|
||||
|
||||
/** 初始化数据 */
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
list = new ArrayList<>(5);
|
||||
list.add(1);
|
||||
list.add(2);
|
||||
list.add(3);
|
||||
list.add(4);
|
||||
list.add(2);
|
||||
}
|
||||
|
||||
/** 运行无异常,测试符合预期 */
|
||||
@Test
|
||||
@DisplayName("基础for循环中删除元素测试")
|
||||
void testBasicForLoop() {
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (Objects.equals(list.get(i), 2)) {
|
||||
// IDEA警告:Suspicious 'List.remove()' in the loop
|
||||
list.remove(i);
|
||||
}
|
||||
}
|
||||
System.out.println(list); // [1, 3, 4]
|
||||
Assertions.assertEquals(list.size(), 3);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
测试结果也是正常的啊,莫非笔者失手了?别着急 ...
|
||||
|
||||
我们再来测试一下,这回我们稍微调整下重复元素的位置,将重复的元素移动到相邻位置。
|
||||
|
||||
```java {30,33}
|
||||
/**
|
||||
* List集合-循环中删除元素-测试
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/12/8 20:59
|
||||
*/
|
||||
@DisplayName("List集合-循环中删除元素-测试")
|
||||
public class ListRemoveEleInForLoopTest {
|
||||
|
||||
private List<Integer> list;
|
||||
|
||||
/** 初始化数据 */
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
list = new ArrayList<>(5);
|
||||
list.add(1);
|
||||
list.add(2);
|
||||
list.add(2);
|
||||
list.add(3);
|
||||
list.add(4);
|
||||
}
|
||||
|
||||
/** 运行无异常,测试不通过 */
|
||||
@Test
|
||||
@DisplayName("基础for循环中删除元素测试")
|
||||
void testBasicForLoop() {
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (Objects.equals(list.get(i), 2)) {
|
||||
// IDEA警告:Suspicious 'List.remove()' in the loop
|
||||
list.remove(i);
|
||||
}
|
||||
}
|
||||
System.out.println(list); // [1, 2, 3, 4]
|
||||
Assertions.assertEquals(list.size(), 3);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
测试不通过,why?
|
||||
|
||||
**原因很简单:** ArrayList 是基于数组结构而来的,在实现 E remove(int index) 方法时,也是在操作数组而已。
|
||||
|
||||
**E remove(int index) 方法的源代码,如下:**
|
||||
|
||||
```java {20,25}
|
||||
/**
|
||||
* Removes the element at the specified position in this list.
|
||||
* Shifts any subsequent elements to the left (subtracts one from their
|
||||
* indices).
|
||||
*
|
||||
* @param index the index of the element to be removed
|
||||
* @return the element that was removed from the list
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public E remove(int index) {
|
||||
rangeCheck(index);
|
||||
|
||||
modCount++;
|
||||
E oldValue = elementData(index);
|
||||
|
||||
int numMoved = size - index - 1;
|
||||
if (numMoved > 0)
|
||||
// 表面看是在拷贝数组,但是源数组和目标数组都是同一个,所以是移动数组元素而已
|
||||
// 例如:[1, 2, 3, 4] -> [1, 3, 4, 4]
|
||||
System.arraycopy(elementData, index+1, elementData, index,
|
||||
numMoved);
|
||||
// 元素数量-1,并清除多余元素
|
||||
// 例如:[1, 2, 3, 4] -> [1, 3, 4, 4]
|
||||
// 最后一个4就是多余的,置为默认值 null
|
||||
elementData[--size] = null; // clear to let GC do its work
|
||||
|
||||
return oldValue;
|
||||
}
|
||||
```
|
||||
|
||||
这样的话就会导致,在循环索引中删除完某个元素,其后面的元素移动到这个元素的位置,但是循环的索引可没回退,这样在取值时就会 **跳过下一个元素** 。(看不懂的话,可以debug一下,很清晰的)
|
||||
|
||||
如果被删除元素的下一个元素不是匹配条件的,那还问题不显,但是如果被删除元素的下一个元素也是匹配条件的,也就会出现刚才测试的结果了。
|
||||
|
||||
知道了问题的根源,要是还想要用这种循环,加一行代码就可以了。
|
||||
|
||||
```java {30,32,35}
|
||||
/**
|
||||
* List集合-循环中删除元素-测试
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/12/8 20:59
|
||||
*/
|
||||
@DisplayName("List集合-循环中删除元素-测试")
|
||||
public class ListRemoveEleInForLoopTest {
|
||||
|
||||
private List<Integer> list;
|
||||
|
||||
/** 初始化数据 */
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
list = new ArrayList<>(5);
|
||||
list.add(1);
|
||||
list.add(2);
|
||||
list.add(2);
|
||||
list.add(3);
|
||||
list.add(4);
|
||||
}
|
||||
|
||||
/** 运行无异常,测试不通过 */
|
||||
@Test
|
||||
@DisplayName("基础for循环中删除元素测试")
|
||||
void testBasicForLoop() {
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (Objects.equals(list.get(i), 2)) {
|
||||
// IDEA警告:Suspicious 'List.remove()' in the loop
|
||||
list.remove(i);
|
||||
// !!!回退索引!!!
|
||||
i--;
|
||||
}
|
||||
}
|
||||
System.out.println(list); // [1, 3, 4]
|
||||
Assertions.assertEquals(list.size(), 3);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### 增强for循环中删除
|
||||
|
||||
显然,在基础 for 循环中删除元素,这种方法并不是最好的,那我们就再来看看其他的循环方式吧。
|
||||
|
||||
简单改动下代码,看看平时出场频率也很高的增强 for 循环会如何?
|
||||
|
||||
```java {29}
|
||||
/**
|
||||
* List集合-循环中删除元素-测试
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/12/8 20:59
|
||||
*/
|
||||
@DisplayName("List集合-循环中删除元素-测试")
|
||||
public class ListRemoveEleInForLoopTest {
|
||||
|
||||
private List<Integer> list;
|
||||
|
||||
/** 初始化数据 */
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
list = new ArrayList<>(5);
|
||||
list.add(1);
|
||||
list.add(2);
|
||||
list.add(2);
|
||||
list.add(3);
|
||||
list.add(4);
|
||||
}
|
||||
|
||||
/** 运行时异常:java.util.ConcurrentModificationException */
|
||||
@Test
|
||||
@DisplayName("增强for循环中删除元素测试")
|
||||
void testForEachLoop() {
|
||||
for (Integer num : list) {
|
||||
if (Objects.equals(num, 2)) {
|
||||
list.remove(num);
|
||||
}
|
||||
}
|
||||
System.out.println(list);
|
||||
Assertions.assertSame(list.size(), 3);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
测试中断,删除一个元素后继续循环会抛出运行时异常:java.util.ConcurrentModificationException。 Pass ...
|
||||
|
||||
### 迭代器中删除
|
||||
|
||||
最后,我们再尝试一种循环:迭代器,可能对于部分同学来说,平时使用相对要少一些。
|
||||
|
||||
```java {31,34}
|
||||
/**
|
||||
* List集合-循环中删除元素-测试
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/12/8 20:59
|
||||
*/
|
||||
@DisplayName("List集合-循环中删除元素-测试")
|
||||
public class ListRemoveEleInForLoopTest {
|
||||
|
||||
private List<Integer> list;
|
||||
|
||||
/** 初始化数据 */
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
list = new ArrayList<>(5);
|
||||
list.add(1);
|
||||
list.add(2);
|
||||
list.add(2);
|
||||
list.add(3);
|
||||
list.add(4);
|
||||
}
|
||||
|
||||
/** 运行无异常,测试符合预期 */
|
||||
@Test
|
||||
@DisplayName("迭代器中删除元素测试")
|
||||
void testIterator() {
|
||||
Iterator<Integer> iterator = list.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Integer num = iterator.next();
|
||||
if (Objects.equals(num, 2)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
System.out.println(list); // [1, 3, 4]
|
||||
Assertions.assertSame(list.size(), 3);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
测试通过,这种方式也是平时 **推荐大家采用** 的,而且在 Java 8 中,官方还为我们在 Collection 接口中提供了一个 default 方法来简化集合删除元素。
|
||||
|
||||
```java {28}
|
||||
/**
|
||||
* List集合-循环中删除元素-测试
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/12/8 20:59
|
||||
*/
|
||||
@DisplayName("List集合-循环中删除元素-测试")
|
||||
public class ListRemoveEleInForLoopTest {
|
||||
|
||||
private List<Integer> list;
|
||||
|
||||
/** 初始化数据 */
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
list = new ArrayList<>(5);
|
||||
list.add(1);
|
||||
list.add(2);
|
||||
list.add(2);
|
||||
list.add(3);
|
||||
list.add(4);
|
||||
}
|
||||
|
||||
/** 运行无异常,测试符合预期 */
|
||||
@Test
|
||||
@DisplayName("迭代器中删除元素测试")
|
||||
void testIterator() {
|
||||
// Java 8 在 Collection 接口中提供的 default 方法
|
||||
list.removeIf(num -> Objects.equals(num, 2));
|
||||
System.out.println(list); // [1, 3, 4]
|
||||
Assertions.assertSame(list.size(), 3);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
**Collection 接口的 removeIf() 方法的源代码,如下:**
|
||||
|
||||
```java {27,30}
|
||||
public interface Collection<E> extends Iterable<E> {
|
||||
/**
|
||||
* Removes all of the elements of this collection that satisfy the given
|
||||
* predicate. Errors or runtime exceptions thrown during iteration or by
|
||||
* the predicate are relayed to the caller.
|
||||
*
|
||||
* @implSpec
|
||||
* The default implementation traverses all elements of the collection using
|
||||
* its {@link #iterator}. Each matching element is removed using
|
||||
* {@link Iterator#remove()}. If the collection's iterator does not
|
||||
* support removal then an {@code UnsupportedOperationException} will be
|
||||
* thrown on the first matching element.
|
||||
*
|
||||
* @param filter a predicate which returns {@code true} for elements to be
|
||||
* removed
|
||||
* @return {@code true} if any elements were removed
|
||||
* @throws NullPointerException if the specified filter is null
|
||||
* @throws UnsupportedOperationException if elements cannot be removed
|
||||
* from this collection. Implementations may throw this exception if a
|
||||
* matching element cannot be removed or if, in general, removal is not
|
||||
* supported.
|
||||
* @since 1.8
|
||||
*/
|
||||
default boolean removeIf(Predicate<? super E> filter) {
|
||||
Objects.requireNonNull(filter);
|
||||
boolean removed = false;
|
||||
final Iterator<E> each = iterator();
|
||||
while (each.hasNext()) {
|
||||
if (filter.test(each.next())) {
|
||||
each.remove();
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
// 省略其他代码...
|
||||
}
|
||||
```
|
||||
|
||||
很显然,官方也是用的迭代器来实现的。
|
||||
|
||||
## 后记
|
||||
|
||||
**C:** 虽然是一个小问题,但是见到的犯错者无数,以前并未当回事,这次遇到正好记录一下,给各位同学一个提醒。
|
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Command line is too long. Shorten command line for XXX or also for Spring Boot default configuration?
|
||||
author: 查尔斯
|
||||
date: 2021/12/10 22:11
|
||||
categories:
|
||||
- Bug万象集
|
||||
tags:
|
||||
- IDE
|
||||
- IntelliJ_IDEA
|
||||
---
|
||||
|
||||
# Command line is too long. Shorten command line for XXX or also for Spring Boot default configuration?
|
||||
|
||||
## 问题描述
|
||||
|
||||
今天笔者在公司从测试环境拉取了一个 bugfix 分支之后,等待 Maven 依赖也加载完了,点了【Debug】运行按钮,想起身去接杯水,跳过项目启动的这段时间。
|
||||
|
||||
结果,刚握住水杯,就看到 IntelliJ IDEA 在左下角弹出了一个错误提示框,如下:
|
||||
|
||||

|
||||
|
||||
看提示的意思是命令行太长了,让缩短一下命令行。又点了两下【Debug】运行按钮,依然不依不饶的弹出这个提示,那就放下水杯解决吧。
|
||||
|
||||
|
||||
## 原因分析
|
||||
我想了想原因,就明白什么问题了,给大家贴一下启动的项目程序所在位置。
|
||||
|
||||
- 仓库目录(.git目录)
|
||||
- 项目doc目录
|
||||
- src
|
||||
- 项目源码父级项目目录
|
||||
- 启动入口所在项目目录
|
||||
- src\main\java(三级目录)
|
||||
- com\xx\xxx(N级的包目录)
|
||||
- Spring Boot 项目启动类
|
||||
- 若干模块项目目录
|
||||
- pom.xml
|
||||
|
||||
这个结构,不好多说什么,历史遗留, doc 和源码放在了一个仓库,层级的确挺深,但一般情况下也不会出现此问题,这次算是一个特殊情况。
|
||||
|
||||
|
||||
## 解决方案
|
||||
其实问题解决起来也不难,这种问题笔者以前也遇到过,不过当时也忘了怎么切到了 IntelliJ IDEA 修复提示内,选了一下就结束了。
|
||||
|
||||
而这一次笔者没找到正确的修复入口,所以只能采用手动修改配置的方法了。
|
||||
|
||||
双击打开项目根目录下的 `.idea` 目录,这个目录下都是 IntelliJ IDEA 自动保存的项目配置内容,一般情况下我们不需要关注它,但这次我们需要找到其中的 `workspace.xml` 配置文件,手动修改一下配置。
|
||||
|
||||

|
||||
|
||||
按下 【Ctrl + F】,在弹出的搜索框中,输入【PropertiesComponent】回车,定位到该项配置后,在其所在的 `<component>` 标签内最后部分添加一条属性配置,如下:
|
||||
|
||||
```xml
|
||||
<property name="dynamic.classpath" value="true" />
|
||||
```
|
||||
|
||||

|
||||
|
||||
添加完后,关闭该配置文件即可,再次点击【Debug】运行按钮,项目正常启动了,笔者也该去接水了。
|
249
docs/categories/issues/2021/12/11/SQL 注入攻击风险.md
Normal file
249
docs/categories/issues/2021/12/11/SQL 注入攻击风险.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
title: SQL 注入攻击风险
|
||||
author: 查尔斯
|
||||
date: 2021/12/11 22:51
|
||||
categories:
|
||||
- Bug万象集
|
||||
tags:
|
||||
- SQL
|
||||
- 网络攻击
|
||||
---
|
||||
|
||||
# SQL 注入攻击风险
|
||||
|
||||
## 前言
|
||||
|
||||
**C:** Java 开发者都知道,想要用 Java 连接关系型数据库进行操作,就要学习使用 java.sql 包下的一套 JDBC API,这套 API 的使用步骤,大致如下:
|
||||
|
||||
```java {30}
|
||||
/**
|
||||
* JDBC,模拟登录示例
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/12/11 22:51
|
||||
*/
|
||||
public class JdbcLoginDemo {
|
||||
public static void main(String[] args) {
|
||||
// 录入登录信息
|
||||
Scanner input = new Scanner(System.in);
|
||||
System.out.print("请输入用户名:");
|
||||
String username = input.next();
|
||||
System.out.print("请输入密码:");
|
||||
String password = input.next();
|
||||
|
||||
// 查询数据库,验证登录信息
|
||||
boolean loginResult = false;
|
||||
Connection conn = null;
|
||||
Statement statement = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
// 1、注册驱动
|
||||
Class.forName("com.mysql.jdbc.Driver");
|
||||
// 2、获取连接
|
||||
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/school", "root", "root");
|
||||
// 3、创建语句执行平台
|
||||
statement = conn.createStatement();
|
||||
// 4、编写SQL语句
|
||||
// String sql = "SELECT * FROM `user` WHERE `username` = '" + username + "' AND `password` = '" + password + "'";
|
||||
String sql = String.format("SELECT * FROM `user` WHERE `username` = '%s' AND `password` = '%s'", username, password);
|
||||
// 5、执行SQL语句
|
||||
rs = statement.executeQuery(sql);
|
||||
// 6、解析结果集
|
||||
if (rs.next()) {
|
||||
loginResult = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
// 7、释放资源,先开后关
|
||||
try {
|
||||
if (rs != null) {
|
||||
rs.close();
|
||||
}
|
||||
if (statement != null) {
|
||||
statement.close();
|
||||
}
|
||||
if (conn != null) {
|
||||
conn.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 输出登录结果
|
||||
System.out.println(loginResult ? "登录成功!" : "登录失败!用户名或密码错误!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上方是一个非常经典的 JDBC 模拟登录示例,同样的,很多同学可能一眼就看出来了,它存在着一个严重的攻击漏洞:SQL 注入攻击。
|
||||
|
||||
今天,咱们就一块来聊聊 SQL 注入(SQL Injection)这个东西。
|
||||
|
||||
## SQL 注入攻击简介
|
||||
|
||||
SQL 注入(SQL Injection)作为一种比较常见的网络攻击方式,在学习 JDBC 时就肯定会得到老师的重点提醒。它的出现原因就是因为开发者编写的 SQL 语句,采用拼接的方式来接受输入参数。
|
||||
|
||||
看看上方代码的第 4 步骤,一条 通过用户名和密码来查询用户记录 的简单查询 SQL,它在接受用户名和密码两个输入参数时,是直接拼接到查询 SQL 语句上的。
|
||||
|
||||
```java
|
||||
// 下方两种形式都一样,笔者个人相对更喜欢使用格式化字符串而已
|
||||
// String sql = "SELECT * FROM `user` WHERE `username` = '" + username + "' AND `password` = '" + password + "'";
|
||||
String sql = String.format("SELECT * FROM `user` WHERE `username` = '%s' AND `password` = '%s'", username, password);
|
||||
```
|
||||
|
||||
假设是一个正常的用户输入:
|
||||
|
||||
- 用户名:admin
|
||||
- 密码:123456
|
||||
|
||||
那最终执行的查询 SQL 语句,如下:
|
||||
|
||||
```sql
|
||||
SELECT * FROM `user` WHERE `username` = 'admin' AND `password` = '123456';
|
||||
```
|
||||
|
||||
这倒是没什么问题,但是如果是一个攻击者恶意的输入:
|
||||
|
||||
- 用户名:luanShuDe(胡乱输入的)
|
||||
- 密码:luanShuDe' OR '1' = '1(密码也是胡乱输入的,重点在后面部分)
|
||||
|
||||
那最终执行的查询 SQL 语句,如下:
|
||||
|
||||
```sql
|
||||
SELECT * FROM `user` WHERE `username` = 'luanShuDe' AND `password` = 'luanShuDe' OR '1' = '1';
|
||||
```
|
||||
|
||||
胡乱输入的用户名和密码肯定查询不到结果,但是密码后面的内容由于是 SQL 语法,直接拼接到查询 SQL 语句内,最终也是会执行的,1和1是恒等的,而 OR 运算符是只要一个条件满足,就匹配,所以结果就会查询出所有的用户记录。
|
||||
|
||||
这就导致本该登录失败的情况,却判定登录成功了!也就达成了一次相对简单的 SQL 注入攻击了。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### JDBC 的 PreparedStatement
|
||||
|
||||
问题是要解决的,而且 JDBC 早就提供了相应的解决方法。那就是采用 Statement 的子接口 PreparedStatement,使用步骤如下:
|
||||
|
||||
::: tip 笔者说
|
||||
|
||||
Prepared 从单词意思上就知道是:准备好的,有准备的。
|
||||
|
||||
PreparedStatement 的对象包含了编译好的 SQL 语句。这种 “准备好” 的方式不仅能提高安全性,解决 SQL 注入问题,而且在多次执行同一个 SQL 时,无需再编译,能够提高效率。
|
||||
|
||||
:::
|
||||
|
||||
```java {27}
|
||||
/**
|
||||
* JDBC,模拟登录示例
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/12/11 22:51
|
||||
*/
|
||||
public class JdbcLoginDemo2 {
|
||||
public static void main(String[] args) {
|
||||
// 录入登录信息
|
||||
Scanner input = new Scanner(System.in);
|
||||
System.out.print("请输入用户名:");
|
||||
String username = input.next();
|
||||
System.out.print("请输入密码:");
|
||||
String password = input.next();
|
||||
|
||||
// 查询数据库,验证登录信息
|
||||
boolean loginResult = false;
|
||||
Connection conn = null;
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
// 1、注册驱动
|
||||
Class.forName("com.mysql.jdbc.Driver");
|
||||
// 2、获取连接
|
||||
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/school", "root", "root");
|
||||
// 3、编写SQL语句,使用 ? 进行参数占位
|
||||
String sql = "SELECT * FROM `user` WHERE `username` = ? AND `password` = ?";
|
||||
// 4、创建语句执行平台
|
||||
ps = conn.prepareStatement(sql);
|
||||
// 5、设置参数
|
||||
ps.setString(1, username);
|
||||
ps.setString(2, password);
|
||||
// 6、执行SQL语句
|
||||
rs = ps.executeQuery();
|
||||
// 7、解析结果集
|
||||
if (rs.next()) {
|
||||
loginResult = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
// 8、释放资源,先开后关
|
||||
try {
|
||||
if (rs != null) {
|
||||
rs.close();
|
||||
}
|
||||
if (ps != null) {
|
||||
ps.close();
|
||||
}
|
||||
if (conn != null) {
|
||||
conn.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 输出登录结果
|
||||
System.out.println(loginResult ? "登录成功!" : "登录失败!用户名或密码错误!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MyBatis 的 #{}
|
||||
|
||||
在平时的开发中,我们基本上都在采用 ORM 框架来解决持久层问题,MyBatis 作为一个在国内常用的半自动 ORM 框架,底层就是对 JDBC 的封装,简化了大量模板化的代码。
|
||||
|
||||
如果你也使用了 MyBatis,那么在 SQL 语句传参时,一定要注意使用 #{} 的方式,它最终的实现就是 JDBC 的 PreparedStatement。
|
||||
|
||||
### 特殊符号检查过滤
|
||||
|
||||
MyBatis 还有一种 ${} 也可以来接受参数,但是这种方式最终就是直接在 SQL 语句中拼接输入参数,所以它存在 SQL 注入攻击的风险。
|
||||
|
||||
如果真的想用,可以采用对输入参数进行特殊符号检查过滤。检查过滤的代码,可参考如下:
|
||||
|
||||
```java
|
||||
public class CheckUtils {
|
||||
/**
|
||||
* 校验条件参数不可包含特殊字符,并且小于255个字符
|
||||
*
|
||||
* @param 条件参数内容
|
||||
* @throws Exception 具体错误信息
|
||||
*/
|
||||
public static void checkCondition(String param) throws Exception {
|
||||
if (param != null) {
|
||||
String regEx = "[`~!@#$%^&*+=|{}':;',\\[\\]<>?~!@#¥%……&*()+|{}【】‘;:”“’。,、?]|\n|\r|\t";
|
||||
Matcher matcher = Pattern.compile(regEx).matcher(param);
|
||||
if (param.length() < 0 || param.length() > 255) {
|
||||
throw new Exception("查询条件最长字符255!");
|
||||
}
|
||||
if (matcher.find()) {
|
||||
throw new Exception("查询条件中不应包含特殊字符!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当然了,笔者个人认为基本上能用 ${} 的地方都可以采用 #{} 替代。不过,倒也是见过一些同事在写代码时坚持用 ${} ,代码片段类似如下:
|
||||
|
||||
```sql
|
||||
SELECT * FROM user WHERE username LIKE '%${username}%'
|
||||
```
|
||||
|
||||
的确,LIKE 模糊查询时,后面模糊条件的 `%` 等符号是不能直接出现在 SQL 语句里的,而是要写在由 `'` (单引号)引起的字符串内。但是 MyBatis 的 `#{}` 又无法写在由 `'` (单引号)引起的字符串内,即无法直接写成 `'%#{username}%'` (如果你不相信,可以自行尝试一下,看看控制台会有什么 “惊喜” 输出),这应该就是这部分同事不得不采用 `'%${username}%'` 写法的原因。
|
||||
|
||||
好在,笔者这正好也提供一种解决方法,可以解决此问题,那就是使用 SQL 函数 CONCAT(),代码片段类似如下:
|
||||
|
||||
```sql
|
||||
SELECT * FROM user WHERE username LIKE CONCAT('%', #{username}, '%')
|
||||
```
|
||||
|
||||
没错,既然要拼接字符串,那就用 CONCAT() 函数,这个函数就是专门用来拼接字符串的,在拼接时可以使用 #{} ,所以也就不会存在 SQL 注入的问题了。
|
60
docs/categories/issues/2021/12/13/无法访问F盘。文件或目录损坏且无法读取.md
Normal file
60
docs/categories/issues/2021/12/13/无法访问F盘。文件或目录损坏且无法读取.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: 无法访问 F:\。文件或目录损坏且无法读取。
|
||||
author: 查尔斯
|
||||
date: 2021/12/13 22:57
|
||||
categories:
|
||||
- Bug万象集
|
||||
tags:
|
||||
- Windows
|
||||
---
|
||||
|
||||
# 无法访问 F:\。文件或目录损坏且无法读取。
|
||||
|
||||
## 问题描述
|
||||
|
||||
笔者这块西数的移动硬盘最近真的是问题频发,前段时间无法删除损坏的回收站,这两天在家里电脑上插上之后,双击 F 盘提示已损坏,较之以前问题更甚。
|
||||
|
||||
这的确给了笔者一个 “惊喜”,最近两周好像没开 Drive 备份到 NAS 。硬盘要是坏了,这两周的东西还能剩下多少就不好说了。
|
||||
|
||||
不过好在最后问题解决了,跟笔者来一起看看解决方法吧。
|
||||
|
||||

|
||||
|
||||
|
||||
## 解决方案
|
||||
### 尝试1:尝试检查与修复
|
||||
|
||||
首先,在出现问题的磁盘上【右键】单击,然后选择【属性】。
|
||||
|
||||

|
||||
|
||||
在弹出的【属性】对话框中,选择【工具】选项卡,然后点击【检查】按钮。这个功能是用来检查磁盘文件系统错误的,检查完还会有个错误修复的环节。
|
||||
|
||||

|
||||
|
||||
可惜的是,不知道是笔者这台电脑登录的账号权限问题,还是系统错误,这项修复手段,笔者用不了。
|
||||
|
||||

|
||||
|
||||
### 尝试2:命令行修复
|
||||
|
||||
还是老规矩,桌面可视化中的功能只是一种手段,每一项功能都有其对应的系统命令。
|
||||
|
||||
按下【Windows】键,弹出【开始】菜单,直接输入【cmd】来在菜单中搜索。搜索出来后,在【cmd.exe/命令行】上【右键】单击,选择【以管理员身份运行】。
|
||||
|
||||

|
||||
|
||||
在弹出的 CMD 命令行窗口中,输入以下命令:
|
||||
|
||||
```bash
|
||||
# 这条命令是用来检查磁盘并修复的,中间的 f: 换成你出现上方问题的盘符即可。
|
||||
chkdsk f: /f
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
等待检查修复结束,笔者的 F 盘又回来了。
|
||||
|
||||

|
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: JavaScript 无法存储 Java Long 类型数据问题
|
||||
author: 查尔斯
|
||||
date: 2022/01/26 09:07
|
||||
categories:
|
||||
- Bug万象集
|
||||
tags:
|
||||
- JavaScript
|
||||
---
|
||||
|
||||
# JavaScript 无法存储 Java Long 类型数据问题
|
||||
|
||||
## 问题描述
|
||||
|
||||
今天在解决一个需求问题的时候,遇到了一个难得一见的 JS 问题。这个问题大概是和一些同学在开发环境使用 == 来比较包装类型的整数一样,由于比较的数值在缓存范围内,因缘际会的错过了 bug 的出现。
|
||||
|
||||
简单说一下问题经过,是这样的,笔者这个需求最终要求接口返回一组自定义结构的 k-v (不是单纯的键值对)数据,用于在前端表单中进行分类展示。
|
||||
|
||||
后端响应的数据结构类似如下:
|
||||
|
||||
```json {11,16,29,34}
|
||||
{
|
||||
"code": 200,
|
||||
"errorMsg": "",
|
||||
"result": [
|
||||
{
|
||||
"extend": null,
|
||||
"items": [
|
||||
{
|
||||
"extend": null,
|
||||
"key": "CentOS 8.1 64位",
|
||||
"val": 8014753905961037835
|
||||
},
|
||||
{
|
||||
"extend": null,
|
||||
"key": "CentOS 8.0 64位",
|
||||
"val": 8014753905961037838
|
||||
},
|
||||
],
|
||||
"key": "CentOS",
|
||||
"pubProperty": "",
|
||||
"val": 14
|
||||
},
|
||||
{
|
||||
"extend": null,
|
||||
"items": [
|
||||
{
|
||||
"extend": null,
|
||||
"key": "RedHat Enterprise Linux 8.0 64位",
|
||||
"val": 7979917486755315712
|
||||
},
|
||||
{
|
||||
"extend": null,
|
||||
"key": "RedHat Enterprise Linux 7.7 64位",
|
||||
"val": 8014753905961037829
|
||||
}
|
||||
],
|
||||
"key": "Redhat",
|
||||
"pubProperty": "",
|
||||
"val": 5
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
在前端表单中的展示效果大概如下:
|
||||
|
||||

|
||||
|
||||
## 原因分析
|
||||
|
||||
笔者相信各位同学都应该猜得到,当提交表单的时候,前端肯定会把选中的镜像的 val 值传递给后端,然后由后端继续进行处理。
|
||||
|
||||
但是就在这个环节,由前端传给后端的 val 值竟然错了,例如:当选中了 CentOS 8.1 64 位这个镜像时,本该传递的 val 值为 8014753905961037835,实际传递的却是: 8014753905961038000。
|
||||
|
||||
后端接口测试的响应数据没问题,那问题显然就是出在前端了。
|
||||
|
||||
最终,配合前端开发定位这个问题的原因是因为: JavaScript 中无法存储 Java 中的 Long 类型数据,当位数超过 JavaScript 整数存储的范围,就会以0来代替了。
|
||||
|
||||

|
||||
|
||||
## 解决方案
|
||||
|
||||
JavaScript 存储不了就存储不了吧,咱们这个需求还得解决啊,最终的解决方法就是将后端响应回来的 Long 类型数据转换为字符串。
|
||||
|
||||
```java {2}
|
||||
// 在序列化为 JSON 时将该字段转换为 String 类型
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
private Long val;
|
||||
```
|
||||
|
||||
后端响应的数据结构类似如下:
|
||||
|
||||
```json {11,16,29,34}
|
||||
{
|
||||
"code": 200,
|
||||
"errorMsg": "",
|
||||
"result": [
|
||||
{
|
||||
"extend": null,
|
||||
"items": [
|
||||
{
|
||||
"extend": null,
|
||||
"key": "CentOS 8.1 64位",
|
||||
"val": "8014753905961037835"
|
||||
},
|
||||
{
|
||||
"extend": null,
|
||||
"key": "CentOS 8.0 64位",
|
||||
"val": "8014753905961037838"
|
||||
},
|
||||
],
|
||||
"key": "CentOS",
|
||||
"pubProperty": "",
|
||||
"val": 14
|
||||
},
|
||||
{
|
||||
"extend": null,
|
||||
"items": [
|
||||
{
|
||||
"extend": null,
|
||||
"key": "RedHat Enterprise Linux 8.0 64位",
|
||||
"val": "7979917486755315712"
|
||||
},
|
||||
{
|
||||
"extend": null,
|
||||
"key": "RedHat Enterprise Linux 7.7 64位",
|
||||
"val": "8014753905961037829"
|
||||
}
|
||||
],
|
||||
"key": "Redhat",
|
||||
"pubProperty": "",
|
||||
"val": 5
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
193
docs/categories/issues/2022/03/24/创建一个自身类的静态对象变量,究竟会如何执行?.md
Normal file
193
docs/categories/issues/2022/03/24/创建一个自身类的静态对象变量,究竟会如何执行?.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
title: 创建一个自身类的静态对象变量,究竟会如何执行?
|
||||
author: 查尔斯
|
||||
date: 2022/03/24 21:30
|
||||
categories:
|
||||
- Bug万象集
|
||||
tags:
|
||||
- Java
|
||||
- JVM
|
||||
---
|
||||
|
||||
# 创建一个自身类的静态对象变量,究竟会如何执行?
|
||||
|
||||
## 前言
|
||||
|
||||
**C:** 近两周在疯狂给项目组面试招聘,昨天晚上10点多,产品总监在面试群里发了一道题,问运行结果是什么,题目如下:
|
||||
|
||||
```java {2-4}
|
||||
class Singleton {
|
||||
private static Singleton singleton = new Singleton();
|
||||
public static int count1;
|
||||
public static int count2 = 3;
|
||||
|
||||
private Singleton() {
|
||||
count1++;
|
||||
count2++;
|
||||
}
|
||||
|
||||
public static Singleton getInstance() {
|
||||
return singleton;
|
||||
}
|
||||
}
|
||||
|
||||
public class Test {
|
||||
public static void main(String[] args) {
|
||||
Singleton singleTon = Singleton.getInstance();
|
||||
System.out.println("count1=" + singleTon.count1);
|
||||
System.out.println("count2=" + singleTon.count2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这激起了我们几个干技术的热情,那就分析一下吧。
|
||||
|
||||
## 简单分析
|
||||
|
||||
1、简单看了下题目,这不是一个采用了饿汉式单例模式的单例类嘛,接下来当然是去找程序入口了。
|
||||
|
||||
2、在 Test 类的 main 方法中,首先调用了 Singleton 类的 getInstance() 方法,很显然这是要获取 Singleton 这个单例类的唯一对象(实例)了。
|
||||
|
||||
3、然后在获取到唯一对象(实例)之后,输出了 Singleton 类的两个静态成员变量 count1、count2 的值。(虽然通过对象名调用静态信息这种方式不推荐,但是对结果没有影响)
|
||||
|
||||
4、看到这儿,两个类里也没别的地方有输出语句,所以最终运行结果就是要看看 count1、count2 的输出值了。
|
||||
|
||||
5、**重点来了:** 在调用 getInstance() 方法前,由于 Singleton 类没有加载,所以肯定要先加载类,由于 count1、count2、Singleton 的唯一对象(实例)都是静态的,所以它们会随着类的加载而加载。其中 int 类型的 count1 变量没有指定初始值,那默认值就是 0,count2 指定了初始值是 3, Singleton 类的唯一对象(实例)要创建会调用构造方法,构造方法里又对 count1 和 count2 进行了自增 1 的运算,那结果自然就是 count1 是 1,count2 是 4。
|
||||
|
||||
这么一顿火花带闪电的分析后,自信的将答案发到了群里。
|
||||
|
||||
```
|
||||
count1=1
|
||||
count2=4
|
||||
```
|
||||
|
||||
## 深度分析
|
||||
|
||||
很显然答错了,不然也不会单独记录了。之所以答错了,是因为忽略了静态信息的加载顺序,静态信息的加载顺序是由编码顺序决定的,上方分析中先入为主的把 count1 和 count2 加载完了,但实际上最先执行的是 Singleton 的唯一对象(实例)创建及变量赋值,随后才是执行 count1、count2。
|
||||
|
||||
我们可以通过 `javap -c Singleton.class` 反汇编一下字节码文件,反汇编后的 JVM 指令如下:
|
||||
|
||||
```java
|
||||
Compiled from "Test.java"
|
||||
class org.example.Singleton {
|
||||
public static int count1;
|
||||
|
||||
public static int count2;
|
||||
|
||||
public static org.example.Singleton getInstance();
|
||||
Code:
|
||||
// 获取 singleton 静态对象变量,并将其值压入栈顶
|
||||
0: getstatic #4 // Field singleton:Lorg/example/Singleton;
|
||||
// 从当前方法返回 singleton 对象引用
|
||||
3: areturn
|
||||
|
||||
static {};
|
||||
Code:
|
||||
// 1、创建 Singleton 类的对象,并赋值给静态对象变量 singleton
|
||||
// 1.1 创建对象
|
||||
0: new #5 // class org/example/Singleton
|
||||
// 1.2 复制栈顶数值并将复制值压入栈顶
|
||||
3: dup
|
||||
// 1.3 调用 Singleton 类构造方法,count1 和 count2 自增 1,此时 count1 为 1,count2 为 1
|
||||
4: invokespecial #6 // Method "<init>":()V
|
||||
// 1.4 对象创建成功将对象引用赋值给静态对象变量 singleton
|
||||
7: putstatic #4 // Field singleTon:Lorg/example/Singleton;
|
||||
|
||||
// 2、将 3 赋值给 count2
|
||||
// 2.1 将 int 型 3 推送至栈顶
|
||||
10: iconst_3
|
||||
// 2.2 为 count2 静态变量赋值
|
||||
11: putstatic #3 // Field count2:I
|
||||
|
||||
// 3、结束方法
|
||||
14: return
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
很显然了,count2 最后是被赋值为 3 了。
|
||||
|
||||
正确答案就是:
|
||||
|
||||
```
|
||||
count1=1
|
||||
count2=3
|
||||
```
|
||||
|
||||
## 额外扩展
|
||||
|
||||
那如果真的想得到之前的结果呢?
|
||||
|
||||
```
|
||||
count1=1
|
||||
count2=4
|
||||
```
|
||||
|
||||
只需要将 count1、count2 两个静态变量的顺序调整到 Singleton 类的唯一对象(实例)变量上方就可以了。
|
||||
|
||||
```java {2-4}
|
||||
class Singleton {
|
||||
public static int count1;
|
||||
public static int count2 = 3;
|
||||
private static Singleton singleton = new Singleton();
|
||||
|
||||
private Singleton() {
|
||||
count1++;
|
||||
count2++;
|
||||
}
|
||||
|
||||
public static Singleton getInstance() {
|
||||
return singleton;
|
||||
}
|
||||
}
|
||||
|
||||
public class Test {
|
||||
public static void main(String[] args) {
|
||||
Singleton singleTon = Singleton.getInstance();
|
||||
System.out.println("count1=" + singleTon.count1);
|
||||
System.out.println("count2=" + singleTon.count2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们再次通过 `javap -c Singleton.class` 反汇编一下字节码文件,反汇编后的 JVM 指令如下:
|
||||
|
||||
```java
|
||||
Compiled from "Test.java"
|
||||
class org.example.Singleton {
|
||||
public static int count1;
|
||||
|
||||
public static int count2;
|
||||
|
||||
public static org.example.Singleton getInstance();
|
||||
Code:
|
||||
// 获取 singleton 静态对象变量,并将其值压入栈顶
|
||||
0: getstatic #4 // Field singleton:Lorg/example/Singleton;
|
||||
// 从当前方法返回 singleton 对象引用
|
||||
3: areturn
|
||||
|
||||
static {};
|
||||
Code:
|
||||
// 1、将 3 赋值给 count2,count2 此时为 3
|
||||
// 1.1 将 int 型 3 推送至栈顶
|
||||
0: iconst_3
|
||||
// 1.2 为 count2 静态变量赋值
|
||||
1: putstatic #3 // Field count2:I
|
||||
|
||||
// 2、创建 Singleton 类的对象,并赋值给静态对象变量 singleton
|
||||
// 2.1 创建对象
|
||||
4: new #5 // class org/example/Singleton
|
||||
// 2.2 复制栈顶数值并将复制值压入栈顶
|
||||
7: dup
|
||||
// 2.3 调用 Singleton 类构造方法,count1 和 count2 自增 1,count1 此时为 1,count2 此时为 4
|
||||
8: invokespecial #6 // Method "<init>":()V
|
||||
// 2.4 对象创建成功将对象引用赋值给静态对象变量 singleton
|
||||
11: putstatic #4 // Field singleTon:Lorg/example/Singleton;
|
||||
|
||||
// 3、结束方法
|
||||
14: return
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
很显然了,count2 最后是被自增为 4 了。
|
13
docs/categories/issues/index.md
Normal file
13
docs/categories/issues/index.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
editLink: false
|
||||
lastUpdated: false
|
||||
aside: false
|
||||
showComment: false
|
||||
---
|
||||
|
||||
# Bug万象集
|
||||
|
||||
::: tip 笔者说
|
||||
你读过的书,遇过的人,扛过的事,写过的 Bug,构成了你作为开发者的人生格局。
|
||||
|
||||
:::
|
107
docs/categories/solutions/2021/11/18/用Java8获取近N天日期.md
Normal file
107
docs/categories/solutions/2021/11/18/用Java8获取近N天日期.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: 用Java8获取近N天日期
|
||||
author: 查尔斯
|
||||
date: 2021/11/18 20:55
|
||||
categories:
|
||||
- 方案春秋志
|
||||
tags:
|
||||
- Java
|
||||
---
|
||||
|
||||
# 用Java8获取近N天日期
|
||||
|
||||
## 前言
|
||||
|
||||
**C:** 登录进入管理类系统,首页一般都是以展示数据仪表盘为主。例如:展示一些总量、展示近一周或是近 N 天的某数据的折线图、柱状图等等。
|
||||
|
||||
那在展示这类近 N 天的图表时,后端必然要给前端提供一个近 N 天的日期集合用于显示。
|
||||
|
||||
至于实现的方法也有很多种,笔者在这儿就记录一种目前看来扩展性相对较好的方案。
|
||||
|
||||
## 涉及技术栈
|
||||
|
||||
- Spring Boot 2.3.1.RELEASE
|
||||
- MyBatis Plus 3.1.0(使用了 MyBatis Plus 的代码生成器)
|
||||
|
||||
## Controller层
|
||||
|
||||
```java
|
||||
/**
|
||||
* 统计控制器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/11/18 20:55
|
||||
*/
|
||||
@Api(value = "统计接口", tags = "统计接口集")
|
||||
@RestController
|
||||
@RequestMapping("/statistics")
|
||||
public class StatisticsController {
|
||||
|
||||
@Resource
|
||||
private IRequestService requestService;
|
||||
|
||||
@GetMapping("/request/{days}")
|
||||
@ApiOperation(value = "日请求数据统计", notes = "日请求数据统计接口")
|
||||
public R<Map<String, Object>> requestByDays(@ApiParam(value = "days", required = true) @PathVariable("days") Integer days) {
|
||||
return R.ok(requestService.getRequestTotal(day));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Service层
|
||||
|
||||
::: tip 笔者说
|
||||
Service 层接口内容略,这个应该对你没影响吧?
|
||||
:::
|
||||
|
||||
```java {28,30,35,37}
|
||||
/**
|
||||
* 请求服务实现类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @date 2021/11/18 20:55
|
||||
*/
|
||||
@Service
|
||||
public class RequestServiceImpl extends ServiceImpl<RequestMapper, Request> implements IRequestService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getRequestTotal(int days) {
|
||||
// 响应数据
|
||||
Map<String, Object> respMap = MapUtil.newHashMap(2);
|
||||
|
||||
// 日期列表
|
||||
List<String> dateList = new ArrayList<>(days);
|
||||
// 请求列表
|
||||
List<String> requestList = new ArrayList<>();
|
||||
// ...
|
||||
|
||||
// 指定日期格式
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
// 遍历生成日期列表
|
||||
// 例如:days 为 5,现在是 2021-11-18,
|
||||
// 则可以获取到 2021-11-14、2021-11-15、2021-11-16、2021-11-17、2021-11-18
|
||||
for (int i = days - 1; i >= 0; i--) {
|
||||
// 当前日期 - i天
|
||||
LocalDateTime plusDate = LocalDateTime.now().plusDays(-i);
|
||||
// 将日期转换为 yyyy-MM-dd 格式字符串
|
||||
String plusDateStr = formatter.format(plusDate);
|
||||
// 添加到日期列表
|
||||
dateList.add(plusDateStr);
|
||||
|
||||
// [根据日期查询指定统计数据列表(具体使用时,根据你自己需求决定查询什么表,什么字段...,此处仅为样例)]
|
||||
LambdaQueryWrapper<Request> queryWrapper = Wrappers.lambdaQuery();
|
||||
// 拼接 SQL,apply 用法,参见:https://mp.baomidou.com/guide/wrapper.html#apply
|
||||
queryWrapper.apply("date_format(create_time, '%Y-%m-%d') = {0}", plusDateStr);
|
||||
requestList.add(baseMapper.selectCount(queryWrapper).toString());
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
// 添加响应数据
|
||||
respMap.put("dateList", dateList);
|
||||
respMap.put("requestList", requestList);
|
||||
// ...
|
||||
return respMap;
|
||||
}
|
||||
}
|
||||
```
|
87
docs/categories/solutions/2021/11/22/一条SQL查询今年每月统计信息.md
Normal file
87
docs/categories/solutions/2021/11/22/一条SQL查询今年每月统计信息.md
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: 一条SQL查询今年每月统计信息
|
||||
author: 查尔斯
|
||||
date: 2021/11/22 18:22
|
||||
categories:
|
||||
- 方案春秋志
|
||||
tags:
|
||||
- SQL
|
||||
---
|
||||
|
||||
# 一条SQL查询今年每月统计信息
|
||||
|
||||
## 前言
|
||||
|
||||
**C:** 前不久,笔者介绍过一种统计近 N 天记录数的需求解决方案。今天,笔者再介绍一种也很常见的统计需求。
|
||||
|
||||
::: info 示例需求: 统计今年每月的注册用户数。
|
||||
你可以基于这个示例需求,去完成各种类似的月统计需求。而且啊,笔者今天这个需求解决方案的重点是在 SQL 上,这类需求问题在 SQL 语句笔试上也挺常见,所以下回再见到类似的需求,你可以好好回想下本篇实现。
|
||||
:::
|
||||
|
||||
## 涉及技术栈
|
||||
|
||||
- Spring Boot 2.3.1.RELEASE
|
||||
- MyBatis Plus 3.1.0(使用了 MyBatis Plus 的代码生成器)
|
||||
- MySQL 5.6
|
||||
|
||||
## Controller层
|
||||
|
||||
略
|
||||
|
||||
## Service层
|
||||
|
||||
略
|
||||
|
||||
## DAO层
|
||||
|
||||
::: warning 笔者说
|
||||
记得要采用 LinkedHashMap,这样可以保证结果集的有序,即:1月、2月、......。
|
||||
:::
|
||||
|
||||
```java
|
||||
@Select({
|
||||
"SELECT",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '1' THEN 1 ELSE 0 END) AS `1月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '2' THEN 1 ELSE 0 END) AS `2月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '3' THEN 1 ELSE 0 END) AS `3月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '4' THEN 1 ELSE 0 END) AS `4月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '5' THEN 1 ELSE 0 END) AS `5月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '6' THEN 1 ELSE 0 END) AS `6月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '7' THEN 1 ELSE 0 END) AS `7月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '8' THEN 1 ELSE 0 END) AS `8月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '9' THEN 1 ELSE 0 END) AS `9月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '10' THEN 1 ELSE 0 END) AS `10月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '11' THEN 1 ELSE 0 END) AS `11月`,",
|
||||
"SUM(CASE MONTH(`create_time`) WHEN '12' THEN 1 ELSE 0 END) AS `12月`,",
|
||||
"FROM `t_user`",
|
||||
"WHERE YEAR(`create_time`)= YEAR(NOW())"
|
||||
})
|
||||
LinkedHashMap<String, Integer> countRegisterByMonth();
|
||||
```
|
||||
|
||||
## SQL语句
|
||||
|
||||
::: tip 笔者说
|
||||
这条 SQL 的思路就是将每条记录的 create_time(创建时间)求一下月份信息,求出的月份如果是对应的月份,那么就记为 1,否则记为 0,这样每月最后再做个 SUM 求和,就可以快速得到对应月份的记录数量了,不用 COUNT 依然可以计数。
|
||||
|
||||
SQL语句单独放在下面,方便各位同学复制。:smile:
|
||||
:::
|
||||
|
||||
```sql
|
||||
# 统计今年每月的注册用户数
|
||||
SELECT
|
||||
SUM(CASE MONTH(`create_time`) WHEN '1' THEN 1 ELSE 0 END) AS `1月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '2' THEN 1 ELSE 0 END) AS `2月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '3' THEN 1 ELSE 0 END) AS `3月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '4' THEN 1 ELSE 0 END) AS `4月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '5' THEN 1 ELSE 0 END) AS `5月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '6' THEN 1 ELSE 0 END) AS `6月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '7' THEN 1 ELSE 0 END) AS `7月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '8' THEN 1 ELSE 0 END) AS `8月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '9' THEN 1 ELSE 0 END) AS `9月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '10' THEN 1 ELSE 0 END) AS `10月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '11' THEN 1 ELSE 0 END) AS `11月`,
|
||||
SUM(CASE MONTH(`create_time`) WHEN '12' THEN 1 ELSE 0 END) AS `12月`
|
||||
FROM `t_user` # 根据自身需要确定实际业务表
|
||||
WHERE YEAR(`create_time`)= YEAR(NOW());
|
||||
```
|
13
docs/categories/solutions/index.md
Normal file
13
docs/categories/solutions/index.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
editLink: false
|
||||
lastUpdated: false
|
||||
aside: false
|
||||
showComment: false
|
||||
---
|
||||
|
||||
# 方案春秋志
|
||||
|
||||
::: tip 笔者说
|
||||
上学的时候除了有错题本之外,一般还会额外准备一个用来记录名言佳剧的本子,按当时的意图是希望日积月累来提升写作能力。
|
||||
在写代码的时候,经常会想到或遇到一些小方案,在此记录下来,大抵亦是如此。
|
||||
:::
|
Reference in New Issue
Block a user