JAVA对接企业微信微盘分块上传接口 (JNI开发学习记录)

前言

公司项目需要对接企业微信的微盘接口,然后卡在了“分块文件上传”这一步上,原因是因为上传文件前,对文件分块后,需要计算文件的累积 SHA 值,官方提供了计算的 C++ DEMO,但是我查阅了很多资料都没办法用 JAVA 去实现这个流程。

企业微信微盘-文件分块上传接口:拖动到最下面可见到累积sha值的介绍
https://developer.work.weixin.qq.com/document/path/98004

官方提供的分块计算累积 sha 值 C++ DEMO:
https://github.com/wecomopen/file_block_digest

官方 DEMO 里介绍了 累积sha值的计算流程:

分块的累积sha值计算过程如下:

  • 将要上传的文件内容,按2M分块;
  • 对每一个分块,依次sha1_update;
  • 每次update,记录当前的state,转成16进制,作为当前块累积sha值
  • 当为最后一块(可能小于2M),update完再sha1_final得到的sha1值(即整个文件的sha1),作为最后一块累积sha值

以上过程得到的sha值,保持顺序依次放到数组,作为file_upload_init接口的block_sha参数输入。

而我在改写中主要卡在了第三步:每次update,记录当前的state,转成16进制,作为当前块的累积sha值

而该步骤在 C++ 里的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
static std::string StrToHex(const char* src, size_t len) {
std::stringstream ss;
char hex[3] = {0};
for (size_t i = 0; i < len; ++i) {
snprintf(hex, sizeof(hex), "%02x", (unsigned char)(src[i]));
ss << hex;
}
return ss.str();
}

static std::string SHA1State(SHA_CTX* ctx) {
return StrToHex((char *)&ctx->h0, SHA1_LENGTH);
}

SHA_CTX的结构体如下

1
2
3
4
5
6
7
8
typedef struct {
union {
u_int32_t h0; // 兼容openssl SHA_CTX结构
u_int32_t state[5];
};
u_int32_t count[2];
unsigned char buffer[64];
} SHA_CTX;

可以看到在 C++ 里将 state 转换成16进制字符串, 可以直接将类型强转成字符串类型去操作, 但是在 JAVA 不能这么转。我查了很多资料, 尝试了很多写法, 例如遍历state数组将int转hex后concat, 或者将数组写进流里把流转成字符串, 转出来的结果都与 C++ 算出来的结果不一致。

https://github.com/wecomopen/file_block_digest/tree/main/demo

至此我放弃了用 JAVA 改写 C++ DEMO 的想法,尝试用 JNI 去实现。

JNI(Java Native Interface): Java调用C/C++,C/C++调用Java的一套API

将 state 值转换为16进制字符串部分的逻辑, 使用 C++ 来实现。

我本人也是毫无 C++ 编程经验, 也是现学现搞, 查阅了大量资料, 现将实践过程记录如下。

注: 以下代码已脱敏处理。

正文

一、编写 JAVA 类代码

WedriveSha1StateToHexStr.java

1
2
3
4
5
6
7
public class WedriveSha1StateToHexStr {
public native String call(int[] state);

static {
System.loadLibrary("WedriveSha1StateToHexStr");
}
}

二、生成头文件

在终端输入:

1
javah -classpath JAVA项目工程目录/src/main/java cn.包地址.xxx.WedriveSha1StateToHexStr

生成出来的头文件内容如下, 无需编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class cn_包地址_WedriveSha1StateToHexStr */

#ifndef _Included_cn_包地址_WedriveSha1StateToHexStr
#define _Included_cn_包地址_WedriveSha1StateToHexStr
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: cn_包地址_WedriveSha1StateToHexStr
* Method: call
* Signature: ([I)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_cn_包地址_WedriveSha1StateToHexStr_call
(JNIEnv *, jobject, jintArray);

#ifdef __cplusplus
}
#endif
#endif

三、编辑 C++ 实现代码

WedriveSha1StateToHexStr.cpp

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
#include <stdio.h>
#include <iostream>
#include <sstream>
#include "生成的头文件名称.h"

using namespace std;

union data {
u_int32_t state[5];
u_int32_t h0;
};

// 此方法直接copy自企微的demo代码
static std::string StrToHex(const char* src, size_t len) {
std::stringstream ss;
char hex[3] = {0};
for (size_t i = 0; i < len; ++i) {
snprintf(hex, sizeof(hex), "%02x", (unsigned char)(src[i]));
ss << hex;
}
return ss.str();
}

/**
* jni实现
*/
JNIEXPORT jstring JNICALL Java_cn_包地址_WedriveSha1StateToHexStr_call(JNIEnv *env, jobject jobj, jintArray jarr) {
// 获取数组指针
jint *arr = env -> GetIntArrayElements(jarr, NULL);

// 赋值
union data data;
data.state[0] = arr[0];
data.state[1] = arr[1];
data.state[2] = arr[2];
data.state[3] = arr[3];
data.state[4] = arr[4];

// 释放资源
env -> ReleaseIntArrayElements(jarr, arr, JNI_COMMIT);

// 转16进制
string hex = StrToHex((char*)&data.h0, 20);
return env->NewStringUTF(hex.c_str());
}

四、编译生成 jnilib 文件

终端输入:

1
g++ -std=c++11 -I/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/include/darwin -dynamiclib WedriveSha1StateToHexStr.cpp -o WedriveSha1StateToHexStr.jnilib

注: 自行根据 JDK 版本及路径修改以上命令

五、JAVA 验证

此处直接在第一步写的类里, 编写 main 方法来测试 JNI 是否调通

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class WedriveSha1StateToHexStr {
public native String call(int[] state);

static {
System.load("/Users/crazykid/Downloads/file_block_digest-main/WedriveSha1StateToHexStr.jnilib");//此行有修改, 调整成直接load生成的jnilib文件
}

public static void main(String[] args) {
// 这是企微的官方demo示例文件分块后第一块的state值
int[] intArray = {-469858735, 2004787070, -1463880031, 942072788, 1148000469};

WedriveSha1StateToHexStr obj = new WedriveSha1StateToHexStr();
String result = obj.call(intArray);
System.out.println(result);
}
}

运行程序, 成功输出 sha 值, 且与官方提供的第一个分块的 sha 值一致。

六、完整测试

编写测试方法, 读取企微提供的实例文件 sha_calc_demo.txt, 根据企微提供的demo, 计算其累积sha值。

https://github.com/wecomopen/file_block_digest/blob/main/demo/sha_calc_demo.txt

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
67
68
69
70
71
72
73
74
75
76
public static void main(String[] args) {
// 模拟待上传文件
File sourceFile = new File("/Users/crazykid/Downloads/sha_calc_demo.txt");
// 定义块文件大小 2MiB
int chunkFileSize = 2097152;
// 块数
int chunkFileNum = ((Double) Math.ceil(sourceFile.length() * 1.0 / chunkFileSize)).intValue();
// 累积sha值数组
List<String> sha1List = Lists.newArrayListWithCapacity(chunkFileNum);

FileInputStream in = null;
try {
in = new FileInputStream(sourceFile);
Digest digest = new SHA1Digest();

// 利用反射读取当前sha1加密进度的state值
Field h1 = SHA1Digest.class.getDeclaredField("H1");
Field h2 = SHA1Digest.class.getDeclaredField("H2");
Field h3 = SHA1Digest.class.getDeclaredField("H3");
Field h4 = SHA1Digest.class.getDeclaredField("H4");
Field h5 = SHA1Digest.class.getDeclaredField("H5");
h1.setAccessible(true);
h2.setAccessible(true);
h3.setAccessible(true);
h4.setAccessible(true);
h5.setAccessible(true);

// jni调用类
WedriveSha1StateToHexStr wedriveSha1StateToHexStr = new WedriveSha1StateToHexStr();

byte[] buffer = new byte[chunkFileSize];
int len;
for (int i = 0; i < chunkFileNum; i++) {
// 读取文件块
len = in.read(buffer);
if (len <= 0) {
break;
}
// sha1 update
digest.update(buffer, 0, len);

if (i == chunkFileNum - 1) {
// 最后一块跳出循环, 执行final后取最终文件sha1
break;
}

// 当前sha1的state值
int[] state = {(int) h1.get(digest),
(int) h2.get(digest),
(int) h3.get(digest),
(int) h4.get(digest),
(int) h5.get(digest)};

// jni调用c++代码获取16进制字符串作为当前块的累积sha值
String hex = wedriveSha1StateToHexStr.call(state);
sha1List.add(hex);
}

// 文件最终sha值
byte[] sha1Bytes = new byte[digest.getDigestSize()];
digest.doFinal(sha1Bytes, 0);
String finalSha1 = Hex.toHexString(sha1Bytes);
System.out.println("文件最终sha值:" + finalSha1);
sha1List.add(finalSha1);
// 打印结果
System.out.println(sha1List);
} catch (Exception ignored) {
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException ignored) {
}
}
}

运行程序, 检验打印出来的 sha 值列表, 与官方 DEMO 算出来的一致, 完工!