最近项目的图片处理的系统出问题比较多,典型的一个问题就是因为计算md5出现的问题。
1.问题
问题的现象很简单,使用这套代码批量切图,会出现一张灵异图片,灵异之处在于,很多人批量切图都会出现这张图片,且这张图片不在他们的原始图片中。
2.原因
最后分析的原因是计算图片md5的代码,使用的是Java自带的MessageDigest,大致使用如下
1
2
3
4
5
6
7
8
9
10
11
12
13
//声明
private static MessageDigest md = null;
//初始化
static {
md = MessageDigest.getInstance("MD5");
}
//使用
public static byte[] getMD5(byte[] bytes) {
md.update(bytes); //-----------------------A
return md.digest(); //-----------------------B
}
问题的原因就是多线程计算md5的时候getMD5这个函数有问题。
2.1 问题md5
最后定位发现重复出现的md5是
d41d8cd98f00b204e9800998ecf8427e
这是一个特殊的md5,即其是空字符串的md5:
1
MD5("") = d41d8cd98f00b204e9800998ecf8427e
参考: http://zh.wikipedia.org/wiki/MD5
2.1 代码问题
在MessageDigest类的源代码注释有这样一段话:
1
2
3
4
5
6
7
8
9
10
* <p>A MessageDigest object starts out initialized. The data is
* processed through it using the {@link #update(byte) update}
* methods. At any point {@link #reset() reset} can be called
* to reset the digest. Once all the data to be updated has been
* updated, one of the {@link #digest() digest} methods should
* be called to complete the hash computation.
*
* <p>The <code>digest</code> method can be called once for a given number
* of updates. After <code>digest</code> has been called, the MessageDigest
* object is reset to its initialized state.
上面这段话第二部分的意思是说,digest()这个方法只能被调用一次,一旦调用MessageDigest对象会被重置到初始状态。
根据这个逻辑,getMD5这个函数的代码,多个线程进入这段代码的时候(这里假设两个线程),线程A执行完步骤1,在执行步骤2之前,线程B也执行完步骤1,之后线程B调用了步骤2得到正确的md5,之后线程A再调用步骤2的时候得到的md5就是d41d8cd98f00b204e9800998ecf8427e。
本地验证:
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
String src = "HelloWorld";
messageDigest.update(src.getBytes());
String md5 = bytesToHex(messageDigest.digest());
System.out.println("md5 = " + md5.toLowerCase());
String md5Again = bytesToHex(messageDigest.digest());
System.out.println("md5Again = " + md5Again.toLowerCase());
}
得到的结果:
1
2
md5 = 68e109f0f40ca72a15e05cc22786f8e6
md5Again = d41d8cd98f00b204e9800998ecf8427e
3.解决
3.1 MessageDigest正确用法
MessageDigest类的注释里面其实给出了正确的用法,注意其md.clone()的调用:
1
2
3
4
5
6
7
8
9
10
11
MessageDigest md = MessageDigest.getInstance("MD5");
try {
md.update(toChapter1);
MessageDigest tc1 = md.clone();
byte[] toChapter1Digest = tc1.digest();
md.update(toChapter2);
...etc.
} catch (CloneNotSupportedException cnse) {
throw new DigestException("couldn't make digest of partial content");
}
3.1 Apache的DigestUtils
Apache的DigestUtils是一个线程安全的类,是对MessageDigest的封装,使用很简单,首先加入依赖:
1
2
3
4
5
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
然后就可以使用DigestUtils计算md5: