Skip to content

Commit 0c00b0d

Browse files
committed
Added 'zone' attribute to @scheduled
Issue: SPR-10456
1 parent 39ff1e2 commit 0c00b0d

File tree

3 files changed

+126
-24
lines changed

3 files changed

+126
-24
lines changed

spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@
5959
*/
6060
String cron() default "";
6161

62+
/**
63+
* A time zone for which the cron expression will be resolved.
64+
* By default, the server's local time zone will be used.
65+
* @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)}
66+
* @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone)
67+
* @see java.util.TimeZone
68+
* @since 4.0
69+
*/
70+
String zone() default "";
71+
6272
/**
6373
* Execute the annotated method with a fixed period between the end
6474
* of the last invocation and the start of the next.

spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.reflect.Method;
2020
import java.util.HashMap;
2121
import java.util.Map;
22+
import java.util.TimeZone;
2223
import java.util.concurrent.ScheduledExecutorService;
2324

2425
import org.springframework.aop.support.AopUtils;
@@ -36,6 +37,7 @@
3637
import org.springframework.scheduling.config.CronTask;
3738
import org.springframework.scheduling.config.IntervalTask;
3839
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
40+
import org.springframework.scheduling.support.CronTrigger;
3941
import org.springframework.scheduling.support.ScheduledMethodRunnable;
4042
import org.springframework.util.Assert;
4143
import org.springframework.util.ReflectionUtils;
@@ -183,10 +185,23 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean)
183185
if (!"".equals(cron)) {
184186
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
185187
processedSchedule = true;
188+
String zone = scheduled.zone();
186189
if (this.embeddedValueResolver != null) {
187190
cron = this.embeddedValueResolver.resolveStringValue(cron);
191+
zone = this.embeddedValueResolver.resolveStringValue(zone);
188192
}
189-
this.registrar.addCronTask(new CronTask(runnable, cron));
193+
TimeZone timeZone;
194+
if (!"".equals(zone)) {
195+
timeZone = TimeZone.getTimeZone(zone);
196+
// Check for that silly TimeZone fallback...
197+
if ("GMT".equals(timeZone.getID()) && !zone.startsWith("GMT")) {
198+
throw new IllegalArgumentException("Invalid time zone id '" + zone + "'");
199+
}
200+
}
201+
else {
202+
timeZone = TimeZone.getDefault();
203+
}
204+
this.registrar.addCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)));
190205
}
191206

192207
// At this point we don't need to differentiate between initial delay set or not anymore

spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424
import java.lang.reflect.Method;
25+
import java.util.Calendar;
26+
import java.util.Date;
2527
import java.util.List;
2628
import java.util.Properties;
29+
import java.util.TimeZone;
2730

2831
import org.junit.Test;
2932

@@ -33,10 +36,14 @@
3336
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
3437
import org.springframework.beans.factory.support.RootBeanDefinition;
3538
import org.springframework.context.support.StaticApplicationContext;
39+
import org.springframework.scheduling.Trigger;
40+
import org.springframework.scheduling.TriggerContext;
3641
import org.springframework.scheduling.config.CronTask;
3742
import org.springframework.scheduling.config.IntervalTask;
3843
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
44+
import org.springframework.scheduling.support.CronTrigger;
3945
import org.springframework.scheduling.support.ScheduledMethodRunnable;
46+
import org.springframework.scheduling.support.SimpleTriggerContext;
4047
import org.springframework.tests.Assume;
4148
import org.springframework.tests.TestGroup;
4249

@@ -47,6 +54,7 @@
4754
* @author Juergen Hoeller
4855
* @author Chris Beams
4956
* @author Sam Brannen
57+
* @author Stevo Slavić
5058
*/
5159
public class ScheduledAnnotationBeanPostProcessorTests {
5260

@@ -128,16 +136,13 @@ public void fixedRateTaskWithInitialDelay() {
128136
assertEquals(3000L, task.getInterval());
129137
}
130138

131-
// TODO Reinstate repeated @Scheduled tests once we have full Java 8 support in the
132-
// IDEs.
133-
// @Test
134-
// public void severalFixedRatesWithRepeatedScheduledAnnotation() {
135-
// BeanDefinition processorDefinition = new
136-
// RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
137-
// BeanDefinition targetDefinition = new RootBeanDefinition(
138-
// SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean.class);
139-
// severalFixedRates(context, processorDefinition, targetDefinition);
140-
// }
139+
@Test
140+
public void severalFixedRatesWithRepeatedScheduledAnnotation() {
141+
BeanDefinition processorDefinition = new
142+
RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
143+
BeanDefinition targetDefinition = new RootBeanDefinition(SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean.class);
144+
severalFixedRates(context, processorDefinition, targetDefinition);
145+
}
141146

142147
@Test
143148
public void severalFixedRatesWithSchedulesContainerAnnotation() {
@@ -207,6 +212,63 @@ public void cronTask() throws InterruptedException {
207212
Thread.sleep(10000);
208213
}
209214

215+
@Test
216+
public void cronTaskWithZone() throws InterruptedException {
217+
Assume.group(TestGroup.LONG_RUNNING);
218+
219+
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
220+
BeanDefinition targetDefinition = new RootBeanDefinition(
221+
ScheduledAnnotationBeanPostProcessorTests.CronWithTimezoneTestBean.class);
222+
context.registerBeanDefinition("postProcessor", processorDefinition);
223+
context.registerBeanDefinition("target", targetDefinition);
224+
context.refresh();
225+
Object postProcessor = context.getBean("postProcessor");
226+
Object target = context.getBean("target");
227+
ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar)
228+
new DirectFieldAccessor(postProcessor).getPropertyValue("registrar");
229+
@SuppressWarnings("unchecked")
230+
List<CronTask> cronTasks = (List<CronTask>)
231+
new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
232+
assertEquals(1, cronTasks.size());
233+
CronTask task = cronTasks.get(0);
234+
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
235+
Object targetObject = runnable.getTarget();
236+
Method targetMethod = runnable.getMethod();
237+
assertEquals(target, targetObject);
238+
assertEquals("cron", targetMethod.getName());
239+
assertEquals("0 0 0-4,6-23 * * ?", task.getExpression());
240+
Trigger trigger = task.getTrigger();
241+
assertNotNull(trigger);
242+
assertTrue(trigger instanceof CronTrigger);
243+
CronTrigger cronTrigger = (CronTrigger) trigger;
244+
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+10"));
245+
cal.clear();
246+
cal.set(2013, 3, 15, 4, 0); // 15-04-2013 4:00 GMT+10
247+
Date lastScheduledExecutionTime = cal.getTime();
248+
Date lastActualExecutionTime = cal.getTime();
249+
cal.add(Calendar.MINUTE, 30); // 4:30
250+
Date lastCompletionTime = cal.getTime();
251+
TriggerContext triggerContext = new SimpleTriggerContext(lastScheduledExecutionTime, lastActualExecutionTime, lastCompletionTime);
252+
cal.add(Calendar.MINUTE, 30);
253+
cal.add(Calendar.HOUR_OF_DAY, 1); // 6:00
254+
Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext);
255+
assertEquals(cal.getTime(), nextExecutionTime); // assert that 6:00 is next execution time
256+
Thread.sleep(10000);
257+
}
258+
259+
@Test(expected = BeanCreationException.class)
260+
public void cronTaskWithInvalidZone() throws InterruptedException {
261+
Assume.group(TestGroup.LONG_RUNNING);
262+
263+
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
264+
BeanDefinition targetDefinition = new RootBeanDefinition(
265+
ScheduledAnnotationBeanPostProcessorTests.CronWithInvalidTimezoneTestBean.class);
266+
context.registerBeanDefinition("postProcessor", processorDefinition);
267+
context.registerBeanDefinition("target", targetDefinition);
268+
context.refresh();
269+
Thread.sleep(10000);
270+
}
271+
210272
@Test
211273
public void metaAnnotationWithFixedRate() {
212274
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
@@ -453,23 +515,42 @@ public void fixedRate() {
453515
}
454516

455517

456-
// TODO Reinstate repeated @Scheduled tests once we have full Java 8 support in the
457-
// IDEs.
458-
// static class SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean {
459-
//
460-
// @Scheduled(fixedRate=4000)
461-
// @Scheduled(fixedRate=4000, initialDelay=2000)
462-
// public void fixedRate() {
463-
// }
464-
// }
518+
static class SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean {
519+
520+
// can use Java 8 repeated @Scheduled once we have Eclipse IDE support for it
521+
@Schedules({
522+
@Scheduled(fixedRate=4000),
523+
@Scheduled(fixedRate=4000, initialDelay=2000)
524+
})
525+
public void fixedRate() {
526+
}
527+
}
528+
465529

466530
static class CronTestBean {
467531

468532
@Scheduled(cron="*/7 * * * * ?")
469533
public void cron() throws IOException {
470534
throw new IOException("no no no");
471535
}
536+
}
537+
538+
539+
static class CronWithTimezoneTestBean {
540+
541+
@Scheduled(cron="0 0 0-4,6-23 * * ?", zone = "GMT+10")
542+
public void cron() throws IOException {
543+
throw new IOException("no no no");
544+
}
545+
}
546+
547+
548+
static class CronWithInvalidTimezoneTestBean {
472549

550+
@Scheduled(cron="0 0 0-4,6-23 * * ?", zone = "FOO")
551+
public void cron() throws IOException {
552+
throw new IOException("no no no");
553+
}
473554
}
474555

475556

@@ -478,7 +559,6 @@ static class EmptyAnnotationTestBean {
478559
@Scheduled
479560
public void invalid() {
480561
}
481-
482562
}
483563

484564

@@ -487,7 +567,6 @@ static class InvalidCronTestBean {
487567
@Scheduled(cron="abc")
488568
public void invalid() {
489569
}
490-
491570
}
492571

493572

@@ -497,7 +576,6 @@ static class NonVoidReturnTypeTestBean {
497576
public String invalid() {
498577
return "oops";
499578
}
500-
501579
}
502580

503581

@@ -506,7 +584,6 @@ static class NonEmptyParamListTestBean {
506584
@Scheduled(fixedRate=3000)
507585
public void invalid(String oops) {
508586
}
509-
510587
}
511588

512589

0 commit comments

Comments
 (0)