前言
公司项目需要对接企业微信的微盘接口,然后卡在了“分块文件上传”这一步上,原因是因为上传文件前,对文件分块后,需要计算文件的累积 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; 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 #include <jni.h> #ifndef _Included_cn_包地址_WedriveSha1StateToHexStr #define _Included_cn_包地址_WedriveSha1StateToHexStr #ifdef __cplusplus extern "C" {#endif 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; }; 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(); } 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); 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" ); } public static void main (String[] args) { 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" ); int chunkFileSize = 2097152 ; int chunkFileNum = ((Double) Math.ceil(sourceFile.length() * 1.0 / chunkFileSize)).intValue(); List<String> sha1List = Lists.newArrayListWithCapacity(chunkFileNum); FileInputStream in = null ; try { in = new FileInputStream(sourceFile); Digest digest = new SHA1Digest(); 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 ); 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 ; } digest.update(buffer, 0 , len); if (i == chunkFileNum - 1 ) { break ; } int [] state = {(int ) h1.get(digest), (int ) h2.get(digest), (int ) h3.get(digest), (int ) h4.get(digest), (int ) h5.get(digest)}; String hex = wedriveSha1StateToHexStr.call(state); sha1List.add(hex); } 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 算出来的一致, 完工!