# 使用 HttpClient 進行 POST 和 GET request 並進行上下載
一般簡單的需求可用 FileUtils 做到,例如下載可用 copyURLToFile 達成
但在商業邏輯中,可能連 GET 下載都需要先在 request 設定 header 值才能取得回應,為了對 HTTP 連線做更細膩的設定,推薦使用 HttpClient
## GET 下載檔案
取得的 httpEntity.getContent() 是 InputStream
可以用 FileUtils.copyInputStreamToFile 或 ByteArrayOutputStream 來處理
但不可以用 copyURLToFile 處理
```
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
HttpGet httpGet = new HttpGet(url);
setGetHead(httpGet, headMap);
CloseableHttpResponse response1 = httpclient.execute(httpGet);
try {
// 印出狀態(例如 200 或 404 )
System.out.println(response1.getStatusLine());
// httpEntity 就是我們主要想取得的回應實體,下載物在裡面
HttpEntity httpEntity = response1.getEntity();
// 長度預設是 -1,有抓到東西才會是正數
long contentLength = httpEntity.getContentLength();
// 內建內容是 InputStream,這邊利用 ByteArrayOutputStream 輸出到一個檔案實體的路徑 String filepath,建議先檢查 httpEntity != null 才做後續
// 也可以用:FileUtils.copyInputStreamToFile(imageEntity.getContent(), new File(imgSavePath));
InputStream is = httpEntity.getContent();
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int r = 0;
long totalRead = 0;
while ((r = is.read(buffer)) > 0) {
output.write(buffer, 0, r);
totalRead += r;
if (progress != null) {// 回调进度
progress.onProgress((int) (totalRead * 100 / contentLength));
}
}
FileOutputStream fos = new FileOutputStream(filePath);
output.writeTo(fos);
output.flush();
output.close();
fos.close();
EntityUtils.consume(httpEntity)
```
## POST 上傳檔案
用 FileBody 讀入本地檔案路徑
HttpPost 本身支援用 setHeader 設定 header,所以 MultipartEntityBuilder 應該是用來設定其他 request 參數,例如寫在 body 的那種。不確定,沒試過
```
String respStr = null;
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
HttpPost httppost = new HttpPost(serverUrl);
FileBody binFileBody = new FileBody(new File(localFilePath));
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
// add the file params
multipartEntityBuilder.addPart(serverFieldName, binFileBody);
// 設定上傳的其他參數
if (params != null && params.size() > 0) {
Set<String> keys = params.keySet();
for (String key : keys) {
multipartEntityBuilder.addPart(key, new StringBody(params.get(key),ContentType.TEXT_PLAIN));
}
}
HttpEntity reqEntity = multipartEntityBuilder.build();
httppost.setEntity(reqEntity);
CloseableHttpResponse response = httpclient.execute(httppost);
try {
System.out.println(response.getStatusLine());
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream is = entity.getContent();
StringBuffer strBuf = new StringBuffer();
byte[] buffer = new byte[4096];
int r = 0;
while ((r = is.read(buffer)) > 0) {
strBuf.append(new String(buffer, 0, r, "UTF-8"));
}
}
String responseStr = strBuf.toString();
EntityUtils.consume(entity);
System.out.println("resp=" + responseStr);
```
## Header 設定秘文被 RFC7230 限制擋下
在與銀行或電信系統對接時,常會要求 request 帶有金鑰簽署後的秘文。然而這些單位的系統,或只要高一點的 Tomcat 版本都會基於 RFC 限制去過濾 request,所以包含有特殊字元的秘文會成為被擋下的理由
特別須注意的是,這個問題無法透過 Talend API Tester 或 Postman 去交叉驗證,因為前者是瀏覽器插件、後者是前身為插件的軟體,而主流瀏覽器本身通常不太遵守 RFC 規範,也有自己的手段去繞過這些限制,所以同樣的 header 設定在工具反而能通過
另外,URL 本身也很容易撞到此問題,所以 URL 常常會需要 encode 來避免掉符號或中文字元造成的混亂
要解決這問題可從 request 端或伺服器端著手
### request 端
先行 encode 秘文可以迴避此問題,但**千萬不要使用內建的 java.net.URLEncoder**
各種惱人問題例如:沒有斷行的地方當作斷行轉碼、空格變+號
這邊寧可使用 spring 的 encoder 或 android.net.Uri 據說也可以過
### server 端
1. 在 tomcat 的 conf/server.xml 下面的Connector中添加放行屬性:
```
relaxedQueryChars = "{}|[],%"
relaxedPathChars = "[]|{},%"
```
需要放行什麼特殊字元就寫什麼
詳細添加位置在:
```
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000"
relaxedQueryChars="[]|{}^\`"<>"
relaxedPathChars="[]|{}^\`"<>"
redirectPort="8443" />
```
2. 在 SpringBoot 中設定全域屬性 bean
其實跟方法1意思一樣,只是是透過 bean 物件來設定,如下
```
@Bean
public void allowRfc() {
System.setProperty("tomcat.util.http.parser.HttpParser.requestTargetAllow", "|{}");
}
```
**使用 HttpClient 進行 POST 和 GET request 並進行上下載**
https://blog.csdn.net/nupt123456789/article/details/42721003/
**簽名無法透過 HTTP 傳遞**
https://www.jianshu.com/p/bfda250989ef
**Tomcat9 以後,對 request header 的嚴格字元要求**
https://www.cnblogs.com/onroad2019/p/11642096.html
###### tags: `Java`