最近项目的图片处理的系统出问题比较多,典型的一个问题就是因为计算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: