diff --git a/src/main/java/chanjarster/weixin/api/WxConsts.java b/src/main/java/chanjarster/weixin/api/WxConsts.java index 0f4fbb8e..d2ae9050 100644 --- a/src/main/java/chanjarster/weixin/api/WxConsts.java +++ b/src/main/java/chanjarster/weixin/api/WxConsts.java @@ -26,7 +26,6 @@ public class WxConsts { public static final String MEDIA_THUMB = "thumb"; public static final String FILE_JPG = "jpeg"; - public static final String FILE_PNG = "png"; public static final String FILE_MP3 = "mp3"; public static final String FILE_ARM = "arm"; public static final String FILE_MP4 = "mp4"; diff --git a/src/main/java/chanjarster/weixin/api/WxService.java b/src/main/java/chanjarster/weixin/api/WxService.java index b39fbf6c..fe37ed15 100644 --- a/src/main/java/chanjarster/weixin/api/WxService.java +++ b/src/main/java/chanjarster/weixin/api/WxService.java @@ -44,6 +44,7 @@ public interface WxService { /** *
    * 上传多媒体文件
+   * 
    * 上传的多媒体文件有格式和大小限制,如下:
    *   图片(image): 1M,支持JPG格式
    *   语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
@@ -67,6 +68,18 @@ public interface WxService {
    */
   public WxUploadResult uploadMedia(String mediaType, File file) throws WxErrorException;
   
+  /**
+   * 
+   * 下载多媒体文件
+   * 
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
+   * 
+ * @params media_id + * @return 保存到本地的临时文件 + * @throws WxErrorException + */ + public File downloadMedia(String media_id) throws WxErrorException; + /** *
    * 发送客服消息
@@ -75,7 +88,7 @@ public interface WxService {
    * @param message
    * @throws WxErrorException
    */
-  public String sendCustomMessage(WxCustomMessage message) throws WxErrorException;
+  public void sendCustomMessage(WxCustomMessage message) throws WxErrorException;
   
   /**
    * 
@@ -85,7 +98,7 @@ public interface WxService {
    * @param menu
    * @throws WxErrorException
    */
-  public String createMenu(WxMenu menu) throws WxErrorException;
+  public void createMenu(WxMenu menu) throws WxErrorException;
   
   /**
    * 
@@ -94,7 +107,7 @@ public interface WxService {
    * 
* @throws WxErrorException */ - public String deleteMenu() throws WxErrorException; + public void deleteMenu() throws WxErrorException; /** *
diff --git a/src/main/java/chanjarster/weixin/api/WxServiceImpl.java b/src/main/java/chanjarster/weixin/api/WxServiceImpl.java
index 51d7421c..f6e4b19f 100644
--- a/src/main/java/chanjarster/weixin/api/WxServiceImpl.java
+++ b/src/main/java/chanjarster/weixin/api/WxServiceImpl.java
@@ -1,7 +1,6 @@
 package chanjarster.weixin.api;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.MessageDigest;
@@ -10,15 +9,9 @@ import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.commons.lang3.StringUtils;
-import org.apache.http.Consts;
-import org.apache.http.HttpEntity;
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.entity.mime.MultipartEntityBuilder;
 import org.apache.http.impl.client.BasicResponseHandler;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
@@ -29,7 +22,12 @@ import chanjarster.weixin.bean.WxMenu;
 import chanjarster.weixin.bean.result.WxError;
 import chanjarster.weixin.bean.result.WxUploadResult;
 import chanjarster.weixin.exception.WxErrorException;
-import chanjarster.weixin.util.Utf8ResponseHandler;
+import chanjarster.weixin.util.fs.FileUtil;
+import chanjarster.weixin.util.http.MediaDownloadRequestExecutor;
+import chanjarster.weixin.util.http.MediaUploadRequestExecutor;
+import chanjarster.weixin.util.http.RequestExecutor;
+import chanjarster.weixin.util.http.SimpleGetRequestExecutor;
+import chanjarster.weixin.util.http.SimplePostRequestExecutor;
 
 public class WxServiceImpl implements WxService {
 
@@ -62,7 +60,7 @@ public class WxServiceImpl implements WxService {
     }
   }
   
-  protected static String bytesToHex(byte[] b) {
+  protected String bytesToHex(byte[] b) {
     char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7',
                        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
     StringBuffer buf = new StringBuffer();
@@ -110,25 +108,25 @@ public class WxServiceImpl implements WxService {
     }
   }
   
-  public String sendCustomMessage(WxCustomMessage message) throws WxErrorException {
+  public void sendCustomMessage(WxCustomMessage message) throws WxErrorException {
     String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send";
-    return post(url, message.toJson());
+    execute(new SimplePostRequestExecutor(), url, message.toJson());
   }
   
-  public String createMenu(WxMenu menu) throws WxErrorException {
+  public void createMenu(WxMenu menu) throws WxErrorException {
     String url = "https://api.weixin.qq.com/cgi-bin/menu/create";
-    return post(url, menu.toJson());
+    execute(new SimplePostRequestExecutor(), url, menu.toJson());
   }
   
-  public String deleteMenu() throws WxErrorException {
+  public void deleteMenu() throws WxErrorException {
     String url = "https://api.weixin.qq.com/cgi-bin/menu/delete";
-    return get(url, null);
+    execute(new SimpleGetRequestExecutor(), url, null);
   }
 
   public WxMenu getMenu() throws WxErrorException {
     String url = "https://api.weixin.qq.com/cgi-bin/menu/get";
     try {
-      String resultContent = get(url, null);
+      String resultContent = execute(new SimpleGetRequestExecutor(), url, null);
       return WxMenu.fromJson(resultContent);
     } catch (WxErrorException e) {
       // 46003 不存在的菜单数据
@@ -140,30 +138,28 @@ public class WxServiceImpl implements WxService {
   }
 
   public WxUploadResult uploadMedia(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException {
-    return uploadMedia(mediaType, createTmpFile(inputStream, fileType));
+    return uploadMedia(mediaType,FileUtil.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
   }
   
   public WxUploadResult uploadMedia(String mediaType, File file) throws WxErrorException {
     String url = "http://file.api.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType;
-    String json = post(url, file);
-    return WxUploadResult.fromJson(json);
+    return execute(new MediaUploadRequestExecutor(), url, file);
   }
   
-  protected String post(String uri, Object data) throws WxErrorException {
-    return execute("POST", uri, data);
-  }
-  
-  protected String get(String uri, Object data) throws WxErrorException {
-    return execute("GET", uri, data);
+  public File downloadMedia(String media_id) throws WxErrorException {
+    String url = "http://file.api.weixin.qq.com/cgi-bin/media/get";
+    return execute(new MediaDownloadRequestExecutor(), url, "media_id=" + media_id);
   }
 
   /**
    * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求
-   * @param request
-   * @return 微信服务端返回的结果
-   * @throws WxErrorException 
+   * @param executor
+   * @param uri
+   * @param data
+   * @return
+   * @throws WxErrorException
    */
-  protected String execute(String method, String uri, Object data) throws WxErrorException {
+  public  T execute(RequestExecutor executor, String uri, E data) throws WxErrorException {
     if (StringUtils.isBlank(wxConfigStorage.getAccessToken())) {
       refreshAccessToken();
     }
@@ -173,38 +169,9 @@ public class WxServiceImpl implements WxService {
     uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken;
     
     try {
-      String resultContent = null;
-      if ("POST".equals(method)) {
-        HttpPost httpPost = new HttpPost(uriWithAccessToken);
-        if (data != null) {
-          if (data instanceof String) {
-            StringEntity entity = new StringEntity((String)data, Consts.UTF_8);
-            httpPost.setEntity(entity);
-          }
-          if (data instanceof File) {
-            File file = (File) data;
-            HttpEntity entity = MultipartEntityBuilder
-                  .create()
-                  .addBinaryBody("media", file)
-                  .build();
-            httpPost.setEntity(entity);
-            httpPost.setHeader("Content-Type", ContentType.MULTIPART_FORM_DATA.toString());
-          }
-        }
-        CloseableHttpResponse response = httpclient.execute(httpPost);
-        resultContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
-      } else if ("GET".equals(method)) {
-        if (data != null) {
-          if (data instanceof String) {
-            uriWithAccessToken += uriWithAccessToken.endsWith("&") ? data : '&' + (String)data;
-          }
-        }
-        HttpGet httpGet = new HttpGet(uriWithAccessToken);
-        CloseableHttpResponse response = httpclient.execute(httpGet);
-        resultContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
-      }
-      
-      WxError error = WxError.fromJson(resultContent);
+      return executor.execute(uriWithAccessToken, data);
+    } catch (WxErrorException e) {
+      WxError error = e.getError();
       /*
        * 发生以下情况时尝试刷新access_token
        * 40001 获取access_token时AppSecret错误,或者access_token无效
@@ -212,12 +179,24 @@ public class WxServiceImpl implements WxService {
        */
       if (error.getErrcode() == 42001 || error.getErrcode() == 40001) {
         refreshAccessToken();
-        return execute(method, uri, data);
+        return execute(executor, uri, data);
+      }
+      /**
+       * -1 系统繁忙, 500ms后重试
+       */
+      if (error.getErrcode() == -1) {
+        try {
+          System.out.println("微信系统繁忙,500ms后重试");
+          Thread.sleep(500);
+          return execute(executor, uri, data);
+        } catch (InterruptedException e1) {
+          throw new RuntimeException(e1);
+        }
       }
       if (error.getErrcode() != 0) {
         throw new WxErrorException(error);
       }
-      return resultContent;
+      return null;
     } catch (ClientProtocolException e) {
       throw new RuntimeException(e);
     } catch (IOException e) {
@@ -225,38 +204,8 @@ public class WxServiceImpl implements WxService {
     }
   }
   
-  protected File createTmpFile(InputStream inputStream, String fileType) throws IOException {
-    FileOutputStream fos = null;
-    try {
-      File tmpFile = File.createTempFile(UUID.randomUUID().toString(), '.' + fileType);
-      tmpFile.deleteOnExit();
-      fos = new FileOutputStream(tmpFile);
-      int read = 0;
-      byte[] bytes = new byte[1024 * 100];
-      while ((read = inputStream.read(bytes)) != -1) {
-        fos.write(bytes, 0, read);
-      }
-      fos.flush();
-      return tmpFile;
-    } finally {
-      if (inputStream != null) {
-        try {
-          inputStream.close();
-        } catch (IOException e) {
-        }
-      }
-      if (fos != null) {
-        try {
-          fos.close();
-        } catch (IOException e) {
-        }
-      }
-    }
-  }
-  
   public void setWxConfigStorage(WxConfigStorage wxConfigProvider) {
     this.wxConfigStorage = wxConfigProvider;
   }
 
-
 }
diff --git a/src/main/java/chanjarster/weixin/bean/WxAccessToken.java b/src/main/java/chanjarster/weixin/bean/WxAccessToken.java
index 90b357d0..8c02f538 100644
--- a/src/main/java/chanjarster/weixin/bean/WxAccessToken.java
+++ b/src/main/java/chanjarster/weixin/bean/WxAccessToken.java
@@ -1,6 +1,6 @@
 package chanjarster.weixin.bean;
 
-import chanjarster.weixin.util.WxGsonBuilder;
+import chanjarster.weixin.util.json.WxGsonBuilder;
 
 public class WxAccessToken {
 
diff --git a/src/main/java/chanjarster/weixin/bean/WxCustomMessage.java b/src/main/java/chanjarster/weixin/bean/WxCustomMessage.java
index 9b41f8bf..2ca24e67 100644
--- a/src/main/java/chanjarster/weixin/bean/WxCustomMessage.java
+++ b/src/main/java/chanjarster/weixin/bean/WxCustomMessage.java
@@ -3,7 +3,7 @@ package chanjarster.weixin.bean;
 import java.util.ArrayList;
 import java.util.List;
 
-import chanjarster.weixin.util.WxGsonBuilder;
+import chanjarster.weixin.util.json.WxGsonBuilder;
 
 /**
  * 客服消息
diff --git a/src/main/java/chanjarster/weixin/bean/WxMenu.java b/src/main/java/chanjarster/weixin/bean/WxMenu.java
index a1662c7d..be79386c 100644
--- a/src/main/java/chanjarster/weixin/bean/WxMenu.java
+++ b/src/main/java/chanjarster/weixin/bean/WxMenu.java
@@ -5,7 +5,7 @@ import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.List;
 
-import chanjarster.weixin.util.WxGsonBuilder;
+import chanjarster.weixin.util.json.WxGsonBuilder;
 
 /**
  * 公众号菜单
diff --git a/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java b/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java
index a89f649e..71650567 100644
--- a/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java
+++ b/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java
@@ -9,8 +9,8 @@ import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
 
-import chanjarster.weixin.util.AdapterCDATA;
-import chanjarster.weixin.util.XmlTransformer;
+import chanjarster.weixin.util.xml.AdapterCDATA;
+import chanjarster.weixin.util.xml.XmlTransformer;
 
 /**
  * 
diff --git a/src/main/java/chanjarster/weixin/bean/result/WxError.java b/src/main/java/chanjarster/weixin/bean/result/WxError.java
index 051a777e..69a4ddef 100644
--- a/src/main/java/chanjarster/weixin/bean/result/WxError.java
+++ b/src/main/java/chanjarster/weixin/bean/result/WxError.java
@@ -3,7 +3,7 @@ package chanjarster.weixin.bean.result;
 import java.util.HashMap;
 import java.util.Map;
 
-import chanjarster.weixin.util.WxGsonBuilder;
+import chanjarster.weixin.util.json.WxGsonBuilder;
 
 /**
  * 微信错误码说明
diff --git a/src/main/java/chanjarster/weixin/bean/result/WxUploadResult.java b/src/main/java/chanjarster/weixin/bean/result/WxUploadResult.java
index cf605fe9..98bee3e3 100644
--- a/src/main/java/chanjarster/weixin/bean/result/WxUploadResult.java
+++ b/src/main/java/chanjarster/weixin/bean/result/WxUploadResult.java
@@ -1,6 +1,6 @@
 package chanjarster.weixin.bean.result;
 
-import chanjarster.weixin.util.WxGsonBuilder;
+import chanjarster.weixin.util.json.WxGsonBuilder;
 
 public class WxUploadResult {
 
diff --git a/src/main/java/chanjarster/weixin/util/fs/FileUtil.java b/src/main/java/chanjarster/weixin/util/fs/FileUtil.java
new file mode 100644
index 00000000..3faeb4dd
--- /dev/null
+++ b/src/main/java/chanjarster/weixin/util/fs/FileUtil.java
@@ -0,0 +1,47 @@
+package chanjarster.weixin.util.fs;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class FileUtil {
+
+  /**
+   * 创建临时文件
+   * @param inputStream
+   * @param name  文件名
+   * @param ext   扩展名
+   * @return
+   * @throws IOException
+   */
+  public static File createTmpFile(InputStream inputStream, String name, String ext) throws IOException {
+    FileOutputStream fos = null;
+    try {
+      File tmpFile = File.createTempFile(name, '.' + ext);
+      tmpFile.deleteOnExit();
+      fos = new FileOutputStream(tmpFile);
+      int read = 0;
+      byte[] bytes = new byte[1024 * 100];
+      while ((read = inputStream.read(bytes)) != -1) {
+        fos.write(bytes, 0, read);
+      }
+      fos.flush();
+      return tmpFile;
+    } finally {
+      if (inputStream != null) {
+        try {
+          inputStream.close();
+        } catch (IOException e) {
+        }
+      }
+      if (fos != null) {
+        try {
+          fos.close();
+        } catch (IOException e) {
+        }
+      }
+    }
+  }
+  
+}
diff --git a/src/main/java/chanjarster/weixin/util/http/InputStreamResponseHandler.java b/src/main/java/chanjarster/weixin/util/http/InputStreamResponseHandler.java
new file mode 100644
index 00000000..37d57790
--- /dev/null
+++ b/src/main/java/chanjarster/weixin/util/http/InputStreamResponseHandler.java
@@ -0,0 +1,27 @@
+package chanjarster.weixin.util.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.client.HttpResponseException;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.util.EntityUtils;
+
+public class InputStreamResponseHandler implements ResponseHandler {
+
+  public static final ResponseHandler INSTANCE = new InputStreamResponseHandler();
+  
+  public InputStream handleResponse(final HttpResponse response) throws HttpResponseException, IOException {
+    final StatusLine statusLine = response.getStatusLine();
+    final HttpEntity entity = response.getEntity();
+    if (statusLine.getStatusCode() >= 300) {
+      EntityUtils.consume(entity);
+      throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
+    }
+    return entity == null ? null : entity.getContent();
+  }
+
+}
diff --git a/src/main/java/chanjarster/weixin/util/http/MediaDownloadRequestExecutor.java b/src/main/java/chanjarster/weixin/util/http/MediaDownloadRequestExecutor.java
new file mode 100644
index 00000000..46e563d5
--- /dev/null
+++ b/src/main/java/chanjarster/weixin/util/http/MediaDownloadRequestExecutor.java
@@ -0,0 +1,68 @@
+package chanjarster.weixin.util.http;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.Header;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.entity.ContentType;
+
+import chanjarster.weixin.bean.result.WxError;
+import chanjarster.weixin.exception.WxErrorException;
+import chanjarster.weixin.util.fs.FileUtil;
+
+/**
+ * 下载媒体文件请求执行器,请求的参数是String, 返回的结果是String
+ * @author chanjarster
+ *
+ */
+public class MediaDownloadRequestExecutor implements RequestExecutor {
+
+  @Override
+  public File execute(String uri, String queryParam) throws WxErrorException, ClientProtocolException, IOException {
+    if (queryParam != null) {
+      if (uri.indexOf('?') == -1) {
+        uri += '?';
+      }
+      uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+    }
+    
+    HttpGet httpGet = new HttpGet(uri);
+    CloseableHttpResponse response = httpclient.execute(httpGet);
+
+    Header[] contentTypeHeader = response.getHeaders("Content-Type");
+    if (contentTypeHeader != null && contentTypeHeader.length > 0) {
+      // 下载媒体文件出错
+      if (ContentType.TEXT_PLAIN.getMimeType().equals(contentTypeHeader[0].getValue())) {
+        String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+        throw new WxErrorException(WxError.fromJson(responseContent));
+      }
+    }
+    InputStream inputStream = InputStreamResponseHandler.INSTANCE.handleResponse(response);
+    
+    // 视频文件不支持下载
+    String fileName = getFileName(response);
+    if (StringUtils.isBlank(fileName)) {
+      return null;
+    }
+    String[] name_ext = fileName.split("\\.");
+    File localFile = FileUtil.createTmpFile(inputStream, name_ext[0], name_ext[1]);
+    return localFile;
+  }
+
+  protected String getFileName(CloseableHttpResponse response) {
+    Header[] contentDispositionHeader = response.getHeaders("Content-disposition");
+    Pattern p = Pattern.compile(".*filename=\"(.*)\"");
+    Matcher m = p.matcher(contentDispositionHeader[0].getValue());
+    m.matches();
+    String fileName = m.group(1);
+    return fileName;
+  }
+  
+}
diff --git a/src/main/java/chanjarster/weixin/util/http/MediaUploadRequestExecutor.java b/src/main/java/chanjarster/weixin/util/http/MediaUploadRequestExecutor.java
new file mode 100644
index 00000000..46287ab1
--- /dev/null
+++ b/src/main/java/chanjarster/weixin/util/http/MediaUploadRequestExecutor.java
@@ -0,0 +1,44 @@
+package chanjarster.weixin.util.http;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+
+import chanjarster.weixin.bean.result.WxError;
+import chanjarster.weixin.bean.result.WxUploadResult;
+import chanjarster.weixin.exception.WxErrorException;
+
+/**
+ * 上传媒体文件请求执行器,请求的参数是File, 返回的结果是String
+ * @author chanjarster
+ *
+ */
+public class MediaUploadRequestExecutor implements RequestExecutor {
+
+  @Override
+  public WxUploadResult execute(String uri, File file) throws WxErrorException, ClientProtocolException, IOException {
+    HttpPost httpPost = new HttpPost(uri);
+    if (file != null) {
+      HttpEntity entity = MultipartEntityBuilder
+            .create()
+            .addBinaryBody("media", file)
+            .build();
+      httpPost.setEntity(entity);
+      httpPost.setHeader("Content-Type", ContentType.MULTIPART_FORM_DATA.toString());
+    }
+    CloseableHttpResponse response = httpclient.execute(httpPost);
+    String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+    WxError error = WxError.fromJson(responseContent);
+    if (error.getErrcode() != 0) {
+      throw new WxErrorException(error);
+    }
+    return WxUploadResult.fromJson(responseContent);
+  }
+
+}
diff --git a/src/main/java/chanjarster/weixin/util/http/RequestExecutor.java b/src/main/java/chanjarster/weixin/util/http/RequestExecutor.java
new file mode 100644
index 00000000..e80bd61c
--- /dev/null
+++ b/src/main/java/chanjarster/weixin/util/http/RequestExecutor.java
@@ -0,0 +1,24 @@
+package chanjarster.weixin.util.http;
+
+import java.io.IOException;
+
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+
+import chanjarster.weixin.exception.WxErrorException;
+
+/**
+ * http请求执行器
+ * @author chanjarster
+ *
+ * @param  返回值类型
+ * @param  请求参数类型
+ */
+public interface RequestExecutor {
+
+  public static final CloseableHttpClient httpclient = HttpClients.createDefault();
+
+  public T execute(String uri, E data) throws WxErrorException, ClientProtocolException, IOException;
+  
+}
diff --git a/src/main/java/chanjarster/weixin/util/http/SimpleGetRequestExecutor.java b/src/main/java/chanjarster/weixin/util/http/SimpleGetRequestExecutor.java
new file mode 100644
index 00000000..91ac1e58
--- /dev/null
+++ b/src/main/java/chanjarster/weixin/util/http/SimpleGetRequestExecutor.java
@@ -0,0 +1,37 @@
+package chanjarster.weixin.util.http;
+
+import java.io.IOException;
+
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+
+import chanjarster.weixin.bean.result.WxError;
+import chanjarster.weixin.exception.WxErrorException;
+
+/**
+ * 简单的GET请求执行器,请求的参数是String, 返回的结果也是String
+ * @author chanjarster
+ *
+ */
+public class SimpleGetRequestExecutor implements RequestExecutor {
+
+  @Override
+  public String execute(String uri, String queryParam) throws WxErrorException, ClientProtocolException, IOException {
+    if (queryParam != null) {
+      if (uri.indexOf('?') == -1) {
+        uri += '?';
+      }
+      uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+    }
+    HttpGet httpGet = new HttpGet(uri);
+    CloseableHttpResponse response = httpclient.execute(httpGet);
+    String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+    WxError error = WxError.fromJson(responseContent);
+    if (error.getErrcode() != 0) {
+      throw new WxErrorException(error);
+    }
+    return responseContent;
+  }
+
+}
diff --git a/src/main/java/chanjarster/weixin/util/http/SimplePostRequestExecutor.java b/src/main/java/chanjarster/weixin/util/http/SimplePostRequestExecutor.java
new file mode 100644
index 00000000..4f3c5bc4
--- /dev/null
+++ b/src/main/java/chanjarster/weixin/util/http/SimplePostRequestExecutor.java
@@ -0,0 +1,37 @@
+package chanjarster.weixin.util.http;
+
+import java.io.IOException;
+
+import org.apache.http.Consts;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+
+import chanjarster.weixin.bean.result.WxError;
+import chanjarster.weixin.exception.WxErrorException;
+
+/**
+ * 简单的POST请求执行器,请求的参数是String, 返回的结果也是String
+ * @author chanjarster
+ *
+ */
+public class SimplePostRequestExecutor implements RequestExecutor {
+
+  @Override
+  public String execute(String uri, String postEntity) throws WxErrorException, ClientProtocolException, IOException {
+    HttpPost httpPost = new HttpPost(uri);
+    if (postEntity != null) {
+      StringEntity entity = new StringEntity(postEntity, Consts.UTF_8);
+      httpPost.setEntity(entity);
+    }
+    CloseableHttpResponse response = httpclient.execute(httpPost);
+    String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+    WxError error = WxError.fromJson(responseContent);
+    if (error.getErrcode() != 0) {
+      throw new WxErrorException(error);
+    }
+    return responseContent;
+  }
+
+}
diff --git a/src/main/java/chanjarster/weixin/util/Utf8ResponseHandler.java b/src/main/java/chanjarster/weixin/util/http/Utf8ResponseHandler.java
similarity index 96%
rename from src/main/java/chanjarster/weixin/util/Utf8ResponseHandler.java
rename to src/main/java/chanjarster/weixin/util/http/Utf8ResponseHandler.java
index 9884425d..384205e5 100644
--- a/src/main/java/chanjarster/weixin/util/Utf8ResponseHandler.java
+++ b/src/main/java/chanjarster/weixin/util/http/Utf8ResponseHandler.java
@@ -1,4 +1,4 @@
-package chanjarster.weixin.util;
+package chanjarster.weixin.util.http;
 
 import java.io.IOException;
 
diff --git a/src/main/java/chanjarster/weixin/util/GsonHelper.java b/src/main/java/chanjarster/weixin/util/json/GsonHelper.java
similarity index 98%
rename from src/main/java/chanjarster/weixin/util/GsonHelper.java
rename to src/main/java/chanjarster/weixin/util/json/GsonHelper.java
index 0b138a5e..7487b9fc 100644
--- a/src/main/java/chanjarster/weixin/util/GsonHelper.java
+++ b/src/main/java/chanjarster/weixin/util/json/GsonHelper.java
@@ -6,7 +6,7 @@
  * arose from modification of the original source, or other redistribution of this source
  * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
  */
-package chanjarster.weixin.util;
+package chanjarster.weixin.util.json;
 
 
 import com.google.gson.JsonElement;
diff --git a/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java b/src/main/java/chanjarster/weixin/util/json/WxCustomMessageGsonAdapter.java
similarity index 98%
rename from src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java
rename to src/main/java/chanjarster/weixin/util/json/WxCustomMessageGsonAdapter.java
index b6f0a972..cfe9c289 100644
--- a/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java
+++ b/src/main/java/chanjarster/weixin/util/json/WxCustomMessageGsonAdapter.java
@@ -6,7 +6,7 @@
  * arose from modification of the original source, or other redistribution of this source
  * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
  */
-package chanjarster.weixin.util;
+package chanjarster.weixin.util.json;
 
 import java.lang.reflect.Type;
 
diff --git a/src/main/java/chanjarster/weixin/util/WxGsonBuilder.java b/src/main/java/chanjarster/weixin/util/json/WxGsonBuilder.java
similarity index 93%
rename from src/main/java/chanjarster/weixin/util/WxGsonBuilder.java
rename to src/main/java/chanjarster/weixin/util/json/WxGsonBuilder.java
index 60e17b48..c77fafc6 100644
--- a/src/main/java/chanjarster/weixin/util/WxGsonBuilder.java
+++ b/src/main/java/chanjarster/weixin/util/json/WxGsonBuilder.java
@@ -1,4 +1,4 @@
-package chanjarster.weixin.util;
+package chanjarster.weixin.util.json;
 
 import chanjarster.weixin.bean.WxCustomMessage;
 import chanjarster.weixin.bean.WxMenu;
diff --git a/src/main/java/chanjarster/weixin/util/WxMenuGsonAdapter.java b/src/main/java/chanjarster/weixin/util/json/WxMenuGsonAdapter.java
similarity index 98%
rename from src/main/java/chanjarster/weixin/util/WxMenuGsonAdapter.java
rename to src/main/java/chanjarster/weixin/util/json/WxMenuGsonAdapter.java
index 020fa2cf..78b6cc74 100644
--- a/src/main/java/chanjarster/weixin/util/WxMenuGsonAdapter.java
+++ b/src/main/java/chanjarster/weixin/util/json/WxMenuGsonAdapter.java
@@ -6,7 +6,7 @@
  * arose from modification of the original source, or other redistribution of this source
  * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
  */
-package chanjarster.weixin.util;
+package chanjarster.weixin.util.json;
 
 import java.lang.reflect.Type;
 
diff --git a/src/main/java/chanjarster/weixin/util/AdapterCDATA.java b/src/main/java/chanjarster/weixin/util/xml/AdapterCDATA.java
similarity index 92%
rename from src/main/java/chanjarster/weixin/util/AdapterCDATA.java
rename to src/main/java/chanjarster/weixin/util/xml/AdapterCDATA.java
index 581fcde4..84b96fdf 100644
--- a/src/main/java/chanjarster/weixin/util/AdapterCDATA.java
+++ b/src/main/java/chanjarster/weixin/util/xml/AdapterCDATA.java
@@ -1,4 +1,4 @@
-package chanjarster.weixin.util;
+package chanjarster.weixin.util.xml;
 
 import javax.xml.bind.annotation.adapters.XmlAdapter;
 
diff --git a/src/main/java/chanjarster/weixin/util/XmlTransformer.java b/src/main/java/chanjarster/weixin/util/xml/XmlTransformer.java
similarity index 98%
rename from src/main/java/chanjarster/weixin/util/XmlTransformer.java
rename to src/main/java/chanjarster/weixin/util/xml/XmlTransformer.java
index 7f52efa7..081fc67f 100644
--- a/src/main/java/chanjarster/weixin/util/XmlTransformer.java
+++ b/src/main/java/chanjarster/weixin/util/xml/XmlTransformer.java
@@ -1,4 +1,4 @@
-package chanjarster.weixin.util;
+package chanjarster.weixin.util.xml;
 
 import java.io.IOException;
 import java.io.InputStream;
diff --git a/src/test/java/chanjarster/weixin/api/WxServiceTest.java b/src/test/java/chanjarster/weixin/api/WxServiceTest.java
index 65feaab5..4649a161 100644
--- a/src/test/java/chanjarster/weixin/api/WxServiceTest.java
+++ b/src/test/java/chanjarster/weixin/api/WxServiceTest.java
@@ -2,6 +2,8 @@ package chanjarster.weixin.api;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
 
 import javax.xml.bind.JAXBException;
 import javax.xml.bind.annotation.XmlAccessType;
@@ -19,14 +21,17 @@ import chanjarster.weixin.bean.WxMenu;
 import chanjarster.weixin.bean.WxMenu.WxMenuButton;
 import chanjarster.weixin.bean.result.WxUploadResult;
 import chanjarster.weixin.exception.WxErrorException;
-import chanjarster.weixin.util.XmlTransformer;
+import chanjarster.weixin.util.xml.XmlTransformer;
 
 public class WxServiceTest {
 
   private WxServiceImpl wxService;
   
+  private List media_ids = new ArrayList();
+  
   @BeforeTest
   public void prepare() throws JAXBException {
+    this.wxService = null;
     InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml");
     WxXmlConfigStorage config = XmlTransformer.fromXml(WxXmlConfigStorage.class, is1);
     this.wxService = new WxServiceImpl();
@@ -66,32 +71,51 @@ public class WxServiceTest {
     Assert.assertNotNull(wxService.getMenu());
   }
   
-  @Test(dependsOnMethods = { "testRefreshAccessToken", "testGetMenu" }, enabled = true)
+  @Test(dependsOnMethods = { "testRefreshAccessToken", "testGetMenu"}, enabled = true)
   public void testDeleteMenu() throws WxErrorException {
     wxService.deleteMenu();
   }
   
-  @Test(dependsOnMethods = { "testRefreshAccessToken" }, dataProvider="uploadFiles", enabled = true)
+  @Test(dependsOnMethods = { "testRefreshAccessToken" }, dataProvider="uploadMedia", enabled = true)
   public void testUploadMedia1(String mediaType, String fileType, String fileName) throws WxErrorException, IOException {
     InputStream inputStream = ClassLoader.getSystemResourceAsStream(fileName);
     WxUploadResult res = wxService.uploadMedia(mediaType, fileType, inputStream);
     Assert.assertNotNull(res.getType());
     Assert.assertNotNull(res.getCreated_at());
     Assert.assertTrue(res.getMedia_id() != null || res.getThumb_media_id() != null);
+    
+    if (res.getMedia_id() != null) {
+      media_ids.add(res.getMedia_id());
+    }
+    if (res.getThumb_media_id() != null) {
+      media_ids.add(res.getThumb_media_id());
+    }
   }
   
   @DataProvider
-  public Object[][] uploadFiles() {
+  public Object[][] uploadMedia() {
     return new Object[][] {
-        new Object[] { WxConsts.MEDIA_IMAGE, WxConsts.FILE_PNG, "mm.png" },
         new Object[] { WxConsts.MEDIA_IMAGE, WxConsts.FILE_JPG, "mm.jpeg" },
         new Object[] { WxConsts.MEDIA_VOICE, WxConsts.FILE_MP3, "mm.mp3" },
         new Object[] { WxConsts.MEDIA_VIDEO, WxConsts.FILE_MP4, "mm.mp4" },
-        new Object[] { WxConsts.MEDIA_THUMB, WxConsts.FILE_PNG, "mm.png" },
         new Object[] { WxConsts.MEDIA_THUMB, WxConsts.FILE_JPG, "mm.jpeg" }
     };
   }
   
+  @Test(dependsOnMethods = { "testUploadMedia1" }, dataProvider="downloadMedia")
+  public void testDownloadMedia(String media_id) throws WxErrorException {
+    wxService.downloadMedia(media_id);
+  }
+  
+  @DataProvider
+  public Object[][] downloadMedia() {
+    Object[][] params = new Object[this.media_ids.size()][];
+    for (int i = 0; i < this.media_ids.size(); i++) {
+      params[i] = new Object[] { this.media_ids.get(i) };
+    }
+    return params;
+  }
+  
   @Test(enabled = true)
   public void testCheckSignature() throws WxErrorException {
     String timestamp = "23234235423246";
diff --git a/src/test/resources/mm.png b/src/test/resources/mm.png
deleted file mode 100644
index a060d443..00000000
Binary files a/src/test/resources/mm.png and /dev/null differ