記一次 RestTemplate 請求失敗問題的排查 → RestTemplate 預設會對特殊字元進行跳脫

2023-11-27 12:03:17

開心一刻

  今天中午,侄子在沙發上玩手機,他妹妹屁顛屁顛的跑到他面前

  小侄女:哥哥,給我一塊錢

  侄子:叫媽給你

  小侄女朝著侄子,毫不猶豫的叫到:媽!

  侄子:不是,叫媽媽給你

  小侄女繼續朝他叫到:媽媽

  侄子受不了,從兜裡掏出一塊錢說道:我就只有這一塊錢了,拿去拿去

  小侄女最後還不忘感謝到:謝謝媽媽!

  侄子徹底奔潰了,我在一旁笑出了鵝叫聲

需求背景

  需求很簡單,就是以 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());
    }
}
View Code

  我們看下執行結果,發現報異常了

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)
View Code

  直接從瀏覽器下載是正常的,用程式碼走 RestTemplate 方式下載則失敗,提示 403 Forbidden 

  是不是有點懵?

   RestTemplate 的請求 URL 已經列印出來了,我們來和原始的 URL 對比一下,看看是不是有區別

   不比不知道,一比嚇一跳,這特喵的 RestTemplate 是做了手腳呀!對 % 進行了跳脫處理,處理成 %25 了

  至於為什麼需要對 GET 方式的 URL 的特殊字元進行跳脫,我就不做過多解釋了(網上資料很多!),舉個例子你們就明白了

     http://localhost:8080/hello?name=青石路 的引數 name 的值是 青石路 ,這個大家都認可吧?

    如果 name 的值是 青石路&路石青 ,這個 URL 應該是怎樣的?

    有人可能會有疑問了:你這說的是 &,跟 % 有什麼關係?

    你是黑子,來搞我的吧?

    求求你別搞我,我很菜的!

    本案例最終採用這種的方式

    通過 debug 紀錄檔是能夠看到, RestTemplate 請求的地址是沒有進行跳脫的(這裡不展示了,大家自行去測試!)

    至於 String 和 URI 的差別,大家去 debug 跟下原始碼就清楚了,底層的實現差別還是很大的哦

  當然還有其他的方式,但是需要結合系統當前的情況,找出最合適的那種方式

總結

  1、別自以為是,該試還得試

  2、 debug 紀錄檔是偵錯的好東西,記得用、用、用!

  3、多學多總結,多和同事分享溝通,有問題了才好請教他們