微服務(wù)中如何使用RestTemplate優(yōu)雅調(diào)用API(攔截器晌坤、異常處理、消息轉(zhuǎn)換)

關(guān)注我旦袋,可以獲取最新知識骤菠、經(jīng)典面試題以及微服務(wù)技術(shù)分享

??在微服務(wù)中,rest服務(wù)互相調(diào)用是很普遍的疤孕,我們該如何優(yōu)雅地調(diào)用商乎,其實在Spring框架使用RestTemplate類可以優(yōu)雅地進行rest服務(wù)互相調(diào)用,它簡化了與http服務(wù)的通信方式祭阀,統(tǒng)一了RESTful的標準鹉戚,封裝了http鏈接,操作使用簡便专控,還可以自定義RestTemplate所需的模式抹凳。其中:

  • RestTemplate默認使用HttpMessageConverter實例將HTTP消息轉(zhuǎn)換成POJO或者從POJO轉(zhuǎn)換成HTTP消息。默認情況下會注冊主mime類型的轉(zhuǎn)換器伦腐,但也可以通過setMessageConverters注冊自定義轉(zhuǎn)換器赢底。
  • RestTemplate使用了默認的DefaultResponseErrorHandler,對40X Bad Request或50X internal異常error等錯誤信息捕捉柏蘑。
  • RestTemplate還可以使用攔截器interceptor幸冻,進行對請求鏈接跟蹤,以及統(tǒng)一head的設(shè)置咳焚。

其中洽损,RestTemplate還定義了很多的REST資源交互的方法,其中的大多數(shù)都對應(yīng)于HTTP的方法革半,如下:

方法 解析
delete() 在特定的URL上對資源執(zhí)行HTTP DELETE操作
exchange() 在URL上執(zhí)行特定的HTTP方法碑定,返回包含對象的ResponseEntity
execute() 在URL上執(zhí)行特定的HTTP方法流码,返回一個從響應(yīng)體映射得到的對象
getForEntity() 發(fā)送一個HTTP GET請求,返回的ResponseEntity包含了響應(yīng)體所映射成的對象
getForObject() 發(fā)送一個HTTP GET請求延刘,返回的請求體將映射為一個對象
postForEntity() POST 數(shù)據(jù)到一個URL旅掂,返回包含一個對象的ResponseEntity
postForObject() POST 數(shù)據(jù)到一個URL,返回根據(jù)響應(yīng)體匹配形成的對象
headForHeaders() 發(fā)送HTTP HEAD請求访娶,返回包含特定資源URL的HTTP頭
optionsForAllow() 發(fā)送HTTP OPTIONS請求,返回對特定URL的Allow頭信息
postForLocation() POST 數(shù)據(jù)到一個URL觉阅,返回新創(chuàng)建資源的URL
put() PUT 資源到特定的URL

1. RestTemplate源碼

1.1 默認調(diào)用鏈路

restTemplate進行API調(diào)用時崖疤,默認調(diào)用鏈:

###########1.使用createRequest創(chuàng)建請求########
resttemplate->execute()->doExecute()
HttpAccessor->createRequest()
//獲取攔截器Interceptor,InterceptingClientHttpRequestFactory典勇,SimpleClientHttpRequestFactory
InterceptingHttpAccessor->getRequestFactory() 
//獲取默認的SimpleBufferingClientHttpRequest
SimpleClientHttpRequestFactory->createRequest()

#######2.獲取響應(yīng)response進行處理###########
AbstractClientHttpRequest->execute()->executeInternal()
AbstractBufferingClientHttpRequest->executeInternal()

###########3.異常處理#####################
resttemplate->handleResponse()

##########4.響應(yīng)消息體封裝為java對象#######
HttpMessageConverterExtractor->extractData()

1.2 restTemplate->doExecute()

在默認調(diào)用鏈中劫哼,restTemplate 進行API調(diào)用都會調(diào)用 doExecute 方法,此方法主要可以進行如下步驟:

1)使用createRequest創(chuàng)建請求割笙,獲取響應(yīng)
2)判斷響應(yīng)是否異常权烧,處理異常
3)將響應(yīng)消息體封裝為java對象

@Nullable
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
        @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

    Assert.notNull(url, "URI is required");
    Assert.notNull(method, "HttpMethod is required");
    ClientHttpResponse response = null;
    try {
        //使用createRequest創(chuàng)建請求
        ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null) {
            requestCallback.doWithRequest(request);
        }
        //獲取響應(yīng)response進行處理
        response = request.execute();
        //異常處理
        handleResponse(url, method, response);
        //響應(yīng)消息體封裝為java對象
        return (responseExtractor != null ? responseExtractor.extractData(response) : null);
    }catch (IOException ex) {
        String resource = url.toString();
        String query = url.getRawQuery();
        resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
        throw new ResourceAccessException("I/O error on " + method.name() +
                " request for \"" + resource + "\": " + ex.getMessage(), ex);
    }finally {
        if (response != null) {
            response.close();
        }
    }
}

1.3 InterceptingHttpAccessor->getRequestFactory()

在默認調(diào)用鏈中,InterceptingHttpAccessor的getRequestFactory()方法中伤溉,如果沒有設(shè)置interceptor攔截器般码,就返回默認的SimpleClientHttpRequestFactory,反之乱顾,返回InterceptingClientHttpRequestFactoryrequestFactory板祝,可以通過resttemplate.setInterceptors設(shè)置自定義攔截器interceptor

//Return the request factory that this accessor uses for obtaining client request handles.
public ClientHttpRequestFactory getRequestFactory() {
        //獲取攔截器interceptor(自定義的)
        List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
        if (!CollectionUtils.isEmpty(interceptors)) {
            ClientHttpRequestFactory factory = this.interceptingRequestFactory;
            if (factory == null) {
                factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
                this.interceptingRequestFactory = factory;
            }
            return factory;
        }
        else {
            return super.getRequestFactory();
        }
    }

然后再調(diào)用SimpleClientHttpRequestFactory的createRequest創(chuàng)建連接:

@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
    HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
    prepareConnection(connection, httpMethod.name());

    if (this.bufferRequestBody) {
        return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
    }
    else {
        return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
    }
}

1.4 resttemplate->handleResponse()

在默認調(diào)用鏈中走净,resttemplate的handleResponse券时,響應(yīng)處理,包括異常處理伏伯,而且異常處理可以通過調(diào)用setErrorHandler方法設(shè)置自定義的ErrorHandler橘洞,實現(xiàn)對請求響應(yīng)異常的判別和處理。自定義的ErrorHandler需實現(xiàn)ResponseErrorHandler接口说搅,同時Spring boot也提供了默認實現(xiàn)DefaultResponseErrorHandler炸枣,因此也可以通過繼承該類來實現(xiàn)自己的ErrorHandler

DefaultResponseErrorHandler默認對40X Bad Request或50X internal異常error等錯誤信息捕捉蜓堕。如果想捕捉服務(wù)本身拋出的異常信息抛虏,需要通過自行實現(xiàn)RestTemplateErrorHandler

ResponseErrorHandler errorHandler = getErrorHandler();
               //判斷響應(yīng)是否有異常
    boolean hasError = errorHandler.hasError(response);
    if (logger.isDebugEnabled()) {
        try {
            int code = response.getRawStatusCode();
            HttpStatus status = HttpStatus.resolve(code);
            logger.debug("Response " + (status != null ? status : code));
        }catch (IOException ex) {
            // ignore
        }
    }
    //有異常進行異常處理
    if (hasError) {
        errorHandler.handleError(url, method, response);
    }
}

1.5 HttpMessageConverterExtractor->extractData()

在默認調(diào)用鏈中套才, HttpMessageConverterExtractorextractData中進行響應(yīng)消息體封裝為java對象迂猴,就需要使用message轉(zhuǎn)換器,可以通過追加的方式增加自定義的messageConverter:先獲取現(xiàn)有的messageConverter背伴,再將自定義的messageConverter添加進去沸毁。

根據(jù)restTemplatesetMessageConverters的源碼可得峰髓,使用追加的方式可防止原有的messageConverter丟失,源碼:

public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        //檢驗
        validateConverters(messageConverters);
        // Take getMessageConverters() List as-is when passed in here
        if (this.messageConverters != messageConverters) {
            //先清除原有的messageConverter
            this.messageConverters.clear();
            //后加載重新定義的messageConverter
            this.messageConverters.addAll(messageConverters);
        }
    }

HttpMessageConverterExtractor的extractData源碼:

MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
    if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
        return null;
    }
    //獲取到response的ContentType類型
    MediaType contentType = getContentType(responseWrapper);

    try {
        //依次循環(huán)messageConverter進行判斷是否符合轉(zhuǎn)換條件息尺,進行轉(zhuǎn)換java對象
        for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
        //會根據(jù)設(shè)置的返回類型responseType和contentType參數(shù)進行匹配携兵,選擇合適的MessageConverter
            if (messageConverter instanceof GenericHttpMessageConverter) {
                GenericHttpMessageConverter<?> genericMessageConverter =
                        (GenericHttpMessageConverter<?>) messageConverter;
                if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
                    if (logger.isDebugEnabled()) {
                        ResolvableType resolvableType = ResolvableType.forType(this.responseType);
                        logger.debug("Reading to [" + resolvableType + "]");
                    }
                    return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
                }
            }
            if (this.responseClass != null) {
                if (messageConverter.canRead(this.responseClass, contentType)) {
                    if (logger.isDebugEnabled()) {
                        String className = this.responseClass.getName();
                        logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
                    }
                    return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
                }
            }
        }
    }
    .....
}

1.6 contentType與messageConverter之間的關(guān)系

HttpMessageConverterExtractorextractData方法中看出,會根據(jù)contentTyperesponseClass選擇messageConverter是否可讀搂誉、消息轉(zhuǎn)換徐紧。關(guān)系如下:

類名 支持的JavaType 支持的MediaType
ByteArrayHttpMessageConverter byte[] application/octet-stream, */*
StringHttpMessageConverter String text/plain, */*
ResourceHttpMessageConverter Resource */*
SourceHttpMessageConverter Source application/xml, text/xml, application/*+xml
AllEncompassingFormHttpMessageConverter Map<K, List<?>> application/x-www-form-urlencoded, multipart/form-data
MappingJackson2HttpMessageConverter Object application/json, application/*+json
Jaxb2RootElementHttpMessageConverter Object application/xml, text/xml, application/*+xml
JavaSerializationConverter Serializable x-java-serialization;charset=UTF-8
FastJsonHttpMessageConverter Object */*

2. springboot集成RestTemplate

??根據(jù)上述源碼的分析學習,可以輕松炭懊,簡單地在項目進行對RestTemplate進行優(yōu)雅地使用并级,比如增加自定義的異常處理、MessageConverter以及攔截器interceptor侮腹。本文使用示例demo嘲碧,詳情請查看接下來的內(nèi)容。

2.1. 導入依賴:(RestTemplate集成在Web Start中)

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.10</version>
  <scope>provided</scope>
</dependency>

2.2. RestTemplat配置:

  • 使用ClientHttpRequestFactory屬性配置RestTemplat參數(shù)父阻,比如ConnectTimeout愈涩,ReadTimeout;
  • 增加自定義的interceptor攔截器和異常處理;
  • 追加message轉(zhuǎn)換器;
  • 配置自定義的異常處理.


 @Configuration
public class RestTemplateConfig {

    @Value("${resttemplate.connection.timeout}")
    private int restTemplateConnectionTimeout;
    @Value("${resttemplate.read.timeout}")
    private int restTemplateReadTimeout;

    @Bean
    //@LoadBalanced
    public RestTemplate restTemplate( ClientHttpRequestFactory simleClientHttpRequestFactory) {
        RestTemplate restTemplate = new RestTemplate();
        //配置自定義的message轉(zhuǎn)換器
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        messageConverters.add(new CustomMappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        //配置自定義的interceptor攔截器
        List<ClientHttpRequestInterceptor> interceptors=new ArrayList<ClientHttpRequestInterceptor>();
        interceptors.add(new HeadClientHttpRequestInterceptor());
        interceptors.add(new TrackLogClientHttpRequestInterceptor());
        restTemplate.setInterceptors(interceptors);
        //配置自定義的異常處理
        restTemplate.setErrorHandler(new CustomResponseErrorHandler());
        restTemplate.setRequestFactory(simleClientHttpRequestFactory);

        return restTemplate;
    }

    @Bean
    public ClientHttpRequestFactory simleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory reqFactory= new SimpleClientHttpRequestFactory();
        reqFactory.setConnectTimeout(restTemplateConnectionTimeout);
        reqFactory.setReadTimeout(restTemplateReadTimeout);
        return reqFactory;
    }
}

2.3. 組件(自定義異常處理、interceptor攔截器加矛、message轉(zhuǎn)化器)

自定義interceptor攔截器履婉,實現(xiàn)ClientHttpRequestInterceptor接口

  • 自定義TrackLogClientHttpRequestInterceptor,記錄resttemplaterequestresponse信息荒椭,可進行追蹤分析谐鼎;
  • 自定義HeadClientHttpRequestInterceptor,設(shè)置請求頭的參數(shù)趣惠。API發(fā)送各種請求狸棍,很多請求都需要用到相似或者相同的Http Header。如果在每次請求之前都把Header填入HttpEntity/RequestEntity味悄,這樣的代碼會顯得十分冗余草戈,可以在攔截器統(tǒng)一設(shè)置。

TrackLogClientHttpRequestInterceptor:

/**
 * @Auther: ccww
 * @Date: 2019/10/25 22:48侍瑟,記錄resttemplate訪問信息
 * @Description:   記錄resttemplate訪問信息
 */
@Slf4j
public class TrackLogClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        trackRequest(request,body);
        ClientHttpResponse httpResponse = execution.execute(request, body);
        trackResponse(httpResponse);
        return httpResponse;
    }

    private void trackResponse(ClientHttpResponse httpResponse)throws IOException {
        log.info("============================response begin==========================================");
        log.info("Status code  : {}", httpResponse.getStatusCode());
        log.info("Status text  : {}", httpResponse.getStatusText());
        log.info("Headers      : {}", httpResponse.getHeaders());
        log.info("=======================response end=================================================");
    }

    private void trackRequest(HttpRequest request, byte[] body)throws UnsupportedEncodingException {
        log.info("======= request begin ========");
        log.info("uri : {}", request.getURI());
        log.info("method : {}", request.getMethod());
        log.info("headers : {}", request.getHeaders());
        log.info("request body : {}", new String(body, "UTF-8"));
        log.info("======= request end ========");
    }
}

HeadClientHttpRequestInterceptor:

@Slf4j
public class HeadClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
       log.info("#####head handle########");
        HttpHeaders headers = httpRequest.getHeaders();
        headers.add("Accept", "application/json");
        headers.add("Accept-Encoding", "gzip");
        headers.add("Content-Encoding", "UTF-8");
        headers.add("Content-Type", "application/json; charset=UTF-8");
        ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes);
        HttpHeaders headersResponse = response.getHeaders();
        headersResponse.add("Accept", "application/json");
        return  response;
    }
}


自定義異常處理唐片,可繼承DefaultResponseErrorHandler或者實現(xiàn)ResponseErrorHandler接口:

  • 實現(xiàn)自定義ErrorHandler的思路是根據(jù)響應(yīng)消息體進行相應(yīng)的異常處理策略,對于其他異常情況由父類DefaultResponseErrorHandler來進行處理涨颜。
  • 自定義CustomResponseErrorHandler進行30x異常處理

CustomResponseErrorHandler:

/**
 * @Auther: Ccww
 * @Date: 2019/10/28 17:00
 * @Description:  30X的異常處理
 */
@Slf4j
public class CustomResponseErrorHandler extends DefaultResponseErrorHandler {
    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        HttpStatus statusCode = response.getStatusCode();
        if(statusCode.is3xxRedirection()){
            return true;
        }
        return super.hasError(response);
    }

    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        HttpStatus statusCode = response.getStatusCode();
        if(statusCode.is3xxRedirection()){
            log.info("########30X錯誤费韭,需要重定向!##########");
            return;
        }
        super.handleError(response);
    }

}


自定義message轉(zhuǎn)化器

/**
 * @Auther: Ccww
 * @Date: 2019/10/29 21:15
 * @Description: 將Content-Type:"text/html"轉(zhuǎn)換為Map類型格式
 */
public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
    public CustomMappingJackson2HttpMessageConverter() {
        List<MediaType> mediaTypes = new ArrayList<MediaType>();
        mediaTypes.add(MediaType.TEXT_PLAIN);
        mediaTypes.add(MediaType.TEXT_HTML);  //加入text/html類型的支持
        setSupportedMediaTypes(mediaTypes);// tag6
    }

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末庭瑰,一起剝皮案震驚了整個濱河市星持,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌弹灭,老刑警劉巖督暂,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揪垄,死亡現(xiàn)場離奇詭異,居然都是意外死亡逻翁,警方通過查閱死者的電腦和手機饥努,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來八回,“玉大人酷愧,你說我怎么就攤上這事〔纾” “怎么了伟墙?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長滴铅。 經(jīng)常有香客問我,道長就乓,這世上最難降的妖魔是什么汉匙? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮生蚁,結(jié)果婚禮上噩翠,老公的妹妹穿的比我還像新娘。我一直安慰自己邦投,他們只是感情好伤锚,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著志衣,像睡著了一般屯援。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上念脯,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天狞洋,我揣著相機與錄音,去河邊找鬼绿店。 笑死吉懊,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的假勿。 我是一名探鬼主播借嗽,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼转培!你這毒婦竟也來了恶导?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤堡距,失蹤者是張志新(化名)和其女友劉穎甲锡,沒想到半個月后兆蕉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡缤沦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年虎韵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缸废。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡包蓝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出企量,到底是詐尸還是另有隱情测萎,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布届巩,位于F島的核電站硅瞧,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏恕汇。R本人自食惡果不足惜腕唧,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瘾英。 院中可真熱鬧枣接,春花似錦、人聲如沸缺谴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽湿蛔。三九已至膀曾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阳啥,已是汗流浹背妓肢。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留苫纤,地道東北人碉钠。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像卷拘,于是被迫代替她去往敵國和親喊废。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容