開心一刻
今天中午,侄子在沙發上玩手機,他妹妹屁顛屁顛的跑到他面前
小侄女:哥哥,給我一塊錢
侄子:叫媽給你
小侄女朝著侄子,毫不猶豫的叫到:媽!
侄子:不是,叫媽媽給你
小侄女繼續朝他叫到:媽媽
侄子受不了,從兜裡掏出一塊錢說道:我就只有這一塊錢了,拿去拿去
小侄女最後還不忘感謝到:謝謝媽媽!
侄子徹底奔潰了,我在一旁笑出了鵝叫聲
需求背景
需求很簡單,就是以 HTTP 的方式下載 OSS 上的檔案,類似如下
分兩步
1、獲取檔案的下載地址( HTTP 地址 )
2、根據下載地址下載檔案
第 1 步不是本文的重點,略過,我們只需要實現第 2 步,是不是很簡單?
問題復現
目前,系統跟其他系統的 HTTP 對接都是用的 RestTemplate
那毫無疑問,也用 RestTemplate 來下載 OSS 檔案
測試程式碼非常簡單,如下
package com.qsl; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; /** * @description: RestTemplate 測試 * @author: 部落格園@青石路 * @date: 2023/11/26 15:31 */ @RunWith(SpringRunner.class) @SpringBootTest public class RestTemplateTest { @Resource private RestTemplate restTemplate; @Test public void testOss() { String ossUrl = "https://qsl-yzb-test.oss-cn-wuhan-lr.aliyuncs.com/company_compare_t.sql?Expires=1700987277&OSSAccessKeyId=TMP.3Kf7vKYWL9RHkroENy7hUyrqAhHBC8YpBCnqXAstCyH3K1j6fkZujtL47V1mFkG5e5hmnLD2dVn4ZJGeD2yDh3GAAQc1k8&Signature=O2qiPYvfZyPmeouwzkXcNqC4Oy0%3D"; ResponseEntity<byte[]> responseEntity = restTemplate.getForEntity(ossUrl, byte[].class); System.out.println(responseEntity.getStatusCode()); } }
我們看下執行結果,發現報異常了
org.springframework.web.client.HttpClientErrorException$Forbidden: 403 Forbidden: [<?xml version="1.0" encoding="UTF-8"?> <Error> <Code>AccessDenied</Code> <Message>Request has expired.</Message> <RequestId>65630E3B05EC713334EDD93D</RequestId> <HostId>qsl-yzb-test.oss-cn-wuh... (443 bytes)] at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:109) at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:170) at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:112) at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63) at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:785) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:743) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:677) at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:345) at com.qsl.RestTemplateTest.testOss(RestTemplateTest.java:27) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74) at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
直接從瀏覽器下載是正常的,用程式碼走 RestTemplate 方式下載則失敗,提示 403 Forbidden
是不是有點懵?
問題排查
系統中已經用 RestTemplate 對接了很多 HTTP 介面,全部都沒問題
這不就是一個很簡單的 HTTP 請求嗎,簡單的不能再簡單了,怎麼會失敗了?
直接把我整不會了,不知道從何下手去排查了
第一時間想到了阿里雲 OSS 售後,聯絡到人工客服,反饋了問題
客服響應倒是很及時,但卻遲遲沒有找到問題原因
然後我又將求助目光轉向了部門內同事
有個同事提到:你開啟 debug 日誌,看看 RestTemplate 請求地址或引數是不是有什麼問題
我內心其實是拒絕的, HTTP 地址都是現成的,都不用拼接, GET 方式的引數也是直接在 URL 中,能有什麼問題?
但我的手卻很誠實,默默的開啟了 debug 日誌(在配置檔案中加上: debug: true )
執行結果依舊失敗,但是多了三行 debug 日誌
RestTemplate 的請求 URL 已經列印出來了,我們來和原始的 URL 對比一下,看看是不是有區別
不比不知道,一比嚇一跳,這特喵的 RestTemplate 是做了手腳呀!對 % 進行了轉義處理,處理成 %25 了
至於為什麼需要對 GET 方式的 URL 的特殊字元進行轉義,我就不做過多解釋了(網上資料很多!),舉個例子你們就明白了
http://localhost:8080/hello?name=青石路 的引數 name 的值是 青石路 ,這個大家都認可吧?
如果 name 的值是 青石路&路石青 ,這個 URL 應該是怎樣的?
有人可能會有疑問了:你這說的是 &,跟 % 有什麼關係?
你是黑子,來搞我的吧?
求求你別搞我,我很菜的!
RFC 3986編碼規範 指明瞭:百分號本身用作對不安全字元進行編碼時使用的特殊字元,因此本身需要編碼
例如: %20 表示空格, %2B 表示 +,等等
問題處理
問題已經找到了,那麼該如何處理了?
拋開上面的問題,處理這種 URL 轉義的問題,方式有很多
1、改成 POST 請求方式
比較推薦這種方式,奈何這種方式不適用本案例
2、使用 HttpClient jar
因為同事用的這種方式實現與本案例一樣的下載,沒有轉義問題
但為了統一,仍想保留統一的 RestTemplate 方式,即沒有采用這種方式
3、 RestTemplate 的 URI 方式
本案例最終採用這種的方式
透過 debug 日誌是能夠看到, RestTemplate 請求的地址是沒有進行轉義的(這裡不展示了,大家自行去測試!)
至於 String 和 URI 的差別,大家去 debug 跟下原始碼就清楚了,底層的實現差別還是很大的哦
當然還有其他的方式,但是需要結合系統當前的情況,找出最合適的那種方式
總結
1、別自以為是,該試還得試
2、 debug 日誌是除錯的好東西,記得用、用、用!
3、多學多總結,多和同事分享溝通,有問題了才好請教他們