Spring Boot - HTTP客户端工具
# Spring Boot - HTTP客户端工具
# 1. RestTemplate简介
RestTemplate是Spring框架提供的一个强大HTTP客户端工具,专为调用RESTful服务而设计。它简化了与HTTP服务的通信方式,统一了RESTful标准,封装了HTTP连接细节。与传统的HttpClient相比,RestTemplate提供了更加优雅且符合Spring风格的API。
RestTemplate的设计理念与Spring中其他模板类(如JdbcTemplate)一致,通过提供默认行为来简化复杂任务的执行。它默认依赖JDK的HttpURLConnection,但可以通过setRequestFactory
方法灵活切换为Apache HttpComponent、Netty或OKHttp等其他HTTP库。
RestTemplate的主要方法与HTTP协议的方法紧密对应:HEAD、GET、POST、PUT、DELETE、OPTIONS等。例如,它提供了headForHeaders()
、getForObject()
、postForEntity()
、put()
和delete()
等方法,便于开发者直观地进行HTTP操作。
# 2. RestTemplate的创建方式
Spring框架虽然提供了RestTemplate类,但并未将其自动加入Spring容器中,需要开发者手动配置。下面介绍两种常用的创建方式:
# 2.1 使用RestTemplateBuilder构建器创建(推荐)
通过Spring Boot提供的RestTemplateBuilder,可以灵活配置连接参数:
@Configuration
public class WebConfiguration {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder){
return builder
// 设置连接超时时间(5秒)
.setConnectTimeout(Duration.ofSeconds(5))
// 设置读取数据超时时间(5秒)
.setReadTimeout(Duration.ofSeconds(5))
// 设置API访问认证信息(根据实际情况替换用户名和密码)
.basicAuthentication("username","password")
// 设置根URI(所有请求都将以此URL为基础)
.rootUri("https://api.example.com/")
// 构建RestTemplate实例
.build();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这种方式的优势在于可以详细配置RestTemplate的参数,使其更符合特定业务需求。
添加自定义拦截器
使用RestTemplateBuilder还可以添加自定义拦截器,对请求和响应进行拦截处理:
@Slf4j
public class CustomClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 记录请求详情
logRequestDetails(request, body);
// 执行实际请求
ClientHttpResponse response = execution.execute(request, body);
// 记录响应详情
logResponseDetails(response);
return response;
}
// 记录请求详情的私有方法
private void logRequestDetails(HttpRequest request, byte[] body){
log.debug("请求头信息: {}", request.getHeaders());
log.debug("请求体内容: {}", new String(body, StandardCharsets.UTF_8));
log.debug("请求方法: {}:{}", request.getMethod(), request.getMethodValue());
}
// 记录响应详情的私有方法
private void logResponseDetails(ClientHttpResponse response) throws IOException {
log.debug("状态码: {}", response.getStatusCode());
log.debug("状态文本: {}", response.getStatusText());
log.debug("响应头: {}", response.getHeaders());
log.debug("响应体: {}", StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8));
}
}
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
将自定义拦截器添加到RestTemplate:
@Configuration
public class WebConfiguration {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder){
return builder
// 其他配置...
// 添加自定义请求拦截器
.additionalInterceptors(new CustomClientHttpRequestInterceptor())
// 构建
.build();
}
}
2
3
4
5
6
7
8
9
10
11
12
注意:请求和响应的流只能被读取一次。当在拦截器中读取了response后,返回的response将无法再次读取相同内容,类似于@ResponseBody的工作方式。
# 2.2 直接使用构造方法创建
也可以通过RestTemplate的构造方法直接创建实例:
@Configuration
public class WebConfiguration {
@Bean
public RestTemplate restTemplate(){
// 创建一个使用默认配置的RestTemplate实例
return new RestTemplate();
}
}
2
3
4
5
6
7
8
RestTemplate有三种构造方法:
- 无参构造:使用默认配置
- 指定ClientHttpRequestFactory的构造方法:可自定义HTTP请求工厂
- 指定HttpMessageConverter的构造方法:可自定义HTTP消息转换器
# 2.3 两种方式的选择
两种创建方式各有优势:
- RestTemplateBuilder方式提供更多自定义选项,配置更灵活完善
- 构造方法方式更简洁,适合基本使用场景,特别是需要创建多个不同根地址的RestTemplate实例时
# 3. RestTemplate API使用
RestTemplate提供了丰富的API,主要可分为以下几类:
- GET请求:获取资源
- POST请求:创建资源
- PUT请求:更新资源
- DELETE请求:删除资源
- HEAD请求:获取头信息
- OPTIONS请求:获取支持的方法
- EXCHANGE请求:通用请求方法
- EXECUTE请求:最底层执行方法
# 3.1 GET请求
RestTemplate提供了两种发送GET请求的方法:
# 3.1.1 getForEntity方法
获取完整的响应实体,包含状态码、头信息和响应体:
/**
* 使用getForEntity获取完整响应信息
*/
public void getForEntityDemo() {
// 发送GET请求并获取完整响应实体
ResponseEntity<String> response = restTemplate.getForEntity("https://api.example.com/users/{id}", String.class, 123);
// 检查响应状态码
if(response.getStatusCode() == HttpStatus.OK) {
// 获取响应状态信息
System.out.println("状态码:" + response.getStatusCode());
System.out.println("状态码值:" + response.getStatusCodeValue());
// 获取响应头信息
HttpHeaders headers = response.getHeaders();
System.out.println("响应头:" + headers);
// 获取响应体内容
System.out.println("响应内容:" + response.getBody());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
getForEntity方法参数说明:
- 第一个参数:请求的URL,可包含路径变量占位符
{变量名}
- 第二个参数:响应体类型
- 第三个参数(可选):URL路径变量值,可以是可变参数或Map
使用Map传递URL参数:
/**
* 使用Map传递URL参数
*/
public void getWithMapParamsDemo() {
// 创建参数Map
Map<String, Object> urlParams = new HashMap<>();
urlParams.put("id", 123);
urlParams.put("name", "张三");
// 使用Map传递URL参数
ResponseEntity<User> response = restTemplate.getForEntity(
"https://api.example.com/users/{id}/profile/{name}",
User.class,
urlParams
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3.1.2 getForObject方法
直接获取响应体内容,忽略状态码和头信息:
/**
* 使用getForObject直接获取响应内容
*/
public void getForObjectDemo() {
// 直接获取响应体内容,转换为User对象
User user = restTemplate.getForObject(
"https://api.example.com/users/{id}",
User.class,
123
);
System.out.println("用户信息: " + user);
}
2
3
4
5
6
7
8
9
10
11
12
13
当仅需要响应内容而不关心响应状态和头信息时,getForObject方法更加简洁。
# 3.2 POST请求
RestTemplate提供了三种发送POST请求的方法:
# 3.2.1 postForEntity方法
发送POST请求并获取完整响应实体:
/**
* 使用postForEntity发送POST请求
*/
public void postForEntityDemo() {
// 创建请求对象
User newUser = new User();
newUser.setName("李四");
newUser.setAge(28);
// 发送POST请求并获取完整响应
ResponseEntity<User> response = restTemplate.postForEntity(
"https://api.example.com/users",
newUser, // 请求体
User.class // 响应体类型
);
// 处理响应
System.out.println("响应状态: " + response.getStatusCode());
System.out.println("创建的用户: " + response.getBody());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 3.2.2 postForObject方法
发送POST请求并直接获取响应体:
/**
* 使用postForObject发送POST请求
*/
public void postForObjectDemo() {
// 创建请求对象
User newUser = new User();
newUser.setName("王五");
newUser.setAge(35);
// 发送POST请求并直接获取响应体
User createdUser = restTemplate.postForObject(
"https://api.example.com/users",
newUser, // 请求体
User.class // 响应体类型
);
System.out.println("创建的用户: " + createdUser);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 3.2.3 postForLocation方法
发送POST请求并获取新资源的URI:
/**
* 使用postForLocation发送POST请求
*/
public void postForLocationDemo() {
// 创建请求对象
User newUser = new User();
newUser.setName("赵六");
newUser.setAge(40);
// 发送POST请求并获取新创建资源的URI
URI resourceUri = restTemplate.postForLocation(
"https://api.example.com/users",
newUser // 请求体
);
System.out.println("新资源的URI: " + resourceUri);
// 可以使用此URI进行后续操作
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
postForLocation特别适用于创建资源后需要跳转到新资源的场景。
# 3.2.4 使用表单方式提交POST请求
除了直接提交对象外,还可以使用表单方式提交POST请求:
/**
* 使用表单方式提交POST请求
*/
public void postFormDataDemo() {
// 设置请求头,指定content-type为application/x-www-form-urlencoded
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 添加自定义header信息
headers.set("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9...");
headers.set("AppId", "myapp123");
// 创建表单参数
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("username", "zhangsan");
formData.add("password", "123456");
formData.add("remember", "true");
// 将请求头和表单数据组合成一个HttpEntity
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
// 发送POST请求
ResponseEntity<String> response = restTemplate.postForEntity(
"https://api.example.com/login",
requestEntity,
String.class
);
System.out.println("登录响应: " + response.getBody());
}
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
表单提交需要使用以下几个关键类:
- HttpHeaders: 封装HTTP请求头信息
- MultiValueMap: 封装表单参数,支持一个key对应多个value
- HttpEntity: 将请求头和请求体封装在一起
# 3.3 PUT请求
PUT请求用于更新资源:
/**
* 发送PUT请求更新资源
*/
public void putRequestDemo() {
// 创建更新的对象
User updatedUser = new User();
updatedUser.setName("张三(已更新)");
updatedUser.setAge(31);
// 发送PUT请求,无返回值
restTemplate.put(
"https://api.example.com/users/{id}",
updatedUser, // 请求体
123 // URL变量
);
System.out.println("用户信息已更新");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PUT方法通常不返回内容,只需提供URL和要更新的对象。
# 3.4 DELETE请求
DELETE请求用于删除资源:
/**
* 发送DELETE请求删除资源
*/
public void deleteRequestDemo() {
// 发送DELETE请求,无返回值
restTemplate.delete("https://api.example.com/users/{id}", 123);
System.out.println("用户已删除");
}
2
3
4
5
6
7
8
9
与PUT类似,DELETE通常不返回内容,RestTemplate的delete方法不支持携带请求体。
# 3.5 HEAD请求
HEAD请求用于获取资源的头信息:
/**
* 发送HEAD请求获取头信息
*/
public void headRequestDemo() {
// 获取资源的头信息
HttpHeaders headers = restTemplate.headForHeaders("https://api.example.com/users");
// 查看特定头信息
System.out.println("Content-Type: " + headers.getContentType());
System.out.println("Content-Length: " + headers.getContentLength());
System.out.println("所有头信息: " + headers);
}
2
3
4
5
6
7
8
9
10
11
12
HEAD请求只返回头信息,不返回响应体,适用于检查资源是否存在或获取资源元信息。
# 3.6 OPTIONS请求
OPTIONS请求用于获取服务器支持的HTTP方法:
/**
* 发送OPTIONS请求获取支持的方法
*/
public void optionsRequestDemo() {
// 获取服务器支持的HTTP方法
Set<HttpMethod> allowedMethods = restTemplate.optionsForAllow("https://api.example.com/users");
// 输出支持的方法
System.out.println("支持的HTTP方法: " + allowedMethods);
// 检查是否支持特定方法
if (allowedMethods.contains(HttpMethod.DELETE)) {
System.out.println("支持DELETE方法");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
OPTIONS请求通常用于CORS预检请求或检查API支持的操作。
# 3.7 EXCHANGE方法
exchange是一个更通用的方法,可以指定HTTP方法、请求头和请求体:
/**
* 使用exchange方法发送自定义请求
*/
public void exchangeDemo() {
// 创建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth("eyJhbGciOiJIUzI1NiJ9...");
// 创建请求体
User user = new User();
user.setName("赵七");
user.setAge(45);
// 创建请求实体
HttpEntity<User> requestEntity = new HttpEntity<>(user, headers);
// 发送请求并获取响应
ResponseEntity<User> response = restTemplate.exchange(
"https://api.example.com/users/{id}",
HttpMethod.PUT, // 指定HTTP方法
requestEntity, // 请求实体
User.class, // 响应类型
789 // URL变量
);
System.out.println("响应状态: " + response.getStatusCode());
System.out.println("响应体: " + response.getBody());
}
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
exchange方法的优势:
- 可以动态指定HTTP方法
- 可以同时设置请求头和请求体
- 支持泛型作为返回类型
特别适用于需要自定义HTTP方法或需要添加复杂请求头的场景。
# 使用exchange发送GET请求带请求体
标准HTTP规范中,GET请求通常不带请求体,但某些特殊API可能需要这种非标准用法:
/**
* 使用exchange发送带请求体的GET请求
*/
public void getWithRequestBodyDemo() {
// 创建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 创建请求体
Map<String, Object> queryParams = new HashMap<>();
queryParams.put("filter", "active");
queryParams.put("complex", true);
// 创建请求实体
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(queryParams, headers);
// 使用exchange发送带请求体的GET请求
ResponseEntity<User[]> response = restTemplate.exchange(
"https://api.example.com/users/search",
HttpMethod.GET,
requestEntity,
User[].class
);
System.out.println("找到" + response.getBody().length + "个用户");
}
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
注意:虽然技术上可以实现,但发送带请求体的GET请求不符合HTTP标准,某些服务器可能不支持。
# 3.8 EXECUTE方法
execute是RestTemplate的最底层方法,所有其他方法最终都会调用它:
/**
* 使用execute方法发送请求
*/
public void executeDemo() {
// 创建请求实体
User user = new User();
user.setName("孙八");
user.setAge(50);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<User> requestEntity = new HttpEntity<>(user, headers);
// 使用execute方法发送请求
User result = restTemplate.execute(
"https://api.example.com/users/{id}",
HttpMethod.PUT,
// 请求回调
req -> {
// 准备请求阶段的操作
return restTemplate.httpEntityCallback(requestEntity).doWithRequest(req);
},
// 响应提取器
res -> {
// 处理响应阶段的操作
return restTemplate.responseEntityExtractor(User.class).extractData(res).getBody();
},
456 // URL变量
);
System.out.println("执行结果: " + result);
}
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
execute方法通常用于以下场景:
- 需要在请求发送前进行特殊处理
- 需要自定义响应处理逻辑
- 实现RestTemplate没有直接提供的特殊HTTP方法
# 4. URL参数传递技巧
RestTemplate提供了两种传递URL参数的方式:
# 4.1 路径变量(Path Variables)
通过在URL中使用{占位符}
并提供对应值:
/**
* 使用路径变量传递参数
*/
public void pathVariablesDemo() {
// 使用有序参数填充占位符
String url = "https://api.example.com/users/{id}/posts/{postId}";
User user = restTemplate.getForObject(url, User.class, 123, 456);
// 占位符名称可以任意,与参数顺序对应即可
String url2 = "https://api.example.com/users/{userId}/posts/{post}";
User user2 = restTemplate.getForObject(url2, User.class, 123, 456);
// 结果相同,都会访问 https://api.example.com/users/123/posts/456
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4.2 查询参数(Query Parameters)
通过Map传递查询参数:
/**
* 使用Map传递查询参数
*/
public void queryParamsDemo() {
// 基础URL
String baseUrl = "https://api.example.com/users";
// 创建查询参数
Map<String, String> params = new HashMap<>();
params.put("name", "张三");
params.put("age", "30");
// 发送请求,参数会以key=value的形式附加到URL后面
// 结果URL: https://api.example.com/users?name=张三&age=30
User[] users = restTemplate.getForObject(baseUrl, User[].class, params);
System.out.println("查询结果数量: " + users.length);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
此外,也可以直接在URL字符串中添加查询参数:
/**
* 直接在URL中添加查询参数
*/
public void hardcodedQueryParamsDemo() {
String url = "https://api.example.com/users?active=true&role=admin";
User[] users = restTemplate.getForObject(url, User[].class);
}
2
3
4
5
6
7
# 5. 异常处理
RestTemplate在请求过程中可能遇到各种异常,需要进行适当处理:
/**
* RestTemplate异常处理示例
*/
public void exceptionHandlingDemo() {
try {
// 尝试访问可能不存在的资源
User user = restTemplate.getForObject(
"https://api.example.com/users/{id}",
User.class,
99999
);
System.out.println("用户信息: " + user);
} catch (HttpClientErrorException e) {
// 处理客户端错误(4xx状态码)
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
System.out.println("用户不存在");
} else if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) {
System.out.println("未授权访问");
} else {
System.out.println("客户端错误: " + e.getStatusCode());
}
System.out.println("错误响应内容: " + e.getResponseBodyAsString());
} catch (HttpServerErrorException e) {
// 处理服务器错误(5xx状态码)
System.out.println("服务器错误: " + e.getStatusCode());
System.out.println("错误响应内容: " + e.getResponseBodyAsString());
} catch (ResourceAccessException e) {
// 处理网络连接问题
System.out.println("网络错误: " + e.getMessage());
} catch (RestClientException e) {
// 处理其他RestTemplate相关错误
System.out.println("REST客户端错误: " + e.getMessage());
}
}
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
RestTemplate常见异常类型:
- HttpClientErrorException: 4xx客户端错误
- HttpServerErrorException: 5xx服务器错误
- ResourceAccessException: 网络连接问题
- RestClientException: 其他RestTemplate相关错误
# 6. 高级应用
# 6.1 使用ResponseErrorHandler处理错误
自定义错误处理器可以集中处理HTTP错误:
/**
* 自定义RestTemplate错误处理器
*/
public class CustomResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
// 判断响应是否包含错误
return response.getStatusCode().is4xxClientError() ||
response.getStatusCode().is5xxServerError();
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
// 处理错误响应
if (response.getStatusCode().is4xxClientError()) {
// 处理4xx错误
if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new ResourceNotFoundException("请求的资源不存在");
} else if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new UnauthorizedException("未授权访问");
}
// 处理其他4xx错误...
} else if (response.getStatusCode().is5xxServerError()) {
// 处理5xx错误
throw new ServerErrorException("服务器错误: " + response.getStatusCode());
}
}
}
// 自定义异常类
class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String message) {
super(message);
}
}
class ServerErrorException extends RuntimeException {
public ServerErrorException(String message) {
super(message);
}
}
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
将自定义错误处理器应用到RestTemplate:
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
RestTemplate restTemplate = builder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.build();
// 设置自定义错误处理器
restTemplate.setErrorHandler(new CustomResponseErrorHandler());
return restTemplate;
}
2
3
4
5
6
7
8
9
10
11
12
# 6.2 使用HttpMessageConverter自定义序列化和反序列化
可以定制RestTemplate如何转换请求体和响应体:
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// 获取当前的转换器列表
List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
// 移除默认的MappingJackson2HttpMessageConverter
converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
// 添加自定义的Jackson转换器
MappingJackson2HttpMessageConverter customConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
// 配置ObjectMapper
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略null值字段
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 忽略未知属性
customConverter.setObjectMapper(objectMapper);
converters.add(customConverter);
return restTemplate;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 6.3 配置连接池
默认情况下,RestTemplate每次请求都会创建新的连接。对于高并发场景,建议使用连接池:
@Bean
public RestTemplate restTemplate() {
// 创建连接管理器
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
// 设置最大连接数
connectionManager.setMaxTotal(100);
// 设置每个路由的最大连接数
connectionManager.setDefaultMaxPerRoute(20);
// 创建HttpClient构建器
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setConnectionManager(connectionManager);
// 配置请求超时
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000) // 连接超时时间(毫秒)
.setSocketTimeout(5000) // 读取超时时间(毫秒)
.build();
httpClientBuilder.setDefaultRequestConfig(requestConfig);
// 创建HttpClient
CloseableHttpClient httpClient = httpClientBuilder.build();
// 使用HttpComponentsClientHttpRequestFactory
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
// 创建RestTemplate
return new RestTemplate(requestFactory);
}
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
参考资料