Skip to main content

Welcome to a Grasp lesson

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

Lesson 26

Injecting Spring Beans into Camunda Delegates

Recommended resources

Chào bạn,

Trong bài học trước, chúng ta đã tạo ra một JavaDelegate đầu tiên, ExpressShipmentPreparer, và biến nó thành một Spring bean bằng annotation @Component. Chúng ta đã liên kết nó với một Service Task thông qua Delegate Expression. Đây là một bước tiến lớn, nhưng lớp delegate của chúng ta vẫn còn khá "cô độc", tự mình xử lý mọi logic.

Hôm nay, chúng ta sẽ giải quyết vấn đề đó. Bài học này tập trung vào mục tiêu: Inject các Spring beans vào trong các lớp Java delegate của Camunda. Đây là một kỹ thuật cực kỳ quan trọng, giúp bạn xây dựng các ứng dụng Camunda mạnh mẽ, dễ bảo trì và tuân thủ các nguyên tắc thiết kế phần mềm tốt.

Với kinh nghiệm làm developer của bạn, bạn sẽ thấy khái niệm này rất quen thuộc. Chúng ta sẽ áp dụng một trong những sức mạnh cốt lõi của Spring Framework - Dependency Injection - vào các quy trình nghiệp vụ của mình.

1. Tại sao cần Inject Beans vào Delegates?

Hãy tưởng tượng lớp JavaDelegate của bạn như một người quản lý dự án. Người quản lý không tự mình làm mọi việc (viết code, thiết kế database, kiểm thử). Thay vào đó, họ điều phối công việc và giao cho các chuyên gia (developers, DBAs, QAs).

Tương tự, một JavaDelegate không nên chứa đựng logic nghiệp vụ phức tạp. Vai trò chính của nó là điều phối (coordinate) hoặc chuyển đổi (adapt).

  • Điều phối: Nó nhận yêu cầu từ process engine, gọi một hoặc nhiều service khác để thực hiện công việc, và sau đó cập nhật lại trạng thái cho process engine.
  • Chuyển đổi: Nó lấy dữ liệu từ các biến quy trình và chuyển đổi chúng thành định dạng mà một service khác cần, và ngược lại.

Việc tách logic nghiệp vụ ra khỏi các lớp delegate và đưa vào các lớp Service riêng biệt mang lại nhiều lợi ích:

  • Tuân thủ Nguyên tắc Đơn trách nhiệm (Single Responsibility Principle): Mỗi lớp làm một việc duy nhất và làm tốt việc đó. Delegate chỉ lo điều phối, Service chỉ lo nghiệp vụ.
  • Tái sử dụng (Reusability): Cùng một PaymentService có thể được sử dụng bởi nhiều quy trình hoặc thậm chí bởi các phần khác của ứng dụng không liên quan đến Camunda.
  • Dễ kiểm thử (Testability): Bạn có thể viết unit test cho các lớp Service một cách độc lập mà không cần khởi tạo toàn bộ Camunda engine.
  • Dễ bảo trì (Maintainability): Khi logic nghiệp vụ thay đổi, bạn chỉ cần sửa ở một nơi (lớp Service) thay vì phải tìm kiếm trong tất cả các delegate.

2. Dependency Injection hoạt động như thế nào?

Cơ chế giúp chúng ta kết nối các "chuyên gia" (các lớp Service) với "người quản lý" (lớp JavaDelegate) chính là Dependency Injection (DI).

Trong Spring, DI là một phần của nguyên lý lớn hơn gọi là Inversion of Control (IoC). Thay vì bạn tự tạo các đối tượng và các phụ thuộc của chúng (new MyService()), bạn giao quyền kiểm soát đó cho Spring Container. Bạn chỉ cần khai báo "Tôi cần một đối tượng MyService", và Spring sẽ tìm hoặc tạo ra nó rồi "tiêm" (inject) vào lớp của bạn.

Annotation phổ biến nhất để thực hiện việc này là @Autowired.

Spring Boot Dependency Injection Concept
Hình ảnh này minh họa khái niệm cốt lõi của Dependency Injection. `CarService` cần một `EngineService`. Thay vì `CarService` tự tạo `EngineService`, `Spring Container` sẽ cung cấp (inject) `EngineService` cho `CarService`. Tương tự, Spring Container sẽ inject các lớp Service của chúng ta vào `JavaDelegate`.

Điều kỳ diệu ở đây là: Vì JavaDelegate của chúng ta đã là một Spring bean, Spring Container có thể inject các bean khác vào nó. Đây chính là lý do tại sao việc sử dụng Delegate Expression (#{beanName}) ở bài trước lại quan trọng đến vậy. Nó đảm bảo rằng Camunda yêu cầu Spring cung cấp delegate, và Spring sẽ trả về một bean đã được "trang bị đầy đủ" các dependency cần thiết.


3. Tìm hiểu sâu hơn: Cách Spring tìm thấy Beans

Bạn có thể thắc mắc: "Làm thế nào Spring biết được các lớp nào là bean để quản lý và inject?". Câu trả lời nằm ở cơ chế Component Scanning.

Khi ứng dụng Spring Boot khởi động, nó sẽ tự động quét (scan) các package bắt đầu từ package chứa lớp Application chính của bạn, tìm kiếm các lớp được đánh dấu bằng các annotation như @Component, @Service, @Repository, v.v. và đăng ký chúng làm bean trong container.

Để hiểu rõ hơn về cơ chế này, bạn hãy xem video sau. Video được trình bày bằng tiếng Việt và sẽ giúp củng cố kiến thức nền tảng về Spring của bạn.

Học Spring Boot #5 | Lifecycle của Bean, Cơ chế Lazy và cách Spring scan Bean | nutadev

Video 'Học Spring Boot #5' từ kênh nutadev giải thích rất rõ về cách Spring Boot sử dụng Component Scan để khám phá và tạo ra các bean.

Hãy xem phần đầu của video với tiêu đề 'Component Scan' (00:12 - 12:25). Chú ý cách Spring Boot mặc định quét các package và cách bạn có thể tùy chỉnh nó. Điều này giải thích tại sao Spring có thể 'nhìn thấy' các lớp JavaDelegate và Service mà chúng ta tạo ra miễn là chúng nằm trong cùng package hoặc các package con của lớp Application chính.


4. Thực hành: Tách logic nghiệp vụ ra khỏi Delegate

Bây giờ, chúng ta sẽ tái cấu trúc (refactor) lại ví dụ ở bài học trước. Lớp ExpressShipmentPreparer đang tự tạo mã vận đơn. Chúng ta sẽ tách logic này ra một lớp ShipmentService chuyên dụng.

Bước 1: Tạo ShipmentService

Trong dự án của bạn, hãy tạo một lớp Java mới tên là ShipmentService.java trong cùng một package. Lớp này sẽ chứa logic tạo mã vận đơn.

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component // Đánh dấu đây là một Spring bean
public class ShipmentService {

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

    public String generateTrackingNumber(String orderId, String shippingMethod) {
        LOGGER.info("Đang tạo mã vận đơn cho đơn hàng {} với phương thức {}", orderId, shippingMethod);
        
        String prefix = "STD-"; // Mặc định là vận chuyển thường
        if ("express".equalsIgnoreCase(shippingMethod)) {
            prefix = "EXP-";
        }
        
        return prefix + orderId + "-" + System.currentTimeMillis();
    }
}

Lưu ý: Chúng ta chỉ cần dùng @Component. Spring đủ thông minh để tìm thấy và quản lý nó. Lớp này giờ đây chứa logic nghiệp vụ thuần túy và không biết gì về Camunda.

Bước 2: Inject ShipmentService vào JavaDelegate

Bây giờ, hãy quay lại lớp ExpressShipmentPreparer.java và sửa đổi nó để sử dụng ShipmentService vừa tạo.

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

import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component("expressShipmentPreparer")
public class ExpressShipmentPreparer implements JavaDelegate {

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

    // Báo cho Spring inject một instance của ShipmentService vào đây
    @Autowired
    private ShipmentService shipmentService;

    @Override
    public void execute(DelegateExecution execution) throws Exception {
        String orderId = (String) execution.getVariable("orderId");
        LOGGER.info("Đang điều phối việc chuẩn bị vận đơn HỎA TỐC cho đơn hàng: {}", orderId);

        // Ủy thác công việc cho service chuyên dụng
        String trackingNumber = shipmentService.generateTrackingNumber(orderId, "express");

        // Lưu kết quả trở lại quy trình
        execution.setVariable("trackingNumber", trackingNumber);
        
        LOGGER.info("Đã tạo mã vận đơn hỏa tốc: {}", trackingNumber);
    }
}

Phân tích sự thay đổi:

  1. Chúng ta thêm một trường private ShipmentService shipmentService;.
  2. Chúng ta chú thích nó với @Autowired. Khi Spring tạo bean expressShipmentPreparer, nó sẽ thấy annotation này và tự động "tiêm" một instance của ShipmentService vào trường đó.
  3. Trong phương thức execute, chúng ta không tự tạo mã vận đơn nữa mà gọi shipmentService.generateTrackingNumber(...). Lớp delegate giờ chỉ đóng vai trò điều phối.

Bước 3: Tạo Delegate cho vận chuyển thường

Để thấy rõ sức mạnh của việc tái sử dụng, chúng ta sẽ tạo thêm một delegate cho nhánh "Vận chuyển thường" và cũng sử dụng ShipmentService.

Tạo lớp StandardShipmentPreparer.java:

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

import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component("standardShipmentPreparer")
public class StandardShipmentPreparer implements JavaDelegate {

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

    @Autowired
    private ShipmentService shipmentService;

    @Override
    public void execute(DelegateExecution execution) throws Exception {
        String orderId = (String) execution.getVariable("orderId");
        LOGGER.info("Đang điều phối việc chuẩn bị vận đơn THƯỜNG cho đơn hàng: {}", orderId);

        String trackingNumber = shipmentService.generateTrackingNumber(orderId, "standard");

        execution.setVariable("trackingNumber", trackingNumber);
        
        LOGGER.info("Đã tạo mã vận đơn thường: {}", trackingNumber);
    }
}

Bước 4: Cập nhật mô hình BPMN và kiểm tra

  1. Mở file BPMN của bạn trong Camunda Modeler.
  2. Trong nhánh "Vận chuyển thường", thêm một Service Task và đặt tên là "Chuẩn bị vận chuyển thường".
  3. Chọn Service Task này, trong Properties Panel, chọn Implementation là Delegate Expression và điền #{standardShipmentPreparer}.
  4. Lưu lại file BPMN.
  5. Khởi động lại ứng dụng Spring Boot của bạn.
  6. Dùng Postman để bắt đầu một quy trình mới.
  7. Vào Tasklist, hoàn thành User Task "Xác định phương thức vận chuyển" và lần này, đặt giá trị biến shippingMethod là standard (hoặc bất cứ giá trị nào khác express).
  8. Kiểm tra trong Cockpit. Bạn sẽ thấy quy trình đã đi qua nhánh "Chuẩn bị vận chuyển thường" và một biến trackingNumber mới được tạo ra với tiền tố STD-.
  9. Kiểm tra console log của ứng dụng, bạn sẽ thấy các log từ cả StandardShipmentPreparer và ShipmentService.

Bạn đã tái cấu trúc thành công! Cùng một ShipmentService giờ đây được tái sử dụng bởi hai JavaDelegate khác nhau.


Tổng kết

Bài học hôm nay đã trang bị cho bạn một kỹ thuật thiết yếu để xây dựng các ứng dụng Camunda có cấu trúc tốt.

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

  • Tách biệt logic nghiệp vụ ra khỏi JavaDelegate và đặt chúng vào các lớp Service riêng biệt.
  • JavaDelegate nên đóng vai trò là người điều phối, không phải là người thực thi chính.
  • Sử dụng annotation @Autowired của Spring để inject các bean (ví dụ: các lớp Service) vào trong các JavaDelegate.
  • Điều này chỉ hoạt động khi JavaDelegate của bạn cũng là một Spring bean và được gọi thông qua Delegate Expression.
  • Phương pháp này giúp mã nguồn của bạn trở nên module hóa, dễ kiểm thử, dễ tái sử dụng và dễ bảo trì hơn.

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

Bây giờ JavaDelegate của chúng ta đã có thể "nói chuyện" với các service khác bên trong cùng ứng dụng. Nhưng trong thực tế, các quy trình thường cần tương tác với thế giới bên ngoài, ví dụ như hệ thống của đối tác, các dịch vụ thanh toán, hoặc các microservice khác.

Trong bài học tiếp theo, "Gọi một REST API bên ngoài từ Service Task và xử lý kết quả trả về", chúng ta sẽ tìm hiểu cách làm cho ShipmentService (hoặc một service mới) có thể gọi một API của bên thứ ba, nhận kết quả và đưa dữ liệu đó trở lại quy trình Camunda.