서론
지난 포스트에서 Scheduler에 Listener를 적용했었는데, 로그를 출력하는것 외에는 활용하지 못했었다. 그러다가, Spring Batch의 Step처럼 하나의 작업이 끝나면 설정된 다음 작업으로 이어질 수 있도록 연결해보면 좋을것 같다는 생각이들었다.
그래서, 이번 포스트에서는 JobListener를 이용하여 Job을 순차적으로 실행시켜 보도록하겠다.
개발환경
- jdk-11.0.5
- quartz-2.3.2
<!-- Scheduler -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<!-- Scheduler -->
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- Logging -->
소스코드
1. MainJob.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package com.dochi.quartz.step;
import java.util.Date;
import org.quartz.InterruptableJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.UnableToInterruptJobException;
public class MainJob implements InterruptableJob {
private Thread currentThread = null;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
String jobName = context.getJobDetail().getKey().getName();
System.out.println(String.format("[%s][%s][%s] START", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
// 현재 Thread 저장
this.currentThread = Thread.currentThread();
try {
// 강제로 종료를 지연시키기
for(int i=1; i<=5; i++) {
System.out.println(String.format("[%s][%s][%s] 작업중...", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
Thread.sleep(1*1000L);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("[%s][%s][%s] END", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
}
@Override
public void interrupt() throws UnableToInterruptJobException {
// interrupt 설정
// - 강제종료
if( this.currentThread != null ) {
this.currentThread.interrupt();
}
}
}
|
cs |
2. SubJob.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package com.dochi.quartz.step;
import java.util.Date;
import org.quartz.InterruptableJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.UnableToInterruptJobException;
public class SubJob implements InterruptableJob {
private Thread currentThread = null;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
String jobName = context.getJobDetail().getKey().getName();
System.out.println(String.format("[%s][%s][%s] START", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
// 현재 Thread 저장
this.currentThread = Thread.currentThread();
try {
// 강제로 종료를 지연시키기
for(int i=1; i<=5; i++) {
System.out.println(String.format("[%s][%s][%s] 작업중...", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
Thread.sleep(1*1000L);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("[%s][%s][%s] END", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
}
@Override
public void interrupt() throws UnableToInterruptJobException {
// interrupt 설정
// - 강제종료
if( this.currentThread != null ) {
this.currentThread.interrupt();
}
}
}
|
cs |
3. JobStepListener.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
package com.dochi.quartz.step;
import java.util.Date;
import java.util.UUID;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
public class JobStepListener implements JobListener {
@Override
public String getName() {
return this.getClass().getName();
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
System.out.println(String.format("[%s][%s][jobToBeExecuted]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
System.out.println(String.format("[%s][%s][jobExecutionVetoed]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
System.out.println(String.format("[%s][%s][jobWasExecuted] START", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
try {
// JobDataMapp에 Next Step이 등록된 경우 스케줄 생성
if( jobDataMap.containsKey(JobLauncher.NEXT_STEP_CLASS_NAME) ) {
System.out.println("Add next step schedule...");
addNextStepSchedule(context);
}
} catch (ClassNotFoundException | SchedulerException e) {
e.printStackTrace();
}
System.out.println(String.format("[%s][%s][jobWasExecuted] END", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
}
// Next Step Schedule 등록 함수
private void addNextStepSchedule(JobExecutionContext context) throws SchedulerException, ClassNotFoundException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
String mainStepJobName = context.getJobDetail().getKey().getName();
String nextStepClassName = jobDataMap.getString(JobLauncher.NEXT_STEP_CLASS_NAME);
String nextStepJobName = jobDataMap.getString(JobLauncher.NEXT_STEP_JOB_NAME);
if( nextStepJobName == null ) {
// 이름이 없는 경우
// - 중복 방지를 위해 UUID 생성
nextStepJobName = UUID.randomUUID().toString();
}
// Next Step Class
// - 문자열로 Class 탐색
Class<?> jobClass = Class.forName(nextStepClassName);
// JobDetail 생성
// - NextStep에 MainStepName을 전달
JobDetail jobDetail = JobBuilder.newJob((Class<? extends Job>)jobClass)
.withIdentity(JobLauncher.PREFIX_NEXT_STEP_JOB_NAME+nextStepJobName)
.usingJobData(JobLauncher.MAIN_STEP_JOB_NAME, mainStepJobName)
.build();
// Trigger 생성
// - 바로 실행
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(JobLauncher.PREFIX_NEXT_STEP_TRIGGER_NAME+nextStepJobName)
.startNow()
.forJob(jobDetail)
.build();
// 스케줄 등록
context.getScheduler().scheduleJob(jobDetail, trigger);
}
}
|
cs |
4. JobLauncher.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
package com.dochi.quartz.step;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Set;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.UnableToInterruptJobException;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
public class JobLauncher {
// 상수 설정
// - Prefix 설정
public static final String PREFIX_STEP_JOB_NAME = "job_";
public static final String PREFIX_STEP_TRIGGER_NAME = "trigger_";
public static final String PREFIX_NEXT_STEP_JOB_NAME = "step_job_";
public static final String PREFIX_NEXT_STEP_TRIGGER_NAME = "step_trigger_";
// - DateFormat 설정
public static final SimpleDateFormat TIMESTAMP_FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
public static final SimpleDateFormat DATETIME_FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// - JobDataMap에서 사용할 Key 정의
public static final String MAIN_STEP_JOB_NAME = "mainStepJobName";
public static final String NEXT_STEP_CLASS_NAME = "nextStepClassName";
public static final String NEXT_STEP_JOB_NAME = "nextStepJobName";
// Scheduler 객체 생성
private static SchedulerFactory factory = null;
private static Scheduler scheduler = null;
// Main 함수
public static void main(String[] args) throws SchedulerException {
// Scheduler 실행
start();
// Schedule 등록
addSchedule("MainJob");
try {
System.out.println("아무키나 입력하면 종료됩니다...");
System.in.read();
// Scheduler 롱료
stop();
} catch (IOException e) {
e.printStackTrace();
}
}
// Scheduler 실행 함수
public static void start() throws SchedulerException {
// Scheduler 객체 정의
factory = new StdSchedulerFactory();
scheduler = factory.getScheduler();
// Listener 설정
scheduler.getListenerManager().addJobListener(new JobStepListener());
// Scheduler 실행
scheduler.start();
}
// Scheduler 종료 함수
public static void stop() throws SchedulerException {
try {
System.out.println("스케줄러가 종료됩니다...");
// Job Key 목록
Set<JobKey> allJobKeys = scheduler.getJobKeys(GroupMatcher.anyGroup());
// Job 강제 중단
allJobKeys.forEach((jobKey)->{
try {
scheduler.interrupt(jobKey);
} catch (UnableToInterruptJobException e) {
e.printStackTrace();
}
});
// Scheduler 중단
// - true : 모든 Job이 완료될 때까지 대기 후 종료
// - false: 즉시 종료
scheduler.shutdown(true);
System.out.println("스케줄러가 종료되었습니다.");
} catch (SchedulerException e) {
e.printStackTrace();
}
}
// Schedule 등록 함수
public static void addSchedule(String name) throws SchedulerException {
// JobDataMap 설정
// - Step으로 실행시킬 Job Class 이름 설정
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put(NEXT_STEP_CLASS_NAME, "com.dochi.quartz.step.SubJob");
// JobDetail 설정
JobDetail jobDetail = JobBuilder.newJob(MainJob.class)
.withIdentity(PREFIX_STEP_JOB_NAME+name)
.setJobData(jobDataMap)
.build();
// Trigger 설정
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(PREFIX_STEP_TRIGGER_NAME+name)
.startNow()
.forJob(jobDetail)
.build();
// Schedule 등록
scheduler.scheduleJob(jobDetail, trigger);
}
}
|
cs |
실행결과
- MainJob이 완료된 후 SubJob이 수행되는 것을 확인할 수 있음.
마치며
지금까지 JobListener를 통해 Step By Step으로 Job을 수행하는 방법에 대해서 알아보았다. 나중에 Crawler를 적용하였을 때, MainJob은 글목록에서 URL을 파싱하고, SubJob은 상세내용을 다시 크롤링하는 구조로 만들면 좋을것 같다.
'Back-end > JAVA' 카테고리의 다른 글
[JAVA] SQLite 무작정 시작하기 (1) - DATABASE 연결/해제 (0) | 2020.11.15 |
---|---|
[JAVA] Quartz 스케줄러 만들기 (5) - Crawler Job (0) | 2020.11.12 |
[JAVA] Quartz 스케줄러 만들기 (3) - Job 강제 종료 (0) | 2020.11.11 |
[JAVA] Quartz 스케줄러 만들기 (2) - Listener (0) | 2020.11.10 |
[JAVA] Quartz 스케줄러 만들기 (1) - 실행 (0) | 2020.11.09 |
댓글