购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.1 Spring WebMVC

Spring WebMVC框架具有一套遵循模型-视图-控制器架构设计理念的体系结构,可以开发灵活、松耦合的HTTP端点。Spring基础框架就包含WebMVC组件,而基于Spring Boot开发Web服务同样使用到该组件。Web服务的实现涉及服务创建和服务消费两个方面,本节将对这两个方面所涉及的技术组件一一展开讨论。

4.1.1 创建Web服务

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

1. 创建Controller

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

代码清单4-1 UserController示例代码

@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注解时只需要指定参数的名称即可。代码清单4-2是使用@PathVariable注解的典型代码示例,这里在请求路径中同时传入了两个参数。

代码清单4-2 @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,示例代码如代码清单4-3所示。

代码清单4-3 content-type属性使用的示例代码

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

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

代码清单4-4 @RequestBody注解使用的示例代码

@PostMapping(value = "/")
public User generateUser(@RequestBody User user) {
}

这时候,如果想要通过Postman来发起这个POST请求,就需要使用如代码清单4-5所示的一段JSON字符串。

代码清单4-5 请求JSON字符串示例

{
    "username": "tianyalan",
    "password":"123456"
}

4.1.2 消费Web服务

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

1. 创建RestTemplate

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

代码清单4-6 创建RestTemplate实例示例

@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}

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

2. 使用RestTemplate

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

表4-1 RestTemplate发送HTTP请求方法列表

081-1

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

代码清单4-7 URL中带1个参数的Web请求示例

("http://localhost:8080/users/{id}", 100)

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

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

代码清单4-8 URL中带2个参数的Web请求示例

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

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

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

代码清单4-9 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对象,实现代码如代码清单4-10所示。

代码清单4-10 getForObject()方法调用的示例代码

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

可以使用getForEntity()方法实现同样的效果,但写法上有所区别,如代码清单4-11所示。

代码清单4-11 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请求的示例代码如代码清单4-12所示。

代码清单4-12 postForEntity()方法调用的示例代码

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()方法签名,如代码清单4-13所示。

代码清单4-13 exchange ()方法的定义

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

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

代码清单4-14 exchange ()方法使用的示例代码

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

4.1.3 RestTemplate远程调用原理分析

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

1. 远程调用主流程

我们先来看一下RestTemplate类的定义,如代码清单4-15所示。

代码清单4-15 RestTemplate类的定义代码

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations

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

首先,我们来看RestOperations接口的定义,这里截取了部分核心方法,如代码清单4-16所示。

代码清单4-16 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) throws RestClientException;
    <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,它是一个抽象类,包含的核心变量如代码清单4-17所示。

代码清单4-17 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的核心变量如代码清单4-18所示。

代码清单4-18 HttpAccessor中的核心变量定义代码

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

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

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

085-1

图4-1 RestTemplate的类层结构

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

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

代码清单4-19 exchange()方法实现代码

@Override
public <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()方法,该方法定义如代码清单4-20所示。

代码清单4-20 execute()方法定义代码

@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);
}

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

代码清单4-21 doExecute()方法定义代码

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 {
        //创建请求对象
        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. 创建请求对象

创建请求对象的入口方法如代码清单4-22所示。

代码清单4-22 创建请求对象的入口方法代码

ClientHttpRequest request = createRequest(url, method);

分析这里的createRequest()方法,我们发现流程执行到了前面介绍的HttpAccessor类,如代码清单4-23所示。

代码清单4-23 HttpAccessor类实现代码

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

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

代码清单4-24 ClientHttpRequestFactory接口定义代码

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

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

代码清单4-25 SimpleClientHttpRequestFactory的createRequest()方法代码

@Override
public 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中的入口方法如代码清单4-26所示。

代码清单4-26 通过RestTemplate获取响应结果代码

response = request.execute();

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

088-1

图4-2 SimpleBufferingClientHttpRequest类层结构

在图4-2的AbstractClientHttpRequest抽象类中,定义了如代码清单4-27所示的execute()方法。

代码清单4-27 AbstractClientHttpRequest的execute()方法代码

@Override
public final ClientHttpResponse execute() throws IOException {
    assertNotExecuted();
    ClientHttpResponse result = executeInternal(this.headers);
    this.executed = true;
    return result;
}

protected abstract ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException;

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

代码清单4-28 AbstractBufferingClientHttpRequest的executeInternal ()方法代码

@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;

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

代码清单4-29 SimpleBufferingClientHttpRequest的executeInternal ()方法代码

@Override
protected 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中读取输入流,格式化成一个响应体并将其转化为业务对象,如代码清单4-30所示。

代码清单4-30 从ClientHttpResponse中提取结果数据代码

//处理调用结果
handleResponse(url, method, response);
//从结果中提取数据
return (responseExtractor != null ? responseExtractor.extractData(response) : null);

我们先来看这里的handleResponse()方法,如代码清单4-31所示。

代码清单4-31 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中,该接口定义如代码清单4-32所示。

代码清单4-32 ResponseExtractor接口定义代码

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

在RestTemplate类中,定义了一个ResponseEntityResponseExtractor内部类来实现Response-Extractor接口,如代码清单4-33所示。

代码清单4-33 ResponseEntityResponseExtractor类代码

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();
        }
    }
}

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

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

代码清单4-34 AbstractHttpMessageConverter的read()方法代码

@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;

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

代码清单4-35 StringHttpMessageConverter的read()方法代码

@Override
protected 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请求的完整流程就介绍完毕了。

4.1.4 Spring WebMVC案例分析

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

代码清单4-36 Spring WebMVC依赖包定义代码

<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对象的字段定义如代码清单4-37所示,这里的@Document、@Id以及@Field注解都来自MongoDB。

代码清单4-37 User类定义代码

@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,如代码清单4-38所示。

代码清单4-38 UserRepository接口定义代码

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

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

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

类似地,ArticleRepository的定义也非常简单,如代码清单4-39所示。

代码清单4-39 ArticleRepository接口定义代码

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

基于数据访问层组件,Service层组件的实现也并不复杂,基本就是对UserRepository和ArticleRepository中的接口方法的合理利用。UserService和ArticleService的实现过程如代码清单4-40所示。

代码清单4-40 UserService和ArticleService类实现代码

@Service
public 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();
    }
}

@Service
public 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获取其对应的阅读文章信息。为此,我们实现如代码清单4-41所示的ArticleController。

代码清单4-41 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 String userId){
        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-boot-examples/tree/main/SpringWebMvcExample。

在本章4.3节中讨论Spring GraphQL框架时,我们还会使用这个案例并对其重构,从而满足GraphQL的应用场景。 NcdrvSP2EMj4hPG54u7yDitmFnx52EIjorugtmQJTlLRnNSZbVW0Z6tIWkvpBH4p

点击中间区域
呼出菜单
上一章
目录
下一章
×