13 Service Invocation How to Understand the Remote Invocation Implementation Principles of Rest Template Correctly

13 Service Invocation - How to Understand the Remote Invocation Implementation Principles of RestTemplate Correctly #

In Lesson 12, we described in detail how to use RestTemplate to access HTTP endpoints, involving core steps such as RestTemplate initialization, sending requests, and obtaining response results. Today, based on these steps from the previous lesson, we will start from the source code to help you truly understand the underlying principle of RestTemplate’s implementation of remote invocation.

Initializing the RestTemplate instance #

In Lesson 12, we mentioned that RestTemplate can be initialized using several constructors provided by RestTemplate. Before analyzing these constructors, it is necessary to first look at the definition of the RestTemplate class, as shown in the following code:

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations

From the above code, we can see that RestTemplate extends the abstract class InterceptingHttpAccessor and implements the RestOperations interface. Next, we will organize our thoughts around the method definitions of RestTemplate.

First, let’s take a look at the definition of the RestOperations interface. Here, we excerpted some core methods, as shown in the following code:

public interface RestOperations {

    <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;

    <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;

    <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;

    void put(String url, @Nullable Object request, Object... uriVariables) throws RestClientException;

    void delete(String url, Object... uriVariables) throws RestClientException;

    <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity,

    Class<T> responseType, Object... uriVariables) throws RestClientException;

    

}

Obviously, the RestOperations interface defines all the remote invocation method groups mentioned in Lesson 12, such as get/post/put/delete/exchange. These methods are designed following the RESTful architecture style. RestTemplate provides an implementation mechanism for these interfaces, which is one of its code branches.

Next, let’s take a look at InterceptingHttpAccessor. It is an abstract class that includes the following core variables:

public abstract class InterceptingHttpAccessor extends HttpAccessor {

    private final List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();

    private volatile ClientHttpRequestFactory interceptingRequestFactory;

    
}

Through the variable definitions, we can see that InterceptingHttpAccessor contains two parts of processing functionality. One part is responsible for setting up and managing the request interceptors ClientHttpRequestInterceptor, and the other part is responsible for obtaining the ClientHttpRequestFactory class used to create client-side HTTP requests.

At the same time, we notice that InterceptingHttpAccessor also has a parent class HttpAccessor, which actually implements the creation of ClientHttpRequestFactory and how to obtain the ClientHttpRequest object that represents the client request through the ClientHttpRequestFactory. The core variables of HttpAccessor are as follows:

public abstract class HttpAccessor {

    private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

    ...
}

From the above code, we can see that HttpAccessor creates a SimpleClientHttpRequestFactory as the default ClientHttpRequestFactory for the system. We will discuss ClientHttpRequestFactory in detail later in this lesson.

Finally, let’s organize the class hierarchy of RestTemplate for this part of the content, as shown in the following diagram:

Image

Class hierarchy of RestTemplate

In the class hierarchy of RestTemplate, we can quickly understand its design ideas. The entire class hierarchy is clearly divided into two branches. The branch on the left is used to complete the implementation mechanism related to the HTTP request, while the branch on the right provides an operating entry based on the RESTful style. It uses interfaces and abstract classes in object-oriented programming to aggregate these two parts of functionality.

Core execution process of RestTemplate #

After introducing the instantiation process of RestTemplate, let’s analyze its core execution process.

As a template utility class used for remote invocation, we can start with the exchange method, which supports multiple request methods. The definition of this method is as follows:


@Override

public <T> ResponseEntity<T> exchange(String url, HttpMethod method,

        @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)

        throws RestClientException {

    // Construct the request callback

    RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);

    // Construct the response body extractor

    ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);

    // Execute the remote invocation

    return nonNull(execute(url, method, requestCallback, responseExtractor, uriVariables));

}

Obviously, we should further pay attention to the execute method here. In fact, regardless of whether we use the get/put/post/delete method to initiate a request, when RestTemplate is responsible for executing the remote invocation, it always uses the execute method, whose definition is as follows:


@Override

@Nullable

public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {

    URI expanded = getUriTemplateHandler().expand(url, uriVariables);

    return doExecute(expanded, method, requestCallback, responseExtractor);

}

From the above code, we find that the execute method first constructs a URI using the UriTemplateHandler, and then delegates the request process to the doExecute method. The definition of the doExecute method is as follows:

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 {

        // Create the request object

        ClientHttpRequest request = createRequest(url, method);

From the above code, we can see that the execute method first constructs a URI using the UriTemplateHandler, and then delegates the request process to the doExecute method. The definition of the doExecute method is as follows:

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 {

        // Create the request object

        ClientHttpRequest request = createRequest(url, method);
            writeHeaders();
    
            if (this.requestBody != null) {
    
                writeRequestBody(this.requestBody);
    
            }
    
            boolean chunkedRequestBody = isChunkedRequestBody();
    
            if (chunkedRequestBody) {
    
                HttpHeaders headers = getHeaders();
    
                if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
    
                    headers.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
    
                }
    
                if (headers.containsKey(HttpHeaders.CONTENT_LENGTH)) {
    
                    headers.remove(HttpHeaders.CONTENT_LENGTH);
    
                }
    
            }
    
            ClientHttpResponse result;
    
            try {
    
                result = executeInternal(getHeaders(), (chunkedRequestBody ? null : this.requestBody));
    
            }
    
            catch (IOException ex) {
    
                close();
    
                throw ex;
    
            }
    
            this.executed = true;
    
            if (result == null) {
    
                throw new IllegalStateException("executeInternal return null, not supported");
    
            }
    
            return result;
    
    }

在上述 execute 方法中,首先会调用 writeHeaders 方法写入请求头,然后再根据请求体是否为空调用 writeRequestBody 方法写入请求体。接着,根据请求体是否为分块传输设置请求头,如果是分块传输,则将 Transfer-Encoding 设置为 “chunked”,并移除 Content-Length 请求头。

最后,我们通过调用 executeInternal 方法执行实际的远程调用,并将返回结果保存在 result 变量中。在这个方法中,我们可以根据具体的实现类来查看具体的远程调用过程。

比如,RestTemplate 中默认的实现类是 SimpleClientHttpRequest,这个类重写了 executeInternal 方法,具体实现了远程调用的逻辑。

处理响应结果 #

handleResponse(url, method, response);

通过上面的代码,我们发现在调用完远程服务后,RestTemplate 会进入到 handleResponse(url, method, response) 方法进行响应结果的处理。

在 handleResponse 方法中,首先会检查响应状态码,如果响应状态码表示出现了错误,则通过 HttpResponseException 异常抛出。如果响应状态码表示成功,则会通过 createResponse 方法创建 ClientHttpResponse 对象,并返回给上层组件进行使用。

在创建 ClientHttpResponse 对象的过程中,又涉及到了使用 AbstractHttpMessageConverter 对响应数据进行转换的过程,我们可以通过跟踪 createResponse 方法来进一步了解。

在 createResponse 方法中,会调用 getResponseHeaders 方法获取响应头,并调用 getResponseBodyInternal 方法获取响应体输入流,然后再将这两个数据封装成 ClientHttpResponse 对象返回。

至此,我们已经了解了 RestTemplate 远程调用的主要过程。其中,创建请求对象涉及到选择 ClientHttpRequestFactory 的实现类,执行远程调用涉及到涉及到创建具体的请求体以及发送请求,并获取响应结果,处理响应结果涉及到校验响应状态码,并将响应数据封装成 ClientHttpResponse 对象返回。

ClientHttpResponse result = executeInternal(this.headers);

this.executed = true;

return result;

}

protected abstract ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException;

The purpose of the AbstractClientHttpRequest class is to prevent the HTTP request headers and body from being written multiple times. Therefore, before the execute method returns, we set an executed flag. In the execute method, we ultimately call an abstract method executeInternal, which is implemented in the AbstractBufferingClientHttpRequest subclass of AbstractClientHttpRequest, as shown in the following code:

@Override

protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {

    byte[] bytes = this.bufferedOutput.toByteArray();

    if (headers.getContentLength() < 0) {

        headers.setContentLength(bytes.length);

    }

    ClientHttpResponse result = executeInternal(headers, bytes);

    this.bufferedOutput = new ByteArrayOutputStream(0);

    return result;

}

protected abstract ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException;

Similar to the AbstractClientHttpRequest class, we further clarify an abstract method executeInternal, which is implemented by the lowest-level SimpleBufferingClientHttpRequest class, as shown in the following code:

@Override

protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {

    addHeaders(this.connection, headers);

    // JDK <1.8 doesn't support getOutputStream with HTTP DELETE

    if (getMethod() == HttpMethod.DELETE && bufferedOutput.length == 0) {

        this.connection.setDoOutput(false);

    }

    if (this.connection.getDoOutput() && this.outputStreaming) {

        this.connection.setFixedLengthStreamingMode(bufferedOutput.length);

    }

    this.connection.connect();

    if (this.connection.getDoOutput()) {

        FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());

    }

    else {

        // Immediately trigger the request in a no-output scenario as well

        this.connection.getResponseCode();

    }

    return new SimpleClientHttpResponse(this.connection);

}

Here, we use the FileCopyUtils.copy utility method to write the result to the output stream. The executeInternal method ultimately returns a SimpleClientHttpResponse object that wraps the Connection object.

Handling the response #

The last step in processing an HTTP request is to read the input stream from the ClientHttpResponse, format it as a response body, and convert it to a business object. The entry code is as follows:

// Process the response
handleResponse(url, method, response);

// Extract data from the response
return (responseExtractor != null ? responseExtractor.extractData(response) : null);

Let’s first look at the handleResponse method defined here, which is defined as follows:

protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {

    ResponseErrorHandler errorHandler = getErrorHandler();

    boolean hasError = errorHandler.hasError(response);

    if (logger.isDebugEnabled()) {

        try {

            logger.debug(method.name() + " request for \"" + url + "\" resulted in " +

                    response.getRawStatusCode() + " (" + response.getStatusText() + ")" +

                    (hasError ? "; invoking error handler" : ""));

        }

        catch (IOException ex) {

            // ignore

        }

    }

    if (hasError) {

        errorHandler.handleError(url, method, response);

    }

}

In the above code, we retrieve a ResponseErrorHandler through the getErrorHandler method. If the response status code is incorrect, we can call handleError to handle the error and throw an exception. Here, we find that this code actually does not handle the returned data, but only executes error handling.

The task of retrieving the response data and completing the conversion is done in a ResponseExtractor. This interface is defined as follows:

public interface ResponseExtractor<T> {

@Nullable
T extractData(ClientHttpResponse response) throws IOException;

}

In the RestTemplate class, we define an inner class ResponseEntityResponseExtractor that implements the ResponseExtractor interface, as shown in the following code:

private class ResponseEntityResponseExtractor <T> implements ResponseExtractor<ResponseEntity<T>> {

@Nullable
private final HttpMessageConverterExtractor<T> delegate;

public ResponseEntityResponseExtractor(@Nullable Type responseType) {

    if (responseType != null && Void.class != responseType) {

        this.delegate = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);

    }

    else {

        this.delegate = null;

    }

}

@Override

public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {

    if (this.delegate != null) {

        T body = this.delegate.extractData(response);

        return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(body);

    }

    else {

        return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();

    }

}

In the above code, the extractData method in the ResponseEntityResponseExtractor essentially delegates the data extraction work to a proxy object named delegate. And the type of this delegate is HttpMessageConverterExtractor.

From the naming, it is not difficult to see that the HttpMessageConverterExtractor class internally uses the HttpMessageConverter that we discussed in Lesson 12 to implement message conversion. The code is as follows (with some parts trimmed):

public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {

    private final List<HttpMessageConverter<?>> messageConverters;

    @Override
    @SuppressWarnings({"unchecked", "rawtypes", "resource"})
    public T extractData(ClientHttpResponse response) throws IOException {

        MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);

        if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
            return null;
        }

        MediaType contentType = getContentType(responseWrapper);

        try {
            for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
                if (messageConverter instanceof GenericHttpMessageConverter) {
                    GenericHttpMessageConverter<?> genericMessageConverter = 
                        (GenericHttpMessageConverter<?>) messageConverter;
                    if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
                        return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
                    }
                }
                if (this.responseClass != null) {
                    if (messageConverter.canRead(this.responseClass, contentType)) {
                        return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
                    }
                }
            }
        }
    ...
}

The above method may seem a bit complex, but the core logic is simple. It first iterates through the list of HttpMessageConverter, then checks if it can read the data, and if it can, it calls the read method to read the data.

Finally, let’s discuss how read is implemented in HttpMessageConverter.

Let’s first look at the abstract implementation of HttpMessageConverter, AbstractHttpMessageConverter. In its read method, we also define an abstract method readInternal, as shown in the following code:

@Override
public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
    return readInternal(clazz, inputMessage);
}

protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;

In Lesson 12, we mentioned that Spring provides a series of HttpMessageConverter implementations for message conversion, and the simplest implementation is StringHttpMessageConverter. The read method of this class is as follows:

@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
    Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
    return StreamUtils.copyToString(inputMessage.getBody(), charset);
}

The implementation process of StringHttpMessageConverter is as follows: first, it gets the message body, which is a ClientHttpResponse object, from the input message HttpInputMessage using the getBody method. Then, it uses the copyToString method to read the data from the object and returns the string result.

So far, we have completed the entire process of sending, executing, and responding to an HTTP request using RestTemplate.

From Source Code Analysis to Daily Development #

This section covers a lot of implementation details on how to handle HTTP requests. Understanding these implementation details will greatly help developers understand and master the HTTP protocol and remote method invocation. Later, you can further analyze specific details according to your actual needs.

At the same time, by analyzing the design and implementation process of RestTemplate itself and the multiple utility classes around it, we can deepen our understanding of the standard design concepts of abstract classes and interfaces, and apply these design concepts to our daily development process.

Summary and Preview #

To deepen our understanding and mastery of the process of handling an HTTP request, it is necessary to analyze the implementation of the RestTemplate utility class.

RestTemplate provides complete implementation ideas for creating request objects, executing remote calls, and handling response results. In this lesson, we have provided detailed explanations of these steps and analyzed the design concepts and implementation techniques involved.