Web服務
本部分讨論針對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所示。
代碼清單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"
}
消費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請求方法清單
在一個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);
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) 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,它是一個抽象類,包含的核心變量如代碼清單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所示。
圖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所示。
圖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.ge
tHeaders()).body(body);
}
else {
returnResponseEntity.status(response.getRawStatusCode()).headers(response.ge
tHeaders()).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請求的完整流程就介紹完畢了。
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-bootexamples/tree/main/SpringWebMvcExample。
本文給大家講解的内容是springweb服務建構輕量級Web技術體系:Spring WebMVC
- 下文給大家講解的是springweb服務建構輕量級Web技術體系: Spring HATEOAS