Spring RestTemplate 总结

URI 构造

要轻松地操作 URL,可以使用 Spring 的 org.springframework.web.util.UriComponentsBuilder 类。相比手动拼接字符串更易维护,还可以处理 URL 编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// `java.net.URI#toURL()` 可以转为 `java.net.URL`
private URI getUri(Object body) {
String rootUrl = "";
String path = "";
return UriComponentsBuilder
.fromHttpUrl(rootUrl)
.path(path)
// Append the given query parameter to the existing query parameters.
// .queryParam("key", "value1", "value2", "value3")
// Add the given query parameters.
.queryParams(GenericUtils.toMultiValueMap(body)) // 反射递归遍历对象的字段值,并转成 org.springframework.util.MultiValueMap
.build()
.encode()
.toUri();
}

GenericUtils 参考:https://github.com/qidawu/java-api-test/blob/master/src/main/java/reflect/GenericUtils.java

HTTP 请求

RestTemplate

构造出 java.net.URI 之后,可以使用 org.springframework.web.client.RestTemplate 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Slf4j
public class HttpApiService {

@Autowired
private RestTemplate restTemplate;

public void request(HttpMethod httpMethod, Object body) {
try {
URI uri;
HttpHeaders requestHeaders = getHttpHeaders();
HttpEntity<Object> httpEntity;

// GET 请求,请求参数作为 query parameter 放入 URL
// 对方 Controller 方法入参需标注 @RequestParam,以接收 query parameter
if (httpMethod == HttpMethod.GET) {
uri = getUri(body);
// 请求参数不能放到 HttpEntity,因为 GET 请求的话不会带上 request body(因为底层 HttpURLConnection#setDoOutput(false))
httpEntity = new HttpEntity<>(requestHeaders);
}
// POST 请求,请求参数作为 request body 而不放入 URL
else {
uri = getUri(null);
// POST 表单(Content-Type: application/x-www-form-urlencoded)
// request body 类型必须为 MultiValueMap。MultiValueMap 参数最终会转为形如:key1=value1&key2=value2&...
// Writing [{key=[value]}] with org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
//
// POST JSON(Content-Type: application/json)
// request body 类型为普通 POJO
// Writing [ReqDTO(key=value)] with org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
// 对方 Controller 方法入参需标注 @RequestBody,以接收整个 request body
httpEntity = new HttpEntity<>(body, requestHeaders);
}

ResponseEntity<String> response = restTemplate.exchange(uri, httpMethod, httpEntity, String.class);
if (HttpStatus.OK.equals(response.getStatusCode())) {
log.info(response.getBody());
}
} catch (ResourceAccessException e) {
log.error("TCP 连接建立失败", e);
} catch (HttpClientErrorException | HttpServerErrorException e) {
log.error("HTTP 请求失败,状态码:{}", e.getRawStatusCode(), e);
}
}

private HttpHeaders getHttpHeaders() {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Accept", MediaType.APPLICATION_JSON_VALUE);
return requestHeaders;
}

}

GET 请求、POST 表单时,对方 Controller 方法入参需标注 @RequestParam,以接收 query parameter。但要注意,官方文档提醒如下:

Supported for annotated handler methods in Spring MVC and Spring WebFlux as follows:

  • In Spring MVC, “request parameters” map to query parameters, form data, and parts in multipart requests. This is because the Servlet API combines query parameters and form data into a single map called “parameters”, and that includes automatic parsing of the request body.
  • In Spring WebFlux, “request parameters” map to query parameters only. To work with all 3, query, form data, and multipart data, you can use data binding to a command object annotated with ModelAttribute.

CURL 形式

GET 请求

1
2
curl --location --request GET 'http://rootUrl/path?key=value' \
--header 'Content-Type: application/x-www-form-urlencoded'

POST 表单

1
2
3
curl --location --request POST 'http://rootUrl/path' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'key=value'

POST JSON

1
2
3
4
5
curl --location --request POST 'http://rootUrl/path' \
--header 'Content-Type: application/json' \
--data-raw '{
"key": "value"
}'

核心类解析

RestTemplate UML

涉及的核心类如下:

org.springframework.*

org.springframework.web.client.RestTemplate

Synchronous client to perform HTTP requests, exposing a simple, template method API over underlying HTTP client libraries such as the JDK HttpURLConnection, Apache HttpComponents, and others.

The RestTemplate offers templates for common scenarios by HTTP method, in addition to the generalized exchange and execute methods that support of less frequent cases.

org.springframework.http.converter.HttpMessageConverter

Strategy interface for converting from and to HTTP requests and responses.

参考:

java.net.*

java.net.URL

Class URL represents a Uniform Resource Locator, a pointer to a “resource” on the World Wide Web. A resource can be something as simple as a file or a directory, or it can be a reference to a more complicated object, such as a query to a database or to a search engine. More information on the types of URLs and their formats can be found at: Types of URL

java.net.URLConnection

The abstract class URLConnection is the superclass of all classes that represent a communications link between the application and a URL. Instances of this class can be used both to read from and to write to the resource referenced by the URL.

URLConnection 的继承结构如下:

URLConnection

java.net.HttpURLConnection

A URLConnection with support for HTTP-specific features. See the spec for details.

java.net.Socket

This class implements client sockets (also called just “sockets”). A socket is an endpoint for communication between two machines.

The actual work of the socket is performed by an instance of the SocketImpl class. An application, by changing the socket factory that creates the socket implementation, can configure itself to create sockets appropriate to the local firewall.

java.net.SocketImpl

The abstract class SocketImpl is a common superclass of all classes that actually implement sockets. It is used to create both client and server sockets.

当通过 Socket 类的默认无参构造方法 new Socket() 创建 socket 对象时,其底层实现如下图。从下图可见,将会创建抽象类 SocketImpl 的默认实现类 SocksSocketImpl

Default implementation of SocketImpl

SocksSocketImpl 的继承结构如下。

Socket 类提供了 getOutputStream()getInputStream() 方法,其底层实现获取 AbstractPlainSocketImpl 的两个私有成员变量,如下:

  • java.net.SocketInputSteram,核心方法:

    SocketInputStream#socketRead0

  • java.net.SocketOutputStream,核心方法:

    SocketOutputStream#socketWrite0

    这两个类的继承结构如下:

java.net.SocketInputSteram & java.net.SocketOutputStream

java.io.*

java.io.FileDescriptor

Instances of the file descriptor class serve as an opaque handle to the underlying machine-specific structure representing an open file, an open socket, or another source or sink of bytes. The main practical use for a file descriptor is to create a FileInputStream or FileOutputStream to contain it.

Applications should not create their own file descriptors.

常见异常

下面介绍网络编程时,常见的 Socket、HTTP 异常。详见:https://docs.oracle.com/javase/8/docs/api/java/net/package-summary.html

IOException

In case the TCP handshakes are not complete, the connection remains unsuccessful. Consequently, the program throws an IOException indicating an error occurred while establishing a new connection.

BindException

java.net.BindException

Signals that an error occurred while attempting to bind a socket to a local address and port.

问题:

1
2
3
4
5
6
7
java.net.BindException: Address already in use (Bind failed)
at java.net.PlainSocketImpl.socketBind(Native Method)
at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:513)
at java.net.ServerSocket.bind(ServerSocket.java:375)
at java.net.ServerSocket.<init>(ServerSocket.java:237)
at java.net.ServerSocket.<init>(ServerSocket.java:181)
...

原因:server-side 未成功 bind() 到指定端口号(如端口被其它服务占用)。

ConnectException

java.net.ConnectException

Signals that an error occurred while attempting to connect a socket to a remote address and port.

Connection refused

问题:

1
2
3
4
5
6
7
8
9
10
11
java.net.ConnectException: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:476)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:218)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:200)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:394)
at java.net.Socket.connect(Socket.java:606)
at java.net.Socket.connect(Socket.java:555)
at java.net.Socket.<init>(Socket.java:451)
at java.net.Socket.<init>(Socket.java:228)
...

ResourceAccessException

原因:client-side connect() 建立 TCP 连接失败

  • client-side connect() 错了服务端口号;
  • server-side 服务未启动、未在 listen() 监听、或 listen()backlog 队列数无法满足 client-side 并发连接请求数;
  • 无法完成 TCP 三次握手:

WireShark 抓包

Connection timed out

问题:

1
2
3
4
5
6
7
8
9
10
11
java.net.ConnectException: Connection timed out (Connection timed out)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at java.net.Socket.connect(Socket.java:538)
at java.net.Socket.<init>(Socket.java:434)
at java.net.Socket.<init>(Socket.java:211)
...

原因:client-side connect() 超时:

1
2
3
Socket socket = new Socket(); 
SocketAddress socketAddress = new InetSocketAddress(host, port);
socket.connect(socketAddress, 5 * 1000); // timeout for connect()

Socket#connect 方法

连接超时原因:client-side 发出 sync 包之后,server-side 未在指定时间内回复 ack 导致的。没有回复 ack 的原因可能是网络丢包、防火墙阻止服务端返回 synack 包等。

  • 防火墙原因

    Sometimes, firewalls block certain ports due to security reasons. As a result, a “connection timed out” error can occur when a client is trying to establish a connection to a server. Therefore, we should check the firewall settings to see if it’s blocking a port before binding it to a service.

SocketTimeoutException

java.net.SocketTimeoutException

Signals that a timeout has occurred on a socket accept() or read().

Accept timed out

问题:

1
2
3
4
5
6
java.net.SocketTimeoutException: Accept timed out
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:535)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
...

原因:server-side accept() 超时:

1
2
ServerSocket serverSocket = new ServerSocket(port, backlog);
serverSocket.setSoTimeout(5 * 1000); // timeout for accept()

setSoTimeout 方法

Read timed out

问题:

1
2
3
4
5
6
7
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at java.net.SocketInputStream.read(SocketInputStream.java:127)
...

原因:server-side / client-side read() 超时:

1
2
3
4
5
6
7
// server-side
Socket socket = serverSocket.accept();
socket.setSoTimeout(5 * 1000); // timeout for read()

// client-side
Socket socket = new Socket(host, port);
socket.setSoTimeout(5 * 1000); // timeout for read()

setSoTimeout 方法

SocketException

java.net.SocketException

Thrown to indicate that there is an error creating or accessing a Socket.

Connection reset by peer

1
2
3
4
5
6
7
8
java.net.SocketException: Connection reset by peer (connect failed)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:476)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:218)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:200)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:394)
at java.net.Socket.connect(Socket.java:606)
...

Bad file descriptor

1
2
3
4
5
java.net.SocketException: Bad file descriptor (Write failed)
at java.net.SocketOutputStream.socketWrite0(Native Method)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
...

Broken pipe

1
2
3
4
5
java.net.SocketException: Broken pipe (Write failed)
at java.net.SocketOutputStream.socketWrite0(Native Method)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
...

HttpClientErrorException

org.springframework.web.client.HttpClientErrorException

Exception thrown when an HTTP 4xx is received.

HttpClientErrorException

HttpServerErrorException

org.springframework.web.client.HttpServerErrorException

Exception thrown when an HTTP 5xx is received.

HttpServerErrorException

参考

https://docs.oracle.com/javase/8/docs/technotes/guides/net/index.html

https://www.baeldung.com/a-guide-to-java-sockets

https://www.baeldung.com/category/java/tag/exception/