How to use RestTemplate to call API (interceptor, exception handling, message transformation) elegantly in microservice

Posted by scription on Wed, 13 Nov 2019 04:39:08 +0100

Follow me to get the latest knowledge, classic interview questions and technical sharing

                                    . Among them:

  • RestTemplate uses HttpMessageConverter instance by default to convert HTTP message to POJO or from POJO to HTTP message. By default, converters of the primary mime type are registered, but custom converters can also be registered through setMessageConverters.
  • RestTemplate uses the default DefaultResponseErrorHandler to capture error information such as 40X Bad Request or 50X internal exception error.
  • RestTemplate can also use interceptor interceptor to track request links and unify head settings.

<br>

Among them, RestTemplate also defines many REST resource interaction methods, most of which correspond to HTTP methods, as follows:

Method analysis
delete() HTTP DELETE on a specific URL for a resource
exchange() Execute a specific HTTP method on the URL to return the ResponseEntity containing the object
execute() Execute a specific HTTP method on the URL to return an object mapped from the response body
getForEntity() Send an HTTP GET request, and the returned ResponseEntity contains the object mapped by the response body
getForObject() Send an HTTP GET request, and the returned request body will be mapped to an object
postForEntity() POST data to a URL, return the ResponseEntity containing an object
postForObject() POST data to a URL, return the object formed according to the response body matching
headForHeaders() Send HTTP HEAD request, return HTTP header containing specific resource URL
optionsForAllow() Send HTTP OPTIONS request, return Allow header information for specific URL
postForLocation() POST data to a URL, return the URL of the newly created resource
put() PUT resource to specific URL

1. RestTemplate source code

1.1 default call link

When restTemplate makes API calls, the default call chain is:

###########1.Use createRequest Create request########
resttemplate->execute()->doExecute()
HttpAccessor->createRequest()
//Get Interceptor, intercepting clienthttprequestfactory, SimpleClientHttpRequestFactory
InterceptingHttpAccessor->getRequestFactory() 
//Get the default SimpleBufferingClientHttpRequest
SimpleClientHttpRequestFactory->createRequest()

#######2.Get response response Processing###########
AbstractClientHttpRequest->execute()->executeInternal()
AbstractBufferingClientHttpRequest->executeInternal()

###########3.exception handling#####################
resttemplate->handleResponse()

##########4.The response message body is encapsulated as java object#######
HttpMessageConverterExtractor->extractData()

1.2 restTemplate->doExecute()

In the default call chain, when restTemplate makes API calls, it will call the doExecute method. The main steps of this method are as follows:

1) use createRequest to create a request and get a response
2) judge whether the response is abnormal and deal with it
3) encapsulate the response message body as a java object

[@Nullable](https://my.oschina.net/u/2896689)
protected <T> T doExecute(URI url, [@Nullable](https://my.oschina.net/u/2896689) HttpMethod method, [@Nullable](https://my.oschina.net/u/2896689) RequestCallback requestCallback,
		[@Nullable](https://my.oschina.net/u/2896689) ResponseExtractor<T> responseExtractor) throws RestClientException {

	Assert.notNull(url, "URI is required");
	Assert.notNull(method, "HttpMethod is required");
	ClientHttpResponse response = null;
	try {
		//Create a request using createRequest
		ClientHttpRequest request = createRequest(url, method);
		if (requestCallback != null) {
			requestCallback.doWithRequest(request);
		}
		//Get response for processing
		response = request.execute();
		//exception handling
		handleResponse(url, method, response);
		//The response message body is encapsulated as a java object
		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()

In the default call chain, if the interceptor blocker is not set in the getRequestFactory() method of InterceptingHttpAccessor, the default SimpleClientHttpRequestFactory will be returned. Otherwise, the requestFactory of InterceptingClientHttpRequestFactory will be returned. The user-defined interceptor can be set through resttemplate.setInterceptors.

//Return the request factory that this accessor uses for obtaining client request handles.
public ClientHttpRequestFactory getRequestFactory() {
        //Get interceptor (custom)
		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();
		}
	}

Then call createRequest of SimpleClientHttpRequestFactory to create the connection:

[@Override](https://my.oschina.net/u/1162528)
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()

In the default call chain, the handleResponse of resttemplate, response handling, including exception handling, and exception handling can be realized by calling setErrorHandler method to set a user-defined ErrorHandler to distinguish and handle the request response exception. The custom ErrorHandler needs to implement the ResponseErrorHandler interface, and Spring boot also provides the default implementation of DefaultResponseErrorHandler, so you can also implement your own ErrorHandler by inheriting this class.

DefaultResponseErrorHandler captures error information such as 40X Bad Request or 50X internal exception error by default. If you want to catch the exception information thrown by the service itself, you need to implement the ErrorHandler of the RestTemplate by yourself.

ResponseErrorHandler errorHandler = getErrorHandler();
               //Judge whether the response is abnormal
	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
		}
	}
	//Exception handling in case of exception
	if (hasError) {
		errorHandler.handleError(url, method, response);
	}
}

1.5 HttpMessageConverterExtractor->extractData()

In the default call chain, the response message body in the extractData of HttpMessageConverterExtractor is encapsulated as a java object. You need to use the message converter. You can add a custom messageConverter by appending: first get the existing messageConverter, and then add the custom messageConverter.

According to the source code of setMessageConverters of restTemplate, the original messageconverters can be prevented from being lost by adding. Source code:

public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        //test
		validateConverters(messageConverters);
		// Take getMessageConverters() List as-is when passed in here
		if (this.messageConverters != messageConverters) {
		    //Clear the original messageConverter first
			this.messageConverters.clear();
			//Load the redefined messageConverter after
			this.messageConverters.addAll(messageConverters);
		}
	}

The extractData source code of HttpMessageConverterExtractor:

MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
	if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
		return null;
	}
	//Get ContentType type of response
	MediaType contentType = getContentType(responseWrapper);

	try {
	    //Loop the messageConverter in turn to determine whether the conversion conditions are met and convert the java object
		for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
		//According to the set return type responseType and contentType parameters, the appropriate MessageConverter will be selected
			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 relationship between contenttype and messageConverter

From the extractData method of HttpMessageConverterExtractor, it can be seen that whether the messageConverter is readable or not and message conversion will be selected according to the contentType and responseClass. The relationship is as follows:

Class name Supported javatypes Supported mediatypes
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 integration RestTemplate

                          . This article uses the sample demo, which you can see later.

2.1. Import dependency: (RestTemplate is integrated in 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 configuration:

  • Use the ClientHttpRequestFactory property to configure the RestTemplat parameter, such as ConnectTimeout and ReadTimeout;
  • Add custom interceptor and exception handling;
  • The message converter is added;
  • Configure custom exception handling

<br>

 @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();
        //Configure a custom message converter
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        messageConverters.add(new CustomMappingJackson2HttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);
        //Configure a custom interceptor
        List<ClientHttpRequestInterceptor> interceptors=new ArrayList<ClientHttpRequestInterceptor>();
        interceptors.add(new HeadClientHttpRequestInterceptor());
        interceptors.add(new TrackLogClientHttpRequestInterceptor());
        restTemplate.setInterceptors(interceptors);
        //Configure custom exception handling
        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. Components (custom exception handling, interceptor, message converter)

Custom interceptor to implement ClientHttpRequestInterceptor interface

  • Customize TrackLogClientHttpRequestInterceptor to record the request and response information of resttemplate, which can be tracked and analyzed;
  • Customize the HeadClientHttpRequestInterceptor, and set the parameters of the request Header. API sends various requests, many of which need to use similar or the same Http Header. If the Header is filled in HttpEntity/RequestEntity before each request, such code will appear very redundant and can be set uniformly in the interceptor.

<br>

TrackLogClientHttpRequestInterceptor:

/**
 * @Auther: ccww
 * @Date: 2019/10/25 22:48,Record resttemplate access information
 * @Description:   Record resttemplate access information
 */
@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;
    }
}

<br>

Custom exception handling can inherit DefaultResponseErrorHandler or implement the ResponseErrorHandler interface:

  • The idea of implementing the user-defined ErrorHandler is to carry out the corresponding exception handling strategy according to the response message body. For other exceptions, the parent class DefaultResponseErrorHandler will handle them.
  • Custom response error handler for 30x exception handling

<br>

CustomResponseErrorHandler:

/**
 * @Auther: Ccww
 * @Date: 2019/10/28 17:00
 * @Description:  30X Exception handling for
 */
@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 Error, redirection required!##########");
            return;
        }
        super.handleError(response);
    }

}

<br>

Custom message converter

/**
 * @Auther: Ccww
 * @Date: 2019/10/29 21:15
 * @Description: Convert content type: "text / HTML" to Map type format
 */
public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
    public CustomMappingJackson2HttpMessageConverter() {
        List<MediaType> mediaTypes = new ArrayList<MediaType>();
        mediaTypes.add(MediaType.TEXT_PLAIN);
        mediaTypes.add(MediaType.TEXT_HTML);  //Add text/html type support
        setSupportedMediaTypes(mediaTypes);// tag6
    }

}

Finally, I would like to ask you to give me a compliment, thank you for your support!!!

Topics: Java xml JSON Spring