Spring MVC 中使用 JetCache

JetCache 是一个基于 Java 的缓存系统封装,提供统一的API和注解来简化缓存的使用。 JetCache 提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了 Cache 接口用于手工缓存操作。 当前有四个实现,RedisCacheTairCache (此部分未在 github 开源)、CaffeineCache (in memory) 和一个简易的LinkedHashMapCache (in memory),要添加新的实现也是非常简单的。

网上很多文章介绍 JetCache 的文章包括官方文档主要是基于 Spring Boot 的,也介绍了未使用 SpringBoot 的配置方式,但是估计很多同学还是不明白怎么在传统的 Spring MVC 的 Web 项目里使用 JetCache 吧,毕竟不是所有 Web 项目都使用 Spring Boot,接下来就一步一步的介绍使用的方法。

有动画效果的 CheckBox

在 Android 或者 iOS 的界面中经常看到下图这样的 check box,并且选中状态 checked 变化时还有动画效果把白色的小圆球 indicator 从一边移动到另一边。Qt 提供了 QCheckBox,不过不能直接修改 QSS 实现这样的样式,而且也没有 indicator 移动的动画效果。


在这一篇文章中将介绍自定义一个 check box 的类 AnimatedCheckBox,实现上面样式的 check box,并且使用动画移动 indicator,实现时需要注意一下几点:

  • AnimatedCheckBox 继承了 QCheckBox,这样在使用 QCheckBox 的地方就能直接替换为 AnimatedCheckBox 了,并且不需要我们自己维护 check box 的各种状态,相关的信号槽直接使用 QCheckBox 的就可以了
  • 使用 QPropertyAnimation 实现动画效果
  • 虽然 QCheckBox 有一个 indicator 的 subcontrol,但是满足不了需求,所以我们使用了一个 QLabel 来代替默认的 indicator
  • 为了使用动画移动 indicator,不能使用布局管理器来布局 indicator,而是使用绝对坐标定位的方式来控制 indicator 的位置
  • 默认点击 QCheckBox 的 indicator 或者文字时才能切换选中状态,点击 QCheckBox 上其他空白的地方没有反应,我们希望点击 AnimatedCheckBox 上的任何地方都能够切换选中状态,需要重写 mousePressEvent
  • 为了不使用 QCheckBox 的默认样式,实现一个空的 paintEvent
  • 由于 AnimatedCheckBox 的大小是不固定的,所以 indicator 的大小和位置应该在 resizeEvent 中根据 AnimatedCheckBox 的大小和 checked 的值进行计算
  • 使用 QPainter 绘制的方式也能够实现这样的效果 (实现阴影就不那么容易了),这里我们使用 QSS 的方式调整显示的效果,还可以把样式保存到文件中,修改样式不需要重新编译程序

有了这些基础知识后, 逐步的来实现这样的 check box 就容易多了。

Vue 的递归组件

层级结构,也即树结构,是很常见的,例如部门的组织结构,书的章节目录,论坛里帖子的回复等等,可以使用递归的方式遍历树结构的数据,Vue 的组件也能够使用递归的方式展示层级结构的数据,例如下图中所示的回复:

使用 Vue 的递归组件实现组件 Reply 来完成上图的效果。

线段切割法生成红包

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
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;

public class Test {
/**
* amount 元随机生成 n 个红包
*
* @param amount 红包总额
* @param count 红包数量
* @return 返回红包的 list
*/
public static List<Double> createRedEnvelopes(double amount, int count) {
// 使用线段切割法生成红包:
// 1. 把 [0, amount] 随机分成 count 段 (共需要 count+1 个数, 上下边界为 0 和 amount),每段长度为红包的金额
// 在区间 (0, amount) 中生成 count-1 个不重复的随机数 (即段的位置),保存到 segments
// 2. 把 segments 小到大排序,使用了 TreeSet
// 3. 遍历 segments,每个段的长度 segments[i]-segments[i-1] 则为红包的金额 (保留 2 位小数)
// 4. 确保红包的总额为 amount

Set<Double> segments = new TreeSet<>();

// [1] 把 [0, amount] 随机分成 count 段,每段长度为红包的金额,加上边际 0 和 amount,共需要 count 个数
// 在区间 (0, amount) 中生成 count-1 个不重复的随机数 (即段的位置),保存到 segments
// [2] 把 segments 小到大排序,使用了 TreeSet
Random random = new Random();
segments.add(0.0);
segments.add(amount);
while (segments.size() < count-1) {
double t = random.nextDouble() * amount;
segments.add(t);
}

// [3] 遍历 segments,每个段的长度 segments[i]-segments[i-1] 则为红包的金额 (保留 2 位小数)
List<Double> redEnvelopes = new LinkedList<>();

Double[] temp = segments.toArray(new Double[0]);
for (int i = 1; i < temp.length; ++i) {
redEnvelopes.add(toFixed(temp[i]-temp[i-1], 2));
}

// [4] 确保红包的总额为 amount
double sum = redEnvelopes.stream().mapToDouble(a->a).sum();
redEnvelopes.set(0, toFixed(redEnvelopes.get(0) + amount - sum, 2));

return redEnvelopes;
}

/**
* 浮点数保留 scale 位小数位
*
* @param value 浮点数
* @param scale 小数位
* @return 返回小数位为 scale 位的浮点数
*/
public static double toFixed(double value, int scale) {
return BigDecimal.valueOf(value).setScale(scale, RoundingMode.HALF_UP).doubleValue();
}

public static void main(String[] args) throws Exception {
List<Double> redEnvelopes = createRedEnvelopes(100, 19);
System.out.println(redEnvelopes);
System.out.println(redEnvelopes.stream().mapToDouble(a->a).sum()); // 红包总额
}
}

输出:

1
2
[2.44, 0.07, 0.4, 4.26, 3.7, 1.36, 22.94, 9.69, 5.37, 2.68, 1.78, 1.01, 3.58, 8.65, 11.68, 2.41, 17.98]
100.0

Vue 同步初始化

有这样的一个需求,在组件的 beforeCreated 中需要先从服务器获取登录用户的信息,然后才能继续往下初始化。使用 jQuery 的同步 Ajax 可以实现这个功能,但是 jQuery 的同步 API 已经不再推荐使用 (浏览器里会有警告),而且现在 Ajax 更多推荐使用 Axios,但是它只支持异步 Ajax 请求,此外,Vue 的生命周期函数 beforeCreated, created, mounted 等函数不支持 async + await,看上去实现这个需求挺不容易的。

幸运的是,在 Vue 的路由守卫函数 beforeRouterEnter 中可以等待条件满足后才继续跳转路由进行新路由组件的渲染,使用这个特点能实现阻塞请求后再渲染组件的需求了:

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
<script>
import { LoadingBar } from 'iview';

export default {
beforeRouteEnter(to, from, next) {
LoadingBar.start(); // 加载动画

// // [1] Ajax 异步请求数据
requestUser().then((user) => {
// [2] 数据保存到 localStorage,结束加载动画
LoadingBar.finish();
localStorage.setItem('user', JSON.stringify(user));
next();
}).catch(() => {
LoadingBar.finish();
});
},
data() {
return {
user: null,
};
},
created() {
// [3] 从 localStorage 中获取数据
this.user = JSON.parse(localStorage.getItem('user'));
console.log(this.user);
}
};
</script>

提示:

  • 函数 beforeRouteEnter 中访问不到当前组件的上下文 (这时 this 为 undefined),所以获取到的数据需要先存储起来 (例如使用 localStorage, 不要使用 vuex,还是 this 的问题),然后在组件的生命周期函数如 beforeCreated 中获取
  • next(vm => {}),next 的回调函数在 created, mounted 等执行完后才执行,所以在它里面设置登录用户的数据满足不了我们的要求
  • 为了更好的用户体验,请求数据的时候显示加载进度条

枚举与 QFlags

传统的 C++ 编程中,通常使用整数来保存 enum 的逻辑运算结果 (与、或、非、异或等),在进行逻辑运算的时候没有进行类型检查,一个枚举类型可以和其他的枚举类型进行逻辑运算,运算的结果可以直接传递给接收参数为整数的函数。

Qt 中,模板类 QFlags<Enum> 提供了类型安全的方式保存 enum 的逻辑运算结果解决上面的这几个问题,这种方式在 Qt 里很常见,例如设置 QLabel 对齐方式的函数是 QLabel::setAlignment(Qt::Alignment) (typedef QFlags<Qt::AlignmentFlag> Qt::Alignment),这就意味着传给 setAlignment 的参数只能是枚举 Qt::AlignmentFlag 的变量、它们的逻辑运算结果或者 0,如果传入其他的枚举类型或者非 0 值,编译时就会报错:

1
2
3
4
label->setAlignment(0); // OK
label->setAlignment(Qt::AlignLeft | Qt::AlignTop); // OK

label->setAlignment(Qt::WA_Hover); // Error: 编译时报错

想要把我们定义的枚举类型和 QFlags 一起使用,需要用到两个宏:

  • Q_DECLARE_FLAGS(Flags, Enum):
    • Enum 是已经定义好的枚举类型
    • 展开的结果为 typedef QFlags<Enum> Flags
  • Q_DECLARE_OPERATORS_FOR_FLAGS(Flags):
    • Flags 就是类型 QFlags<Enum>
    • 给 Flags 定义了运算符 |,使得 Enum 和 Enum,Enum 和 Flags 能够使用或运算符 |,结果为 Flags

使用 QFlags 时需要留意以下几点:

  • QFlags 其实就是用于位操作,设置它保存的数值的某一位为 1 或者为 0,所以和 QFlags 一起使用的枚举类型,其变量的值需要是 2 的 n 次方,即 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, …,它们的特点是其二进制位中只有一位是 1,其他位全部为 0,这样每一个枚举值只会和 QFlags 中的某一个位对应,不会出现交叉的情况
  • 调用函数 QFlags::setFlag(Enum flag, bool on = true),on 为 true 时设置 flag 对应的位为 1,on 为 false 时设置 flag 对应的位为 0,设置 flag 对应的位为 1 还可以使用运算符 |
  • 调用函数 QFlags::testFlag(Enum flag) 测试 flag 对应的位是否为 1
  • 整数转为 QFlags: 把整数作为 QFlags 构造函数的参数创建一个 QFlags 变量
  • QFlags 转为整数: 调用 int(flags) 把 QFlags 变量转换为整数值

C++ 临时变量的析构

Qt 中经常会使用 QString::toUtf8() 获取字符串的 UTF-8 数组,例如下面这样使用:

1
2
3
4
void foo(const char *data) { ... }

const char *data = QString::toUtf8().constData();
foo(data);

看上去没啥问题,其实这个代码有很严重的问题,一般不是经验很丰富的程序员很难发现这个 Bug,很可能会导致程序崩溃退出,但就是找不到为什么。这是因为 QString::toUtf8() 返回的是一个 QByteArray 的栈变量,第 3 行语句中的 QByteArray 是一个临时变量,这行语句结束时这个变量就被析构了,指针 data 指向的内存也被回收,所以下面使用的 data 指向的内存已经被释放了,难怪程序会崩溃了。把代码修改为下面的样子则就能够正确运行了:

1
2
3
4
5
6
7
// [1]
foo(QString::toUtf8().constData()); // 很奇怪是不是?

// [2] 先保存 ba
QByteArray ba = QString::toUtf8();
const char *data = ba.constData();
foo(data);

这个问题涉及到临时变量的析构,也许还不太明白是怎么回事,下面的程序就来模拟上面的情况,一看就明白了:

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
#include <QDebug>

class B {
public:
B() {
qDebug() << "B()";
}

~B() {
qDebug() << "~B()";
}

int value = 10;
};

// A 中有一个 B* 的成员变量
class A {
public:
A() {
qDebug() << "A()";
b = new B();
}

~A() {
delete b;
qDebug() << "~A()";
}

B *b = nullptr;
};

// 返回 A 的栈变量对象
A a() {
return A();
}

void foo(B *b) {
qDebug() << b->value;
}

int main(int argc, char** argv) {
// 提示: 下面的语句中 a() 返回的变量是一个临时变量,在语句结束时才析构提示

foo(a().b); // OK: 函数 foo() 执行完时 a() 创建的 A 和 B 才析构,在函数 foo() 中放心的使用 b

B *b = a().b; // Error: 语句结束时 A 和 B 才析构,此时 b 已经被回收,所以不要在后面的语句中使用 b
qDebug() << b->value; // Error: b 已经被析构,b->value 的值不是 10,而是随机的

qDebug() << "main() 结束";
return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
A()
B()
10
~B()
~A()

A()
B()
~B()
~A()
1859919056 // 注意: 这里不是 10 呢

main() 结束

Vue 尽量不用 scoped

在 Vue 中,为了减少样式的冲突,可以使用 scoped 使得生成的 DOM element 和 class 都带上随机属性,但有利有弊,总的来说,不推荐使用 scoped,因为使用 scoped 后就不能跨级设置组件的样式了。

跨级: 组件 A 中直接使用组件 B,组件 B 中直接使用组件 C,因为 C 没有在 A 中直接使用,所以我们说 A 和 C 跨级了。

线程的上下文

Qt 中每个 QObject 及其子类的对象都有自己的线程上下文环境,即对象所属的线程,对象属于创建它的函数执行时所在的线程,例如在 Ui 线程中的函数里创建了对象 ops,则 ops 属于 Ui 线程。使用 QObject::thread() 获取对象所属线程,可以使用 QObject::moveToThread(otherThread) 移动一个对象到另一个线程。由于跨线程调用函数有可能会造成程序崩溃,所以有比较了解代码在执行时它所处的线程。

下面以示例演示不同情况下代码执行时所处的线程:

  • Widget 所属线程
  • 线程对象所属线程
  • Lambda 的方式处理信号槽
  • Qt 5 函数指针的方式处理信号槽
  • 传统 signal slot 的方式处理信号槽

注意: 下面的注释都是基于示例代码的,为了不让描述的太过繁琐,便于帮助理解,有些描述理论上可能是不精确的,需要大家自行更进一步的分析。

上传 Blob URL 指向的文件

Blob 是 Binary large object 的简称,格式为 blob:http://localhost:8080/da126298-1b6b-4dfb-8a92-2e3ccbee611d, 是一种伪协议,只能在浏览器本地使用,例如在使用 TinyMCE 粘贴从 Word 复制的图片时,得到一个 Blob URL,可以使用下面的方式把 Blob 对应的图片上传到服务器:

  1. 使用 XMLHttpRequest 获取 Blob URL 源数据的 Blob 文件对象
  2. 上传 Blob 到服务器

下面以上传图片 Blob 为例:

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
const xhr = new XMLHttpRequest();
xhr.open('GET', blobUrl); // blob:http://localhost:8080/da126298-1b6b-4dfb-8a92-2e3ccbee611d
xhr.responseType = 'blob';

xhr.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
const imageBlob = this.response;
const imageType = imageBlob.type;
let imageName = null;

// 根据文件的类型确定文件名,否则服务端得到的文件名为 blob,没有后缀
if (imageType.includes('png')) {
imageName = 'uploaded-image.png';
} else if (imageType.includes('gif')) {
imageName = 'uploaded-image.gif';
} else if (imageType.includes('bmp')) {
imageName = 'uploaded-image.bmp';
} else {
imageName = 'uploaded-image.jpg';
}

// 上传 Blob 图片
const form = new FormData();
form.append('file', imageBlob, imageName); // 第三个参数为文件名

$.ajax({
type: 'POST',
url : Urls.FORM_UPLOAD_TEMPORARY_FILE,
data: form,
processData: false,
contentType: false,
}).done(function(result) {
// 把服务器返回的图片 URL 插入到需要的地方
console.log(result);
});
}
};

xhr.send();

获取 Blob 对象的核心代码为:

1
2
3
4
5
6
7
8
9
var xhr = new XMLHttpRequest();
xhr.open('GET', 'blob:http://your.blob.url.here', true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var blob = this.response; // blob is now the blob that the object URL pointed to.
}
};
xhr.send();

使用 Axios 的为:

1
2
3
4
5
6
7
axios({
method: 'get',
url: blobUrl,
responseType: 'blob',
}).then(function(response) {
let blob = response.data;
})

Qt 生成报表

开发中经常会需要输出报表,方法很多,这里就不一一去列举对比各种方法的优缺点,只介绍使用字符串模板输出 PDF 报表的方式,能够满足绝大多数时候的需求:

  1. 字符串模板 + 数据输出 HTML
  2. HTML 输出 PDF

输出 HTML

为了生成 HTML,可以在程序中使用字符串拼接的方式,但这种方式对于复杂点的 HTML 就会非常麻烦,不推荐使用,我们这里将使用字符串模板引擎 Inja 来生成 HTML,使用步骤如下:

  1. 下载 Inja

  2. 解压,得到的目录结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    inja-2.1.0
    ...
    ├── single_include
    │   └── inja
    │   └── inja.hpp
    └── third_party
    └── include
    └── nlohmann
    ├── LICENSE.MIT
    └── json.hpp
  3. 复制 single_include 下的 inja 目录和 third_party/include 下的 nlohmann 目录到项目的 lib 目录下:

    1
    2
    3
    4
    5
    6
    lib
    ├── inja
    │   └── inja.hpp
    └── nlohmann
    ├── LICENSE.MIT
    └── json.hpp
  4. Qt 工程的 pro 文件中添加包含路径: INCLUDEPATH += lib

  5. 最简单的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <inja/inja.hpp>

    using namespace inja;
    using json = nlohmann::json;

    int main(int argc, char *argv[]) {
    json data;
    data["name"] = "world";

    render("Hello {{ name }}", data); // Returns std::string "Hello world"

    return 0;
    }

    Inja 的模板使用了 Mustache 风格的语法,上面例子中 { { name } }data["name"] 的值 world 替换掉,生成字符串 Hello world,其中 Inja 使用 JSON 库 nlohmann json 传递数据给模板。

动态加载 JS 和 CSS

动态加载 JS 文件的正确姿势中列举了多种加载 JS 的方式,下面摘出个人最喜欢的方式,用于动态加载 JS 和 CSS。

动态加载 JS

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Load JS</title>
</head>

<body>
<script>
/**
* 使用 Promise 异步加载 JS
*
* @param {String} url JS 的路径
* @param {String} id JS 的 <style> 的 ID,如果已经存在则不再重复加载,默认为时间戳+随机数
* @return 返回 Promise 对象, then 的参数为加载成功的信息,无多大意义
*/
function loadJs(url, id = Date.now() + '-' + Math.random()) {
return new Promise(function(resolve, reject) {
// 避免重复加载
if (document.getElementById(id)) {
resolve('success: ' + url);
return;
}

var script = document.createElement('script');

if (script.readyState) { // IE
script.onreadystatechange = function() {
if (script.readyState == 'loaded' || script.readyState == 'complete') {
script.onreadystatechange = null;
resolve('success: ' + url);
}
};
} else { // Other Browsers
script.onload = function() {
resolve('success: ' + url);
};
}

script.onerror = function() {
reject(Error(url + ' load error!'));
};

script.type = 'text/javascript';
script.id = id;
script.src = `${url}?hash=${id}`;
document.getElementsByTagName('head').item(0).appendChild(script);
});
}

// 加载 JS 后执行 then 的回调函数
loadJs('http://cdn.bootcss.com/jquery/1.9.1/jquery.min.js', 'jq').then(msg => {
console.log(msg);
});

// 加载完所有 JS 后执行 then 的回调函数
const one = 'http://cdn.bootcss.com/jquery/1.9.1/jquery.min.js';
const two = 'https://github.githubassets.com/assets/frameworks-5c304257.js';
Promise.all([loadJs(one, 'jq'), loadJs(two)]).then(results => {
console.log(results);
})
</script>
</body>

</html>

汉若塔

传说越南河内某间寺院有三根银棒,上串 64 个金盘,金盘尺寸由下到上依次变小。寺院里的僧侣依照一个古老的预言,以下述规则把这些盘子从第一根银棒移至第三根银棒上 (可以借助第二根银棒):

  • 每次只能移动一个圆盘
  • 大盘不能迭在小盘上面

预言说当这些盘子移动完毕,世界就会灭亡。这个传说叫做梵天寺之塔问题,即汉若塔问题。

用递归实现的代码很简洁:

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
public class Test {
public static void main(String[] args) {
hanoi(3, 'A', 'B', 'C');
}

/**
* 把 n 个盘子从 a 柱移动到 c 柱,可以借助 b 柱
*
* @param n: 盘子的数量
*/
public static void hanoi(int n, char a, char b, char c) {
if (n == 1) {
move(1, a, c);
return;
}

hanoi(n-1, a, c, b); // 小和尚借助 c 柱把 a 柱最上面的 n-1 个盘子移动到 b 柱
move(n, a, c); // 老和尚把最下面的一个盘子从 a 柱移动到 c 柱
hanoi(n-1, b, a, c); // 小和尚再借助 a 柱把 b 柱上的 n-1 个盘子移动到 c 柱
}

// sn 是盘子的序号: 开始时 a 柱上的盘子序号从上到下依次为 1, 2, 3, ..., n
public static void move(int sn, char from, char to) {
System.out.printf("%d: %c -> %c\n", sn, from, to);
}
}

代码不多,但是一直理解不了这个递归为什么要这么写,直到大学时人工智能的老师给了个记忆的办法,从此就理解了:

老和尚比较懒 (有点大不敬,忽略之),于是就叫小和尚把第一根柱子最上面的 n-1 个盘子借助第三根柱子先搬到第二根柱子上,然后老和尚自己把第一根柱子最下面的那个大盘子搬到第三根柱子上,再叫小和尚把第二根柱子上的 n-1 个盘子借助第一根柱子搬到第三根柱子上。

斐波那契数列

斐波那契数列 (意大利语: Successione di Fibonacci),又称黄金分割数列、费波那西数列、费波拿契数、费氏数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……在数学上,斐波那契数列以如下被以递归的方法定义:F1=1,F2=1,Fn=Fn-1+Fn-2 (n>2,n∈N*),使用递归实现如下:

1
2
3
4
5
public static int fib(int n) {
if (n == 1 || n == 2) { return 1; }

return fib(n-1) + fib(n-2);
}

求 fib(6) 斐波那契数列的递归过程如图:

重叠子问题 (Overlap subproblem): 把大问题被分解为多个小问题进行求解,小问题重复的出现多次。

答疑表设计

答疑主要涉及 2 个表,问题表 qa_queston 和回复表 qa_reply,核心字段如下:


数据存储逻辑:

  • 问题存储到表 qa_question
  • 每个回复都存储问题 ID: question_id
  • 每个回复都保存第一级回复 ID: top_reply_id,为了方便找到回复树下的所有回复
  • 对问题进行的回复存储到表 qa_reply
    • parent_id 为 0
    • top_reply_id 为此回复的 ID
  • 对回复进行的回复存储到表 qa_reply
    • 回复是一个有层级的树状结构,所以使用了 parent_id
    • parent_id 为被回复的回复 ID
    • top_reply_id 为此回复所在第一级回复 ID

此设计的优点:

可以使用一条 SQL 语句分页查询出问题的 N 个一级回复、以及这些一级回复下所有相关的回复:

  • 查找到问题的 n 个第一级回复的 top_reply_id x
  • 查找问题的回复中所有第一级回复为 x 的回复
1
2
3
4
5
SELECT q.*, r.*
FROM qa_question q
JOIN (SELECT question_id, top_reply_id FROM qa_reply WHERE question_id = 1 AND parent_id = 0 LIMIT 0, 10) t ON t.question_id = q.id
LEFT JOIN qa_reply r ON r.question_id = q.id AND r.top_reply_id = t.top_reply_id
ORDER BY r.created_at DESC;

parent_id 为 0,说明是第一级回复。

如果回复再增加 level 属性,还可以对回复层数进行过滤,当层数很多时,可以动态请求更深层次的回复,避免一次获取太多数据。

此外,根据业务,可以对问题表增加字段如是否加精、推荐置顶、点赞数量、回复数量等,回复表增加点赞数量、被回复者的名字 (方便显示) 等,对点赞、取消点赞、点击推荐等行为记录到日志表中。


以下部分用于测试

问题和回复

1
2
3
4
5
6
7
8
9
1. 特别定制「乌龙茶」,有人跟我一样只喜欢有味道的,或者冰的饮品嘛?
1. 东方树叶,每天基本都会喝 1,2 瓶
2. 抹茶粉确实很流行,但是和我提的浓缩粉其实是两种产品
3. 炭焙的一般口味略重
4. 好处就是完全不伤胃
2. 找一个靠谱的设计师长期合作
5. 明天早上起来加你
6. 怎么联系
7. 本人做一些兼职

清空数据库

1
2
TRUNCATE TABLE qa_question;
TRUNCATE TABLE qa_reply;

问题一

1
2
3
4
5
6
INSERT INTO qa_question(id, clazz_id, user_id, user_name, content) VALUES (1, 1, 30, 'Bob', '特别定制「乌龙茶」,有人跟我一样只喜欢有味道的,或者冰的饮品嘛?');

INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (1, 1, 0, 1, 31, 'Tom', '东方树叶,每天基本都会喝 1,2 瓶');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (2, 1, 0, 2, 31, 'Tom', '抹茶粉确实很流行,但是和我提的浓缩粉其实是两种产品');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (3, 1, 2, 2, 31, 'Tom', '炭焙的一般口味略重');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (4, 1, 2, 2, 31, 'Tom', '好处就是完全不伤胃');

问题二

1
2
3
4
5
INSERT INTO qa_question(id, clazz_id, user_id, user_name, content) VALUES (2, 1, 30, 'Bob', '找一个靠谱的设计师长期合作?');

INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (5, 2, 0, 5, 32, 'Max', '明天早上起来加你');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (6, 2, 5, 5, 32, 'Max', '怎么联系');
INSERT INTO qa_reply(id, question_id, parent_id, top_reply_id, user_id, user_name, content) VALUES (7, 2, 0, 7, 32, 'Max', '本人做一些兼职');

查询问题和回复

只在第一级回复上分页,同时查询出这个回复的所有后代:

  1. 查找到问题的 n 个第一级回复的 top_reply_id x
  2. 查找问题的回复中所有第一级回复为 x 的回复
1
2
3
4
5
SELECT q.*, r.*
FROM qa_question q
JOIN (SELECT question_id, top_reply_id FROM qa_reply WHERE question_id = 1 AND parent_id = 0 LIMIT 0, 10) t ON t.question_id = q.id
LEFT JOIN qa_reply r ON r.question_id = q.id AND r.top_reply_id = t.top_reply_id
ORDER BY r.created_at DESC;

修改 question_id 查询不同问题的回复。

iView 使用 JSX render 树

iView 提供了树的组件 Tree,复杂的节点可以使用 render 自定义显示:

  • <Tree>:render 渲染所有节点
  • 节点的属性 render 覆盖全局的 render,优先级更高

文档中使用的 render 语法比较繁琐,下面使用 JSX 进行改写,实现下面的树,只是额外增加了样式的变化:

  • 高亮当前行
  • 高亮被点击的节点名字 (使用了 jQuery)
  • 鼠标移动到节点所在的行时才显示这一行的按钮


MyBatis 的 ResultMap 注意事项

MyBatis 的 ResultMap 比较灵活,使用时需要注意以下几点:

  • ResultMap 中 <result> 的 property 对应 Bean 的属性名,column 对应 SELECT 的字段名:
    • column 存在,property 对应的属性存在时正确
    • column 存在,property 对应的属性不存在时报错
    • column 存在,没有写相关 <result>不报错,Bean 有同名属性时自动设置属性的值,没有时忽略
    • column 不存在,property 对应的属性存在时不报错
    • column 不存在,property 对应的属性不存在时不报错
  • SELECT 的字段名和 ResultMap 里属性名相同时,可以不用写 <result>,MyBatis 会自动映射对应的 column 和 property,也就是说如果 SELECT 的字段名和 Bean 的属性名都是同名的,即使 ResultMap 为空,Bean 的属性都会自动设置好,例如下面的 id 和 info 都注释掉了,它们的属性值仍然被正确的设置了
  • SELECT 中的字段可以比 ResultMap 里的 <result> 多或者少
  • SELECT 的字段名和 ResultMap 中 <result> 的 property 不同时,需要使用 column 进行映射
  • 但是,如果使用了 <collection> 或者 <association>,只有使用 <result> 或者 <id> 设置过的属性会被设置,诡异的规则

下面的代码能够正常运行,对这几条进行了展示:

Java Bean:

1
2
3
4
5
6
7
public class Demo {
private int id;
private String info;
private boolean marked;

// Getters and setters
}

SQL:

1
2
3
<select id="findDemoById" parameterType="int" resultMap="DemoResultMap">
SELECT id, info, is_marked FROM demo WHERE id=#{id}
</select>

ResultMap:

1
2
3
4
5
6
7
8
9
<resultMap id="DemoResultMap" type="Demo">
<!--<id property="id" column="id"/>-->
<!--<result property="info" column="info"/>-->

<result property="marked" column="is_marked"/>

<!-- extra 在 SELECT 的字段名和 Bean 的属性里都不存在 -->
<result property="extra" column="extra"/>
</resultMap>

常用排序

下面列举常用的排序算法的实现:

  • 冒泡排序
  • 插入排序
  • 选择排序
  • 快速排序
  • 归并排序
  • 堆排序

排序的时候, 需要大量的交换数组中的 2 个元素, 使用下面的函数 swap() 进行交换:

1
2
3
4
5
6
7
8
/**
* 交换数组中指定下标的 2 个元素的值
*/
public static void swap(int[] a, int indexA, int indexB) {
int temp = a[indexA];
a[indexA] = a[indexB];
a[indexB] = temp;
}

冒泡排序

原理:

  1. 比较相邻的元素,如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 冒泡排序
*/
public static void bubbleSort(int[] a) {
for (int i = 1; i < a.length; ++i) {
for (int j = 0; j < a.length-i; ++j) {
// 如果 a[j] 大于 a[j+1], 则把大的交换到后面
if (a[j] > a[j+1]) {
swap(a, j, j+1);
}
}
}
}

FastJson 反序列化 Boolean

JSON 字符串反序列化为 Java 对象时, 不只是 true 和 false 能够转换为 boolean 变量的值:

  • boolean:
    • true: true
    • false: false
  • 数字:
    • 1: true
    • 0: false
    • 其他数字: false
  • 字符串:
    • “true”: true (大小写不敏感)
    • “false”: false (大小写不敏感)
    • “1”: true
    • “0”: false
    • 其他字符串抛异常
1
2
3
4
5
6
7
8
9
10
11
{ "visible": true }    // true
{ "visible": false } // false
{ "visible": 1 } // true
{ "visible": 0 } // false
{ "visible": 3 } // false
{ "visible": "true" } // true
{ "visible": "True" } // true
{ "visible": "false" } // false
{ "visible": "falsE" } // false
{ "visible": " true" } // Exception
{ "visible": "Bla" } // Exception

Vue 中使用 TinyMCE

TinyMCE 是一个功能强大的富文本编辑器:

  • 支持从 Word 中复制的文本格式
  • 拖拽修改图片的大小
  • 表格拖拽修改单元格大小
  • 提供了三种编辑模式
    • Full featured: 默认显示工具栏
    • Inline: 编辑器的到焦点时才显示工具栏
    • Distraction Free: 选中文本后才显示工具栏
  • 同一页面中可以创建多个编辑器
  • 界面美观简洁, 使用 CSS 修改样式很方便, 工具栏按钮使用 SVG 图片
  • 插件开发简单, 甚至不需要开发插件就能向工具栏插入按钮

TinyMCE 提供了 cloud 版本, 也可以下载到本地使用.

使用 TinyMCE 只需要 3 步:

  1. 引入 TinyMCE
  2. 为 TinyMCE 准备一个 DOM
  3. 基于准备好的 DOM,初始化 TinyMCE 实例

下图是使用默认参数创建的 TinyMCE 编辑器:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>TinyMCE</title>
<!-- [1] 引入 TinyMCE -->
<script src="tinymce/tinymce.min.js"></script>
</head>

<body>
<!-- [2] 为 TinyMCE 准备一个 DOM -->
<div id="editor">道格拉斯•狗</div>

<script>
// [3] 基于准备好的 DOM,初始化 TinyMCE 实例
tinymce.init({ selector: '#editor' });
</script>
</body>

</html>

官方提供了非常丰富的文档, 请访问 https://www.tiny.cloud/docs 进行阅读, 了解 TinyMCE 更多的使用方法.

Vue 中使用 Echarts

Echarts, 一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),底层依赖轻量级的矢量图形库 ZRender,提供直观,交互丰富,可高度个性化定制的数据可视化图表。

Vue 项目中使用 yarn 安装 Echarts: yarn add echarts, 然后在页面中使用 Echarts 步骤如下:

  1. 为 ECharts 准备一个具备大小 (宽高) 的 DOM

  2. 引入 echarts:

    • 全部引入:

      1
      import echarts from 'echarts'; // 方便, 但是也同时引入了很多不需要的组件
    • 按需引入:

      1
      2
      import echarts from 'echarts/lib/echarts'; // 引入基本模板
      import bar from 'echarts/lib/chart/bar'; // 引入柱状图组件
  3. 基于准备好的 DOM,初始化 Echarts 实例

  4. 设置 Echarts 图表数据

入门示例

使用 Echarts 实现的柱状图:

Tio WebSocket 经验

使用 Tio 实现 WebSocket, 可以在 tio-websocket-showcase 的基础上进行修改 (据说能够达到企业级性能, 单机支持 30 万连接). 利用 Tio 提供的绑定功能实现消息群发和给指定的用户发送消息, 并且把用户对象直接存储到 ChannelContext 上, 还能省去自己管理用户的麻烦, Tio 也提供了心跳检测功能, IP 黑名单, 流量监控等, 我们只需要关注与业务层代码即可, 下面介绍一些相关的经验:

  • 应用中只有一个 GroupContext, 发送消息, 获取小组信息等

  • 一个连接对应一个 ChannelContext (ip:port), 可以使用 setAttribute() 存储业务数据, getAttribute() 获取数据

  • 绑定 (使用 Tio 进行绑定, 可参考让网络编程更轻松和有趣 t-io):

    • userid: 一个 userid 可以绑定多个 ChannelContext (实现同一个账号多个设备登录)

      1
      2
      bindUser(ChannelContext channelContext, String userid)
      SetWithLock<ChannelContext> getChannelContextsByUserid(GroupContext groupContext, String userid)
    • token: 一个 token 可以绑定多个 ChannelContext (实现同一个账号多个设备登录)

      1
      2
      bindToken(ChannelContext channelContext, String token)
      SetWithLock<ChannelContext> getChannelContextsByToken(GroupContext groupContext, String token)
    • bsId: 一个 bsId 只能绑定一个 ChannelContext (实现同一个账号只允许登录一个设备)

      1
      2
      bindBsId(ChannelContext channelContext, String bsId)
      ChannelContext getChannelContextByBsId(GroupContext groupContext, String bsId)

      下面介绍的绑定以 bsId 为例, 其他的方式参考实现即可

简单的 Mindmap

树形结构如果作为侧边栏, 使用 zTree 比较合适, 下图所示的平铺展开整棵树, 使用 zTree 估计就不适合了, 在此我们使用 jQuery 的另一个插件 Simple jQuery Mind Map Diagram Plugin - mindmap 来实现.

文档的例子使用静态的 HTML 标签准备 mindmap 需要的树形结构的数据, 实际中大多情况下树形结构的数据是存储在文件或者通过接口返回, 我们这里展示递归的方式遍历树形结构的数据创建 mindmap 需要的 HTML 标签. 因为每个节点都是 HTML 的标签, 能很方便的使用 CSS 定制样式.

Ckeditor 5 简介

新版富文本编辑器 Ckeditor 5 比 Ckeditor 4 使用更简单, 下载 zip 包后只需要里面的 ckeditor.js (连样式文件都不需要), 像下面这样 3 步就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en" dir="ltr">

<head>
<meta charset="utf-8">
<title>Ckeditor Demo</title>
</head>

<body>
<div id="editor"></div> <!-- [1] -->

<script src="ckeditor.js"></script> <!-- [2] -->
<script>
ClassicEditor.create(document.querySelector('#editor')); // [3]
</script>
</body>

</html>

只是简单的使用, 默认的功能就可以了. 如果还需要更多的功能, 例如集成多种编辑模式, 使用非默认自带插件, 自定义插件等就不能使用直接下载的代码 (You can’t add a plugin to an existing build), 需要我们自己编译源码才行, 下面就一一进行介绍.

圆角 button and widget 组合

下图的控件估计很多数人会使用绘图的方式实现, 代码实现起来虽然不会很复杂, 但是要控制得到满意的效果就不太容易, 对于我来说, 能不使用继承就不使用继承 (例如给某些控件处理事件时, 能用 eventFilter 实现就尽量避免使用继承, 因为继承又要多出几个源码文件 =_=!!!). 巧妙的利用 QSS, 一个 QPushButton 和 QWidget 也能实现.

下面就来看实现的方法吧:

  1. 在 QtCreator 中如下图在一个 QWidget 中放一个 QPushButton, 右边放一个 Horizontal Spacer, 然后水平布局

  2. 使用下面的 QSS 就可以得到我们想要的效果了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #widget {
    min-height: 40px;
    max-height: 40px;
    border: 4px solid white;
    border-top-left-radius: 24px;
    border-bottom-left-radius: 24px;
    padding: 0;
    background: #AAA;
    }

    QPushButton {
    min-height: 40px;
    max-height: 40px;
    min-width: 40px;
    max-width: 40px;
    border: 4px solid white;
    border-radius: 24px;
    background: lightblue;
    }

    QPushButton:hover {
    background: orange;
    }

补充说明:

  • 高: widget 和 button 的最大最小高都设置为 40px, 这样就不会随布局的变化改变了, 圆角就不会受影响
  • 宽: button 的最大最小宽都设置为 40px, 这样就可以把按钮设置为圆形, 而不会变成圆角矩形
  • 圆角半径: 为 24px 而不是 20px, 因为圆角的计算需要把边框的宽度也计算在内, 边框的宽度是 4px, 所以圆角半径设置为 24px
  • Padding: 为了 widget 和 button 之间不留空隙, 设置 widget 的 padding 为 0
  • 边框和背景色就无需多说了, 根据自己的爱好设置即可

Gradle 管理 Scala 项目

可以使用 Gradle 来管理 Scala 项目, 和管理 Java 的项目是一样的.

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
scala
├── build.gradle
└── src
├── main
│   ├── java
│   ├── resources
│   └── scala
│   └── AppDemo.scala
└── test
├── java
├── resources
└── scala

初始文件

build.gradle 的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
plugins {
id 'java'
id 'scala'
id 'application'
id 'com.github.johnrengelman.shadow' version '4.0.3'
}

// [1] Scala 需要的依赖
dependencies {
compile "org.scala-lang:scala-library:2.12.7"
compile "org.scala-lang:scala-compiler:2.12.7"
compile "org.scala-lang:scala-reflect:2.12.7"
}

// [2.1] 从命令行运行默认类: gradle run
// [2.2] 从命令行运行某个类: gradle run -DmainClass=Foo
ext {
project.mainClassName = System.getProperty("mainClass", "AppDemo")
}

// [3] 打包: gradle clean shadowJar [-DmainClass=Foo]
shadowJar {
mergeServiceFiles('META-INF/spring.*')
}

插件 shadowJar 用于项目打包, 更多信息请参考 https://qtdebug.com/misc-gradle-app/

AppDemo.scala 的内容为:

1
2
3
4
5
object AppDemo {
def main(args: Array[String]): Unit = {
println("Hello World")
}
}

运行打包

  • 运行: gradle run

    运行对象 AppDemo 的 main 函数, 因为 project.mainClassName 中定义了默认运行的类名为 AppDemo

  • 打包: gradle clean shadowJar

Scala 语法摘要

Scala: Scalable language, 面向对象的函数式编程语言.

类型

类型的首字母大写, 没有基本类型:

  • Boolean
  • Byte
  • Char
  • Short
  • Int
  • Long
  • Float
  • Double
  • String
  • Unit (即是 void)
  • Any
  • AnyRef
  • AnyValue

变量

常量: val greeting: String = "Hello world" (value), val π = 3.1415926

变量: var greeting: String = "Hello world" (variable)

名字: 字母, 数字, 特殊操作符如 +, -, *, /, π, θ 等, 不能以数字开头

MongoDB 账号密码登录

配置MongoDB 账号密码登录的步骤如下 (假设有 2 个数据库 admin (自带的) 和 foo):

  1. 启动 MongoDB: mongod --config /usr/local/etc/mongod.conf

  2. 进入数据库 admin: use admin

  3. 创建用户 admin:

    1
    2
    3
    4
    5
    6
    7
    db.createUser(
    {
    user: "admin",
    pwd: "ebag",
    roles: [ { role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase" ]
    }
    )
  4. 进入数据库 foo: use foo

  5. 创建用户 bar:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    db.createUser(
    {
    user: "bar",
    pwd: "bar",
    roles: [
    { role: "dbAdmin", db: "foo" },
    { role: "readWrite", db: "foo" }
    ]
    }
    )
  6. 需要授权的方式启动: mongod --auth --config /usr/local/etc/mongod.conf

  7. 授权登录

    • 方式一: mongo 先进入然后 db.auth("bar", "bar") 授权
    • 方式二: mongo --port 27017 -u "bar" -p "bar" --authenticationDatabase "foo"

Vue-Cli 3 创建多页项目

Vue-Cli 3 创建单页项目 一文中介绍了创建单页项目, 现实中复杂一点的项目使用多页项目更合适一些, Vue-Cli 3 创建多页项目很容易, 创建的项目结构如下:

下面就简要的介绍下使用 Vue-Cli 3 创建多页项目的步骤:

  1. Vue-Cli 3 创建单页项目

  2. 在 src 目录下创建目录 pages/page1, pages/page2 (每个目录表示一个单页)

  3. 把 views 目录, App.vue, main.js, router.js, store.js 等都分别复制一份到 page1 和 page2 目录 (参考上面的项目结构图)

  4. 修改 vue.config.js 中的 pages, 配置每个单页的入口 (page1, page2 就是单页访问地址):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    module.exports = {
    devServer: {
    port: 8888,
    },
    pages: {
    page1: 'src/pages/page1/main.js',
    page2: 'src/pages/page2/main.js',
    },
    };
  5. 启动项目: yarn serve

  6. 访问页面:

  7. 打包项目: yarn build

  8. 自定义单页的入口模板

    有眼神好的同学可能看到了 public/index.htmlpublic/page2.html, 但是发现没有 public/page1.html, 这是因为 Vue-Cli 在 public 目录下发现页面对应的 ${subpage}.html 文件 (pages 下配置的 page1, page2), 就使用它生成这个页面的入口 html 文件, 如果没有则会使用 public/index.html 作为模板生成这个页面的入口 html 文件.

    由于不同页面引入的 js, css 或者第三方库等都可能不一样, 所以为页面定制自己的入口模板文件也是有必要的.

vue.config.js 参考

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
process.env.VUE_APP_VERSION = new Date().getTime();

module.exports = {
devServer: {
port: 8888,
proxy: 'http://localhost:8080'
},

// 多页的页面
pages: {
sample: 'src/pages/sample/main.js',
teacher: 'src/pages/teacher/main.js',
'admin-school': 'src/pages/admin-school/main.js',
'admin-system': 'src/pages/admin-system/main.js',
},

// yarn build 的输出目录
outputDir: '../ebag-web-app/src/main/webapp/WEB-INF/page-vue',
assetsDir: 'static',

css: {
loaderOptions: {
sass: {
// SCSS 全局变量
data: `
@import "@/../public/static/css/variables.scss";
`
}
}
}
};

页面的 HTML 中引入自定义 JS 和 CSS 文件时最好也加上打包时的时间戳 process.env.VUE_APP_VERSION, 避免缓存引起的问题:

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
<!DOCTYPE html>
<html>

<% hash = VUE_APP_VERSION %>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>老师</title>

<link rel="stylesheet" type="text/css" href="/static/css/global.css?hash=<%= hash %>">
</head>

<body>

<div id="app"></div>
<!-- 库文件 -->
<script src="/static/lib/jquery.min.js"></script>
<script src="/static/lib/jquery.rest.js"></script>

<!-- 项目的 JS: 增加了 hash 使得更新后不用刷新缓存就能使用最新的 JS -->
<script src="/static/lib/tio/Message.js?hash=<%= hash %>"></script>
<script src="/static/js/urls.js?hash=<%= hash %>"></script>

<!-- built files will be auto injected -->
</body>
</html>

ZooKeeper 实现分布式锁

在 Java 中使用 ZooKeeper 实现分布式锁可按以下几步进行:

  1. 下载安装 ZooKeeper 3.4.13,参考 本机安装 ZooKeeper 集群 进行安装单机版 ZooKeeper,有必要的时候再安装集群
  2. 引入 Curator 的依赖,它实现了 ZooKeeper 的分布式锁
  3. Java 测试程序

下载安装

参考 本机安装 ZooKeeper 集群

引入 Curator 的依赖

1
compile group: 'org.apache.curator', name: 'curator-recipes', version: '2.12.0'

注意: Curator 和 ZooKeeper 的版本需要对应,否则会报错

Curator 2.x.x: compatible with both ZooKeeper 3.4.x and ZooKeeper 3.5.x

Curator 3.x.x: compatible only with ZooKeeper 3.5.x and includes support for new features such as dynamic reconfiguration, etc.

Java 测试程序

官方文档: http://curator.apache.org/getting-started.html,先演示 Curator 连接 ZooKeeper 并使用分布式锁 InterProcessMutex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void testDistributedLock() throws Exception {
// [1] This will create a connection to a ZooKeeper cluster using default values.
// The only thing that you need to specify is the retry policy. For most cases, you should use:
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(10000, 3));

// [2] The client must be started (and closed when no longer needed).
client.start();

InterProcessMutex lock = new InterProcessMutex(client, "/ebag/lock");

// [3] 获取全局锁
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// [4] 业务代码
System.out.println("Do something");
} finally {
// [5] 释放全局锁
lock.release();
}
}

// [6] Close the client
client.close();
}

多线程测试分布式锁,结果 sn 是按顺序输出的,说明锁生效了:

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
public static void testZooKeeperThread() {
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(10000, 3));
client.start();
List<Thread> threads = new LinkedList<>();

for (int i = 0; i < 100; ++i) {
threads.add(new Thread(new ZooKeeperRunnable(client, "/ebag/lock")));
}

for (Thread thread : threads) {
thread.start();
}

// client.close();
}

class ZooKeeperRunnable implements Runnable {
public static int sn = 0;
private InterProcessMutex lock;

public ZooKeeperRunnable(CuratorFramework client, String path) {
lock = new InterProcessMutex(client, path);
}

public void run() {
try {
// 获取全局锁
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// Thread.sleep(100);
sn++;
System.out.println(sn);
} finally {
// 释放全局锁
lock.release();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

提示: 上面的多线程是同一个 JVM 中的,可以很简单的修改为在多个 JVM 中运行,演示真正的分布式锁的效果,共享资源 sn 可以保存到 Redis,数据库等中。

ZooKeeper 常用命令

  • 创建: create path data: create /foo "Hello"
  • 修改: set path data: set /foo "Vox"
  • 获取: get path: get /foo
  • 删除: delete path: delete /foo
  • 查看: ls path: ls /foo

CentOS7 安装 LibreOffice

使用 CentOS7 无界面版本作为服务器的操作系统,在里面安装 LibreOffice 用于转换各种文档到 PDF,LibreOffice 的安装步骤如下:

  1. 下载:

    1. 访问 https://www.libreoffice.org/download/download/
    2. 选择 Linux x86_64(rpm) 的版本
    3. 下载得到 LibreOffice_6.0.5_Linux_x86-64_rpm.tar.gz (目前最新版为 6.0.5)
  2. 安装:

    1. 删除: 在安装之前,先删除已经安装的 LibreOffice: yum remove libreoffice*
    2. 解压: tar -xvf LibreOffice_6.0.5_Linux_x86-64_rpm.tar.gz
    3. 安装:
      1. cd LibreOffice_6.0.5.2_Linux_x86-64_rpm/RPMS
      2. yum localinstall *.rpm
    4. 查看:
      1. which libreoffice6.0 看到路径为 /usr/bin/libreoffice6.0
      2. ll /usr/bin/libreoffice6.0 得到 /opt/libreoffice6.0/program/soffice,说明安装到了 /opt/libreoffice6.0
  3. 依赖:

    执行 libreoffice6.0 可能会提示库文件找不到,如 libcairo.so.2,libcups.so.2,libSM.so.6 等,执行下面几条命令安装需要的库:

    • yum install cairo -y
    • yum install cups-libs -y
    • yum install libSM -y

安装 LibreOffice 可参考 https://www.tecmint.com/install-libreoffice-on-rhel-centos-fedora-debian-ubuntu-linux-mint/

Java 中可使用 JODConverter 调用 LibreOffice 进行文件格式转换,可参考 Office 文档转为 PDF 和 HTML

QLineEdit 中增加按钮

下图为 Safari 的地址栏,在输入框右边有一个刷新按钮:

在输入框中增加按钮的设计是比较常见的,例如 Chrome 的地址栏、Firefox 的搜索框、Tim 的搜索框等,这种控件 Qt 没有提供,那应该怎么实现呢?下面提供 2 种思路:

  • QLineEdit + QPushButton 使用 QHBoxLayout 布局到一个 QWidget 中,去掉 QLineEdit 得到焦点时的高亮效果,此时应该高亮它的父控件,失去焦点时取消它的父控件的高亮效果
  • QLineEdit 作为一个普通的 QWidget,也就是它能够使用 QLayout 把 QPushButton 作为子控件布局到它里面

思路有了,第一种方式需要写很多代码进行控制,实现比较麻烦,下面只介绍第二种思路的实现。

模型视图编程

模型视图控制器 (MVC) 编程哪家讲的好,莫非是 Qt 帮助文档里的 Model/View Programming 了,可惜没有中文的,看过不少书里这方面的内容都是从这篇文档里抄的,我们就不要再花心思去创新了,直接翻译吧。

模型视图编程简介

Qt contains a set of item view classes that use a model/view architecture to manage the relationship between data and the way it is presented to the user. The separation of functionality introduced by this architecture gives developers greater flexibility to customize the presentation of items, and provides a standard model interface to allow a wide range of data sources to be used with existing item views. In this document, we give a brief introduction to the model/view paradigm, outline the concepts involved, and describe the architecture of the item view system. Each of the components in the architecture is explained, and examples are given that show how to use the classes provided.

Qt 中有几个 item view 的类 (视图),使用模型/视图架构来管理和显示数据。这种分离的设计给开发者很高的灵活性,可以自定义数据的显示方式,视图通过模型提供的标准接口可以支持各种各样的数据源。本文涉及到模型/视图编程范例、简要的概念介绍以及描述视图系统的架构。每一个部分都会进行解释以及给出相关代码展示怎么使用。

模型视图架构

Model-View-Controller (MVC) is a design pattern originating from Smalltalk that is often used when building user interfaces. In Design Patterns, Gamma et al. write:

MVC consists of three kinds of objects. The Model is the application object, the View is its screen presentation, and the Controller defines the way the user interface reacts to user input. Before MVC, user interface designs tended to lump these objects together. MVC decouples them to increase flexibility and reuse.

Smalltalk 经常用来构建用户界面,模型-视图-控制器 (MVC) 这种设计模式就是从 Smalltalk 借鉴而来的。Gamma et al. 在设计模式中写到:

MVC 由 3 种对象组成:Model 是应用的数据,View 显示数据,Controller 定义了用户界面对用户输入的响应。在使用 MVC 之前,用户界面的设计常常把这些对象耦合在一起。MVC 能够解耦它们从而提供更高的灵活性和重用性。

The model/view architecture
The model communicates with a source of data, providing an interface for the other components in the architecture. The nature of the communication depends on the type of data source, and the way the model is implemented.

The view obtains model indexes from the model; these are references to items of data. By supplying model indexes to the model, the view can retrieve items of data from the data source.
In standard views, a delegate renders the items of data. When an item is edited, the delegate communicates with the model directly using model indexes.

在模型/视图架构中,模型访问数据源中的数据,视图等控件通过模型提供的接口访问数据。通信的方式取决于数据源的类型和模型实现的方式。
数据由数据项组成,index 是数据项的引用,视图从模型中获得 index。视图通过访问模型中的 index 就能够访问数据源中的数据了。
在标准的视图中,代理用来渲染数据项。当数据项被编辑时,代理使用模型的 index 和模型交互。

Generally, the model/view classes can be separated into the three groups described above: models, views, and delegates. Each of these components is defined by abstract classes that provide common interfaces and, in some cases, default implementations of features. Abstract classes are meant to be subclassed in order to provide the full set of functionality expected by other components; this also allows specialized components to be written.

Models, views, and delegates communicate with each other using signals and slots:

  • Signals from the model inform the view about changes to the data held by the data source.
  • Signals from the view provide information about the user’s interaction with the items being displayed.
  • Signals from the delegate are used during editing to tell the model and view about the state of the editor.

根据上面的介绍,模型/视图的类可以分为三类:模型、视图和代理,它们都有相应的抽象类提供通用的接口和某些功能的默认实现。抽象类就意味着要被其他类继承,根据需求提供对应的实现。

模型、视图和代理之间使用信号槽进行通信:

  • 数据源中的数据发生变化时,模型发射信号通知视图
  • 用户和视图交互时视图会发射信号,例如点击 view item,信号的参数包含了被交互的 view item 的信息
  • 编辑数据项的时候代理会把编辑器的状态通过信号通知模型和视图

模型

All item models are based on the QAbstractItemModel class. This class defines an interface that is used by views and delegates to access data. The data itself does not have to be stored in the model; it can be held in a data structure or repository provided by a separate class, a file, a database, or some other application component.

The basic concepts surrounding models are presented in the section on Model Classes.

QAbstractItemModel provides an interface to data that is flexible enough to handle views that represent data in the form of tables, lists, and trees. However, when implementing new models for list and table-like data structures, the QAbstractListModel and QAbstractTableModel classes are better starting points because they provide appropriate default implementations of common functions. Each of these classes can be subclassed to provide models that support specialized kinds of lists and tables.

The process of subclassing models is discussed in the section on Creating New Models.

所有的 item 模型都是基于类 QAbstractItemModel 实现的。视图和代理使用 QAbstractItemModel 定义的接口访问数据。数据不一定是保存在模型中,也可以保存在其他类、文件、数据库或者其他应用中。

模型相关的概念在模型的类一节中进行介绍。

模型类 QAbstractItemModel 定义的访问数据接口 (函数) 是很灵活的,能够满足表格、列表和树用来显示模型的数据。然而,当给列表和表格自定义新的模型时,继承 QAbstractListModel 或者 QAbstractTableModel 是个很好的选择,因为他们提供了很多通用操作的默认实现,就不需要我们再重复实现。

实现自定义模型在创建新的模型类一节中进行介绍。

Qt provides some ready-made models that can be used to handle items of data:

  • QStringListModel is used to store a simple list of QString items.
  • QStandardItemModel manages more complex tree structures of items, each of which can contain arbitrary data.
  • QFileSystemModel provides information about files and directories in the local filing system.
  • QSqlQueryModel, QSqlTableModel, and QSqlRelationalTableModel are used to access databases using model/view conventions.

If these standard models do not meet your requirements, you can subclass QAbstractItemModel, QAbstractListModel, or QAbstractTableModel to create your own custom models.

Qt 已经提供了一些可用于处理数据项的模型:

  • QStringListModel 用于存储简单的列表数据,数据项为 QString
  • QStandardItemModel 用于管理有树结构关系的数据项,每个数据项可以包含任意的数据
  • QFileSystemModel 访问本地文件系统的文件和文件夹
  • QSqlQueryModel, QSqlTableModel 和 QSqlRelationalTableModel 使用模型视图的方式访问数据库

如果这些标准的模型还不能满足我们的需求,可以继承 QAbstractItemModel, QAbstractListModel, or QAbstractTableModel 实现自定义的模型类。

分组布局

在进行界面布局的时候,常把控件根据功能分组放在一起,最常用的就是使用 QGroupBox 来放置一组控件。QGroupBox 虽然使用起来很方便,但就是有点丑,在要求较高的设计中,还得使用控件组合加自定义绘图或者 QSS 等才能实现,例如下面这个软件界面,直接使用 Qt 提供的控件是满足不了的:

上图中分组的布局没有使用 QGroupBox,而是用几个控件组合起来实现的,设计如下:

异形按钮组

不少软件里看到过如下的按钮组,有 5 个按钮,中间 1 个,上下左右各一个:

可以通过绘图的方式实现:计算每一个按钮的位置、大小、图片、点击的时候判断点击到了哪个按钮然后刷新绘制它的样式,并调用相应的函数执行点击操作,难度还是相当大的。

操作图像像素,实现各种效果

Qt 中图像相关的类主要是 QPixmap 和 QImage,QPixmap 没有提供访问图像像素数据的接口,访问图像的像素数据需要使用 QImage,主要的函数有 (相关重载函数没有列出来):

1
2
3
4
5
6
7
8
// 获取图像的像素数据
QRgb pixel(int x, int y) const
QColor pixelColor(int x, int y) const
uchar* scanLine(int i)

// 设置图像的像素数据
void setPixel(int x, int y, uint index_or_rgb)
void setPixelColor(int x, int y, const QColor &color)

下面把一个图像转为灰度图为例介绍怎么操作图像的像素:

  1. 取得图像的宽、高
  2. 根据宽、高遍历每一个像素
  3. 得到每一个像素的 RGBA 颜色分量
  4. 对得到的颜色分量 RGBA 进行灰度计算得到新的颜色
  5. 使用计算得到的颜色设置对应像素

自定义按钮组

如下的按钮组相信大家都看到过,最左和最右的按钮是圆角的,中间的按钮是矩形的,同时只能有一个按钮是选中状态:

按钮的样式使用 QSS 实现,使用 setProperty 设置按钮的 class 属性为 GroupButton,就可以利用类选择器 .GroupButton 选择按钮组的按钮,避免它们的样式影响到普通按钮。为了单独设置最左和最右按钮的样式,使用 setProperty 为其设置一个属性 position,最左按钮的为 first,最右按钮的为 last,然后就能使用属性选择器 .GroupButton[position="first"].GroupButton[position="last"] 选择它们了。

SpringMVC 使用 @PathVariable 获取有 . 的 URL 中的变量

1
2
3
4
5
@GetMapping("/api/file/{filename}")
@ResponseBody
public String foo(@PathVariable String filename) {
return filename;
}

默认配置时,不同的 URL ,获取到的 filename 为:

  • /api/file/foo,filename 为 foo
  • /api/file/foo.pdf,filename 为 foo
  • /api/file/foo.pdf.png,filename 为 foo.pdf
  • /api/file/foo.pdf.png.doc,filename 为 foo.pdf.png

最后一个 . 被截断了,解决这个问题有 2 中方法:

  • 使用正则表达式进行路径匹配,映射为:@GetMapping("/api/file/{filename:.+}")

    • 缺点:每个路径的映射都要写一遍,不方便
    • 优点:缺点也是优点,只影响需要的路径
  • 配置 annotation-driven,映射为:@GetMapping("/api/file/{filename}")

    • 优点:只需要配置一次,整个应用都生效,方便
    • 缺点:优点也是缺点,影响了整个系统,不过还没有发现对整个系统有什么副作用
    1
    2
    3
    4
    <mvc:annotation-driven>
    <mvc:path-matching registered-suffixes-only="true"/>
    ...
    </mvc:annotation-driven>

QTreeView 小集

树形控件是非常常用的,例如组织结构、目录树、省市县的地区结构等都是典型的树形结构,Qt 里可以使用 QTreeView 和 QTreeWidget 来展示树形结构,这里我们只介绍 QTreeView 的使用,QTreeView 本身只用于树的显示,树的数据由 QStandardItemModel 来存储。

创建单列树

创建单列树的节点分两种情况:

  • 创建第一级节点调用函数 QStandardItemModel::appendRow(QStandardItem *item)
  • 创建第二级、第三级等非第一级节点调用函数 QStandardItem::(QStandardItem *item)

下面的例子创建省市县的树形结构展示如何创建只有一列的树,为了更好的从变量名上看出地区的关系,使用数字和层级的方式进行命名,程序运行结果如下:

去掉 png 图片的 iCCP 警告

Qt 中使用 png 图片有时候会给出警告 libpng warning: iCCP: known incorrect sRGB profile:

Libpng-1.6 is more stringent about checking ICC profiles than previous versions. You can ignore the warning. To get rid of it, remove the iCCP chunk from the PNG image.

Some applications treat warnings as errors; if you are using such an application you do have to remove the chunk.

解决办法:

  1. 安装 ImageMagick (Mac: brew install ImageMagick)
  2. 到图片文件夹,执行命令 mogrify *.png 去掉此文件夹下 png 图片的 iCCP 警告

要想找出有 iCCP 问题的 png 图片,可以使用工具 pngcrush:

  1. 安装 pngcrush (Mac: brew install pngcrush)
  2. 到图片文件夹,执行命令 pngcrush -n -q *.png 找出有 iCCP 警告的图片

更多细节请参考 libpng warning: iCCP: known incorrect sRGB profile

一次 HTTP 被运营商劫持的血泪史

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en" dir="ltr">

<head>
<script src="/js/jquery.js" charset="utf-8"></script>
<script src="/js/paper.js" charset="utf-8"></script>
</head>

<body>
<script>
$(document).ready(function() {
__Exam_PaperInit();
});
</script>
</body>

</html>

很简单的页面,jquery.js 和 paper.js 加载完,然后调用 __Exam_PaperInit() 进行初始化。在办公室、家里都没出现过问题,但是在学校的机房里访问这个页面,时不时的出错,提示如 __Exam_PaperInit() 不存在,这么简单直接的逻辑,咋会出错呢,想不明白,猜测例如是不是机房的环境网络设置有问题等,但花了很久仍然找不到原因,更要命的是,系统过几天就有几千人要用来考试了,问题解决不了的话,可以想象影响会有多大。

iTerm 设置

高亮输出

不同的输出显示为不同的颜色,例如下面的 [DEBUG] 的信息暗一些

Preferences > Profiles > Advanced > Triggers > Edit: 使用正则表达式进行设置,Action 选择为 Highlight Text...

GitBook 使用 Coding.net 的 Pages 访问

GitBook 生成的静态网页文件在 _book 目录中,下面介绍怎么把它发布到 coding.net 的 Pages 服务中,这样就能够通过网络访问了。

  1. https://coding.net 创建一个账号 xtuer(下面请换为自己的账号)
  2. 创建仓库 2 个仓库 fox 和 fox-doc (仓库名字随意取):
    • fox-doc: GitBook 源文件
    • fox: GitBook 生成的静态文件
  3. 克隆这 2 个仓库到本地的同一个文件夹下
    • git clone git@git.coding.net:xtuer/fox.git
    • git clone git@git.coding.net:xtuer/fox-doc.git

右键菜单

右键菜单有多种实现方式:

  • 设置 contextMenuPolicy 为:
    • Qt::ActionsContextMenu
    • Qt::CustomContextMenu
  • 重写 contextMenuEvent 函数

下面就分别介绍这几种右键菜单的实现。

iView 的 Table 中插入按钮

很多时候需要在 Table 的单元格中使用按钮,iView 的官方例子使用函数 createElement (简写 h) 来创建,但是代码很繁杂、不直观、难以实现复杂的 DOM 结构。还好除此之外可以使用 JSX 来实现,能够方便的增加 class、wrapper、图标、任意的 DOM 等。

JSX 实现

1
2
3
4
5
6
7
8
9
10
11
{ title: '操作', key: 'action', width: 160, align: 'center',
// 编辑和删除按钮
render: (h, params) => {
return (
<div class="cell-button-container">
<i-button type="primary" size="small" onClick={()=>{this.editSchool(params.index)}} icon="edit">编辑</i-button>
<i-button type="error" size="small" onClick={()=>{this.deleteSchool(params.index)}} icon="android-delete">删除</i-button>
</div>
);
}
}

提示:

  • 按钮的标签使用 <i-button>,不能使用 <Button>
  • 按钮的事件处理 vue 中为 on-click,但在 JSX 中为 onClick