单体项目拆分和RestTemplate

1. 单体项目拆分

在之前的项目中,虽然我们利用了 maven 的『多模块』知识点,将一个项目(project)拆分成了多个模块(module),各个模块单独打包,但是,整个项目的最终的『成果』、产出仍然是一个 spring boot 的 jar 包。

各个模块的关系和整体关系如下:

整个项目
│   
│── 前端项目(vue-cli 项目)
│   
└── 后端项目
    │   
    ├── web 项目(整个后端项目的入口和最终代码成果)
    │   
    │── xxx 模块
    │   │── service
    │   └── dao
    │   
    │── yyy 模块
    │   │── service
    │   └── dao
    │   
    └── zzz 模块
        │── service
        └── dao

Copied!

整个项目的『代码成果』有 2 个:

  1. 前端项目执行 build 命令后生成的 dist 目录下的 html、css、js 等文件;
  2. web 项目执行 build 命令后生成的 target 中生成的 web.jar 包。逻辑上,它是一个 war 包。

而在微服务的架构思想中,我们一个项目会被『真正地』拆分成几个项目,每个项目都有独立的 MVC 三层。那么,原来的『每个模块』,现在就变成了真正意义上的『每个项目』。

各个模块(各个项目)的关系和整体关系如下:

整个项目
│   
│── 前端项目 - vue 项目
│   
│── 后端项目 - xxx 项目(入口1和成果1)
│   ├── web 
│   │── service
│   └── dao
│   
│── 后端项目 - yyy 项目(入口2和成果2)
│   ├── web 
│   │── service
│   └── dao
│   
└── 后端项目 - zzz 项目(入口3和成果3)
    ├── web 
    │── service
    └── dao

Copied!

整个项目的『代码成果』有 N 个:

  1. 前端项目执行 build 命令后生成的 dist 目录下的 html、css、js 等文件;
  2. xxx 项目执行 build 命令后生成的 target 中生成的 xxx.jar 包。逻辑上,它是一个 war 包。
  3. yyy 项目执行 build 命令后生成的 target 中生成的 yyy.jar 包。逻辑上,它是一个 war 包。
  4. zzz 项目执行 build 命令后生成的 target 中生成的 zzz.jar 包。逻辑上,它是一个 war 包。

那么,在涉及到跨模块调用时,我们就无法像以前一样从代码层面直接调用。那么怎么办?在 xxx 项目的代码中编写代码,向 B 项目发起 HTTP 请求。

#2. RestTemplate 发起 HTTP 请求

说明

你的项目只要直接或间接引入了 spring-web 包,你就可以使用 RestTemplate 。

RestTemplate 类似于 Slf4J,它本身并没有做『更多』的什么事情,它的主要功能和目的是对背后真正干活的“人”做二次包装,以提供统一的、简洁的使用方式。

在默认的(未引入其它包的)情况下,在 RestTemplate 背后真正干活的是 JDK 中的网络相关类:HttpURLConnection 。如此之外,RestTemplate 还支持使用 HttpClient 和 OkHTTP 库,前提是,你要额外引入这两个包。

Spring RestTemplate 是 Spring 提供的用于访问 Rest 服务的客端。 RestTemplate 提供了多种便捷访问远程 HTTP 服务的方法,能够大大提高客户端的编写效率,所以很多客户端比如 Android 或者第三方服务商都是使用 RestTemplate 请求 restful 服务。

在以前的 Spring Boot 版本中,Spring IoC 容器中已经有一个创建好了的 RestTemplate 供我们使用,不过到了新版本的 Spring Boot 中,需要我们自己创建 RestTemplate 的单例对象:

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder.build();
}

Copied!

#2.1 API 方法介绍

常见方法有:

请求类型

API

说明

GET 请求

getForEntity 方法

返回的 ResponseEntity 包含了响应体所映射成的对象

GET 请求

getForObject 方法

返回的请求体将映射为一个对象

POST 请求

postForEntity 方法

返回包含一个对象的 ResponseEntity ,这个对象是从响应体中映射得到的

POST 请求

postForObject 方法

返回根据响应体匹配形成的对象

PUT 请求

put 方法

PUT 资源到特定的 URL

DELETE 请求

delete 方法

对资源执行 HTTP DELETE 操作

任何请求

exchange 方法

返回包含对象的 ResponseEntity ,这个对象是从响应体中映射得到的

任何请求

execute 方法

返回一个从响应体映射得到的对象

GET / POST / DELETE / PUT 都有专门的方法发出对应方式的请求。这些方法的底层方式都是 execute 方法,不过该方法的使用有些繁琐:

exchange(String url, HttpMeghod method, HttpEntity requestEntity, 
         Class responseType, Object... uriVariables);

exchange(String url, HttpMethod method, HttpEntity requestEntity, 
         Class responseType, Map uriVariables);

exchange(String url, HttpMethod method, HttpEntity requestEntity, 
         ParameterizedTypeReference responseType, Object... uriVariables);

exchange(String url, HttpMethod method, HttpEntity requestEntity, 
         ParameterizedTypeReference responseType, Map uriVariables);

Copied!

exchange 方法参数说明:

参数

说明

参数 url

向哪个 url 发起请求。

参数 method

发起哪种请求。

参数 requestEntity

用以封装请求头(header)和请求体(body)的对象。

参数 responseType

指定返回响应中的 body 的数据类型。

返回值 ResponseEntity

其中封装的响应数据。包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。在输出结果中我们能够看到这些与 HTTP 协议有关的数据。

#2.2 发起 GET 请求

回顾一下 GET 请求的特点:

  • GET 请求不用『管』header 中的 content-type 的值。
  • GET 请求的参数是『追加』到 URL 中,而不是附带在请求的 body 部分的。

因此,如果没有其他的设置请求头的要求,GET 请求在调用 exchange() 方法时,不需要 HttpEntity 参数,因为它就是用来封装请求 body 和请求 header 的。

#参数

无参的情况

有参的情况

  • 服务端代码
略。服务端返回一个简单的 String 。

Copied!

  • 客户端调用
@Resource
private RestTemplate restTemplate;

@Test
public void test1() {
  String url = "http://localhost:8080/get1";
  // RestTemplate template = new RestTemplate();
  ResponseEntity<String> responseEntity = template.exchange(url, HttpMethod.GET, null, String.class);

  log.info("{}", responseEntity.getStatusCode());
  log.info("{}", responseEntity.getHeaders());
  log.info("{}", responseEntity.getBody());
}

Copied!

#返回

返回一个对象

返回一个对象的集合

这里的关键在于,要在调用 exchange() 方法时明确说明返回的是一个 User 类型的对象。(这背后是因为,RestTemplate 需要知道要将收到的 JSON 格式的字符串按什么规则转换)。

  • 服务端代码
略。服务端返回一个 User 对象。

Copied!

  • 客户端调用
String url = "http://localhost:8080/get3?username={1}&age={2}";

ResponseEntity<User> responseEntity1 = template.exchange(url, HttpMethod.GET, null, User.class, "tom", 10);
log.info("{}", responseEntity1.getBody());

Copied!

#2.3 发起 POST 请求

回顾一下 POST 请求的特点:

  • POST 请求的 header 中的 content-type 的值是 application/x-www-form-urlencoded 。
  • POST 请求的参数是附加到请求的 body 部分的。

因此,在调用 exchange() 方法时,需要为其提供一个 HttpEntity 类型参数。

#参数

无参的情况

有参的情况

客户端调用代码:

String url = "http://localhost:8080/post1";

// 准备请求头部信息
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

// 无参数情况下,不需要设置请求 body 部分
HttpEntity<?> entity = new HttpEntity<>(null, headers);

ResponseEntity<String> responseEntity = template.exchange(url, HttpMethod.POST, entity, String.class);

log.warn("{}", responseEntity.getStatusCode());
log.warn("{}", responseEntity.getHeaders());
log.warn("{}", responseEntity.getBody());

Copied!

#返回

返回一个对象

返回一个对象的集合

理论上,和 GET 请求方式是一样的,只需要按照上面所述做出响应修改即可。

#2.4 请求头和响应头

有时候你可能有些额外的信息需要附带在请求头中发送到后台,此时,你可以使用如下形式的代码:

String url = "http://47.xxx.xxx.96/register/checkEmail";

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("...", "...");
headers.add("...", "...");
headers.add("...", "...");

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("...", "...");
params.add("...", "...");
params.add("...", "...");

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

// Xxx obj = template.postForObject(url, params, Xxx.class);
Xxx obj = template.postForObject(url, request, Xxx.class);

Copied!

如果我们想获取更多的 HTTP 响应信息(例如响应头),可以使用 postForEntity 方法。如下:

ResponseEntity<String> entity = template.postForEntity(url, params, String.class);

// 查看响应的状态码
System.out.println(entity.getStatusCodeValue());

// 查看响应的响应体
System.out.println(entity.getBody());

// 查看响应头
HttpHeaders headMap = entity.getHeaders();
for (Map.Entry<String, List<String>> m : headMap.entrySet()) {
    System.out.println(m.getKey() + ": " + m.getValue());
}

Copied!

#3. 切换底层实现(了解)

RestTemplate 底层实现最常用的有以下三种:

  • SimpleClientHttpRequestFactory 封装 JDK 的 URLConnection。默认实现
  • HttpComponentsClientHttpRequestFactory 封装第三方类库 HttpClient
  • OkHttp3ClientHttpRequestFactory 封装封装第三方类库 OKHttp

HttpClient 和 OKHttp 执行效率都要比 JDK 的 URLConnection 高不少。OKHttp 的性能最好,而 HttpClient(因为出现早)使用率更高、更流行。

引用 apache 基金会的 httpclient :

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

Copied!

所以,在你将 RestTemplate 配置成单例时,你可以指定它使用何种底层库:

@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate(); // 默认实现
//  RestTemplate restTemplate = new RestTemplate(new SimpleClientHttpRequestFactory()); // 等同默认实现
//  RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); // 使用 HttpClient
//  RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory()); // 使用 OkHttp
    return restTemplate;
}

Copied!

在实际的应用中,只需要选择上面的代码中的其中一种 RestTemplate Bean 即可。当然,无论 RestTemplate 背后是谁在真正地处理 HTTP 请求和响应,RestTemplate 对外提供的接口都是一致的,并且更简洁。