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를 그대로 사용하도록 변경할 수 있습니다.
XxlJobServiceImpl의add메소드에서 ID 할당 로직 제거- SQL 매퍼에서
INSERT구문 수정
6. 테스트
- 관리자 웹 페이지에서 기존 작업 목록 확인
- Postman 등 API 도구로
/create호출 → 새 작업 5초 간격 실행 확인 /stop/{id}호출 → 작업 중지 상태 변경/list호출 → 전체 작업 목록 조회
위 패턴을 응용하여 start, delete, update 등도 동일한 방식으로 호출할 수 있습니다. 각 API를 조합하면 복잡한 비즈니스 시나리오를 코드 레벨에서 자유롭게 제어할 수 있습니다.