為什麼寫這篇部落格?
1.目前很多系統使用了微服務架構,那麼各個微服務之間進行內部通訊一般採用http協定的方式,springcloud中提供了ribbon,feign,openFeign等元件。
但是這些元件底層無非就是基於java原生的程式碼傳送http請求或者使用了RestTemplate來封裝了像okHttp等這些開源元件,他們到底是如何工作的?
2.現在很多系統互動比較複雜,往往會有存取第三方api的場景,那麼使用什麼方式比較方便呢?
下面從幾個方面來聊吧:
這裡資料就不存資料庫了,先暫時儲存了,目的是演示。
如果這些介面編寫你比較熟悉,可以略過。
@RestController public class MemberController { private static final String FILE_PATH = System.getProperty("user.dir"); private static ConcurrentHashMap<Integer, Member> memberMap = new ConcurrentHashMap<>(16); private static ConcurrentHashMap<String, String> fileMap = new ConcurrentHashMap<>(16); static { Member m1 = new Member(); m1.setId(1); m1.setBirthday(new Date()); m1.setBalance(new BigDecimal("1000")); m1.setName("張三"); memberMap.put(1, m1); m1 = new Member(); m1.setId(2); m1.setBirthday(new Date()); m1.setBalance(new BigDecimal("1000")); m1.setName("李四"); memberMap.put(2, m1); } }
使用memberMap來儲存提交的會員資料。
使用fileMap來儲存上傳的檔名稱資訊和絕對路徑。(因為業務開發中檔案上傳是常見的需求)
預製兩個資料。
(1)新增會員介面。(post + json方式)
@PostMapping("/member") public NormalResponseObject addMember(@RequestBody MemberVO memberVO) { if (memberMap.containsKey(memberVO.getId())) { return NormalResponseObject.fail("id不能重複"); } memberMap.put(memberVO.getId(), Member.of(memberVO)); return NormalResponseObject.sucess(); }
(2)新增會員介面。(post + param方式)
@PostMapping("/member/param") public NormalResponseObject addMemberUseParam(MemberVO memberVO) { return addMember(memberVO); }
(3)新增會員介面。(get + param方式)
@GetMapping("/member/param") public NormalResponseObject addMemberUseParam2(MemberVO memberVO) { return addMember(memberVO); }
(4)查詢會員詳情介面。(get)
@GetMapping("/member/{id}") public NormalResponseObject<MemberVO> getMember(@PathVariable("id") Integer id) { if (!memberMap.containsKey(id)) { return NormalResponseObject.fail("不存在對應會員資訊"); } return NormalResponseObject.sucess(Member.toMemberVO(memberMap.get(id))); }
(5)刪除會員介面。(delete)
@DeleteMapping("/member/{id}") public NormalResponseObject deleteMember(@PathVariable("id") Integer id) { memberMap.remove(id); return NormalResponseObject.sucess(); }
(6)編輯會員介面。(put + param)
@PutMapping("/member/{id}") public NormalResponseObject editMember(@PathVariable("id") Integer id, MemberVO memberVO) { if (!memberMap.containsKey(id)) { return NormalResponseObject.fail("不存在對應會員資訊"); } memberMap.put(id, Member.of(memberVO)); return NormalResponseObject.sucess(); }
(7)查詢所有會員介面。(get)
@GetMapping("/member") public NormalResponseObject<List<MemberVO>> getAllMember() { if(memberMap.size() == 0) { return NormalResponseObject.sucess(new ArrayList<>()); } List<MemberVO> voList = memberMap.values().stream().map(Member::toMemberVO) .collect(Collectors.toList()); return NormalResponseObject.sucess(voList); }
(8)檔案上傳介面。(post + multipar/form-data)
@PostMapping("/member/fileUpload") public NormalResponseObject uploadFile(@RequestParam("file") MultipartFile multipartFile, @RequestParam("fileName") String fileName) { if(multipartFile == null || multipartFile.getSize() <= 0) { return NormalResponseObject.fail("檔案為空"); } System.out.println("上傳的檔名為:" + multipartFile.getOriginalFilename()); System.out.println("傳入的fileName引數為:" + fileName); // 儲存檔案 File file = Paths.get(FILE_PATH, fileName).toFile(); if(!file.exists()) { try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); return NormalResponseObject.fail("檔案操作異常"); } } try ( FileOutputStream fos = new FileOutputStream(file) ) { InputStream inputStream = multipartFile.getInputStream(); byte[] buf = new byte[1024]; int len = 0; while ((len = inputStream.read(buf)) > 0) { fos.write(buf, 0, len); } } catch (Exception e) { e.printStackTrace(); return NormalResponseObject.fail("檔案操作異常"); } fileMap.put(fileName, file.getAbsolutePath()); return NormalResponseObject.sucess(); }
(9)檔名稱列表查詢介面。(get)
@GetMapping("/member/files") public NormalResponseObject<List<FileObject>> getAllFiles() { if(fileMap.size() == 0) { return NormalResponseObject.sucess(new ArrayList<>()); } List<FileObject> files = new ArrayList<>(); fileMap.forEach((key, value) -> { FileObject fileObject = new FileObject(); fileObject.setFileName(key); fileObject.setFileAbsolutePath(value); files.add(fileObject); }); return NormalResponseObject.sucess(files); }
(10)檔案下載介面。(get)
@GetMapping("/member/file/download") public void doloadFile(@RequestParam("fileName") String fileName, HttpServletResponse response) { // 設定響應頭 try { response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } response.setContentType("text/plain"); // 輸出檔案 if (fileMap.containsKey(fileName)) { String abs = fileMap.get(fileName); try ( FileInputStream fis = new FileInputStream(abs); ) { byte[] buf = new byte[1024]; int len = 0; while ((len = fis.read(buf)) > 0) { response.getOutputStream().write(buf, 0, len); } }catch (Exception e) { e.printStackTrace(); } } }
為了方便,在頁面就使用jquery來驗證這些介面是否能正常工作。
下面就是 js發起這些請求的程式碼和截圖,如果你對js熟悉或者不想關注,可以跳過。
首頁頁面載入,查詢所有會員資料。
js程式碼如下:
function loadAllMembers() { let url = "/member"; $.ajax(url, { method: "get", success: function (result) { let data = result.data; if(data) { var body = $("#dataTable tbody"); body.empty(); for(let index in data) { let trHtml = "<tr>"; trHtml += "<td>" + data[index].id + "</td>"; trHtml += "<td>" + data[index].name + "</td>"; trHtml += "<td>" + data[index].balance + "</td>"; trHtml += "<td>" + data[index].birthday + "</td>"; trHtml += '<td>' + '<button class="detail" trid=' + data[index].id + '>詳情</button>' + '</td>'; trHtml += '<td>' + '<button class="edit" trid=' + data[index].id + '>編輯</button>' + '</td>'; trHtml += '<td>' + '<button class="del" trid=' + data[index].id + '>刪除</button>' + '</td>'; trHtml += "</tr>"; body.append($(trHtml)); } } } }) }
這個table每行的按鈕包含了詳情,編輯,刪除。
分別使用三種方式進行新增。
(1)post+json新增
程式碼如下:
function addMember01(event) { event.preventDefault(); let url = "/member"; let member = {}; member.id = $('#addMemberForm [name="id"]').val(); member.name = $('#addMemberForm [name="name"]').val(); member.balance = $('#addMemberForm [name="balance"]').val(); member.birthday = $('#addMemberForm [name="birthday"]').val(); $.ajax(url, { method: "post", contentType: "application/json", data: JSON.stringify(member), success: function (result) { if (result.statusCode == 200) { $('#addMemberForm [name="id"]').val(""); $('#addMemberForm [name="name"]').val(""); $('#addMemberForm [name="balance"]').val(""); $('#addMemberForm [name="birthday"]').val(""); loadAllMembers(); } else { window.alert(result.message); } } }); }
截圖:
(2)其他兩種方式新增的程式碼和截圖。
function addMember02(event) { event.preventDefault(); let url = "/member/param"; let member = {}; member.id = $('#addMemberForm [name="id"]').val(); member.name = $('#addMemberForm [name="name"]').val(); member.balance = $('#addMemberForm [name="balance"]').val(); member.birthday = $('#addMemberForm [name="birthday"]').val(); $.ajax(url, { method: "get", data: member, success: function (result) { if (result.statusCode == 200) { $('#addMemberForm [name="id"]').val(""); $('#addMemberForm [name="name"]').val(""); $('#addMemberForm [name="balance"]').val(""); $('#addMemberForm [name="birthday"]').val(""); loadAllMembers(); } else { window.alert(result.message); } } }); } function addMember03(event) { event.preventDefault(); let url = "/member/param"; let member = {}; member.id = $('#addMemberForm [name="id"]').val(); member.name = $('#addMemberForm [name="name"]').val(); member.balance = $('#addMemberForm [name="balance"]').val(); member.birthday = $('#addMemberForm [name="birthday"]').val(); $.ajax(url, { method: "post", data: member, success: function (result) { if (result.statusCode == 200) { $('#addMemberForm [name="id"]').val(""); $('#addMemberForm [name="name"]').val(""); $('#addMemberForm [name="balance"]').val(""); $('#addMemberForm [name="birthday"]').val(""); loadAllMembers(); } else { window.alert(result.message); } } }); }
(1)對三個按鈕的事件委託
修改會員資料其實就是查詢回填,加修改。
由於table的行是動態生成的,所以需要對三個按鈕進行事件委託,保證新加入的按鈕事件也能得到響應。
js程式碼如下:
// 對table中的操作按鈕進行事件委託 $("#dataTable tbody").on( "click", "button", function(event) { let id = $(event.target).attr("trid"); let clz = $(event.target).attr("class"); if("detail" == clz) { let url = "/member/" + id; $.ajax(url, { method: "get", success: function (result) { if(result.statusCode == 200) { alert(JSON.stringify(result.data)); } else { alert(result.message); } } }); } else if("del" == clz){ let url = "/member/" + id; $.ajax(url, { method: "delete", success: function (result) { if(result.statusCode == 200) { loadAllMembers(); } else { alert(result.message); } } }); } else { let url = "/member/" + id; $.ajax(url, { method: "get", success: function (result) { if(result.statusCode == 200) { $('#editMemberForm [name="id"]').val(result.data.id); $('#editMemberForm [name="name"]').val(result.data.name); $('#editMemberForm [name="balance"]').val(result.data.balance); $('#editMemberForm [name="birthday"]').val(result.data.birthday); $('#showOrHidden').show(); } else { alert(result.message); } } }); } });
上述程式碼中,完成了詳情檢視,編輯回填和刪除操作。
(2)執行修改操作
回填結束後,需要呼叫修改介面進行資料修改。
js程式碼如下:
function editMember(event) { event.preventDefault(); let url = "/member/"; let member = {}; member.id = $('#editMemberForm [name="id"]').val(); member.name = $('#editMemberForm [name="name"]').val(); member.balance = $('#editMemberForm [name="balance"]').val(); member.birthday = $('#editMemberForm [name="birthday"]').val(); url += member.id; $.ajax(url, { method: "put", data: member, success: function (result) { if (result.statusCode == 200) { $("#showOrHidden").hide(); loadAllMembers(); } else { window.alert(result.message); } } }); }
截圖如下:
後端在開發檔案上傳介面的時候需要引入maven依賴。
<!--檔案上傳需要該依賴--> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency>
對於前端,需要使用對應的特定form如下:
<h2>檔案上傳的form</h2> <div> <form id="fileForm" action="/member/fileUpload" enctype="multipart/form-data"> 請選擇檔案:<input type="file" name="file"><br> 輸入後臺儲存的檔名稱:<input type="text" name="fileName"> <input type="submit" value="上傳" onclick="uploadFile(event)"> </form> </div>
然後js程式碼中注意阻止預設事件就行了:
function uploadFile(event) { // 防止預設表單提交行為 event.preventDefault(); // 獲取檔案物件 var file = $('input[type=file]')[0].files[0]; if (!file) { alert('請選擇檔案!') return; } // 建立 FormData 物件 var formData = new FormData(); formData.append('file', file); formData.append("fileName", $('[name="fileName"]').val()); // 傳送 AJAX 請求 $.ajax({ url: '/member/fileUpload', type: 'POST', data: formData, processData: false, contentType: false, success: function (response) { alert('上傳成功!'); getAllUploadedFile(); }, error: function (xhr, status, error) { alert('上傳失敗:' + error); } }); }
至於檔案下載,前端只需要提供一個超連結去呼叫後端介面就行了。
這裡需要注意的就是需要對url進行編碼操作,js程式碼如下:
function getAllUploadedFile() { let url = "/member/files"; $.ajax(url, { method: "get", success: function (result){ if(result.statusCode == 200) { $("#fileOlList").empty(); for(let index in result.data) { let li = '<li><a href="/member/file/download?fileName=' + encodeURI(result.data[index].fileName) + '">' + result.data[index].fileName + '</a></li>'; $("#fileOlList").append($(li)) } } else { alert(result.message); } } }); }
由於後端介面已經使用了對應的響應頭設定,瀏覽器就會下載檔案:
到這裡呢,既驗證了介面的正確性,同時也將前端js如何發起ajax請求存取伺服器進行了描述。
下面就是使用java來傳送http請求了。
上面的截圖就是使用HttpUrlConnection進行http請求傳送的幾個重要步驟。
還是挺複雜的。。。
程式碼如下:
@Test public void test01() { // 1.需要通過URL來獲取UrlConnection範例 String url = "http://localhost:8080/member"; URL urlObject = null; HttpURLConnection httpURLConnection = null; OutputStream os = null; InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); try { urlObject = new URL(url); } catch (MalformedURLException e) { e.printStackTrace(); return; } try { URLConnection urlConnection = urlObject.openConnection(); if(urlConnection instanceof HttpURLConnection) { httpURLConnection = (HttpURLConnection) urlConnection; } } catch (IOException e) { e.printStackTrace(); return; } if(null == httpURLConnection) { return; } try { // 2.設定請求的一些引數 httpURLConnection.setRequestMethod(HttpMethod.GET.name()); httpURLConnection.setDoOutput(false); httpURLConnection.setDoInput(true); // 3.開啟連線 httpURLConnection.connect(); // 4.是否需要使用OutputStream來向請求體中設定資料 // 暫時不需要 // 5.從InputStream中獲取資料 int responseCode = httpURLConnection.getResponseCode(); if(responseCode != 200) { System.out.println("請求出錯了"); return; } is = httpURLConnection.getInputStream(); byte[] buf = new byte[1024]; int len = 0; while ((len = is.read(buf)) > 0) { // 使用ByteArrayOutputStream來快取資料 baos.write(buf, 0, len); } String retStr = new String(baos.toByteArray(), "utf-8"); System.out.println(retStr); }catch (Exception e) { e.printStackTrace(); }finally { // 6.關閉連線 closeSomething(os); closeSomething(is); closeSomething(baos); if(null != httpURLConnection) { httpURLConnection.disconnect(); } } }
結果如下圖:
此時要弄清楚一個問題,這幾個步驟,哪一步發起的連線呢?
下面進行驗證:
(1)openConnection()
(2)connect()
說明是在connect()方法呼叫只有才向伺服器發起的tcp連線。
其實上一步看完,我相信基本說清楚了使用HttpUrlConnection來進行http請求的過程基本說清楚了。
下面我們看看如何新增資料呢?
(1)傳送json資料
下面我就貼出部分程式碼就行了。
// 2.設定請求的一些引數 httpURLConnection.setRequestMethod(HttpMethod.POST.name()); // 設定是否需要輸出資料和接收資料 httpURLConnection.setDoOutput(true); httpURLConnection.setDoInput(true); httpURLConnection.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); // 3.開啟連線 httpURLConnection.connect(); // 4.是否需要使用OutputStream來向請求體中設定資料 os = httpURLConnection.getOutputStream(); MemberVO memberVO = new MemberVO(); memberVO.setId(3); memberVO.setBirthday("2010-11-11"); memberVO.setBalance("1000"); memberVO.setName("httpConnection新增01"); String reqBodyStr = JSON.toJSONString(memberVO); os.write(reqBodyStr.getBytes(StandardCharsets.UTF_8)); os.flush(); // 5.從InputStream中獲取資料 int responseCode = httpURLConnection.getResponseCode(); if(responseCode != 200) { System.out.println("請求出錯了"); return; } is = httpURLConnection.getInputStream(); byte[] buf = new byte[1024]; int len = 0; while ((len = is.read(buf)) > 0) { // 使用ByteArrayOutputStream來快取資料 baos.write(buf, 0, len); } String retStr = new String(baos.toByteArray(), "utf-8"); JSONObject jsonObject = JSON.parseObject(retStr); if (jsonObject.getInteger("statusCode") != 200) { throw new Exception(jsonObject.getString("message")); }
執行程式碼,可以看見新增成功了:
通過param來新增也試試:
(2)使用get傳送param資料
// 這裡在構建url的時候使用spring中的工具類 MemberVO memberVO = new MemberVO(); memberVO.setId(4); memberVO.setBirthday("2010-11-11"); memberVO.setBalance("1000"); memberVO.setName("httpConnection通過get+param方式新增"); UriComponents urlCom = UriComponentsBuilder.fromHttpUrl(url) .queryParam("name", memberVO.getName()) .queryParam("id", memberVO.getId()) .queryParam("birthday", memberVO.getBirthday()) .queryParam("balance", memberVO.getBalance()) .build().encode(StandardCharsets.UTF_8); URL urlObject = null; HttpURLConnection httpURLConnection = null; OutputStream os = null; InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); try { urlObject = new URL(urlCom.toUriString()); } catch (MalformedURLException e) { e.printStackTrace(); return; }
資料新增成功:
(3)使用post傳送param資料
這裡我只貼部分程式碼吧。
// 4.是否需要使用OutputStream來向請求體中設定資料 // 這裡在構建url的時候使用spring中的工具類 MemberVO memberVO = new MemberVO(); memberVO.setId(5); memberVO.setBirthday("2010-11-11"); memberVO.setBalance("2000"); memberVO.setName("httpConnection通過post+param方式新增"); UriComponents urlCom = UriComponentsBuilder.fromHttpUrl(url) .queryParam("name", memberVO.getName()) .queryParam("id", memberVO.getId()) .queryParam("birthday", memberVO.getBirthday()) .queryParam("balance", memberVO.getBalance()) .build().encode(StandardCharsets.UTF_8); os = httpURLConnection.getOutputStream(); os.write(urlCom.getQuery().getBytes(StandardCharsets.UTF_8)); os.flush();
發現資料新增成功:
其實程式碼寫到這裡,我詳情傳送put請求和delete請求按照這個套路寫就行了,問題不打了吧。
為啥要講這麼多原生的東西,其實目的很簡單,
第一,並不是所有系統都能像我們平常開發的業務系統一樣,引入很多開源的jar包。例如:大廠自己封裝的工具,還有物聯網裝置。
第二,我們只有知道了原生的東西,才知道如何理解和優化有些框架。
這裡有一個問題,我們在使用原生socket程式設計的時候,其實OutputStream的flush操作是將os緩衝區的內容推給網路卡,那上述程式碼中的flush有啥作用呢?
能不呼叫嗎?
驗證如下:
程式碼執行通過並新增成功了:
其實相當於,java為我們計算了body體的長度,並設定了對應的請求頭給伺服器端。
(我理解這個操作應該是我們嘗試獲取InputStream流的時候做的。)
通過上述的幾個例子,至少我們明白了,只要明白了傳送封包的包結構,那麼就完全可以使用原生的方式傳送http請求。
下面我們來看看這種multipart/form-data型別的資料怎麼傳送呢?
我先截個網上對該MIME-TYPE的描述:
深入瞭解可以看看:https://blog.csdn.net/dreamerrrrrr/article/details/111146763
下面就是java程式碼了:
@Test public void uploadFileTest() { // 1.需要通過URL來獲取UrlConnection範例 String url = "http://localhost:8080/member/fileUpload"; URL urlObject = null; HttpURLConnection httpURLConnection = null; OutputStream os = null; InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); try { urlObject = new URL(url); } catch (MalformedURLException e) { e.printStackTrace(); return; } try { URLConnection urlConnection = urlObject.openConnection(); if(urlConnection instanceof HttpURLConnection) { httpURLConnection = (HttpURLConnection) urlConnection; } } catch (IOException e) { e.printStackTrace(); return; } if(null == httpURLConnection) { return; } try { // 2.設定請求的一些引數 httpURLConnection.setRequestMethod(HttpMethod.POST.name()); // 設定是否需要輸出資料和接收資料 httpURLConnection.setDoOutput(true); httpURLConnection.setDoInput(true); // 生成boundary,理論上只要不重複就行。 String boundary = "JAVA-HttpUrlConnection-" + UUID.randomUUID().toString().replace("-", ""); String boundaryPrefix = "--"; httpURLConnection.setRequestProperty(HttpHeaders.CONTENT_TYPE, "multipart/form-data; boundary=" + boundary); // 3.開啟連線 httpURLConnection.connect(); // 4.是否需要使用OutputStream來向請求體中設定資料 os = httpURLConnection.getOutputStream(); /* 設定引數 */ // 分割符 os.write((boundaryPrefix + boundary + "\r\n").getBytes(StandardCharsets.UTF_8)); // 資料頭 os.write((HttpHeaders.CONTENT_DISPOSITION + ": form-data; name=\"fileName\"\r\n") .getBytes(StandardCharsets.UTF_8)); // 空行 os.write("\r\n".getBytes(StandardCharsets.UTF_8)); // 引數資料 os.write(("urlConnection上傳的檔案.txt").getBytes(StandardCharsets.UTF_8)); // 換行 os.write("\r\n".getBytes(StandardCharsets.UTF_8)); /* 設定檔案 */ // 分割符 os.write((boundaryPrefix + boundary + "\r\n").getBytes(StandardCharsets.UTF_8)); // 資料頭 os.write((HttpHeaders.CONTENT_DISPOSITION + ": form-data; name=\"file\"; filename=\"temp.txt\"\r\n") .getBytes(StandardCharsets.UTF_8)); // 空行 os.write("\r\n".getBytes(StandardCharsets.UTF_8)); // 檔案資料 Files.copy(Paths.get("d:", "temp.txt"), os); // 換行 os.write("\r\n".getBytes(StandardCharsets.UTF_8)); // 結尾 os.write((boundaryPrefix + boundary + boundaryPrefix).getBytes(StandardCharsets.UTF_8)); // 5.從InputStream中獲取資料 int responseCode = httpURLConnection.getResponseCode(); if(responseCode != 200) { System.out.println("請求出錯了"); return; } is = httpURLConnection.getInputStream(); byte[] buf = new byte[1024]; int len = 0; while ((len = is.read(buf)) > 0) { // 使用ByteArrayOutputStream來快取資料 baos.write(buf, 0, len); } String retStr = new String(baos.toByteArray(), "utf-8"); JSONObject jsonObject = JSON.parseObject(retStr); if (jsonObject.getInteger("statusCode") != 200) { throw new Exception(jsonObject.getString("message")); } }catch (Exception e) { e.printStackTrace(); }finally { // 6.關閉連線 closeSomething(os); closeSomething(is); closeSomething(baos); if(null != httpURLConnection) { httpURLConnection.disconnect(); } } }
上述程式碼偵錯了很多遍才成功。
其實就是要理解multipart/form-data這種型別資料的報文結構。
我嘗試把內容通過檔案的方式展示出來:
所以一個part包含:
--${boundary}\r\n
資料頭\r\n
\r\n
資料部分\r\n
最後再以--${boundary}--結尾就可以了。
當然驗證截圖如下:
講完上述第一部分,我相信我們對http協定本身常用的MIME型別細節已經熟悉了,甚至對java原生的傳送請求的api也熟悉了。
但是畢竟太複雜了,確實不友好。
現在我們講講apache的httpclient元件傳送http請求有多絲滑。
我們引入4.x.x版本:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.14</version> </dependency>
這是個經典的版本。
我先貼出官網給的一個簡單例子。
CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet("http://targethost/homepage"); CloseableHttpResponse response1 = httpclient.execute(httpGet); // The underlying HTTP connection is still held by the response object // to allow the response content to be streamed directly from the network socket. // In order to ensure correct deallocation of system resources // the user MUST call CloseableHttpResponse#close() from a finally clause. // Please note that if response content is not fully consumed the underlying // connection cannot be safely re-used and will be shut down and discarded // by the connection manager. try { System.out.println(response1.getStatusLine()); HttpEntity entity1 = response1.getEntity(); // do something useful with the response body // and ensure it is fully consumed EntityUtils.consume(entity1); } finally { response1.close(); }
下面我們仿照著寫一個查詢會員列表的功能
程式碼:
@Test public void testGetMemberList() throws Exception{ CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet("http://localhost:8080/member"); CloseableHttpResponse response1 = httpclient.execute(httpGet); ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); try { System.out.println(response1.getStatusLine()); HttpEntity entity1 = response1.getEntity(); InputStream is = entity1.getContent(); byte[] buf = new byte[1024]; int len = 0; while ((len = is.read(buf)) > 0) { // 使用ByteArrayOutputStream來快取資料 baos.write(buf, 0, len); } String retStr = new String(baos.toByteArray(), "utf-8"); JSONObject jsonObject = JSON.parseObject(retStr); if (jsonObject.getInteger("statusCode") != 200) { throw new Exception(jsonObject.getString("message")); } System.out.println(retStr); EntityUtils.consume(entity1); } finally { response1.close(); baos.close(); } }
列印截圖如下:
直接上程式碼吧
@Test public void testPostAddMember() throws Exception { CloseableHttpClient httpclient = HttpClients.createDefault(); HttpPost httpPost = new HttpPost("http://localhost:8080/member/param"); List<NameValuePair> nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("id", "10")); nvps.add(new BasicNameValuePair("name", "使用httpClient+Post+Param新增")); nvps.add(new BasicNameValuePair("birthday", "2010-11-12")); nvps.add(new BasicNameValuePair("balance", "9999")); httpPost.setEntity(new UrlEncodedFormEntity(nvps)); CloseableHttpResponse response2 = httpclient.execute(httpPost); ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); try { System.out.println(response2.getStatusLine()); HttpEntity entity2 = response2.getEntity(); InputStream is = entity2.getContent(); byte[] buf = new byte[1024]; int len = 0; while ((len = is.read(buf)) > 0) { // 使用ByteArrayOutputStream來快取資料 baos.write(buf, 0, len); } String retStr = new String(baos.toByteArray(), "utf-8"); JSONObject jsonObject = JSON.parseObject(retStr); if (jsonObject.getInteger("statusCode") != 200) { throw new Exception(jsonObject.getString("message")); } System.out.println(retStr); EntityUtils.consume(entity2); } finally { response2.close(); baos.close(); } }
新增成功的截圖:
程式碼如下:
@Test public void testGetParamAddMember() throws Exception { String url = "http://localhost:8080/member/param"; MemberVO memberVO = new MemberVO(); memberVO.setId(11); memberVO.setBirthday("2010-11-11"); memberVO.setBalance("2000"); memberVO.setName("hc通過getParam方式新增"); UriComponents urlCom = UriComponentsBuilder.fromHttpUrl(url) .queryParam("name", memberVO.getName()) .queryParam("id", memberVO.getId()) .queryParam("birthday", memberVO.getBirthday()) .queryParam("balance", memberVO.getBalance()) .build().encode(StandardCharsets.UTF_8); CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet(urlCom.toUriString()); // 下面這種方式已經試過,不行 // BasicHttpParams httpParams = new BasicHttpParams(); // httpParams.setParameter("id", 11); // httpParams.setParameter("name", "使用hcGetParam方式新增"); // httpParams.setParameter("birthday", "2011-03-11"); // httpParams.setParameter("balance", 99999); // httpGet.setParams(httpParams); CloseableHttpResponse response2 = httpclient.execute(httpGet); ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); try { System.out.println(response2.getStatusLine()); HttpEntity entity2 = response2.getEntity(); InputStream is = entity2.getContent(); byte[] buf = new byte[1024]; int len = 0; while ((len = is.read(buf)) > 0) { // 使用ByteArrayOutputStream來快取資料 baos.write(buf, 0, len); } String retStr = new String(baos.toByteArray(), "utf-8"); JSONObject jsonObject = JSON.parseObject(retStr); if (jsonObject.getInteger("statusCode") != 200) { throw new Exception(jsonObject.getString("message")); } System.out.println(retStr); EntityUtils.consume(entity2); } finally { response2.close(); baos.close(); } }
@Test public void testPostJsonAddMember() throws Exception { // 獲取hc範例 CloseableHttpClient httpclient = HttpClients.createDefault(); // 構造HttpUriRequest範例 HttpPost httpPost = new HttpPost("http://localhost:8080/member"); // 設定entity MemberVO memberVO = new MemberVO(); memberVO.setId(12); memberVO.setBirthday("2010-11-11"); memberVO.setBalance("8888"); memberVO.setName("hc通過PostJson方式新增"); StringEntity stringEntity = new StringEntity(JSON.toJSONString(memberVO), "utf-8"); stringEntity.setContentType(MediaType.APPLICATION_JSON_VALUE); httpPost.setEntity(stringEntity); // 傳送請求 CloseableHttpResponse response2 = httpclient.execute(httpPost); ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); try { System.out.println(response2.getStatusLine()); // 獲取響應實體 HttpEntity entity2 = response2.getEntity(); InputStream is = entity2.getContent(); byte[] buf = new byte[1024]; int len = 0; while ((len = is.read(buf)) > 0) { // 使用ByteArrayOutputStream來快取資料 baos.write(buf, 0, len); } String retStr = new String(baos.toByteArray(), "utf-8"); JSONObject jsonObject = JSON.parseObject(retStr); if (jsonObject.getInteger("statusCode") != 200) { throw new Exception(jsonObject.getString("message")); } System.out.println(retStr); EntityUtils.consume(entity2); } finally { response2.close(); baos.close(); } }
驗證結果:
程式碼基本是參照官網給的example來編寫的,這裡要強調,需要引入一個依賴:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpmime</artifactId> <version>4.5.14</version> </dependency>
java程式碼如下:
@Test public void hcUploadFileTest() throws Exception{ CloseableHttpClient httpclient = HttpClients.createDefault(); try { HttpPost httppost = new HttpPost("http://localhost:8080/member/fileUpload"); FileBody file = new FileBody(Paths.get("d:", "temp.txt").toFile()); StringBody fileName = StringBody.create("hc傳入的檔名稱.txt", ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")); HttpEntity reqEntity = MultipartEntityBuilder.create() .addPart("file", file) .addPart("fileName", fileName) .build(); httppost.setEntity(reqEntity); System.out.println("executing request " + httppost.getRequestLine()); CloseableHttpResponse response = httpclient.execute(httppost); try { System.out.println("----------------------------------------"); System.out.println(response.getStatusLine()); HttpEntity resEntity = response.getEntity(); if (resEntity != null) { System.out.println("Response content length: " + resEntity.getContentLength()); } EntityUtils.consume(resEntity); } finally { response.close(); } } finally { httpclient.close(); } }
我先把其列印的請求報文紀錄檔貼出來,更加能幫助你理解multipart/form-data的報文結構:
06:21:07.936 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> POST /member/fileUpload HTTP/1.1
06:21:07.936 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Content-Length: 426
06:21:07.936 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Content-Type: multipart/form-data; boundary=RrxE9BM4vDUS-0Liy4BUeB4WldSN9gub
06:21:07.936 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Host: localhost:8080
06:21:07.936 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Connection: Keep-Alive
06:21:07.936 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> User-Agent: Apache-HttpClient/4.5.14 (Java/1.8.0_211)
06:21:07.936 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Accept-Encoding: gzip,deflate
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "POST /member/fileUpload HTTP/1.1[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Length: 426[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Type: multipart/form-data; boundary=RrxE9BM4vDUS-0Liy4BUeB4WldSN9gub[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Host: localhost:8080[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.5.14 (Java/1.8.0_211)[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Accept-Encoding: gzip,deflate[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "--RrxE9BM4vDUS-0Liy4BUeB4WldSN9gub[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Disposition: form-data; name="file"; filename="temp.txt"[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Type: application/octet-stream[\r][\n]"
06:21:07.937 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Transfer-Encoding: binary[\r][\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "aabb[\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "bbaad[\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "cc[\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "dd[\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "--RrxE9BM4vDUS-0Liy4BUeB4WldSN9gub[\r][\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Disposition: form-data; name="fileName"[\r][\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Type: text/plain; charset=UTF-8[\r][\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Transfer-Encoding: 8bit[\r][\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
06:21:07.938 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "hc[0xe4][0xbc][0xa0][0xe5][0x85][0xa5][0xe7][0x9a][0x84][0xe6][0x96][0x87][0xe4][0xbb][0xb6][0xe5][0x90][0x8d][0xe7][0xa7][0xb0].txt"
06:21:07.939 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
06:21:07.939 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "--RrxE9BM4vDUS-0Liy4BUeB4WldSN9gub--[\r][\n]"
看到這裡,發現httpclient傳送http請求的確簡單了很多。
RestTemplate其實是spring提供的一種高階的api去發起http請求,我們可以參考官網對RestTemplate的解釋:
意思就是說,預設情況RestTemplate使用了HttpURLConnection作為其底層的實現。
但是也可以自己切換到其他庫,只要這些庫實現了ClientHttpRequestFactory就可以。
比如:Apache HttpComponents,netty,OkHttp等。這裡說的Apache HttpComponents你可以理解成就是我們上面講到的httpclient。
程式碼如下:
@Test public void testGetUseParamAddMember() { String url = "http://localhost:8080/member/param"; MemberVO memberVO = new MemberVO(); memberVO.setId(100); memberVO.setBirthday("2010-11-11"); memberVO.setBalance("9888"); memberVO.setName("通過restTemplate的get+param方式新增"); UriComponents urlCom = UriComponentsBuilder.fromHttpUrl(url) .queryParam("name", memberVO.getName()) .queryParam("id", memberVO.getId()) .queryParam("birthday", memberVO.getBirthday()) .queryParam("balance", memberVO.getBalance()) .build().encode(StandardCharsets.UTF_8); String retStr = restTemplate.getForObject(urlCom.toUriString(), String.class); if (StringUtils.isEmpty(retStr)) { throw new RuntimeException(""); } JSONObject jsonObject = JSON.parseObject(retStr); if (jsonObject.getInteger("statusCode") != null && 200 != jsonObject.getInteger("statusCode")) { throw new RuntimeException(""); } System.out.println(retStr); }
成功截圖:
但是有問題,我們看看查詢到的列表:
這條資料是有很大問題的,如果url傳送前沒有編碼的話,呼叫是不會成功的,那這是為什麼呢?
看看下面的截圖:
得到的結論就是:如果在使用RestTemplate的api的時候,如果傳入uri物件範例,那麼其內部不會進行uri編碼操作,而如果傳入的是string,那麼
內部會自動進行一次uri編碼。而在我們程式碼裡面,傳入string之前,自己進行了一次編碼,而api內部又進行了一次編碼,所以有問題。
所以我們發現,api中,一旦傳入的是uri物件,後面的引數值就沒有了,意味著自己必須提前準備好uri。
(1)嘗試傳入uri物件
// 構造UriComponents的時候編碼 UriComponents urlCom = UriComponentsBuilder.fromHttpUrl(url) .queryParam("name", memberVO.getName()) .queryParam("id", memberVO.getId()) .queryParam("birthday", memberVO.getBirthday()) .queryParam("balance", memberVO.getBalance()) .encode() .build(); // 傳入uri物件 String retStr = restTemplate.getForObject(urlCom.toUri(), String.class);
(2)傳入string但是自己首先不編碼
// 構造UriComponents的時候不進行編碼 UriComponents urlCom = UriComponentsBuilder.fromHttpUrl(url) .queryParam("name", memberVO.getName()) .queryParam("id", memberVO.getId()) .queryParam("birthday", memberVO.getBirthday()) .queryParam("balance", memberVO.getBalance()) .build(); // 傳入的string是未編碼的 String retStr = restTemplate.getForObject(urlCom.toUriString(), String.class);
首先我們要為RestTemplate設定一個UriTemplateHandler範例。
@BeforeAll public static void init() { restTemplate = new RestTemplate(); String baseUrl = "http://localhost:8080"; DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl); factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.TEMPLATE_AND_VALUES); restTemplate.setUriTemplateHandler(factory); }
該設定已經把請求地址的基礎部分設定好了,並設定了地址模板的處理模式。
其次在發起呼叫的時候加上對應的引數值就行了。
@Test public void testGetUseParamAddMember2() { String uri = "/member/param?id={a}&name={b}&birthday={c}&balance={d}"; String retStr = restTemplate.getForObject(uri, String.class, 101, "rtGetParam使用模板方式新增", "2001-10-23", 998); if (StringUtils.isEmpty(retStr)) { throw new RuntimeException(""); } JSONObject jsonObject = JSON.parseObject(retStr); if (jsonObject.getInteger("statusCode") != null && 200 != jsonObject.getInteger("statusCode")) { throw new RuntimeException(""); } System.out.println(retStr); }
程式碼執行沒問題,而且設定了這個UriTemplateHandler範例和基地址,RestTemplate同樣可以傳入絕對地址進行呼叫。
@Test public void testGetUseParamAddMemberPrepareUri() { String uri = "/member/{1}?id={2}&name={3}&birthday={4}&balance={5}"; URI expand = restTemplate.getUriTemplateHandler().expand(uri, 100, 100, "put+param修改之後", "2019-10-10", 999); restTemplate.put(expand, null); }
修改後:
程式碼如下:
@Test public void testPostUseParamAddMember() { // 地址資訊 String url = "http://localhost:8080/member/param"; // 使用post傳送資料要有請求體 LinkedMultiValueMap<String,Object> param = new LinkedMultiValueMap<>(); param.add("id", 102); param.add("name", "通過RT的post+param新增"); param.add("birthday", "2001-03-09"); param.add("balance", 99999); RequestEntity<LinkedMultiValueMap<String,Object>> requestEntity = RequestEntity.post(url) .body(param); String s = restTemplate.postForObject(url, requestEntity, String.class); System.out.println(s); }
驗證成功。
程式碼如下:
@Test public void testPostJsonAddMember() { // 地址資訊 String url = "http://localhost:8080/member"; // 使用post傳送資料要有請求體 MemberVO memberVO = new MemberVO(); memberVO.setId(103); memberVO.setBirthday("2010-11-11"); memberVO.setBalance("12356"); memberVO.setName("rt的post+json新增"); RequestEntity<String> requestEntity = RequestEntity.post(url) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .body(JSON.toJSONString(memberVO)); String s = restTemplate.postForObject(url, requestEntity, String.class); System.out.println(s); }
驗證成功。
程式碼如下:
@Test public void testFileUpload() { // 地址資訊 String url = "http://localhost:8080/member/fileUpload"; // 使用multipart/form-data傳送資料 // 1.準備好body MultiValueMap<String,Object> allParts = new LinkedMultiValueMap<>(); allParts.add("fileName", "使用rt上傳的檔案.txt"); allParts.add("file", new FileSystemResource(Paths.get("d:", "temp.txt"))); // 2.準備request物件 RequestEntity<MultiValueMap<String, Object>> requestEntity = RequestEntity.post(url) .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .body(allParts); // 3.傳送請求 String s = restTemplate.postForObject(url, requestEntity, String.class); System.out.println(s); }
傳送成功:
設定的程式碼修改:
@BeforeAll public static void init() { restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); String baseUrl = "http://localhost:8080"; DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl); factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.TEMPLATE_AND_VALUES); restTemplate.setUriTemplateHandler(factory); }
獲取會員詳情程式碼
@Test public void testMemberDetail() { // 獲取會員資訊 String uri = "/member/{id}"; String retString = restTemplate.getForObject(uri, String.class, 100); // 使用TypeRefercece在反序列化的時候獲取泛型引數 NormalResponseObject<MemberVO> resObj = JSON.parseObject(retString, new TypeReference<NormalResponseObject<MemberVO>>() { }); System.out.println(resObj.getData()); }
看紀錄檔列印如下:
看上述紀錄檔就知道httpclient生效了,如何設定okHttp,一起在下面介紹吧。
這是該框架的地址:https://square.github.io/okhttp/
OkHttp是一個預設情況下高效的HTTP使用者端:(要求jdk1.8及以上版本)
引入依賴:
<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.5.0</version> </dependency>
@Test public void getAllMembers() { // 準備url String url = "http://localhost:8080/member"; // 建立client物件 OkHttpClient client = new OkHttpClient(); // 建立請求物件 Request request = new Request.Builder() .url(url) .build(); // 發起請求 try (Response response = client.newCall(request).execute()) { String retStr = response.body().string(); NormalResponseObject<List<MemberVO>> allMemberRes = JSON.parseObject(retStr, new TypeReference<NormalResponseObject<List<MemberVO>>>() { }); if(allMemberRes.getStatusCode() != 200) { throw new RuntimeException(""); } List<MemberVO> memberList = allMemberRes.getData(); memberList.forEach(System.out::println); }catch (Exception e) { e.printStackTrace(); } }
這個api看起來更加簡潔了。
@Test public void testPostParamAddMember() { // 準備url String url = "http://localhost:8080/member/param"; // 建立client物件 OkHttpClient client = new OkHttpClient(); // 建立請求物件 RequestBody formBody = new FormBody.Builder() .add("id", "1000") .add("name", "okHttpPostParam新增") .add("birthday", "1990-10-23") .add("balance", "99981") .build(); Request request = new Request.Builder() .url(url) .post(formBody) .build(); // 發起請求 try (Response response = client.newCall(request).execute()) { String retStr = response.body().string(); System.out.println(retStr); }catch (Exception e) { e.printStackTrace(); } }
程式碼如下:
@Test public void testFileOperation() throws Exception{ // 建立client物件 OkHttpClient client = new OkHttpClient(); // 構建formData的body RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("fileName", "由okHttp上傳的檔案.txt") .addFormDataPart("file", "temp.txt", RequestBody.create(Paths.get("d:", "temp.txt").toFile(), MediaType.get("text/plain"))) .build(); // 構建請求物件 Request request = new Request.Builder() .url("http://localhost:8080/member/fileUpload") .post(requestBody) .build(); // 傳送 try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } }
甚至在使用的時候都不用自己設定請求頭就搞定了。
在spring類庫搜尋ClientHttpRequestFactory的實現類。
目前我本地spring版本是5.3.0,使用springboot的版本是2.5.5版本。
找到了下面的類:
很明顯,該類是為了適配3.x的okHttp而設計的。我本地是okHttp是4.5.0,我先試試:
呼叫成功了,debug跟蹤呼叫流程:
確實使用了OkHttp的功能。
到此本文就結束了,來做一個小小總結吧:
我首先是使用springboot建立了一個小專案,提供了一些我們常用的http介面定義,其中包含了get,post,put,delete和檔案相關的操作。
然後為了驗證這些介面的正確性,我寫了一個小頁面,引入Jquery來傳送ajax請求,驗證了每個介面是否正確能實現業務邏輯。
所以本文也適合前端開發的人看看,一方面是瞭解http協定常見的這些介面定義和內涵,另一方面也可以學到傳送ajax請求的簡單寫法。
其次,我分別從幾個方面寫了java語言發起http請求的程式碼:
一個是原生的基於HttpURLConnection的方式,
一個是使用apache 的Http Compoments專案中的HttpClient傳送http請求
一個是使用RestTemplate傳送http請求
最後使用okHttp傳送http請求。
其實RestTemplate只是一個殼子,具體裡面使用啥工作,取決於我們設定的RestTemplate物件。
本文沒有具體細講HttpClient和okHttp框架的設定細節。以後有機會再寫吧,總之,希望對大家有幫助吧,謝謝。