XXL-JOB 커스텀 API 레이어를 통한 동적 작업 생성 및 관리

1. 소스 코드 다운로드

XXL-JOB 2.3.0 버전의 소스 코드를 공식 저장소에서 내려받습니다.

git clone https://gitee.com/xuxueli0323/xxl-job.git

2. 데이터베이스 초기화

소스 코드 내 doc/db/tables_xxl_job.sql 파일을 실행하여 필요한 테이블을 생성합니다.

3. 관리자(Admin) 설정

application.properties 파일에서 데이터베이스 연결 정보와 포트를 수정합니다.

  • 관리자 인터페이스 주소: http://localhost:9090/xxl-job-admin
  • 기본 계정: admin / 123456

4. 관리자 모듈에 동적 API 컨트롤러 추가

소스 코드의 관리자 모듈(xxl-job-admin)에 작업을 동적으로 생성, 수정, 삭제, 시작, 중지할 수 있는 REST API 엔드포인트를 추가합니다.

4.1 MyDynamicApiController

package com.xxl.job.admin.controller;

import com.xxl.job.admin.controller.annotation.PermissionLimit;
import com.xxl.job.admin.core.cron.CronExpression;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobQuery;
import com.xxl.job.admin.core.thread.JobScheduleHelper;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.service.LoginService;
import com.xxl.job.admin.service.XxlJobService;
import com.xxl.job.core.biz.model.ReturnT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.ParseException;
import java.util.Date;
import java.util.Map;

@RestController
@RequestMapping("/api/job")
public class MyDynamicApiController {

    private static final Logger log = LoggerFactory.getLogger(MyDynamicApiController.class);

    @Autowired
    private XxlJobService jobService;

    @Autowired
    private LoginService loginService;

    @PostMapping("/query")
    public Map<String, Object> queryJobs(@RequestBody XxlJobQuery query) {
        return jobService.pageList(
                query.getStart(),
                query.getLength(),
                query.getJobGroup(),
                query.getTriggerStatus(),
                query.getJobDesc(),
                query.getExecutorHandler(),
                query.getAuthor()
        );
    }

    @PostMapping("/save")
    public ReturnT<String> saveJob(@RequestBody XxlJobInfo jobInfo) {
        long nextTriggerTime;
        try {
            Date nextValidTime = new CronExpression(jobInfo.getScheduleConf())
                    .getNextValidTimeAfter(new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS));
            if (nextValidTime == null) {
                return new ReturnT<>(ReturnT.FAIL_CODE, I18nUtil.getString("jobinfo_field_cron_never_fire"));
            }
            nextTriggerTime = nextValidTime.getTime();
        } catch (ParseException e) {
            log.error("크론 표현식 파싱 오류: {}", e.getMessage(), e);
            return new ReturnT<>(ReturnT.FAIL_CODE, I18nUtil.getString("jobinfo_field_cron_unvalid") + " | " + e.getMessage());
        }

        jobInfo.setTriggerStatus(1);
        jobInfo.setTriggerLastTime(0);
        jobInfo.setTriggerNextTime(nextTriggerTime);
        jobInfo.setUpdateTime(new Date());

        if (jobInfo.getId() == 0) {
            return jobService.add(jobInfo);
        } else {
            return jobService.update(jobInfo);
        }
    }

    @GetMapping("/delete")
    public ReturnT<String> deleteJob(int id) {
        return jobService.remove(id);
    }

    @GetMapping("/start")
    public ReturnT<String> startJob(int id) {
        return jobService.start(id);
    }

    @GetMapping("/stop")
    public ReturnT<String> stopJob(int id) {
        return jobService.stop(id);
    }

    @GetMapping("/login")
    @PermissionLimit(limit = false)
    public ReturnT<String> login(HttpServletRequest request,
                                  HttpServletResponse response,
                                  String userName,
                                  String password,
                                  String ifRemember) {
        boolean rememberMe = (ifRemember != null && !ifRemember.isEmpty() && "on".equals(ifRemember));
        return loginService.login(request, response, userName, password, rememberMe);
    }
}

4.2 XxlJobQuery 데이터 모델

package com.xxl.job.admin.core.model;

public class XxlJobQuery {
    private int start;
    private int length;
    private int triggerStatus;
    private String jobDesc;
    private String executorHandler;
    private String author;
    private int jobGroup;

    // getter & setter 생략 (본문과 동일)
}

5. 실행기(Executor) 프로젝트에 연동

실행기 애플리케이션에서 관리자 서버의 API를 호출하여 작업을 제어합니다.

5.1 의존성 추가 (pom.xml)

<dependency>
    <groupId>commons-httpclient</groupId>
    <artifactId>commons-httpclient</artifactId>
    <version>3.1</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>

5.2 XxlJobUtil – HTTP 통신 유틸리티

package com.xxl.job.executor.controller;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;

import java.io.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class XxlJobUtil {

    private static String sessionCookie = "";

    /**
     * "yyyy-MM-dd HH:mm:ss" 형식의 문자열을 Cron 표현식으로 변환
     */
    public static String dateToCron(String dateStr) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
        Date dt = sdf.parse(dateStr);

        String sec = String.format("%tS", dt);
        String min = String.format("%tM", dt);
        String hour = String.format("%tH", dt);
        String day = String.format("%td", dt);
        String month = String.format("%tm", dt);

        return String.format("%s %s %s %s %s ?", sec, min, hour, day, month);
    }

    public static JSONObject queryJobList(String baseUrl, JSONObject params) throws IOException {
        return doPost(baseUrl + "/api/job/query", params);
    }

    public static JSONObject addOrUpdateJob(String baseUrl, JSONObject jobData) throws IOException {
        return doPost(baseUrl + "/api/job/save", jobData);
    }

    public static JSONObject deleteJob(String baseUrl, int jobId) throws IOException {
        return doGet(baseUrl + "/api/job/delete?id=" + jobId);
    }

    public static JSONObject startJob(String baseUrl, int jobId) throws IOException {
        return doGet(baseUrl + "/api/job/start?id=" + jobId);
    }

    public static JSONObject stopJob(String baseUrl, int jobId) throws IOException {
        return doGet(baseUrl + "/api/job/stop?id=" + jobId);
    }

    private static JSONObject doPost(String url, JSONObject body) throws IOException {
        HttpClient client = new HttpClient();
        PostMethod post = new PostMethod(url);
        post.setRequestHeader("Cookie", sessionCookie);
        post.setRequestEntity(new StringRequestEntity(body.toString(), "application/json", "UTF-8"));
        client.executeMethod(post);
        return parseResponse(post);
    }

    private static JSONObject doGet(String url) throws IOException {
        HttpClient client = new HttpClient();
        GetMethod get = new GetMethod(url);
        get.setRequestHeader("Cookie", sessionCookie);
        client.executeMethod(get);
        return parseResponse(get);
    }

    private static JSONObject parseResponse(HttpMethod method) throws IOException {
        if (method.getStatusCode() == HttpStatus.SC_OK) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(method.getResponseBodyAsStream()));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            reader.close();
            return JSONObject.parseObject(sb.toString());
        }
        return null;
    }

    /**
     * 로그인 후 세션 쿠키 저장
     */
    public static String login(String baseUrl, String user, String pass) throws IOException {
        String loginUrl = baseUrl + "/api/job/login?userName=" + user + "&password=" + pass;
        HttpClient client = new HttpClient();
        GetMethod get = new GetMethod(loginUrl);
        client.executeMethod(get);

        if (get.getStatusCode() == 200) {
            StringBuilder cookieBuilder = new StringBuilder();
            for (Cookie c : client.getState().getCookies()) {
                cookieBuilder.append(c.toString()).append(";");
            }
            sessionCookie = cookieBuilder.toString();
        } else {
            sessionCookie = "";
        }
        return sessionCookie;
    }
}

5.3 XxlJobController – 예시 컨트롤러

package com.xxl.job.executor.controller;

import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.Date;

@RestController
public class XxlJobController {

    @Value("${xxl.job.admin.addresses}")
    private String adminUrl;

    @Value("${xxl.username}")
    private String adminUser;

    @Value("${xxl.password}")
    private String adminPass;

    @GetMapping("/datetocron")
    public String convertDateToCron(@RequestParam("date") String dateStr) throws Exception {
        return XxlJobUtil.dateToCron(dateStr);
    }

    @GetMapping("/list")
    public Object listJobs() throws IOException {
        JSONObject params = new JSONObject();
        params.put("length", 10);
        XxlJobUtil.login(adminUrl, adminUser, adminPass);
        return XxlJobUtil.queryJobList(adminUrl, params).get("data");
    }

    @GetMapping("/create")
    public String createJob() throws IOException {
        XxlJobInfo info = new XxlJobInfo();
        info.setScheduleConf("0/5 * * * * ?");
        info.setJobGroup(1);
        info.setJobDesc("동적 생성 테스트");
        info.setAddTime(new Date());
        info.setUpdateTime(new Date());
        info.setAuthor("dev");
        info.setAlarmEmail("admin@example.com");
        info.setScheduleType("CRON");
        info.setMisfireStrategy("DO_NOTHING");
        info.setExecutorRouteStrategy("FIRST");
        info.setExecutorHandler("demoJobHandler");
        info.setExecutorParam("paramValue");
        info.setExecutorBlockStrategy("SERIAL_EXECUTION");
        info.setExecutorTimeout(0);
        info.setExecutorFailRetryCount(1);
        info.setGlueType("BEAN");
        info.setGlueSource("");
        info.setGlueRemark("GLUE 초기화");
        info.setGlueUpdatetime(new Date());

        XxlJobUtil.login(adminUrl, adminUser, adminPass);
        JSONObject resp = XxlJobUtil.addOrUpdateJob(adminUrl, (JSONObject) JSONObject.toJSON(info));
        return (resp != null && resp.getInteger("code") == 200) ? "작업 생성 성공" : "작업 생성 실패";
    }

    @GetMapping("/stop/{jobId}")
    public String stop(@PathVariable Integer jobId) throws IOException {
        XxlJobUtil.login(adminUrl, adminUser, adminPass);
        JSONObject resp = XxlJobUtil.stopJob(adminUrl, jobId);
        return (resp != null && resp.getInteger("code") == 200) ? "작업 중지 성공" : "작업 중지 실패";
    }

    @GetMapping("/delete/{jobId}")
    public String delete(@PathVariable Integer jobId) throws IOException {
        XxlJobUtil.login(adminUrl, adminUser, adminPass);
        JSONObject resp = XxlJobUtil.deleteJob(adminUrl, jobId);
        return (resp != null && resp.getInteger("code") == 200) ? "작업 삭제 성공" : "작업 삭제 실패";
    }

    @GetMapping("/start/{jobId}")
    public String start(@PathVariable Integer jobId) throws IOException {
        XxlJobUtil.login(adminUrl, adminUser, adminPass);
        JSONObject resp = XxlJobUtil.startJob(adminUrl, jobId);
        return (resp != null && resp.getInteger("code") == 200) ? "작업 시작 성공" : "작업 시작 실패";
    }
}

5.4 설정 파일 (application.yml)

server:
  port: 8084

xxl:
  username: admin
  password: 123456
  job:
    admin:
      addresses: http://127.0.0.1:9090/xxl-job-admin
    accessToken:
    executor:
      appname: xxl-job-executor-sample
      address:
      ip:
      port: 9998
      logpath: /data/logs/xxljob
      logretentiondays: 30

5.5 작업 ID 직접 지정

작업 ID를 비즈니스 키와 연결하고자 한다면, 관리자 모듈의 save 메소드 및 SQL INSERT 문을 수정하여 외부에서 전달된 ID를 그대로 사용하도록 변경할 수 있습니다.

  • XxlJobServiceImpladd 메소드에서 ID 할당 로직 제거
  • SQL 매퍼에서 INSERT 구문 수정

6. 테스트

  1. 관리자 웹 페이지에서 기존 작업 목록 확인
  2. Postman 등 API 도구로 /create 호출 → 새 작업 5초 간격 실행 확인
  3. /stop/{id} 호출 → 작업 중지 상태 변경
  4. /list 호출 → 전체 작업 목록 조회

위 패턴을 응용하여 start, delete, update 등도 동일한 방식으로 호출할 수 있습니다. 각 API를 조합하면 복잡한 비즈니스 시나리오를 코드 레벨에서 자유롭게 제어할 수 있습니다.

태그: XXL-JOB 동적 API 작업 스케줄러 Spring Boot REST API

5월 21일 08:38에 게시됨