可以使用MD5算法来实现文件去重,因为它可以接受任意大小的数据并输出固定长度的哈希值。所以两个不一样的文件一般情况下使用MD5计算出来的hash值是不可能会相等的。
所以一旦两个文件计算出来的hash值相同那么他们的文件就是相同的。
这时文件上传的一个例子,先使用md5算法计算文件的hash值,再检测我们磁盘是否有相同的文件名的文件,如果有那我们就不上传直接返回访问路径,如果没有才上传
@Override
public String uploadFile(MultipartFile file, String path) {
try {
// 获取文件md5值
String md5 = FileUtils.getMd5(file.getInputStream());
// 获取文件扩展名
String extName = FileUtils.getExtName(file.getOriginalFilename());
// 重新生成文件名
String fileName = md5 + extName;
System.out.println("filename "+fileName);
// 判断文件是否已存在
if (!exists(path + fileName)) {
// 不存在则继续上传
upload(path, fileName, file.getInputStream());
}
// 返回文件访问路径
return getFileAccessUrl(path + fileName);
} catch (Exception e) {
e.printStackTrace();
throw new BizException("文件上传失败");
}
}
我这就先看一下MD5得到Hash值的逻辑
public static String getMd5(InputStream inputStream) {
try {
MessageDigest md5 = MessageDigest.getInstance("md5");
byte[] buffer = new byte[8192];
int length;
while ((length = inputStream.read(buffer)) != -1) {
md5.update(buffer, 0, length);
}
return new String(Hex.encodeHex(md5.digest()));
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里使用的是JDK自带的MD5算法,也可以替换成其他库提供的MD5算法。讲一下得到MD5算法摘要的大致逻辑,当然不看 得到MD5算法摘要的的逻辑也是可以的,没有影响
先传入我们要得到的算法名称
MessageDigest.getInstance("md5");
MessageDigest.getInstance又会调用这个方法
GetInstance.Instance instance = GetInstance.getInstance("MessageDigest",
MessageDigestSpi.class, algorithm);
这里是先得到所有的算法类提供商,再在提供商列表中查找第一个支持该算法的提供商提供的服务类,如果这个服务类能够提供指定的clazz的实例就直接将这个Instance返回,如果不能就找下一个能提供该算法的提供商看行不行。
我们这里的clazz就是MessageDigestSpi类,该类为MessageDigest类定义服务提供者接口(Service Provider Interface, SPI),该类提供消息摘要算法的功能,如MD5或SHA。消息摘要是安全的单向哈希函数,它接受任意大小的数据并输出固定长度的哈希值。
public static Instance getInstance(String type, Class<?> clazz,
String algorithm) throws NoSuchAlgorithmException {
// in the almost all cases, the first service will work
// avoid taking long path if so
ProviderList list = Providers.getProviderList();
Service firstService = list.getService(type, algorithm);
if (firstService == null) {
throw new NoSuchAlgorithmException
(algorithm + " " + type + " not available");
}
NoSuchAlgorithmException failure;
try {
return getInstance(firstService, clazz);
} catch (NoSuchAlgorithmException e) {
failure = e;
}
for (Service s : list.getServices(type, algorithm)) {
if (s == firstService) {
// do not retry initial failed service
continue;
}
try {
return getInstance(s, clazz);
} catch (NoSuchAlgorithmException e) {
failure = e;
}
}
throw failure;
}
最后返回我们期望的消息摘要算法MessageDigest。除了有MD5以外还有SHA1等等算法可以选择
MessageDigest的方法:
- getInstance 得到算法摘要
- update 处理这些数据
- digest 转换并返回结果,也是字节数组
在校验文件重复性的时候,我们最后一步就是将MD5校验返回的字节数组编码成16进制的字符数组,然后用这个字符数组转换为字符串作为我们文件的名字,如果以后还有同样的文件被上传了,会对比是否有文件名相同的文件。
因为一个字节需要2个16进制数来表示,所以字符数组的大小是字节数组的大小的2倍
public static String encodeHex(byte[] byteArray) {
// 首先初始化一个字符数组,用来存放每个16进制字符
char[] hexDigits = {'0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F' };
// new一个字符数组,这个就是用来组成结果字符串的(解释一下:一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方))
char[] resultCharArray =new char[byteArray.length * 2];
// 遍历字节数组,通过位运算(位运算效率高),转换成字符放到字符数组中去
int index = 0;
for (byte b : byteArray) {
resultCharArray[index++] = hexDigits[b>>> 4 & 0xf];
resultCharArray[index++] = hexDigits[b& 0xf];
}
// 字符数组组合成字符串返回
return new String(resultCharArray);
}
对于计算文件的MD5 的Hash值时我们可以像这样使用基础的InputStrem
MessageDigest md5 = MessageDigest.getInstance("md5");
byte[] buffer = new byte[8192];
int length;
while ((length = inputStream.read(buffer)) != -1) {
md5.update(buffer, 0, length);
}
byte[] resultByteArray = MD5.digest();
也可以使用DigestInputStream
MessageDigest messageDigest =MessageDigest.getInstance("MD5");
// 使用DigestInputStream
DigestInputStream digestInputStream = new DigestInputStream(inputStream,messageDigest);
// read的过程中进行MD5处理,直到读完文件
byte[] buffer =new byte[bufferSize];
while (digestInputStream.read(buffer) > 0);
// 获取最终的MessageDigest
messageDigest= digestInputStream.getMessageDigest();
byte[] resultByteArray = messageDigest.digest();