批量和批處理 API 在 Spring 中的實現

REST,Spring Web
Remote
1
03:25 AM · Dec 01 ,2025

1. 概述

實施標準 REST API 涵蓋了大多數典型的用例。但是,基於 REST 的架構風格在處理任何批量或批處理操作時存在一些限制。

在本教程中,我們將學習如何在微服務中應用批量和批處理操作。 此外,我們還將實現幾個自定義的面向寫入的批量和批處理 API。

2. 大批量和批量 API 介紹

術語 大批量和批量操作 經常被互換使用。然而,兩者之間存在本質區別。

通常,大批量操作 指的是對同一類型的多個條目執行相同的操作。一種簡單的做法是,通過對每個請求調用相同的 API 來執行大批量操作。這可能會很慢且浪費資源。相反,我們可以在一輪往返中處理多個條目。

我們可以通過在單個調用中對同一類型的多個條目應用相同的操作來實現大批量操作。這種對項目集合的操作方式可以減少總體延遲並提高應用程序性能。要實現,我們可以重用用於單個條目的現有端點,也可以為批量方法創建單獨的路由。

批量操作 通常意味着對不同資源類型執行不同的操作。批量 API 是在單個調用中對各種資源操作的捆綁包。這些資源操作可能沒有一致性。潛在地,每個請求路由可以獨立於另一個。

簡而言之,“批量”一詞意味着批量不同請求。

我們沒有許多明確定義的標準或規範來實施大批量或批量操作。此外,許多流行的框架,如 Spring, 都沒有內置對大批量操作的支持。

儘管如此,在本教程中,我們將使用常見的 REST 構造查看大批量和批量操作的自定義實現。

3. 示例應用程序在 Spring 中

假設我們需要構建一個簡單的微服務,該微服務支持批量和批處理操作。

3.1. Maven 依賴項

首先,讓我們包含 spring-boot-starter-webspring-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 服務

我們將實現一個創建、更新和刪除數據於倉庫中的服務。

首先,讓我們對 類進行建模:

public class Customer implements Serializable {
    private String id;
    private String name;
    private String email;
    private String address;
    // standard getters and setters
}

接下來,讓我們實現 類,並提供一個 方法,用於將多個 對象存儲在我們的內存倉庫中:

@Service
public class CustomerService {
    private final Map<String, Customer> customerRepoMap = new HashMap<>();

    public List<Customer> createCustomers(List<Customer> customers) {
        return customers.stream()
          .map(this::createCustomer)
          .filter(Optional::isPresent)
          .map(Optional::get)
          .collect(toList());
    }
}

然後,我們將實現一個 方法,用於創建一個單個 對象:

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

在上述方法中,我們僅在客户不在倉庫中時才創建客户,否則,我們返回一個空對象。

同樣,我們將實現一個方法來更新現有 詳細信息:

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

最後,我們將實現一個 方法,用於從倉庫中刪除現有 對象:

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;
    //標準 getter 和 setter
}

然後,我們實現 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. Implement a Bulk API Using a Different Endpoint

It’s not that common to have different actions on the same resource in a bulk API. However, let’s look at the most flexible approach possible to see how it can be done.

We might implement an atomic bulk operation, where the whole request succeeds or fails within a single transaction. Or, we can allow the updates that succeed to happen independently of those that fail, with a response that indicates whether it was a full or partial success. We’ll implement the second of these.

5.1. Define the Request and Response Model

Let’s consider a use case of creating, updating, and deleting multiple customers in a single call.

We’ll define the bulk request as a JSON format:

[
    {
        "bulkActionType": "<CREATE OR UPDATE OR DELETE>",
        "customers": [
            {
                "name": "<name>",
                "email": "<email>",
                "address": "<address>"
            }
        ]
    }
]

First, we’ll model the above JSON format into the CustomerBulkRequest class:

public class CustomerBulkRequest {
    private BulkActionType bulkActionType;
    private List<Customer> customers;
    //standard getters and setters
}

Next, we’ll implement the BulkActionType enum:

public enum BulkActionType {
    CREATE, UPDATE, DELETE
}

Then, let’s define the CustomerBulkResponse class as the HTTP response object:

public class CustomerBulkResponse {
    private BulkActionType bulkActionType;
    private List<Customer> customers;
    private BulkStatus status;
    //standard getters and setters
}

Finally, we’ll define the BulkStatus enum to specify each operation’s return status:

public enum BulkStatus {
    PROCESSED, PARTIALLY_PROCESSED, NOT_PROCESSED
}

5.2. Implement the Bulk Controller

We’ll implement a bulk API that takes the bulk requests and processes based on the bulkActionType enum and then returns the bulk status and customer data together.

First, we’ll create an EnumMap in the BulkController class and map the BulkActionType enum to its own CustomerService’s Function:

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

This EnumMap provides a binding between the request type and the method on the CustomerService that we need to satisfy it. It helps us avoid lengthy switch or if statements.

We can pass the Function returned from the EnumMap against the action type to the map() method on a stream of Customer objects:

List<Customer> customers = customerBulkRequest.getCustomers().stream()
   .map(bulkActionFuncMap.get(customerBulkRequest.getBulkActionType()))
   ...

As all our Function objects map from Customer to Optional<Customer>, this essentially uses the map() operation in the stream to execute the bulk request, leaving the resulting Customer in the stream (if available).

Let’s put this together in the full controller method:

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

Also, we’ll complete the getBulkStatus method to return a specific bulkStatus enum based on the number of customers created:

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

We should note that adding the input validations for any conflicts between each operation can also be considered.

5.3. Validate the Bulk API

We’ll run the application and call the above endpoint i.e. /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": [
            ...
        ]
    }
]

Let’s now verify the successful response:

HTTP/1.1 207 
[{"customers":[{"id":4,"name":"test4","email":"[email protected]","address":"address4"}],"status":"PROCESSED","bulkType":"CREATE"},
...
]

Next, we’ll implement a batch API that processes both the customers and addresses, bundled together in a single batch call.

6. 實現批量 API

通常,一個批量 API 請求是一組具有各自方法、資源 URL 和有效負載的子請求的集合。

我們將實現一個批量 API,用於創建和更新兩種資源類型。當然,我們還可以包含其他操作,如刪除操作。但是,為了簡化起見,我們將只考慮 POSTPATCH 方法。

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 編寫在同一個微服務中,為了簡化起見。在另一個架構模式中,我們可能會選擇在不同的微服務中實現它,該微服務會並行調用各個端點。

有了上述 BatchRequest 類,我們將遇到將 JsonNode 轉換為特定類型 的問題。我們可以很容易地通過使用 ObjectMapper 的 convertValue 方法 來將 JsonNode 轉換為具有類型信息的對象來解決這個問題。

對於批量 API,我們將根據 HttpMethodrelativeUrl 參數,調用 AddressServiceCustomerService 方法。

我們將實現批量端點 BatchController 類 中:

@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

我們將執行上述  端點:

$ 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 processed

7. 結論

在本文中,我們學習瞭如何在 Spring 應用程序中應用批量和批量操作。我們還理解了它們的用途以及差異。

對於批量操作,我們已在兩個不同的 API 中實現它,一個重用現有的 POST 端點以創建多個資源,另一個方法創建單獨的端點以對同一類型的多個資源執行多個操作。

我們還實現了批量 API,該 API 允許將不同的操作應用於不同的資源。批量 API 結合使用不同的子請求,使用 HttpMethodrelativeUrl 以及 payload。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.