Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 28

Ánh xạ dữ liệu API vào biến quy trình

Recommended resources

Chào bạn,

Trong bài học trước, chúng ta đã thành công gọi một REST API bên ngoài từ Service Task và xử lý kết quả trả về. Bạn đã thấy cách chúng ta sử dụng RestTemplate để lấy dữ liệu khách hàng và sau đó dùng execution.setVariable() để lưu thông tin đó (như customerName và trackingNumber) vào các biến của quy trình.

// Cách chúng ta đã làm ở bài trước
execution.setVariable("customerName", customer.getName());
// ...
execution.setVariable("trackingNumber", trackingNumber);

Cách làm này hoạt động tốt, nhưng nó tạo ra một sự liên kết khá chặt chẽ giữa mã Java và tên của các biến trong quy trình. Nếu một nhà phân tích nghiệp vụ muốn đổi tên biến customerName thành clientName trong mô hình BPMN, bạn, với tư cách là developer, sẽ phải tìm và thay đổi chuỗi "customerName" trong mã nguồn Java.

Trong bài học hôm nay, chúng ta sẽ giải quyết vấn đề này. Mục tiêu của chúng ta là: Ánh xạ dữ liệu trả về từ API vào các biến của quy trình bằng một phương pháp linh hoạt và mạnh mẽ hơn, đó là tính năng Input/Output Mapping của Camunda.

Chúng ta sẽ học cách tách biệt logic nghiệp vụ khỏi định nghĩa quy trình, giúp mã nguồn của bạn dễ tái sử dụng hơn và quy trình trở nên rõ ràng, dễ bảo trì hơn.


1. Tại sao cần Input/Output Mapping?

Input/Output Mapping hoạt động như một lớp "adapter" cho các tác vụ của bạn (Service Task, User Task, v.v.). Nó cho phép bạn:

  • Tách biệt (Decouple): Tách mã nguồn thực thi (ví dụ: lớp JavaDelegate) khỏi các tên biến cụ thể trong quy trình. Mã của bạn sẽ làm việc với các tên biến nội bộ, và mô hình BPMN sẽ đảm nhiệm việc ánh xạ chúng với các biến của quy trình.
  • Tăng khả năng tái sử dụng (Promote Reusability): Cùng một JavaDelegate có thể được sử dụng trong nhiều quy trình khác nhau, ngay cả khi các quy trình đó sử dụng các bộ tên biến hoàn toàn khác nhau.
  • Chuyển đổi dữ liệu (Transform Data): Thực hiện các chuyển đổi dữ liệu đơn giản hoặc phức tạp ngay tại mô hình BPMN mà không cần viết thêm mã Java, chẳng hạn như "giải nén" một đối tượng phức tạp thành nhiều biến quy trình.

Cơ chế này được định nghĩa trực tiếp trong Camunda Modeler thông qua tab Input/Output trong Properties Panel.

Để hiểu sâu hơn về cơ chế và lợi ích của Input/Output Mapping, bạn hãy đọc phần tài liệu chính thức từ Camunda.

Input/Output Variable Mapping - Camunda Docs

Phần tài liệu này giải thích rất rõ về khái niệm Input/Output Mapping, cấu trúc XML của nó, và cung cấp một ví dụ tuyệt vời về việc tái sử dụng code thông qua cơ chế ánh xạ.

Hãy đọc toàn bộ phần 'Input/Output Variable Mapping'. Đặc biệt chú ý đến: Cấu trúc XML với <camunda:inputOutput>, <camunda:inputParameter>, và <camunda:outputParameter>. Ví dụ về ComplexCalculation để thấy cách cùng một lớp Java có thể được sử dụng trong hai quy trình khác nhau với các tên biến khác nhau.



2. Thực hành: Tái cấu trúc với Output Mapping

Bây giờ, chúng ta sẽ áp dụng khái niệm này để tái cấu trúc lại ví dụ từ bài học trước. Mục tiêu của chúng ta là làm cho JavaDelegate không cần biết tên của các biến quy trình cuối cùng (trackingNumber, customerName). Thay vào đó, nó sẽ tạo ra một đối tượng kết quả duy nhất, và chúng ta sẽ dùng Output Mapping để "giải nén" đối tượng này.

Bước 1: Tạo đối tượng kết quả (Result Object)

Hãy tạo một POJO mới để đóng gói tất cả thông tin trả về từ service của chúng ta.

Tạo một lớp mới tên là ShipmentResult.java:

package com.example.camunda; // Thay bằng package của bạn

public class ShipmentResult {
    private String trackingNumber;
    private String customerName;

    public ShipmentResult(String trackingNumber, String customerName) {
        this.trackingNumber = trackingNumber;
        this.customerName = customerName;
    }

    // Getters
    public String getTrackingNumber() {
        return trackingNumber;
    }

    public String getCustomerName() {
        return customerName;
    }
}

Bước 2: Tái cấu trúc ShipmentService

Bây giờ, hãy sửa đổi ShipmentService để nó không tương tác trực tiếp với DelegateExecution nữa. Nó sẽ nhận các tham số đầu vào và trả về đối tượng ShipmentResult vừa tạo. Điều này biến nó thành một Spring bean hoàn toàn độc lập và dễ dàng cho việc unit test.

package com.example.camunda; // Thay bằng package của bạn

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
public class ShipmentService {

    private final Logger LOGGER = LoggerFactory.getLogger(ShipmentService.class);

    @Autowired
    private RestTemplate restTemplate;

    // Phương thức giờ đây nhận tham số, không còn phụ thuộc vào DelegateExecution
    public ShipmentResult prepareShipment(String orderId, String shippingMethod) {
        LOGGER.info("Bắt đầu chuẩn bị vận đơn cho đơn hàng {}...", orderId);

        try {
            int customerId = (orderId.hashCode() & Integer.MAX_VALUE) % 10 + 1;
            String apiUrl = "https://jsonplaceholder.typicode.com/users/" + customerId;
            
            LOGGER.info("Đang gọi API: {}", apiUrl);
            CustomerData customer = restTemplate.getForObject(apiUrl, CustomerData.class);

            String customerName = "Unknown";
            if (customer != null) {
                LOGGER.info("Lấy được thông tin khách hàng: Tên = {}", customer.getName());
                customerName = customer.getName();
            }

            String prefix = "STD-";
            if ("express".equalsIgnoreCase(shippingMethod)) {
                prefix = "EXP-";
            }
            String trackingNumber = prefix + customerName.replaceAll("\\s+", "") + "-" + System.currentTimeMillis();

            // Trả về một đối tượng ShipmentResult
            return new ShipmentResult(trackingNumber, customerName);

        } catch (Exception e) {
            LOGGER.error("Lỗi khi gọi API hoặc xử lý dữ liệu", e);
            // Trả về một kết quả dự phòng
            return new ShipmentResult("FALLBACK-" + orderId, "N/A");
        }
    }
}

Bước 3: Cập nhật JavaDelegate

Lớp JavaDelegate (ExpressShipmentPreparer và StandardShipmentPreparer) giờ đây đóng vai trò là cầu nối. Nó sẽ lấy biến từ quy trình, gọi service, và lưu đối tượng kết quả vào một biến tạm thời.

Sửa ExpressShipmentPreparer.java:

@Component("expressShipmentPreparer")
public class ExpressShipmentPreparer implements JavaDelegate {
    
    private final Logger LOGGER = LoggerFactory.getLogger(ExpressShipmentPreparer.class);

    @Autowired
    private ShipmentService shipmentService;

    @Override
    public void execute(DelegateExecution execution) throws Exception {
        // Lấy biến từ quy trình
        String orderId = (String) execution.getVariable("orderId");
        String shippingMethod = (String) execution.getVariable("shippingMethod");
        LOGGER.info("Đang điều phối việc chuẩn bị vận đơn HỎA TỐC cho đơn hàng: {}", orderId);
        
        // Gọi service đã được tái cấu trúc
        ShipmentResult result = shipmentService.prepareShipment(orderId, shippingMethod);
        
        // Lưu toàn bộ đối tượng kết quả vào một biến duy nhất
        // Biến này có thể chỉ là biến tạm thời (local) cho tác vụ này
        execution.setVariable("shipmentResult", result); 
        
        LOGGER.info("Service đã trả về kết quả. Sẽ được ánh xạ vào quy trình.");
    }
}

Hãy cập nhật tương tự cho StandardShipmentPreparer.java.

Bước 4: Cấu hình Output Mapping trong Camunda Modeler

Đây là bước quan trọng nhất. Chúng ta sẽ "dạy" cho Service Task cách "giải nén" đối tượng shipmentResult.

  1. Mở tệp BPMN của bạn trong Camunda Modeler.
  2. Chọn Service Task "Chuẩn bị vận đơn" (Prepare Shipment).
  3. Trong Properties Panel ở bên phải, chọn tab Input/Output.
  4. Trong phần Output Parameters, nhấn nút + để thêm một tham số mới.
    • Name: trackingNumber (Đây là tên biến bạn muốn tạo trong quy trình).
    • Value: ${shipmentResult.trackingNumber} (Đây là một Expression Language - EL. Nó truy cập vào thuộc tính trackingNumber của đối tượng trong biến shipmentResult).
  5. Nhấn nút + một lần nữa để thêm tham số thứ hai.
    • Name: customerName.
    • Value: ${shipmentResult.customerName}.
Mapping DMN Result to Process Variables in Camunda
Giao diện cấu hình Output Mapping. Hình ảnh này minh họa cho một Business Rule Task, nhưng giao diện và nguyên lý cho Service Task là hoàn toàn tương tự.

Sau khi cấu hình, bạn đã tách biệt hoàn toàn mã Java khỏi tên biến quy trình. Mã Java chỉ cần biết nó phải tạo ra một đối tượng ShipmentResult và đặt nó vào biến shipmentResult. Việc lấy trackingNumber và customerName ra như thế nào là do mô hình BPMN quyết định.



3. Nâng cao: Tái cấu trúc với Input Mapping

Chúng ta đã giải quyết được phần đầu ra, nhưng JavaDelegate vẫn đang đọc trực tiếp các biến quy trình: execution.getVariable("orderId"). Chúng ta có thể làm tốt hơn bằng cách sử dụng Input Mapping.

Bước 1: Cấu hình Input Mapping trong Modeler

  1. Vẫn trong tab Input/Output của Service Task, tìm đến phần Input Parameters.
  2. Nhấn nút + để thêm một tham số.
    • Name: orderIdentifier (Đây là tên biến cục bộ mà delegate sẽ thấy).
    • Value: ${orderId} (Giá trị được lấy từ biến quy trình orderId).
  3. Thêm một tham số nữa:
    • Name: deliveryType.
    • Value: ${shippingMethod}.

Bước 2: Cập nhật JavaDelegate lần cuối

Bây giờ, hãy sửa JavaDelegate để nó sử dụng các biến cục bộ vừa được tạo bởi Input Mapping.

Sửa lại ExpressShipmentPreparer.java:

// ...
    @Override
    public void execute(DelegateExecution execution) throws Exception {
        // Lấy biến từ phạm vi CỤC BỘ của tác vụ
        String orderId = (String) execution.getVariable("orderIdentifier");
        String shippingMethod = (String) execution.getVariable("deliveryType");
        LOGGER.info("Đang điều phối việc chuẩn bị vận đơn HỎA TỐC cho đơn hàng: {}", orderId);
        
        ShipmentResult result = shipmentService.prepareShipment(orderId, shippingMethod);
        
        execution.setVariable("shipmentResult", result); 
    }
// ...

Bây giờ, ExpressShipmentPreparer của bạn đã hoàn toàn độc lập. Nó nhận đầu vào là orderIdentifier và deliveryType, và tạo ra đầu ra là shipmentResult. Nó không hề biết đến các biến orderId, shippingMethod, trackingNumber, hay customerName của quy trình. Toàn bộ việc "dịch" tên biến được xử lý bởi Input/Output Mapping.

Camunda Modeler Variables Tab
Sau khi quy trình chạy qua tác vụ đã được cấu hình mapping, bạn có thể vào tab 'Variables' trong Cockpit để xem tất cả các biến đã được tạo và ánh xạ thành công.


4. Ánh xạ trực tiếp với Connectors và Scripts

Input/Output Mapping đặc biệt hữu ích khi bạn không muốn viết một lớp Java đầy đủ. Ví dụ, khi sử dụng Camunda Connectors, bạn có thể gọi một API và ánh xạ trực tiếp kết quả JSON vào các biến quy trình.

Hãy xem một ví dụ từ kho mã nguồn của Camunda Community Hub.

Make REST calls from Camunda Platform 7 Example - GitHub

Ví dụ này minh họa cách sử dụng Camunda Connector để thực hiện một lời gọi GET và sau đó dùng Output Parameters để trích xuất dữ liệu từ phản hồi.

Hãy xem phần 'Connectors'. Chú ý đến hình ảnh thứ hai 'Output' và các biểu thức (expressions) bên dưới nó. Bạn sẽ thấy cách họ dùng ${statusCode} để lấy mã trạng thái HTTP, và ${S(response).prop("health_percentage")} để phân tích chuỗi JSON trả về và lấy một thuộc tính cụ thể. Đây là một cách rất hiệu quả để xử lý các API đơn giản.

Ở đây, S(response) là một cú pháp của thư viện Camunda Spin, cho phép bạn xử lý dữ liệu JSON và XML một cách dễ dàng ngay trong các biểu thức. Cách làm này loại bỏ hoàn toàn nhu cầu viết mã Java cho các tác vụ tích hợp đơn giản.


Tổng kết

Qua bài học này, bạn đã nắm vững một kỹ thuật quan trọng để xây dựng các quy trình Camunda chuyên nghiệp và dễ bảo trì.

Những điểm chính cần nhớ:

  • Input/Output Mapping là một "adapter" mạnh mẽ để quản lý luồng dữ liệu vào và ra khỏi các tác vụ.
  • Nó giúp tách biệt logic nghiệp vụ (mã Java/script) khỏi định nghĩa quy trình (tên biến BPMN).
  • Output Mapping rất lý tưởng để "giải nén" các đối tượng phức tạp hoặc các phản hồi từ API thành nhiều biến quy trình.
  • Input Mapping giúp làm cho các JavaDelegate hoặc script của bạn trở nên chung chung và dễ tái sử dụng hơn.
  • Bằng cách kết hợp mapping với Expression Language và Spin, bạn có thể thực hiện nhiều tác vụ tích hợp ngay trên mô hình mà không cần viết mã Java.

Xem trước bài học tiếp theo

Chúng ta đã học cách xử lý luồng dữ liệu cho "happy path" (trường hợp thành công). Nhưng điều gì sẽ xảy ra nếu API báo lỗi, hoặc JavaDelegate của chúng ta gặp một ngoại lệ? Trong bài học tiếp theo, "Implement cơ chế xử lý lỗi (error handling) trong Java delegates", chúng ta sẽ tìm hiểu sâu hơn về cách xử lý các tình huống không mong muốn, phân biệt giữa lỗi kỹ thuật và lỗi nghiệp vụ, và sử dụng các cơ chế của Camunda như Incident và BPMN Error để xây dựng các quy trình mạnh mẽ và đáng tin cậy.