輕鬆把玩HttpClient之封裝HttpClient工具類(九),新增多檔案上傳功能
在Git上有人給我提Issue,說怎麼上傳檔案,其實我一開始就想上這個功能,不過這半年比較忙,所以一直耽擱了。這次正好沒什麼任務了,趕緊完成這個功能。畢竟作為一款工具類,有基本的請求和下載功能,就差上傳了,有點說不過去。好了,天不早了,咱乾點正事吧。
如果你只想瞭解怎麼用HttpClient來上傳檔案,可以參考這篇文章:http://blog.csdn.net/fengyuzhengfan/article/details/39941851,裡面寫的很清楚了。這裡我主要介紹工具類中的修改。
首先,上傳功能需要用到HttpMime這個,所以首先在pom中新增maven依賴(不會的自行百度)。
其次,修改Utils中的map2List方法。閱讀過原始碼的都知道,我所有的請求引數,都是通過map來封裝的,原來都是返回list,上次升級以後,返回的都是HttpEntity物件(看來下次得修改一下方法名了map2HttpEntity),把所有的引數轉化為HttpEntity物件。
//裝填引數 HttpEntity entity = Utils.map2List(nvps, config.map(), config.inenc()); //設定引數到請求物件中 ((HttpEntityEnclosingRequestBase)request).setEntity(entity);
現在上傳用到的是MultipartEntityBuilder,所以新增一個特定型別,進行特殊處理。
public static final String ENTITY_MULTIPART="$ENTITY_MULTIPART$"; private static final List<String> SPECIAL_ENTITIY = Arrays.asList(ENTITY_STRING, ENTITY_BYTES, ENTITY_FILE, ENTITY_INPUTSTREAM, ENTITY_SERIALIZABLE, ENTITY_MULTIPART); /** * 引數轉換,將map中的引數,轉到引數列表中 * * @param nvps 引數列表 * @param map 引數列表(map) * @throws UnsupportedEncodingException */ public static HttpEntity map2List(List<NameValuePair> nvps, Map<String, Object> map, String encoding) throws UnsupportedEncodingException { HttpEntity entity = null; if(map!=null && map.size()>0){ boolean isSpecial = false; // 拼接引數 for (Entry<String, Object> entry : map.entrySet()) { if(SPECIAL_ENTITIY.contains(entry.getKey())){//判斷是否在之中 isSpecial = true; if(ENTITY_STRING.equals(entry.getKey())){//string entity = new StringEntity(String.valueOf(entry.getValue()), encoding); break; }else if(ENTITY_BYTES.equals(entry.getKey())){//file entity = new ByteArrayEntity((byte[])entry.getValue()); break; }else if(ENTITY_FILE.equals(entry.getKey())){//file //.... break; }else if(ENTITY_INPUTSTREAM.equals(entry.getKey())){//inputstream // entity = new InputStreamEntity(); break; }else if(ENTITY_SERIALIZABLE.equals(entry.getKey())){//serializeable // entity = new SerializableEntity() break; }else if(ENTITY_MULTIPART.equals(entry.getKey())){//MultipartEntityBuilder File[] files = null; if(File.class.isAssignableFrom(entry.getValue().getClass().getComponentType())){ files=(File[])entry.getValue(); }else if(entry.getValue().getClass().getComponentType()==String.class){ String[] names = (String[]) entry.getValue(); files = new File[names.length]; for (int i = 0; i < names.length; i++) { files[i] = new File(names[i]); } } MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.setCharset(Charset.forName(encoding));// 設定請求的編碼格式 builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);// 設定瀏覽器相容模式 int count = 0; for (File file : files) { builder.addBinaryBody(String.valueOf(map.get(ENTITY_MULTIPART+".name")) + count++,file); } boolean forceRemoveContentTypeCharset = (Boolean)map.get(ENTITY_MULTIPART+".rmCharset"); Map<String, Object> m = new HashMap<String, Object>(); m.putAll(map); m.remove(ENTITY_MULTIPART); m.remove(ENTITY_MULTIPART+".name"); m.remove(ENTITY_MULTIPART+".rmCharset"); Iterator<Entry<String, Object>> iterator = m.entrySet().iterator(); // 傳送的資料 while (iterator.hasNext()) { Entry<String, Object> e = iterator.next(); builder.addTextBody(e.getKey(), String.valueOf(e.getValue()), ContentType.create("text/plain", encoding)); } entity = builder.build();// 生成 HTTP POST 實體 //強制去除contentType中的編碼設定,否則,在某些情況下會導致上傳失敗 if(forceRemoveContentTypeCharset){ removeContentTypeChraset(encoding, entity); } break; }else { nvps.add(new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue()))); } }else{ nvps.add(new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue()))); } } if(!isSpecial) { entity = new UrlEncodedFormEntity(nvps, encoding); } } return entity; }
這裡面有一個方法removeContentTypeChraset,主要是為了解決,如果呼叫了setCharset,中文檔名不會亂碼,但是在ContentType檔案頭中會多一個charset=xxx,而導致上傳失敗,解決辦法就是強制去掉這個資訊。而這個HttpEntity實際物件是MultipartFormEntity物件。這個類未宣告public,所以只能包內訪問。而且該類的contentType屬性是private final型別。就算可以通過物件拿到這個屬性,也無法修改。所以我只能通過反射來修改。
private static void removeContentTypeChraset(String encoding, HttpEntity entity) {
try {
Class<?> clazz = entity.getClass();
Field field = clazz.getDeclaredField("contentType");
field.setAccessible(true); //將欄位的訪問許可權設為true:即去除private修飾符的影響
if(Modifier.isFinal(field.getModifiers())){
Field modifiersField = Field.class.getDeclaredField("modifiers"); //去除final修飾符的影響,將欄位設為可修改的
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
BasicHeader o = (BasicHeader) field.get(entity);
field.set(entity, new BasicHeader(HTTP.CONTENT_TYPE, o.getValue().replace("; charset="+encoding,"")));
} catch (NoSuchFieldException e) {
Utils.exception(e);
} catch (SecurityException e) {
Utils.exception(e);
} catch (IllegalArgumentException e) {
Utils.exception(e);
} catch (IllegalAccessException e) {
Utils.exception(e);
}
}
剩下的就是HttpConfig的修改了,所有引數都是通過這個物件封裝的。對於上傳,我沒想再建立一個file陣列物件來傳遞引數,因為在上傳時,可能還會有其他引數,都得需要通過map來傳遞,所以上傳也直接藉助map來完成。
/**
* 上傳檔案時用到
*/
public HttpConfig files(String[] filePaths) {
return files(filePaths, "file");
}
/**
* 上傳檔案時用到
* @param filePaths 待上傳檔案所在路徑
*/
public HttpConfig files(String[] filePaths, String inputName) {
return files(filePaths, inputName, false);
}
/**
* 上傳檔案時用到
* @param filePaths 待上傳檔案所在路徑
* @param inputName 即file input 標籤的name值,預設為file
* @param forceRemoveContentTypeChraset
* @return
*/
public HttpConfig files(String[] filePaths, String inputName, boolean forceRemoveContentTypeChraset) {
synchronized (getClass()) {
if(this.map==null){
this.map= new HashMap<String, Object>();
}
}
map.put(Utils.ENTITY_MULTIPART, filePaths);
map.put(Utils.ENTITY_MULTIPART+".name", inputName);
map.put(Utils.ENTITY_MULTIPART+".rmCharset", forceRemoveContentTypeChraset);
return this;
}
map方法也做了相應的修改: /**
* 傳遞引數
*/
public HttpConfig map(Map<String, Object> map) {
synchronized (getClass()) {
if(this.map==null || map==null){
this.map = map;
}else {
this.map.putAll(map);;
}
}
return this;
}
最後,來一個測試類,
import java.util.HashMap;
import java.util.Map;
import com.tgb.ccl.http.common.HttpConfig;
import com.tgb.ccl.http.common.HttpCookies;
import com.tgb.ccl.http.common.Utils;
import com.tgb.ccl.http.exception.HttpProcessException;
import com.tgb.ccl.http.httpclient.HttpClientUtil;
/**
* 上傳功能測試
*
* @author arron
* @date 2016年11月2日 下午1:17:17
* @version 1.0
*/
public class TestUpload {
public static void main(String[] args) throws HttpProcessException {
//登入後,為上傳做準備
HttpConfig config = prepareUpload();
String url= "http://test.free.800m.net:8080/up.php?action=upsave";//上傳地址
String[] filePaths = {"D:\\中國.txt","D:\\111.txt","C:\\Users\\160049\\Desktop\\中國.png"};//待上傳的檔案路徑
Map<String, Object> map = new HashMap<String, Object>();
map.put("path", "./tomcat/vhost/test/ROOT/");//指定其他引數
config.url(url) //設定上傳地址
.encoding("GB2312") //設定編碼,否則可能會引起中文亂碼或導致上傳失敗
.files(filePaths,"myfile",true)//.files(filePaths),如果伺服器端有驗證input 的name值,則請傳遞第二個引數,如果上傳失敗,則嘗試第三個引數設定為true
.map(map);//其他需要提交的引數
Utils.debug();//開啟列印日誌,呼叫 Utils.debug(false);關閉列印日誌
String r = HttpClientUtil.upload(config);//上傳
System.out.println(r);
}
/**
* 登入,並上傳檔案
*
* @return
* @throws HttpProcessException
*/
private static HttpConfig prepareUpload() throws HttpProcessException {
String url ="http://test.free.800m.net:8080/";
String loginUrl = url+"login.php";
String indexUrl = url+"index.php";
HttpCookies cookies = HttpCookies.custom();
//啟用cookie,用於登入後的操作
HttpConfig config = HttpConfig.custom().context(cookies.getContext());
Map<String, Object> map = new HashMap<String, Object>();
map.put("user_name", "test");
map.put("user_pass", "800m.net");
map.put("action", "login");
String loginResult = HttpClientUtil.post(config.url(loginUrl).map(map));
System.out.println("是否登入成功:"+loginResult.contains("成功"));
//開啟主頁
HttpClientUtil.get(config.map(null).url(indexUrl));
return config;
}
}
最新的完整程式碼請到GitHub上進行下載:https://github.com/Arronlong/httpclientUtil 。