1. 概述
實施標準 REST API 覆蓋了大多數典型的用例。但是,基於 REST 的架構風格在處理任何批量或批處理操作時存在一些限制。
在本教程中,我們將學習如何在微服務中應用批量和批處理操作。此外,我們還將實現幾個自定義的面向寫入的批量和批處理 API。
2. 大批量和批量 API 介紹
術語 大批量和批量操作 經常被互換使用。然而,兩者之間存在本質的區別。
通常,大批量操作 指的是對同一類型的多個條目執行相同的操作。一種簡單的做法是,通過對每個請求調用相同的 API 來應用大批量操作。這可能太慢且浪費資源。相反,我們可以在一輪往返中處理多個條目。
我們可以通過在單個調用中對同一類型的多個條目應用相同的操作來實現大批量操作。這種對項目集合的操作方式可以減少總延遲並提高應用程序性能。要實現,我們可以重用用於單個條目的現有端點,也可以為批量方法創建單獨的路由。
批量操作 通常意味着對不同資源類型執行不同的操作。批量 API 是在單個調用中對資源執行各種操作的包。這些資源操作可能沒有一致性。潛在地,每個請求路由可以獨立於另一個。
簡而言之,“批量”一詞意味着批量不同請求。
我們沒有許多明確定義的標準或規範來實現大批量或批量操作。此外,許多流行的框架(如 Spring)Spring 框架 沒有內置的大批量操作 支持。
儘管如此,在本教程中,我們將使用常見的 REST 構造查看大批量和批量操作的自定義實現。
3. 在 Spring 中使用示例應用程序
讓我們假設我們需要構建一個簡單的微服務,該微服務同時支持批量和批處理操作。
3.1. Maven 依賴
首先,讓我們添加以下 Maven 依賴:spring-boot-starter-web 和 spring-boot-starter-validation。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.1.5</version>
</dependency>藉助上述 spring-boot-starter-validation 依賴項,我們已在應用程序中啓用了輸入數據驗證。我們將要求驗證批量和批量請求的大小。
3.2. 實現第一個 Spring 服務
我們將實現一個服務,該服務能夠創建、更新和刪除倉庫中的數據。
首先,讓我們對 Customer 類進行建模:
// 客户類模型
public class Customer {
// ... 客户屬性和方法 ...
}
public class Customer implements Serializable {
private String id;
private String name;
private String email;
private String address;
// standard getters and setters
}接下來,我們通過實現CustomerService類,並添加createCustomers()方法,將多個Customer對象存儲到我們的內存倉庫中:
@Service
public class CustomerService {
private final Map<String, Customer> customerRepoMap = new HashMap<>();
public List<Customer> createCustomers(List<Customers> customers) {
return customers.stream()
.map(this::createCustomer)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toList());
}
}
然後,我們將實現一個 createCustomer() 方法,用於創建一個單個 Customer 對象:
public Optional<Customer> createCustomer(Customer customer) {
if (!customerRepoMap.containsKey(customer.getEmail()) && customer.getId() == 0) {
Customer customerToCreate = new Customer(customerRepoMap.size() + 1,
customer.getName(), customer.getEmail());
customerToCreate.setAddress(customer.getAddress());
customerRepoMap.put(customerToCreate.getEmail(), customerToCreate);
return Optional.of(customerToCreate);
}
return Optional.empty();
}在上述方法中,我們僅在客户不在存儲庫中時才創建客户,否則返回一個空對象。
同樣,我們還將實現一個用於更新現有 Customer 詳細信息的方法:
private Optional<Customer> updateCustomer(Customer customer) {
Customer customerToUpdate = customerRepoMap.get(customer.getEmail());
if (customerToUpdate != null && customerToUpdate.getId() == customer.getId()) {
customerToUpdate.setName(customer.getName());
customerToUpdate.setAddress(customer.getAddress());
}
return Optional.ofNullable(customerToUpdate);
}最後,我們將實現一個 deleteCustomer() 方法,用於從存儲庫中刪除現有的 Customer 對象:
public Optional<Customer> deleteCustomer(Customer customer) {
Customer customerToDelete = customerRepoMap.get(customer.getEmail());
if (customerToDelete != null && customerToDelete.getId() == customer.getId()) {
customerRepoMap.remove(customer.getEmail());
}
return Optional.ofNullable(customerToDelete);
}3.3. 實現第二個 Spring 服務
我們還將實現另一個服務,該服務負責從存儲庫中檢索和創建地址數據。
首先,我們將定義 Address 類:
public class Address implements Serializable {
private int id;
private String street;
private String city;
//standard getters and setters
}然後,讓我們實現 AddressService 類,幷包含一個 createAddress() 方法:
public Address createAddress(Address address) {
Address createdAddress = null;
String addressUniqueKey = address.getStreet().concat(address.getCity());
if (!addressRepoMap.containsKey(addressUniqueKey)) {
createdAddress = new Address(addressRepoMap.size() + 1,
address.getStreet(), address.getCity());
addressRepoMap.put(addressUniqueKey, createdAddress);
}
return createdAddress;
}
4. 使用現有端點實現批量 API
現在,讓我們創建一個 API 以支持批量和單件創建操作。
4.1. 實現批量控制器
我們將實現一個 BulkController 類,該類包含一個端點,用於在單個調用中創建單個或多個客户。
首先,我們將以 JSON 格式定義批量請求:
[
{
"name": "<name>",
"email": "<email>",
"address": "<address>"
}
]採用這種方法,我們可以使用自定義 HTTP 標頭——X-ActionType——來處理批量操作,從而區分批量操作和單項操作。
接下來,我們將實現 bulkCreateCustomers() 方法,該方法位於 BulkController 類中,並使用上述 CustomerService 方法:
@PostMapping(path = "/customers/bulk")
public ResponseEntity<List<Customer>> bulkCreateCustomers(
@RequestHeader(value="X-ActionType") String actionType,
@RequestBody @Valid @Size(min = 1, max = 20) List<Customer> customers) {
List<Customer> customerList = actionType.equals("bulk") ?
customerService.createCustomers(customers) :
singletonList(customerService.createCustomer(customers.get(0)).orElse(null));
return new ResponseEntity<>(customerList, HttpStatus.CREATED);
}
在上述代碼中,我們使用 X-ActionType 頭部來接受任何批量請求。 此外,我們還添加了使用 @Size 註解進行輸入請求大小驗證。 代碼決定是否將整個列表傳遞給 createCustomers(),或者僅傳遞元素 0 到 createCustomer()。
不同的創建函數返回一個列表或一個 Optional 對象,因此我們將後者轉換為 List 以使 HTTP 響應在兩種情況下都保持一致。
4.2. 驗證批量 API
我們將運行應用程序並通過執行上述端點來驗證批量操作:
$ curl -i --request POST 'http://localhost:8080/api/customers/bulk' \
--header 'X-ActionType: bulk' \
--header 'Content-Type: application/json' \
--data-raw '[
{
"name": "test1",
"email": "[email protected]",
"address": "address1"
},
{
"name": "test2",
"email": "[email protected]",
"address": "address2"
}
]'
我們將會獲得以下成功響應,並創建了客户:
HTTP/1.1 201
[{"id":1,"name":"test1","email":"[email protected]","address":"address1"},
{"id":1,"name":"test2","email":"[email protected]","address":"address2"},
...
接下來,我們將實現一種批量操作的其他方法。
5. 使用不同端點實現批量 API
在批量 API 中,通常不會有對同一資源的不同操作。不過,為了瞭解如何實現最靈活的方法,我們來探討一下。
我們可能會實現原子批量操作,即整個請求在單個事務中要麼成功要麼失敗。或者,我們允許成功的更新獨立於失敗的更新進行,並提供一個響應,指示它是否為完全成功或部分成功。我們將採用後者。
5.1. 定義請求和響應模型
讓我們考慮一個在單個調用中創建、更新和刪除多個客户的用例。
我們將批量請求定義為 JSON 格式:
[
{
"bulkActionType": "<CREATE OR UPDATE OR DELETE>",
"customers": [
{
"name": "<name>",
"email": "<email>",
"address": "<address>"
}
]
}
]首先,我們將上述 JSON 格式建模為 CustomerBulkRequest 類:
public class CustomerBulkRequest {
private BulkActionType bulkActionType;
private List<Customer> customers;
//standard getters and setters
}接下來,我們將實現 BulkActionType 枚舉:
public enum BulkActionType {
CREATE, UPDATE, DELETE
}然後,我們定義 CustomerBulkResponse 類 為 HTTP 響應對象:
public class CustomerBulkResponse {
private BulkActionType bulkActionType;
private List<Customer> customers;
private BulkStatus status;
//standard getters and setters
}最後,我們將定義 BulkStatus 枚舉來指定每個操作的返回狀態:
public enum BulkStatus {
PROCESSED, PARTIALLY_PROCESSED, NOT_PROCESSED
}5.2. 實現批量控制器
我們將實現一個批量 API,它將接收批量請求並根據 <em bulkActionType</em> 枚舉值進行處理,然後返回批量狀態和客户數據。
首先,我們將創建一個 <em EnumMap</em> 在 <em BulkController</em> 類中,並將 <em BulkActionType 枚舉</em> 映射到其自身的 <em CustomerService 的函數</em>:
@RestController
@RequestMapping("/api")
@Validated
public class BulkController {
private final CustomerService customerService;
private final EnumMap<BulkActionType, Function<Customer, Optional<Customer>>> bulkActionFuncMap =
new EnumMap<>(BulkActionType.class);
public BulkController(CustomerService customerService) {
this.customerService = customerService;
bulkActionFuncMap.put(BulkActionType.CREATE, customerService::createCustomer);
bulkActionFuncMap.put(BulkActionType.UPDATE, customerService::updateCustomer);
bulkActionFuncMap.put(BulkActionType.DELETE, customerService::deleteCustomer);
}
}這個 EnumMap 提供了一個與請求類型和 CustomerService 中所需方法的綁定,從而避免了冗長的 switch 或 if 語句。
我們可以將 EnumMap 返回的 Function 傳遞給 map() 方法,該方法應用於 Customer 對象流。
List<Customer> customers = customerBulkRequest.getCustomers().stream()
.map(bulkActionFuncMap.get(customerBulkRequest.getBulkActionType()))
...所有我們的 Function 對象都從 Customer 映射到 Optional<Customer>, 這本質上是在流中執行 map() 操作來執行批量請求,並將結果的 Customer (如果可用) 留在流中。
下面我們將其整合到完整的控制器方法中:
@PostMapping(path = "/customers/bulk")
public ResponseEntity<List<CustomerBulkResponse>> bulkProcessCustomers(
@RequestBody @Valid @Size(min = 1, max = 20)
List<CustomerBulkRequest> customerBulkRequests) {
List<CustomerBulkResponse> customerBulkResponseList = new ArrayList<>();
customerBulkRequests.forEach(customerBulkRequest -> {
List<Customer> customers = customerBulkRequest.getCustomers().stream()
.map(bulkActionFuncMap.get(customerBulkRequest.getBulkActionType()))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toList());
BulkStatus bulkStatus = getBulkStatus(customerBulkRequest.getCustomers(),
customers);
customerBulkResponseList.add(CustomerBulkResponse.getCustomerBulkResponse(customers,
customerbulkRequest.getBulkActionType(), bulkStatus));
});
return new ResponseEntity<>(customerBulkResponseList, HttpStatus.Multi_Status);
}此外,我們還將完成 getBulkStatus 方法,以根據創建的客户數量返回特定的 bulkStatus 枚舉值:
private BulkStatus getBulkStatus(List<Customer> customersInRequest,
List<Customer> customersProcessed) {
if (!customersProcessed.isEmpty()) {
return customersProcessed.size() == customersInRequest.size() ?
BulkStatus.PROCESSED :
BulkStatus.PARTIALLY_PROCESSED;
}
return BulkStatus.NOT_PROCESSED;
}我們應該注意的是,添加任何操作之間的衝突驗證也應該被考慮。
5.3. 驗證批量 API
我們將運行應用程序並調用上述端點,即 /customers/bulk:
$ curl -i --request POST 'http://localhost:8080/api/customers/bulk' \
--header 'Content-Type: application/json' \
--data-raw '[
{
"bulkActionType": "CREATE",
"customers": [
{
"name": "test4",
"email": "[email protected]",
...
}
]
},
{
"bulkActionType": "UPDATE",
"customers": [
...
]
},
{
"bulkActionType": "DELETE",
"customers": [
...
]
}
]'讓我們現在驗證成功的響應:
HTTP/1.1 207
[{"customers":[{"id":4,"name":"test4","email":"[email protected]","address":"address4"}],"status":"PROCESSED","bulkType":"CREATE"},
...接下來,我們將實現一個批量 API,它同時處理客户和地址信息,並通過一次批量調用進行處理。
6. 實現批量 API
通常,批量 API 請求是包含多個子請求的集合,每個子請求都有其自身的 HTTP 方法、資源 URL 和請求負載。
我們將實現一個批量 API,用於創建和更新兩種資源類型。當然,我們還可以包含其他操作,例如刪除操作。但為了簡化起見,我們只考慮 POST 和 PATCH 方法。
6.1. 實現批量請求模型
首先,我們將混合數據請求模型定義為 JSON 格式:
[
{
"method": "POST",
"relativeUrl": "/address",
"data": {
"street": "<street>",
"city": "<city>"
}
},
{
"method": "PATCH",
"relativeUrl": "/customer",
"data": {
"id": "<id>",
"name": "<name>",
"email": "<email>",
"address": "<address>"
}
}
]我們將會實現上述 JSON 結構作為 BatchRequest 類:
public class BatchRequest {
private HttpMethod method;
private String relativeUrl;
private JsonNode data;
//standard getters and setters
}6.2. 實現批量控制器
我們將實現一個批量 API,用於創建地址並以單個請求更新客户的地址信息。為了簡化,我們將此 API 編寫在同一微服務中。在另一種架構模式中,我們可能會選擇將其實現在一個不同的微服務中,該微服務並行調用各個端點。
藉助上述 <em>BatchRequest</em> 類,我們將會遇到將 <em>JsonNode</em> 解序列化為特定類型 <em>class</em> 的問題。我們可以輕鬆地通過使用 <em>ObjectMapper</em> 的 convertValue 方法將 <em>JsonNode</em> 轉換為具有類型提示的對象來解決這個問題。
對於批量 API,我們將根據 <em>BatchRequest</em> 類中的 <em>HttpMethod</em> 和 <em>relativeUrl</em> 參數,調用 <em>AddressService</em> 或 <em>CustomerService</em> 方法。
我們將實現批量端點在 <em>BatchController</em> 類中:
@PostMapping(path = "/batch")
public String batchUpdateCustomerWithAddress(
@RequestBody @Valid @Size(min = 1, max = 20) List<BatchRequest> batchRequests) {
batchRequests.forEach(batchRequest -> {
if (batchRequest.getMethod().equals(HttpMethod.POST) &&
batchRequest.getRelativeUrl().equals("/address")) {
addressService.createAddress(objectMapper.convertValue(batchRequest.getData(),
Address.class));
} else if (batchRequest.getMethod().equals(HttpMethod.PATCH) &&
batchRequest.getRelativeUrl().equals("/customer")) {
customerService.updateCustomer(objectMapper.convertValue(batchRequest.getData(),
Customer.class));
}
});
return "Batch update is processed";
}
6.3. 驗證批量 API
我們將執行上述的 batch 端點:
$ curl -i --request POST 'http://localhost:8080/api/batch' \
--header 'Content-Type: application/json' \
--data-raw '[
{
"method": "POST",
"relativeUrl": "/address",
"data": {
"street": "test1",
"city": "test"
}
},
{
"method": "PATCH",
"relativeUrl": "/customer",
"data": {
"id": "1",
"name": "test1",
"email": "[email protected]",
"address": "address2"
}
}
]'我們將會驗證以下響應:
HTTP/1.1 200
Batch update is processed7. 結論
在本文中,我們學習瞭如何在 Spring 應用中應用批量和批處理操作。我們還理解了它們的功能以及差異。
對於批量操作,我們已在兩個不同的 API 中實現它:一個重用現有的 POST 端點以創建多個資源,另一個則創建單獨的端點以對同一類型的多個資源執行多個操作。
我們還實現了批處理 API,允許我們對不同的資源應用不同的操作。批處理 API 通過結合使用 HttpMethod 和 relativeUrl 以及負載,對不同的子請求進行組合。