写点什么

一次性搞懂 springweb 服务构建轻量级 Web 技术体系:Spring WebMVC

  • 2023-06-17
    湖南
  • 本文字数:13211 字

    阅读完需:约 43 分钟

本部分讨论针对 Web 应用程序开发所提供的最佳实践,包括使用 SpringHATEOAS 开发自解释 Web API,使用 Spring GraphQL 开发查询式 Web API,针对传统 Spring MVC 的异步编程模型,以及新型的基于响应式流的 WebFlux 组件。


同时,我们还将讨论如何使用目前非常流行的、Spring 5 默认内置的 RSocket 协议来提高网络通信的性能。通过这一部分的学习,读者可以系统掌握在使用 Spring 框架时所应掌握的各个 Web 开发组件的特点以及对应的使用技巧.

构建轻量级 Web 技术体系

Web 服务层的构建可以说是开发 Spring Boot 应用程序最主要的工作,现实中几乎所有互联网应用程序都需要对外提供各种形式的 Web 服务。在本章中,我们的讨论的对象是轻量级 Web 服务,其表现形式就是通过 HTTP 暴露的一组端点。Spring Boot 为开发轻量级 Web 服务提供了一系列解决方案。


Spring Boot 框架提供的第一套解决方案就是 WebMVC,这是基于 MVC(Model View Controller,模型-视图-控制器)架构设计并实现的经典技术组件。开发人员使用一组基础注解就可以开发 Controller,并暴露 RESTful 风格的 HTTP 端点。而对服务消费,我们则可以使用 RestTemplate 模板工具类。


Spring Boot 框架提供的第二套解决方案是 HATEOAS,这是在整个 REST 成熟度模型中位于最高层次的技术组件。通过 Spring HATEOAS,我们能够开发超媒体组件,并实现自解释的 Web API。


最后,在前后端分离的开发模式下,开发人员面临的一大挑战是如何设计合理且高效的前后端交互 Web API。这时候就可以引入 Spring Boot 框架提供的第三套解决方案,即 Spring GraphQL。GraphQL 是一种图驱动的查询语言,可以用来设计并实现满足前后端高效交互所需的数据格式、返回结果、请求次数以及请求地址。


本章将对上述 Spring Boot 框架所提供的三套开发轻量级 Web 服务的解决方案展开详细的讨论,并给出精简而又完整的代码案例。

Spring WebMVC

Spring WebMVC 框架具有一套遵循模型-视图-控制器架构设计理念的体系结构,可以开发灵活、松耦合的 HTTP 端点。


Spring 基础框架就包含 WebMVC 组件,而基于 Spring Boot 开发 Web 服务同样使用到该组件。Web 服务的实现涉及服务创建和服务消费两个方面,本节将对这两个方面所涉及的技术组件一一展开讨论。

创建 Web 服务

在 Spring Boot 中,创建 Web 服务的主要工作是实现 Controller。而在创建 Controller 之后,需要对 HTTP 请求进行处理并返回正确的响应结果。我们可以基于一系列注解来开展这些开发工作。

1. 创建 Controller

创建 Controller 的过程比较固定,我们已经在第 1 章中实现过一个简单的 Controller,如代码清单 4-1 所示:

@RestController@RequestMapping(value="users")public class UserController {	@GetMapping(value = "/{id}")	public User getUserById(@PathVariable Long id) {		User user = new User();		...		return user;	}}
复制代码

这是一个典型的 Controller,可以看到上述代码包含了 @RestController、@Request-Mapping 和 @GetMapping 等注解。其中,@RestController 注解继承自 Spring WebMVC 中的 @Controller 注解,顾名思义就是一个 RESTful 风格的 HTTP 端点,并且会自动使用 JSON 实现 HTTP 请求和响应的序列化/反序列化。根据这一特性,我们在构建 Web 服务时可以使用 @RestController 注解来取代 @Controller 注解以简化开发。


@GetMapping 注解和 @RequestMapping 注解的功能类似,只是默认使用 Request-Method.GET 来指定 HTTP 方法。Spring Boot 2 引入了一批新注解,除了 @GetMapping 外还有 @PutMapping、@PostMapping、@DeleteMapping 等,方便开发人员显式指定 HTTP 请求方法。当然,我们也可以继续使用原先的 @RequestMapping 注解来实现同样的效果。


在上述 UserController 中,我们通过静态代码完成根据用户 ID 获取用户信息的业务流程。这里用到了两层 Mapping,第一层的 @RequestMapping 注解在服务层级定义了服务的根路径 users,而第二层的 @GetMapping 注解则在操作级别又定义了 HTTP 请求方法的具体路径及参数信息。

2. 处理 Web 请求

处理 Web 请求的过程涉及获取输入参数以及返回响应结果。Spring Boot 提供了一系列便捷有用的注解来简化对请求输入的控制过程,常用的包括上述 UserController 中所展示的 @PathVariable 和 @RequestBody。


@PathVariable 注解用于获取路径参数,即从类似 url/{id}这种形式的路径中获取{id}参数的值。通常,使用 @PathVariable 注解时只需要指定参数的名称即可。以下代码是使用 @PathVariable 注解的典型代码示例,这里在请求路径中同时传入了两个参数。

@PostMapping(value = "/{username}/{password}")public User generateUser(@PathVariable("username") String username,@PathVariable("password") String password) {	User user = userService.generateUser(username, password);	return user;}
复制代码

在 HTTP 中,content-type 属性用来指定所传输的内容类型。而我们可以通过 @Request-Mapping 注解中的 produces 属性来对其进行设置,通常会将其设置为 application/json,示例代码如下所示:

@RestController@RequestMapping(value = "users", produces="application/json")public class UserController {}
复制代码

而 @RequestBody 注解就是用来处理 content-type 为 application/json 类型时的请求内容。通过 @RequestBody 注解可以将请求体中的 JSON 字符串绑定到相应的实体对象上。我们可以对前面的 generateUser()方法进行重构,通过 @RequestBody 注解来传入参数,如代码清单 4-4 所示:

@PostMapping(value = "/")public User generateUser(@RequestBody User user) {}
复制代码

这时候,如果想要通过 Postman 来发起这个 POST 请求,就需要使用如下所示的一段 JSON 字符串:

{"username": "tianyalan","password":"123456"}
复制代码

消费 Web 服务

当我们创建 Controller 之后,接下来要做的事情就是对它暴露的 HTTP 端点进行消费。这就是本小节要介绍的内容,我们将引入 Spring Boot 提供的 RestTemplate 模板工具类。

1. 创建 RestTemplate

要想创建一个 RestTemplate 对象,最简单也最常见的方法就是直接 new 一个该类的实例,代码如下所示:

@Beanpublic RestTemplate restTemplate(){	return new RestTemplate();}
复制代码

这里创建了一个 RestTemplate 实例,并通过 @Bean 注解将其注入到 Spring 容器中。在 Spring Boot 应用程序中,通常我们会把上述代码放在 Bootstrap 类中,这样在代码工程的其他地方都可以引用这个实例。

2. 使用 RestTemplate

我们明确,通过 RestTemplate 发送的请求和获取的响应都是以 JSON 作为序列化方式。当创建完 RestTemplate 之后,我们就可以使用它内置的工具方法来向远程 Web 服务发起请求。RestTemplate 为开发人员提供了一大批发送 HTTP 请求的工具方法,如表所示:

在一个 Web 请求中,请求路径可以携带参数,在使用 RestTemplate 时也可以在它的 URL 中嵌入路径变量。例如,针对前面介绍的 UserController 中的 HTTP 端点,我们可以发起如下所示的 Web 请求。

("http://localhost:8080/users/{id}", 100)
复制代码

这里我们定义了一个拥有路径变量名为 id 的 URL,然后在实际访问时将该变量值设置为 100。


URL 中也可以包含多个路径变量,因为 Java 支持不定长参数语法,所以多个路径变量的赋值将按参数依次设置。在如下所示的代码中,我们就在 URL 中定义了 username 和 password 这两个路径变量,实际访问时它们将被替换为 tianyalan 和 123456。

("http://localhost:8080/users/{username}/{password}", "tianyalan",123456)
复制代码

一旦准备好了请求 URL,就可以使用 RestTemplate 所提供的一系列工具方法完成远程服务的访问。


我们先来介绍 get 方法组,包括 getForObject()和 getForEntity()这两组方法,每组各有参数完全对应的三个方法。例如,getForObject()方法组中的三个方法如下所示。从方法定义上不难看出它们之间的区别只是在对所传入参数的处理上有所不同。

public <T> T getForObject(URI url, Class<T> responseType)public <T> T getForObject(String url, Class<T> responseType, Object...uriVariables)public <T> T getForObject(String url, Class<T> responseType,Map<String, ?> uriVariables)
复制代码

对于 UserController 暴露的 HTTP 端点,我们就可以通过 getForObject()方法构建一个 HTTP 请求来获取目标 User 对象,实现代码如下所示:

User result = restTemplate.getForObject("http://localhost:8080/users/{id}",User.class, 100);
复制代码

可以使用 getForEntity()方法实现同样的效果,但写法上有所区别,代码如下所示:

ResponseEntity<User> result = restTemplate.getForEntity("http://localhost:8080/users/{id}",User.class, 100);User user = result.getBody();
复制代码

可以看到,getForEntity()方法的返回值是一个 ResponseEntity 对象,在这个对象中还包含了 HTTP 消息头等信息。而 getForObject()方法返回的只是业务对象本身。这是两个方法组的主要区别,我们可以根据需要对其进行选择。


针对 UserController 中用于创建用户信息的 HTTP 端点来说,通过 postForEntity()方法发送 POST 请求的示例代码如下所示:

User user = new User();user.setName("tianyalan");user.setPassword("123456");ResponseEntity<User> responseEntity = restTemplate.postForEntity("http://localhost:8080/users", user,User.class);return responseEntity.getBody();
复制代码

可以看到,这里通过 postForEntity()方法传递一个 User 对象到 UserController 所暴露的端点,并获取了该端点的返回值。postForObject()的操作方式也与此类似。在掌握了 get 方法组和 post 方法组之后,理解 put 方法组和 delete 方法组就显得非常容易了。其中,put 方法组与 post 方法组相比只是在操作语义上有差别,而 delete 方法组的使用过程也和 get 方法组类似,这里就不再展开讲解。


最后,我们还有必要介绍一下 exchange 方法组。对于 RestTemplate 而言,exchange()是一个通用且统一的方法,它既能发送 GET 和 POST 请求,也能用于其他各种类型的请求。我们来看一下 exchange 方法组中的一个 exchange()方法签名,代码如下所示:

public <T> ResponseEntity<T> exchange(String url, HttpMethod method,@Nullable HttpEntity<?> requestEntity, Class<T> responseType,Object... uriVariables) throws RestClientException
复制代码

请注意,这里的 requestEntity 变量是一个 HttpEntity 对象,封装了请求头和请求体。而 responseType 则用于指定返回的数据类型。使用 exchange()方法发起请求的代码示例如下所示:

ResponseEntity<User> result = restTemplate.exchange("http://localhost:8080/users/{id}",HttpMethod.GET, null, User.class, 100);
复制代码

RestTemplate 远程调用原理分析

在 4.1.2 节中,我们详细描述了如何使用 RestTemplate 访问 HTTP 端点,涉及 RestTemplate 初始化、发起请求以及获取响应结果等核心环节。在本节中,我们将基于这些步骤,从源码出发深入理解 RestTemplate 实现远程调用的底层原理。

1. 远程调用主流程

我们先来看一下 RestTemplate 类的定义,代码如下所示:

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations
复制代码

可以看到,RestTemplate 扩展了 InterceptingHttpAccessor 抽象类,并实现了 RestOperations 接口。我们围绕 RestTemplate 的定义来梳理它在设计上的思想。


首先,我们来看 RestOperations 接口的定义,这里截取了部分核心方法,代码如下所示:

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) throwsRestClientException;	<T> ResponseEntity<T> exchange(String url, HttpMethod method,@Nullable HttpEntity<?> requestEntity,Class<T> responseType, Object... uriVariables) throws RestClientException;	...}
复制代码

显然,正是这个 RestOperations 接口定义了所有 get/post/put/delete/exchange 等远程调用方法组,而这些方法都是遵循 RESTful 架构风格而设计的。RestTemplate 对这些接口都提供了实现,这是它的一条代码支线。


然后,我们再来看 InterceptingHttpAccessor,它是一个抽象类,包含的核心变量代码如下所示:

public abstract class InterceptingHttpAccessor extends HttpAccessor {	private final List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();	private volatile ClientHttpRequestFactory interceptingRequestFactory;	...}
复制代码

通过变量定义,我们明确了 InterceptingHttpAccessor 应该包含两部分处理功能,一部分是设置和管理请求拦截器 ClientHttpRequestInterceptor,另一部分则是获取用于创建客户端 HTTP 请求的工厂类 ClientHttpRequestFactory。这是 RestTemplate 的另一条代码支线。


同时,我们注意到 InterceptingHttpAccessor 同样存在一个父类 HttpAccessor,这个父类值得展开讨论一下,因为它真正完成了 ClientHttpRequestFactory 的创建以及通过 ClientHttpRequestFactory 获取了代表客户端请求的 ClientHttpRequest 对象。HttpAccessor 的核心变量如下所示:

public abstract class HttpAccessor {	private ClientHttpRequestFactory requestFactory = new SimpleClientHttpReques-tFactory();	...}
复制代码

可以看到,HttpAccessor 创建了 SimpleClientHttpRequestFactory 作为系统默认的 Client-HttpRequestFactory。关于 ClientHttpRequestFactory,本节还会进行详细的讨论。


作为总结,我们来梳理一下 RestTemplate 的基本类层结构,如下图所示:

通过 RestTemplate 的类层结构,我们可以理解它的设计思想。整个类层结构可以清晰地分成两条线,左边部分用于完成与 HTTP 请求相关的实现机制,而右边部分则提供了 RESTful 风格的操作入口,并使用了面向对象的接口和抽象类完成了对这两部分功能的聚合。


介绍完 RestTemplate 的实例化过程,接下来我们来分析它的核心执行流程。对于远程调用的模板工具类,我们可以从具备多种请求方式的 exchange()方法入手,该方法代码如下所示:

@Overridepublic <T> ResponseEntity<T> exchange(String url, HttpMethod method,@Nullable HttpEntity<?> requestEntity, Class<T> responseType,Object... uriVariables) throws RestClientException {	//构建请求回调	RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);	//构建响应体提取器	ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtra-ctor(responseType);	//执行远程调用	return nonNull(execute(url, method, requestCallback,responseExtractor, uriVariables));}
复制代码

显然,我们应该进一步关注这里的 execute()方法。事实上,无论我们采用 get/put/post/delete 方法组中的哪个方法来发起请求,RestTemplate 负责执行远程调用的都是这个 execute()方法,该方法定义如下所示:

@Override@Nullablepublic <T> T execute(String url, HttpMethod method, @NullableRequestCallback requestCallback, @Nullable ResponseExtractor<T>responseExtractor, Object... uriVariables) throws RestClientException{	URI expanded = getUriTemplateHandler().expand(url, uriVariables);	return doExecute(expanded, method, requestCallback,responseExtractor);}
复制代码

execute()方法首先通过 UriTemplateHandler 构建了一个 URI,然后将请求过程委托给了 doExecute()方法进行处理,该方法定义如代码清单 4-21 所示

protected <T> T doExecute(URI url, @Nullable HttpMethod method,@Nullable RequestCallback requestCallback, @NullableResponseExtractor<T> responseExtractor) throws RestClientException {	Assert.notNull(url, "URI is required");	Assert.notNull(method, "HttpMethod is required");	ClientHttpResponse response = null;	try {		//创建请求对象		ClientHttpRequest request = createRequest(url, method);		if (requestCallback != null) {			//执行对请求的回调			requestCallback.doWithRequest(request);		}		//获取调用结果		response = request.execute();		//处理调用结果		handleResponse(url, method, response);		//从结果中提取数据		return (responseExtractor != null ?responseExtractor.extractData(response) : null);	}	catch (IOException ex) {		...	}	finally {		if (response != null) {			response.close();		}	}}
复制代码

从上述方法中,我们可以清晰地看到使用 RestTemplate 进行远程调用所涉及的三大步骤,即创建请求对象、执行远程调用以及处理响应结果。让我们一起来分别看一下。

2. 创建请求对象

创建请求对象的入口方法如下代码所示:

ClientHttpRequest request = createRequest(url, method);
复制代码

分析这里的 createRequest()方法,我们发现流程执行到了前面介绍的 HttpAccessor 类,如下所示:

public abstract class HttpAccessor {	private ClientHttpRequestFactory requestFactory = new SimpleClientHttpReques-tFactory();	...	protected ClientHttpRequest createRequest(URI url, HttpMethodmethod) throws IOException {		ClientHttpRequest request = getRequestFactory().createRequest(url, method);		...		return request;	}}
复制代码

创建 ClientHttpRequest 的过程是一种典型的工厂模式应用场景,这里直接创建了一个实现 ClientHttpRequestFactory 接口的 SimpleClientHttpRequestFactory 对象,然后再通过这个对象的 createRequest()方法创建了客户端请求对象 ClientHttpRequest,并返回给上层组件进行使用。ClientHttpRequestFactory 接口的定义如下所示:

public interface ClientHttpRequestFactory {	//创建客户端请求对象	ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod)throws IOException;}
复制代码

在 Spring Boot 中,存在一批 ClientHttpRequestFactory 接口的实现类,SimpleClient-HttpRequestFactory 是它的默认实现,开发人员也可以根据需要实现自定义的 ClientHttp-RequestFactory。简单起见,我们直接跟踪 SimpleClientHttpRequestFactory 的代码,来到它的 createRequest()方法,代码如下所示:

@Overridepublic 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);	}}
复制代码

上述 createRequest()方法中,首先通过传入的 URI 对象构建了一个 HttpURLConnection 对象,然后对该对象进行一些预处理,最后构造并返回一个 ClientHttpRequest 的实例。


通过翻阅代码,我们发现在上述代码中只是简单地通过 URL 对象的 openConnection()方法返回了一个 UrlConnection 对象。而在 prepareConnection()方法中,也只是完成了对 HttpUrlConnection 中超时时间、请求方法等常见属性的设置。

3. 执行远程调用

一旦获取了请求对象,就可以发起远程调用并获取响应结果了,RestTemplate 中的入口方法如下所示:

response = request.execute();
复制代码

这里的 request 就是前面创建的 SimpleBufferingClientHttpRequest 类,我们可以先来看一下该类的类层结构,如图所示:

在上图的 AbstractClientHttpRequest 抽象类中,定义了如下所示的 execute()方法:

@Overridepublic final ClientHttpResponse execute() throws IOException {	assertNotExecuted();	ClientHttpResponse result = executeInternal(this.headers);	this.executed = true;	return result;}protected abstract ClientHttpResponse executeInternal(HttpHeadersheaders) throws IOException;
复制代码

AbstractClientHttpRequest 类的作用就是防止 HTTP 请求的 Header 和 Body 被多次写入,所以在这个 execute()方法返回之前设置了 executed 标志位。同时,在 execute()方法中,最终调用了一个抽象方法 executeInternal(),而这个方法的实现是在 AbstractClientHttpRequest 的子类 AbstractBufferingClientHttpRequest 中,代码如下所示:

@Overrideprotected 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(HttpHeadersheaders, byte[] bufferedOutput) throws IOException;
复制代码

和 AbstractClientHttpRequest 类一样,这里进一步梳理了一个抽象方法 executeInternal(),而这个抽象方法则由最底层的 SimpleBufferingClientHttpRequest 类来实现,代码如下所示:

@Overrideprotected ClientHttpResponse executeInternal(HttpHeaders headers,byte[] bufferedOutput) throws IOException {	addHeaders(this.connection, headers);	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 {		this.connection.getResponseCode();	}	return new SimpleClientHttpResponse(this.connection);}
复制代码

这里通过 FileCopyUtils.copy()工具方法将响应结果写入到输出流上。


而 executeInternal()方法最终返回的是一个包装了 Connection 对象的 SimpleClientHttpResponse。

4. 处理响应结果

一个 HTTP 请求处理的最后一步就是从 ClientHttpResponse 中读取输入流,格式化成一个响应体并将其转化为业务对象,代码如下所示:

handleResponse(url, method, response);//从结果中提取数据return (responseExtractor != null ?responseExtractor.extractData(response) : null);
复制代码

我们先来看这里的 handleResponse()方法,代码如下所示:

protected void handleResponse(URI url, HttpMethod method,ClientHttpResponse response) throws IOException {ResponseErrorHandler errorHandler = getErrorHandler();boolean hasError = errorHandler.hasError(response);	if (logger.isDebugEnabled()) {		...	}	if (hasError) {		errorHandler.handleError(url, method, response);	}}
复制代码

这段代码实际上并没有真正处理返回的数据,而只是执行了对错误的处理。通过 getErrorHandler()方法获取了一个 ResponseErrorHandler,如果响应的状态码是错误的,那么就调用 handleError 处理错误并抛出异常。


那么,获取响应数据并完成转化的工作就应该是在 ResponseExtractor 中,该接口定义代码如下所示:

public interface ResponseExtractor<T> {	@Nullable	T extractData(ClientHttpResponse response) throws IOException;}
复制代码

在 RestTemplate 类中,定义了一个 ResponseEntityResponseExtractor 内部类来实现 Response-Extractor 接口,代码如下所示:

private class ResponseEntityResponseExtractor <T> implements ResponseExtractor<ResponseEntity<T>> {	@Nullable	private final HttpMessageConverterExtractor<T> delegate;	public ResponseEntityResponseExtractor(@Nullable TyperesponseType) {		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 {			returnResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();		}	}}
复制代码

可以看到,ResponseEntityResponseExtractor 中的 extractData()方法本质上是将数据提取部分的工作委托给了一个代理对象 delegate,而 delegate 的类型就是 HttpMessageConverter-Extractor。从命名上看,我们不难联想,在 HttpMessageConverterExtractor 类的内部,势必使用了 HttpMessageConverter 来完成消息的转换。其核心逻辑就是遍历 HttpMessageConveter 列表,然后判断其是否能够读取数据,如果能就调用 read()方法读取数据。


最后,我们来讨论一下 HttpMessageConveter 中的 read()方法是如何实现的。让我们来看 HttpMessageConveter 接口的抽象实现类 AbstractHttpMessageConverter,在它的 read()方法中同样定义了一个抽象方法 readInternal(),代码如下所示:

@Overridepublic 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;
复制代码

Spring Boot 内置了一系列的 HttpMessageConveter 来完成消息的转换,这里面最简单的就是 StringHttpMessageConverter,该类的 read()方法代码如下所示:

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

StringHttpMessageConverter 的实现过程就是从输入消息 HttpInputMessage 中通过 getBody()方法获取消息体,也就是获取一个 ClientHttpResponse 对象;然后通过 copy-ToString()方法从该对象中读取数据,并返回字符串结果。


至此,通过 RestTemplate 发起、执行以及响应整个 HTTP 请求的完整流程就介绍完毕了。

Spring WebMVC 案例分析

本节将通过一个案例来演示如何通过 Spring WebMVC 构建 RESTful 风格的 Web API。首先,我们在 Maven 的 pom 文件中添加如下所示的依赖包:

<dependencies>	<dependency>		<groupId>org.springframework.boot</groupId>  	<artifactId>spring-boot-starter-web</artifactId>	</dependency>	<dependency>		<groupId>org.springframework.boot</groupId>		<artifactId>spring-boot-starter-data-mongodb</artifactId>	</dependency>	<dependency>		<groupId>org.springframework.boot</groupId>		<artifactId>spring-boot-starter-test</artifactId>		<scope>test</scope>	</dependency></dependencies>
复制代码

在本案例里,我们采用 MongoDB 来实现数据存储,所以这里还引入了一个 spring-boot-starter-data-mongodb 依赖包。


现在,让我们来定义业务领域对象。在本案例中,我们设计一个 User 对象,该对象可以包含该用户的好友信息以及该用户所阅读的文章信息。User 对象的字段定义如下所示,这里的 @Document、@Id 以及 @Field 注解都来自 MongoDB。

@Document("users")public class User {	@Id	private String id;	@Field("name")	private String name;	@Field("age")	private Integer age;	@Field("createAt")	private Date createdAt;	@Field("nationality") private String nationality;	@Field("friendsIds")	private List<String> friendsIds;	@Field("articlesIds")	private List<String> articlesIds;	//省略getter/setter方法}
复制代码

注意,在这个 User 对象中存在两个数组 friendsIds 和 articlesIds,分别用于保存该用户的好友和所阅读文章的编号,其中好友信息实际上就是 User 对象,而文章信息则涉及另一个领域对象 Article。


有了领域对象之后,我们就可以设计并实现数据访问层组件。这里就需要引入 Spring 家族中的另一个常用框架 Spring Data。Spring Data 是 Spring 家族中专门用于实现数据访问的开源框架,其核心原理是支持对所有存储媒介进行资源配置从而实现数据访问。我们知道,数据访问需要完成领域对象与存储数据之间的映射,并对外提供访问入口。Spring Data 基于 Repository 架构模式抽象出了一套统一的数据访问方式。


Spring Data 的基本使用过程非常简单,我们在本书第 9 章中还会对 Spring Data 详细讲解。

基于 Spring Data,我们可以定义一个 UserRepository,代码如下所示:

public interface UserRepository extends PagingAndSortingRepository<User, String> {	User findUserById(String id);}
复制代码

可以看到 UserRepository 扩展了 PagingAndSortingRepository 接口,而后者针对 User 对象提供了一组 CRUD 以及分页和排序方法,开发人员可以直接使用这些方法完成对数据的操作。


注意,这里我们还定义了一个 findUserById()方法,该方法实际上使用了 Spring Data 提供的方法名衍生查询机制。使用方法名衍生查询是最方便的一种自定义查询方式,开发人员唯一要做的就是在 Repository 接口中定义一个符合查询语义的方法。例如,如果我们希望通过 ID 来查询 User 对象,那么只需要提供 findUserById()这一符合常规语义的方法定义即可。


类似地,ArticleRepository 的定义也非常简单,代码如下所示:

public interface ArticleRepository extends PagingAndSortingRepository<Article, String> {	Article findArticleById(String id);}
复制代码

基于数据访问层组件,Service 层组件的实现也并不复杂,基本就是对 UserRepository 和 ArticleRepository 中的接口方法的合理利用。


UserService 和 ArticleService 的实现过程如下所示:

@Servicepublic class UserService {	private final UserRepository userRepository;	@Autowired	public UserService(UserRepository userRepository) {		this.userRepository = userRepository;	}	public User findUserById(String id) {		return userRepository.findUserById(id);	}	public List<User> findByIds(List<String> ids) {		List<User> list = new ArrayList<>();		ids.forEach(id -> list.add(userRepository.findUserById(id)));		return list;	}	public List<User> findAllUsers() {		return (List<User>) userRepository.findAll();	}}@Servicepublic class ArticleService {	private final ArticleRepository articleRepository;	@Autowired	public ArticleService(ArticleRepository articleRepository) {		this.articleRepository = articleRepository;	}	public List<Article> findAllUserArticles(List<String> articleIds){		List<Article> articles = new ArrayList<>();		articleIds.forEach(id ->articles.add(articleRepository.findArticleById(id)));		return articles;	}}
复制代码

最后,我们来根据用户 ID 获取其对应的阅读文章信息。为此,我们实现下所示的 ArticleController:

@RestController@RequestMapping("/articles")public class ArticleController {	private ArticleService articleService;	private UserService userService;	@Autowired	public ArticleController(ArticleService articleService,UserService userService) {		this.articleService = articleService;		this.userService = userService;	}	@GetMapping(value = "/{userId}")	public List<Article> getArticlesByUserId(@PathVariable StringuserId){		List<Article> articles = new ArrayList<Article>();		User user = userService.findUserById(userId);		if(user != null) {			articles = articleService.findAllUserArticles(user.getArticlesIds());		}		return articles;	}}
复制代码

ArticleController 的实现过程充分展现了使用 Spring Boot 开发 RESTful 风格 Web API 的简便性。完整的案例代码可以参考:

https://github.com/tianminzheng/spring-bootexamples/tree/main/SpringWebMvcExample。

用户头像

加VX:bjmsb02 凭截图即可获取 2020-06-14 加入

公众号:程序员高级码农

评论

发布
暂无评论
一次性搞懂springweb服务构建轻量级Web技术体系:Spring WebMVC_互联网架构师小马_InfoQ写作社区