Chào các bạn!

Mở bài quen thuộc 🙂 Các bạn đã từng sử dụng hoặc từng nghe đến “cronjob” trên hosting chưa, hoặc là “crontab” trong linux? Nếu rồi thì các bạn sẽ rất dễ để tiếp cận bài viết này.

Cronjob trên hosting

Crontab trong linux

Nếu chưa từng nghe thì thử nhìn ảnh dưới đây.

Khá hài hước, đây là cách thức chung của nhiều bạn sinh viên, trong đó có mình. Nhưng giờ thì đỡ nhiều rồi, mình kệ khi nào tỉnh dậy thì đi chứ chả cần báo thức =)) Vâng, đây là chức năng hẹn giờ báo thức trên điện thoại. Dù bạn có dùng smartphone hay điện thoại cục gạch đi nữa thì chức năng Báo thức vẫn là thứ không thể thiếu trên điện thoại. Bấm vào chi tiết xem có gì.

Ồ, hẹn giờ theo thứ trong tuần. Cùng là tính năng hẹn giờ, nhưng ở mức độ cao cấp hơn, ví dụ như tạo email Meeting Request, bạn có thể đặt các lịch định kỳ theo ngày trong tháng, hoặc ngày trong năm, hoặc nhiều kiểu lặp lại khác.

Vậy câu hỏi là tính năng hẹn giờ hay đặt lịch đó hoạt động bằng cách nào, tại sao nó lại thực hiện yêu cầu của mình đúng theo thời điểm mình khai báo được!

Nếu tự xây dựng các bạn hoàn toàn có thể làm bằng cách làm 1 chương trình chạy vô tận, cứ mỗi giây thì lại kiểm tra thời gian hiện tại 1 phát, xem hiện tại là ngày bao nhiêu, thứ mấy, tháng mấy, năm nào,… vân vân mây mây. Sau đó đem so sánh với 1 loại các cấu hình hẹn giờ đã khai báo xem trùng khớp với cái nào thì chạy nghiệp vụ của cái đó. Nghe thì thế nhưng cũng khá mất công đấy, quản lý thread không tốt thì nó tiêu tốn tài nguyên hệ thống không ít, không có chức năng giám sát hay cảnh báo thì nó treo/chết lúc nào chả biết.

Nhưng rất may là Spring nó đã đẻ ra 1 thằng hỗ trợ tận răng vụ này. Mình xin giới thiệu cho các bạn nào chưa biết đến Spring Schedule. Spring Schedule xử lý hết việc đọc cấu hình đặt lịch, quét lịch và chạy nghiệp vụ khi đến lịch, quản lý tiến trình. Công việc của bạn bây giờ chỉ còn là khai báo lịch muốn chạy, và viết hàm main chạy nghiệp vụ đơn giản như nhập môn lập trình 😀

Dài dòng đủ rồi, giờ cụ tỷ cách thực hiện như sau

Bước 1: Đầu tiên với project sử dụng Maven thì bạn cần add dependency spring schedule vào file pom.xml. Spring Schedule nằm trong gói spring-context, nếu pom.xml của bạn chưa có thì add thêm vào

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
</dependency>

Bước 2: Tạo một class hoặc một method nghiệp vụ cần chạy ngầm

package net.tunghuynh.schedule;

import java.util.Date;

public class ShowTime {
    public static void main(String[] args) {
        System.out.println("Now is " + new Date());
    }
}

Bước 3: Tạo file cấu hình đặt lịch

package net.tunghuynh.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import net.tunghuynh.schedule.*;

/**
 * net.tunghuynh.config.ScheduleConfig
 * TungHuynh
 * Date 05/10/2019 11:05 AM
 */
@Configuration
@EnableScheduling
public class ScheduleConfig {

    @Scheduled(cron = "0 * * * * *")//Chạy vào 0s của mỗi phút
    public void showTime() {
        ShowTime.main(null);
    }
}

Bước 4: Chạy thử chương trình và đợi kết quả.

Now is Sat Oct 05 11:21:00 ICT 2019
Now is Sat Oct 05 11:22:00 ICT 2019
Now is Sat Oct 05 11:23:00 ICT 2019
Now is Sat Oct 05 11:24:00 ICT 2019
Now is Sat Oct 05 11:25:00 ICT 2019

Easy game 😀

Nhưng đây mới chỉ là cách tạo tiến trình cơ bản nhất theo đúng cách học của sinh viên, chưa có tính linh động về cấu hình và cũng chưa có tính mở rộng cao.

Để thuận tiện cho cấu hình đặt lịch, thì ta phải chuyển cái cấu hình đó ra file application.properties của project. Thêm một cái cấu hình để cho phép Enable hay Disable module tiến trình trong trường hợp cần thiết.

package net.tunghuynh.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import net.tunghuynh.schedule.*;

/**
 * net.tunghuynh.config.ScheduleConfig
 * TungHuynh
 * Date 05/10/2019 11:05 AM
 */
@Configuration
@EnableScheduling
@ConditionalOnProperty(value = "schedule.enabled", matchIfMissing = true, havingValue = "true")
public class ScheduleConfig {

    @Scheduled(cron = "${schedule.clear.schedule.log}")
    public void clearScheduleLog() {
        new ClearScheduleLog();
    }
}

Để tăng tính mở rộng, giảm thiểu khối lượng duplicate code thì ta phải viết khung tiến trình theo kiểu abstract, các tiến trình nghiệp vụ chỉ cần extend abstract đó và chỉ xử lý liên quan đến nghiệp vụ thôi, không cần duplicate toàn bộ khung nữa. Đồng thời các tham số nghiệp vụ cho tiến trình (ví dụ xóa log đã lưu trữ quá 6 tháng chẳng hạn) thì nên khai báo trong Cơ sở dữ liệu để tiện cho việc sửa tiến trình mà không cần deploy lại hệ thống… vân vân mây mây.

Như vậy cần tạo một abstract class khung tiến trình cơ bản như sau

package net.tunghuynh.schedule;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * net.tunghuynh.schedule.ScheduleManager
 * TungHuynh
 * Date 05/10/2019 11:10 AM
 */
public abstract class ScheduleManager {
    Logger logger = LoggerFactory.getLogger(getClass());

    private String scheduleCode;
    private Map params = new HashMap();

    public ScheduleManager(String scheduleCode) {
        this.scheduleCode = scheduleCode;
        this.process();
    }

    /**
     * Load tham số nghiệp vụ từ DB
     */
    protected void loadParametersFromDB() {
        logger.info("Start load parameters from DB for " + this.scheduleCode);
        //Load cấu hình tham số nghiệp vụ cho tiến trình từ DB theo scheduleCode
        //Fill danh sách tham số vào this.params
    }

    /**
     * Validate tham số trước khi chạy
     * @return
     * @throws Exception
     */
    private boolean validate() throws Exception {
        logger.info("Validate parameter for " + this.scheduleCode);
        return validateParameters();
    }

    /**
     * Dành cho tiến trình implement các khởi tạo tham số
     */
    protected abstract void initParameters();

    /**
     * Dành cho tiến trình implement các validate nghiệp vụ
     * @return
     * @throws Exception
     */
    protected abstract boolean validateParameters() throws Exception;

    /**
     * Dành cho tiến trình implement nghiệp vụ cụ thể
     * @throws Exception
     */
    protected abstract void run() throws Exception;

    /**
     * Hàm xử lý chính của khung tiến trình
     */
    private void process() {
        long start = new Date().getTime();
        logger.info("START " + this.scheduleCode);
        try {
            loadParametersFromDB();
            initParameters();
            if (validate()) {
                logDebug("Loaded params and running");
                run();
                logDebug("Done in " + (new Date().getTime() - start) + "ms");
            } else {
                logDebug("Loaded params and not running");
            }
        } catch (Exception e) {
            logger.error(this.scheduleCode + ": " + e.toString(), e);
            logError(e.toString());
        } finally {
            logger.info("END " + this.scheduleCode + " in " + (new Date().getTime() - start) + "ms");
        }
    }

    protected final void addParam(String key, Object value) {
        if (this.params == null) {
            this.params = new HashMap();
        }
        this.params.put(key, value);
    }

    protected final Object getParam(String key) {
        if (this.params == null) {
            return null;
        }
        return this.params.get(key);
    }

    protected final String getParamAsString(String key, String defaultValue) {
        Object value = getParam(key);
        return value==null?defaultValue:value.toString();
    }

    protected final Integer getParamAsInteger(String key, Integer defaultValue) {
        String ret = getParamAsString(key, null);
        if (ret == null) {
            return defaultValue;
        }
        try {
            return Integer.valueOf(ret);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    protected final Map getAllParams() {
        return this.params == null ? new HashMap() : this.params;
    }

    protected void logDebug(String content) {
        logger.debug(content);
    }

    protected void logInfo(String content) {
        logger.info(content);
    }

    protected void logError(String content) {
        logger.error(content);
    }
}

Tiếp đó tạo class nghiệp vụ như sau

package net.tunghuynh.schedule;

import net.tunghuynh.schedule.ScheduleManager;

/**
 * net.tunghuynh.schedule.ClearScheduleLog
 * TungHuynh
 * Date 05/10/2019 11:16 AM
 */
public class ClearScheduleLog extends ScheduleManager {

    private int numOfExpiredDays;

    public ClearScheduleLog() {
        super("CLEAR_SCHEDULE_LOG");
    }

    @Override
    protected void initParameters() {
        this.numOfExpiredDays = getParamAsInteger("numOfExpiredDays", 180);
    }

    @Override
    protected boolean validateParameters() throws Exception {
        return true;
    }

    @Override
    public void run() {
        int result = 0;
        //Xử lý xóa log theo tham số quá hạn numOfExpiredDays
        logInfo("Deleted " + result + " record(s)");
    }
}

Vậy là các bạn đã dựng được một module tiến trình tương đối và sử dụng tốt trong phần lớn các trường hợp, áp dụng được từ hệ thống nhỏ đến hệ thống to. Mà độ rủi ro lại thấp, quản lý được log từ log4j trên server luôn.

Phần cuối cùng, quay lại chủ đề ban đầu. Giờ muốn đặt lịch cho nghiệp vụ kia chạy theo thứ trong tuần, ngày trong tháng, theo giờ phút giây, hoặc bất kỳ loại định kỳ khác thì như nào?

Schedule Annotation của Spring hỗ trợ 3 loại đặt lịch chính

  • cron: Đặt lịch theo cấu hình chuỗi tương tự cronjob/crontab
  • fixedDelay: Cấu hình thời gian chờ giữa thời điểm hoàn thành của lần chạy trước với thời điểm bắt đầu của lần chạy sau. Nghĩa là sau khi hoàn thành lần chạy 1 thì phải chờ fixedDelay (tính theo mili giây) thì mới start lần tiếp theo
  • fixedRate: Cấu hình thời gian chờ giữa các thời điểm bắt đầu. Nghĩa là sau khi start lần chạy 1 thì phải chờ fixedRate (tính theo mili giây) thì mới start lần tiếp theo. Không quan trọng lần 1 đã chạy xong hay chưa.

Các bạn có thể xem ảnh dưới để hình dung dễ hơn về fixedDelayfixedRate

Đối với fixedDelayfixedRate thì đơn giản dễ hiểu, còn đối với cron thì hơi phức tạp chút, ví dụ như báo thức của mình trên đầu bài viết là 7h sáng các ngày từ thứ 2 đến thứ 6 thì cấu hình cron như sau

0 0 7 * * MON-FRI

Nghĩa là 0 giờ, 0 phút, 7 giờ, các ngày trong tháng, các tháng trong năm, từ thứ MON đến FRI

Các bạn có thể xem ảnh mô tả dưới đây để dễ hình dung hơn

Cấu trúc của 1 chuỗi cron gồm 7 dấu tương ứng với 7 tham số, riêng dấu cuối cùng (Year) có thể có hoặc không. Trường hợp cần đặt lịch theo Year nhưng lại không cần theo Day of Week thì dấu * thứ 6 chuyển thành dấu ? nghĩa là không có giá trị

Một số ví dụ khác

Chạy 12h trưa hàng ngày

0 0 12 * * *

Chạy vào 1h sáng và 12h trưa hàng ngày

0 0 1,12 * * *

Chạy 10s 1 lần (0s, 10s, 20s, …. )

*/10 * * * * *

Chạy 30 phút 1 lần

0 */30 * * * *

Mỗi ngày 1 lần vào 6h tối, chỉ chạy trong năm 2019

0 0 18 * * ? 2019

Các bạn có thể xem thêm nhiều ví dụ chi tiết hơn tại đây:

https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm

Hi vọng các bạn áp dụng được vào các trường hợp thực tế, hoặc ít nhất là các bài tập lớn, thay vì việc phải dùng new Thread() hay cái gì đó để tạo ra cái mà bạn gọi là tiến trình 🙂

One Reply to “[Java] Phần 16 – Tạo tiến trình chạy ngầm cực kỳ đơn giản với Spring Schedule”

  1. tích hợp các time này em nghĩ sẽ giảm tải được ứng dụng chạy trên server tránh discount server 100% đc tốt hơn

Bình luận