Content Table

Qt WebSocket

Qt 自带了 WebSocket 的客户端,使用起来也很方便,我们在其基础上进行了简单的封装,支持自动重连以及定时心跳发送。WebSocket 客户端的调用者只需要关注下面代码中的几个方法和信号就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MainWidget::MainWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MainWidget) {
ui->setupUi(this);

// 提示: 创建 Websocket 连接,服务器的 IP 根据实际情况填写,端口为 9321
wsClient = new WsClient("127.0.0.1:9321", "gw-1", "gw-1");

// 连接成功或者连接断开
connect(wsClient, &WsClient::connected, [this](bool yes) {
this->ui->stateLabel->setText(yes ? "连接成功" : "连接断开");
});

// 收到服务器发来的消息
connect(wsClient, &WsClient::messageReceived, [this](const QString &message) {
this->ui->responseLabel->setText(message);
});

// 点击按钮发送消息
connect(ui->pushButton, &QPushButton::clicked, [this] {
this->wsClient->sendMessage("ECHO", this->ui->lineEdit->text());
});

// 连接到服务器
wsClient->connectToServer();
}

Ajax 异步上传文件

异步上传

先选择文件,然后点击上传按钮使用 Ajax 异步的方式上传文件,代码如下:

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
<template>
<div class="about">
<input ref="fileElem" type="file" accept="image/jpeg"><br><br>
<Button type="primary" :loading="loading" @click="upload">上传文件</Button>
</div>
</template>

<script>
export default {
data() {
return {
file: null,
loading: false,
};
},
mounted() {
// 选择文件的事件处理
this.$refs.fileElem.addEventListener('change', () => {
this.file = this.$refs.fileElem.files[0];
});
},
methods: {
upload() {
this.loading = true;
var formData = new FormData();
formData.append('file', this.file, this.file.name);

uploadFile('/form/upload/temp/file', formData).then(response => {
console.log(response);
this.loading = false;
}).catch(error => {
console.error(error);
this.loading = false;
});
}
},
};

/**
* 上传文件
*
* @param {String} url 上传地址
* @param {JSON} formData 表单数据
*/
function uploadFile(url, formData) {
return new Promise((resolve, reject) => {
axios.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(response => {
// 提示: 响应部分根据使用的服务器端的响应结果进行简单调整
if (response.data.success) {
const uplaodedFile = response.data.data;
resolve(uplaodedFile);
} else {
reject(response.data.message);
}
}).catch(response => {
const error = response.response;
console.error(error);
reject(error);
});
});
}
</script>

自定义 Content Type Prober

在浏览器中点击 PDF 文件的链接:

  • 在 A 网站点击 a.pdf,浏览器自动下载 a.pdf
  • 在 B 网站点击 b.pdf,浏览器直接打开 b.pdf

被访问都是 PDF 文件,为啥在网站 A 和在网站 B 访问时,浏览器的行为不一样,是什么东西影响它在浏览器中的行为呢?答案就是浏览器会根据响应的 Content-Type 来决定下载还是打开它们 (当然 Content-Type 的值只是一个 hit,具体的操作还是要看浏览器的实现)。

文件的类型非常多,怎么获取文件的 Content-Type 是什么呢?Java 1.7 提供 java.nio.file.Files.probeContentType(Path path) 用于尝试获取文件的 Content-Type,但发现支持的文件类型不够全面,查看方法 probeContentType 的帮助文档:

This method uses the installed FileTypeDetector implementations to probe the given file to determine its content type. Each file type detector’s probeContentType is invoked, in turn, to probe the file type. If the file is recognized then the content type is returned. If the file is not recognized by any of the installed file type detectors then a system-default file type detector is invoked to guess the content type.

A given invocation of the Java virtual machine maintains a system-wide list of file type detectors. Installed file type detectors are loaded using the service-provider loading facility defined by the ServiceLoader class. Installed file type detectors are loaded using the system class loader. If the system class loader cannot be found then the extension class loader is used; If the extension class loader cannot be found then the bootstrap class loader is used. File type detectors are typically installed by placing them in a JAR file on the application class path or in the extension directory, the JAR file contains a provider-configuration file named java.nio.file.spi.FileTypeDetector in the resource directory META-INF/services, and the file lists one or more fully-qualified names of concrete subclass of FileTypeDetector that have a zero argument constructor. If the process of locating or instantiating the installed file type detectors fails then an unspecified error is thrown. The ordering that installed providers are located is implementation specific.

Java Classpath 加载 jar 的顺序

以在 Test.java 中加载 app-1.jar 和 app-2.jar 中的类 com.xtuer.Aloha 为例演示 classpath 中 jar 被加载的顺序。

app-1.jar 和 app-2.jar

创建项目,里面只有 1 个类 com.xtuer.Aloha,分别打包成 2 个版本的 jar 包:

  • app-1.jar: 返回 Aloha-1
  • app-2.jar: 返回 Aloha-2
1
2
3
4
5
6
7
8
9
package com.xtuer;

public class Aloha {
@Override
public String toString() {
return "Aloha-1"; // app-1.jar 使用
// return "Aloha-2"; // app-2.jar 使用
}
}

项目的目录结构 (此 gradle 管理的 Java 项目结构仅作为参考):

1
2
3
4
5
6
7
8
app
├── build.gradle
└── src
└── main
└── java
└── com
└── xtuer
└── Aloha.java

Spring Boot 使用 loader.path 加载其他 jar

Classpath

可以使用 classpath 指定类加载的路径,但 classpath 的生效是有条件的:

命令 classpath 生效 说明
java -cp .;lib/x.jar Test 运行 class
java -cp lib/x.jar -jar app.jar 运行 jar

Loader.path

Spring Boot 程序大多是打成 jar 包,使用 java -jar boot.jar 的方式启动 (此时 -cp 无效),可以使用 loader.path 指定类加载路径加载其他 jar,但 loader.path 生效是有条件的:

命令 MANIFEST.MF 的 Main-Class loader.path 生效 打包配置
java -Dloader.path=./lib -jar app.jar JarLauncher 默认配置
java -Dloader.path=./lib -jar app.jar PropertiesLauncher 额外配置

loader.path 实现了 classpath 的功能。

配置 Main-Class

为了使用 loader.path,需要把 jar 包的 Main-Class 配置为 PropertiesLauncher,在 build.gradle 中如下配置,可参考 Using the PropertiesLauncher:

1
2
3
4
5
bootJar {
manifest {
attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher'
}
}

LaunchedURLClassLoader

无论启动类是 JarLauncher 或者 PropertiesLauncher,loader.path 引入的 jar 和 Spring Boot 中 lib/*.jar 都是使用类加载器 org.springframework.boot.loader.LaunchedURLClassLoader 进行加载,也即是说他们使用的是同一个类加载器。

注意到同一个程序,打包成不同类型时,PropertiesLauncher (20s) 比 JarLauncher (8s) 启动慢很多。

Spring Boot Jasypt

使用 Jasypt 加密 Spring Boot 配置中的项,例如数据库密码。详细介绍可阅读 Springboot 配置文件、隐私数据脱敏的最佳实践Spring Boot 配置文件密码加密两种方案

本文主要介绍具体使用部分,注意 jasypt-spring-boot-starter 2.0 和 3.0 的区别。3.0 前后虽然都是使用 jasypt-1.9.3.jar,但是生成密文命令的参数有点区别:

版本 algorithm 默认值 iv-generator 默认值 加密命令
2.1.2 PBEWithMD5AndDES org.jasypt.iv.NoIvGenerator java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input=root password=xtuer algorithm=PBEWithMD5AndDES
3.0.3 PBEWITHHMACSHA512ANDAES_256 org.jasypt.iv.RandomIvGenerator java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input=root password=xtuer algorithm=PBEWITHHMACSHA512ANDAES_256 ivGeneratorClassName=org.jasypt.iv.RandomIvGenerator

提示:

  • input 为要加密的内容,password 为加密的密钥
  • jasypt-1.9.3.jar 可以从 maven 或者 gradle 本地仓库中找到

Spring 与多线程

直接创建线程

1
2
3
4
5
6
7
8
9
10
11
12
public void thread1() {
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
System.out.println("thread-1: " + i);
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}

Go 使用 Yaml

使用 kubernetes-sigs/yaml 来把对象序列化为 yaml 字符串,把 yaml 字符串反序列化为 go 对象。

简单对象

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
package main
import (
"fmt"
"sigs.k8s.io/yaml"
)

type Student struct {
Id int `json:"id"` // 可以是 `yaml:"id"`,但大多数第三方 struct 使用 json,所以这里就不使用 yaml 了
Name string `json:"name`
}

func main() {
// [1] 对象序列化为 yaml 字符串
student1 := Student{1, "Alice"}
yamlBytes, _ := yaml.Marshal(student1)
yamlString := string(yamlBytes)
fmt.Println(yamlString)

// [2] yaml 字符串反序列化为对象
student2 := Student{}
err := yaml.Unmarshal([]byte(yamlString), &student2)

// yaml 有错误时报错
if (err != nil) {
fmt.Println(err)
}

fmt.Println(student2)
}

下载 yaml:

1
2
go mod init
go mod download

编译执行:

1
go run test.go

运行输出:

1
2
3
4
id: 1
name: Alice

{1 Alice}