Content Table

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 跨级了。