Content Table

Spring Boot Converter

Spring Boot 启动时如果发现 ApplicationContext 中某个 Bean 的类继承了 org.springframework.core.convert.converter.Converter,则会自动的把它注册为 Converter。

例如前端传一个字符串格式的日期,Controller 中想自动转换为 java.time.LocalDate 对象,像下面这样做就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.xtuer.converter;

import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@Component
public class DateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String date) {
return LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
}

Java 调用 FFmpeg 转换视频音频

使用 FFmpeg 的命令把一种格式的视频转换为另一种格式的视频,例如把 test.avi 转为 test.mp4 的命令为 ffmpeg -i test.avi -vcodec h264 test.mp4,Java 中可以用 ProcessBuilder 调用这个命令执行转换:

1
2
3
4
5
public static void main(String[] args) throws IOException {
ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", "test.avi", "-vcodec", "h264", "test.mp4");
pb.directory(new File("/Users/Biao/Desktop")); // pb 的工作目录,设置为 test.avi 所在目录
pb.start();
}

直接调用命令转换虽然很方便,但是如果视频比较大,转换需要的时间比较长时,希望能够及时的得到转换进度并反馈给客户端,就要解析命令的输出获取转换进度,这时就比较麻烦了。接下来介绍 ffmpeg-cli-wrapper 的使用,它对 FFmpeg 的命令进行了封装,简化视频转换的开发难度。

ffmpeg-cli-wrapper: A fluent interface to running FFmpeg from Java.

AspectJ with Annotation - 自定义注解

AspectJ with XmlAspectJ with Annotation 中介绍了 2 中实现 AoP 的方式:

  • AspectJ with Xml 中介绍使用纯 XML 的方式配置切面 (Java 类) 和切入点 (类的方法)
  • AspectJ with Annotation 中介绍使用注解配置切面,方法限定表达式配置切入点

这里我们介绍实现 AoP 的第三种方法: 使用注解配置切面和切入点,主要有以下几个部分:

  • Gradle 依赖
  • 自定义注解
  • 自定义注解配置切入点
  • 使用自定义注解
  • Xml 文件中配置自动扫描包路径
  • 测试
  • 三种实现切面方式的比较
  • 使用 SpEL 增强注解

下面就以实现一个分布式锁的注解来进行介绍。

动态链接库和全局变量

同一个动态链接库里定义的全局变量在不同的应用程序 (进程) 里有各自独立的内存空间,互不影响。

如上图所示项目之间的依赖关系:

  • Lib-1 中定义了全局变量 count
  • App-1 连接了 Lib-1,访问 count
  • Lib-2 连接了 Lib-1,访问 count
  • App-2 连接了 Lib-1 和 Lib-2,访问 count

则全局变量 count:

  • App-1 中的 count 和 App-2 中的 count 不是同一个变量 (打印出变量的地址看一下,不一样)
  • App-2 和 Lib-2 中的 count 是同一个变量,因为他们属于同一个进程

类的静态成员变量也是全局变量,单例可使用类的静态成员变量实现,单例的类可编译成 Lib 提供给其他程序放心的使用,不同程序之间同一个类的单例对象不会互相影响。

LibAndGlobalVariable.7z 是按照上图的依赖关系创建的项目,可以下载来运行观察以便更好的加深印象。

Qt 编译 MySQL 驱动

很开心 Qt 5.0 发布后好几个版本 Windows 和 Mac 都自带了 MySQL 的驱动,以为以后就省事了。Qt 5.12 时 Windows 下也还带有 MySQL 驱动,不过忽然发现 Mac 下不带了,哎,又只好自己去编译了。

下面就介绍一下我们在 Mac 中编译 MySQL 驱动成功的步骤:

  1. 下载 MySQL 解压版 (macOS 10.14 (x86, 64-bit), Compressed TAR Archive),解压到 /usr/local (不要用 brew 安装)
  2. 命令行进入 MySQL 驱动源码目录: cd /Users/Biao/Qt5.12.4/5.12.4/Src/qtbase/src/plugins/sqldrivers/mysql
  3. 删除 mysql.pro 中的 QMAKE_USE += mysql 这一行
  4. 生成 Makefile 文件: qmake "INCLUDEPATH+=/usr/local/mysql/include" "LIBS+=-L/usr/local/mysql/lib -lmysqlclient" mysql.pro
  5. 编译安装: make && make install,然后在 Qt 的 plugins/sqldrivers 目录下就能看到 MySQL 的驱动 libqsqlmysql.dylib

编译的过程中出现下面的警告,忽略即可:

Cannot read /Users/Biao/Qt5.12.4/5.12.4/Src/qtbase/src/plugins/sqldrivers/qtsqldrivers-config.pri: No such file or directory


随便提一下,按照 Qt 自带帮助文档中的步骤进行编译:

cd $QTDIR/qtbase/src/plugins/sqldrivers
qmake – MYSQL_PREFIX=/usr/local
make sub-mysql

报错:

Cannot read /Users/Biao/Qt5.12.4/5.12.4/Src/qtbase/src/plugins/sqldrivers/qtsqldrivers-config.pri: No such file or directory
Project ERROR: Library ‘mysql’ is not defined.

可能是因为 MySQL 的安装问题吧,但是文档里也没提示这种情况下 Mac 里 MySQL 应该怎么安装,具体就不深入研究了,反正上面的方式能编译成功。

Windows 编译时参考上面 Mac 的步骤,只需要把第 4 步中的 -lmysqlclient 修改为 -lmysql,其他步骤不变,使用 MinGW 编译参考下面 2 条命令:

  • F:\Qt\Qt5.13.0\5.13.0\mingw73_32\bin\qmake.exe "INCLUDEPATH+=D:/mysql-5.7.29-win32/include" "LIBS+=-LD:/mysql-5.7.29-win32/lib -lmysql" mysql.pro
  • F:\Qt\Qt5.13.0\Tools\mingw730_32\bin\mingw32-make.exe

编译出来的 DLL 在目录 F:\Qt\Qt5.13.0\5.13.0\Src\qtbase\src\plugins\sqldrivers\plugins\sqldrivers

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