前言

公司项目需要对接企业微信的微盘接口,然后卡在了"分块文件上传"这一步上,原因是因为上传文件前,对文件分块后,需要计算文件的累积 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++ 里的实现如下

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的结构体如下

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

public class WedriveSha1StateToHexStr {
    public native String call(int[] state);

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

二、生成头文件

在终端输入:

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

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

/* 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

#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 文件

终端输入:

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 是否调通

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

 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 算出来的一致, 完工!