微服務中如何使用RestTemplate優雅調用API(攔截器、異常處理、消息轉換)

關注我,能夠獲取最新知識、經典面試題以及微服務技術分享html

  在微服務中,rest服務互相調用是很廣泛的,咱們該如何優雅地調用,其實在Spring框架使用RestTemplate類能夠優雅地進行rest服務互相調用,它簡化了與http服務的通訊方式,統一了RESTful的標準,封裝了http連接,操做使用簡便,還能夠自定義RestTemplate所需的模式。其中:java

  • RestTemplate默認使用HttpMessageConverter實例將HTTP消息轉換成POJO或者從POJO轉換成HTTP消息。默認狀況下會註冊主mime類型的轉換器,但也能夠經過setMessageConverters註冊自定義轉換器。
  • RestTemplate使用了默認的DefaultResponseErrorHandler,對40X Bad Request或50X internal異常error等錯誤信息捕捉。
  • RestTemplate還可使用攔截器interceptor,進行對請求連接跟蹤,以及統一head的設置。

其中,RestTemplate還定義了不少的REST資源交互的方法,其中的大多數都對應於HTTP的方法,以下:web

方法 解析
delete() 在特定的URL上對資源執行HTTP DELETE操做
exchange() 在URL上執行特定的HTTP方法,返回包含對象的ResponseEntity
execute() 在URL上執行特定的HTTP方法,返回一個從響應體映射獲得的對象
getForEntity() 發送一個HTTP GET請求,返回的ResponseEntity包含了響應體所映射成的對象
getForObject() 發送一個HTTP GET請求,返回的請求體將映射爲一個對象
postForEntity() POST 數據到一個URL,返回包含一個對象的ResponseEntity
postForObject() POST 數據到一個URL,返回根據響應體匹配造成的對象
headForHeaders() 發送HTTP HEAD請求,返回包含特定資源URL的HTTP頭
optionsForAllow() 發送HTTP OPTIONS請求,返回對特定URL的Allow頭信息
postForLocation() POST 數據到一個URL,返回新建立資源的URL
put() PUT 資源到特定的URL

1. RestTemplate源碼

1.1 默認調用鏈路

restTemplate進行API調用時,默認調用鏈:面試

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

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

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

##########4.響應消息體封裝爲java對象#######
HttpMessageConverterExtractor->extractData()
複製代碼

1.2 restTemplate->doExecute()

在默認調用鏈中,restTemplate 進行API調用都會調用 doExecute 方法,此方法主要能夠進行以下步驟:spring

1)使用createRequest建立請求,獲取響應
2)判斷響應是否異常,處理異常
3)將響應消息體封裝爲java對象json

@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建立請求
		ClientHttpRequest request = createRequest(url, method);
		if (requestCallback != null) {
			requestCallback.doWithRequest(request);
		}
		//獲取響應response進行處理
		response = request.execute();
		//異常處理
		handleResponse(url, method, response);
		//響應消息體封裝爲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()

在默認調用鏈中,InterceptingHttpAccessor的getRequestFactory()方法中,若是沒有設置interceptor攔截器,就返回默認的SimpleClientHttpRequestFactory,反之,返回InterceptingClientHttpRequestFactoryrequestFactory,能夠經過resttemplate.setInterceptors設置自定義攔截器interceptorspringboot

//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();
		}
	}
複製代碼

而後再調用SimpleClientHttpRequestFactory的createRequest建立鏈接:app

@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()

在默認調用鏈中,resttemplate的handleResponse,響應處理,包括異常處理,並且異常處理能夠經過調用setErrorHandler方法設置自定義的ErrorHandler,實現對請求響應異常的判別和處理。自定義的ErrorHandler需實現ResponseErrorHandler接口,同時Spring boot也提供了默認實現DefaultResponseErrorHandler,所以也能夠經過繼承該類來實現本身的ErrorHandler框架

DefaultResponseErrorHandler默認對40X Bad Request或50X internal異常error等錯誤信息捕捉。若是想捕捉服務自己拋出的異常信息,須要經過自行實現RestTemplateErrorHandleride

ResponseErrorHandler errorHandler = getErrorHandler();
               //判斷響應是否有異常
	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()

在默認調用鏈中, HttpMessageConverterExtractorextractData中進行響應消息體封裝爲java對象,就須要使用message轉換器,能夠經過追加的方式增長自定義的messageConverter:先獲取現有的messageConverter,再將自定義的messageConverter添加進去。

根據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 {
	    //依次循環messageConverter進行判斷是否符合轉換條件,進行轉換java對象
		for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
		//會根據設置的返回類型responseType和contentType參數進行匹配,選擇合適的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之間的關係

HttpMessageConverterExtractorextractData方法中看出,會根據contentTyperesponseClass選擇messageConverter是否可讀、消息轉換。關係以下:

類名 支持的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

  根據上述源碼的分析學習,能夠輕鬆,簡單地在項目進行對RestTemplate進行優雅地使用,好比增長自定義的異常處理、MessageConverter以及攔截器interceptor。本文使用示例demo,詳情請查看接下來的內容。

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參數,好比ConnectTimeoutReadTimeout;
  • 增長自定義的interceptor攔截器和異常處理;
  • 追加message轉換器;
  • 配置自定義的異常處理.

@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轉換器
        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轉化器)

自定義interceptor攔截器,實現ClientHttpRequestInterceptor接口

  • 自定義TrackLogClientHttpRequestInterceptor,記錄resttemplaterequestresponse信息,可進行追蹤分析;
  • 自定義HeadClientHttpRequestInterceptor,設置請求頭的參數。API發送各類請求,不少請求都須要用到類似或者相同的Http Header。若是在每次請求以前都把Header填入HttpEntity/RequestEntity,這樣的代碼會顯得十分冗餘,能夠在攔截器統一設置。

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或者實現ResponseErrorHandler接口:

  • 實現自定義ErrorHandler的思路是根據響應消息體進行相應的異常處理策略,對於其餘異常狀況由父類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轉化器

/**
 * @Auther: Ccww
 * @Date: 2019/10/29 21:15
 * @Description: 將Content-Type:"text/html"轉換爲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
    }

}
複製代碼

最後可關注公衆號【Ccww筆記】,一塊兒學習。加羣,天天會分享乾貨,還有學習視頻領取!

相關文章
相關標籤/搜索