• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 20224960533

15 Dec 2025 08:11AM UTC coverage: 84.388% (-0.02%) from 84.404%
20224960533

push

github

TAKETODAY
:white_check_mark: 在测试中排除 jacoco 初始化方法以避免干扰

61869 of 78367 branches covered (78.95%)

Branch coverage included in aggregate %.

145916 of 167860 relevant lines covered (86.93%)

3.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.13
today-context/src/main/java/infra/scheduling/support/CronTrigger.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.scheduling.support;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.time.Instant;
23
import java.time.ZoneId;
24
import java.time.ZonedDateTime;
25
import java.util.TimeZone;
26

27
import infra.lang.Assert;
28
import infra.scheduling.Trigger;
29
import infra.scheduling.TriggerContext;
30

31
/**
32
 * {@link Trigger} implementation for cron expressions. Wraps a
33
 * {@link CronExpression} which parses according to common crontab conventions.
34
 *
35
 * <p>Supports a Quartz day-of-month/week field with an L/# expression. Follows
36
 * common cron conventions in every other respect, including 0-6 for SUN-SAT
37
 * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week
38
 * convention in cron through 1-7 for SUN-SAT whereas Infra strictly follows
39
 * cron even in combination with the optional Quartz-specific L/# expressions.
40
 *
41
 * @author Juergen Hoeller
42
 * @author Arjen Poutsma
43
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
44
 * @see CronExpression
45
 * @since 4.0
46
 */
47
public class CronTrigger implements Trigger {
48

49
  private final CronExpression expression;
50

51
  private final @Nullable ZoneId zoneId;
52

53
  /**
54
   * Build a {@code CronTrigger} from the pattern provided in the default time zone.
55
   * <p>This is equivalent to the {@link CronTrigger#forLenientExecution} factory
56
   * method. Original trigger firings may be skipped if the previous task is still
57
   * running; if this is not desirable, consider {@link CronTrigger#forFixedExecution}.
58
   *
59
   * @param expression a space-separated list of time fields, following cron
60
   * expression conventions
61
   * @see CronTrigger#forLenientExecution
62
   * @see CronTrigger#forFixedExecution
63
   */
64
  public CronTrigger(String expression) {
2✔
65
    this.expression = CronExpression.parse(expression);
4✔
66
    this.zoneId = null;
3✔
67
  }
1✔
68

69
  /**
70
   * Build a {@code CronTrigger} from the pattern provided in the given time zone,
71
   * with the same lenient execution as {@link CronTrigger#CronTrigger(String)}.
72
   * <p>Note that such explicit time zone customization is usually not necessary,
73
   * using {@link infra.scheduling.TaskScheduler#getClock()} instead.
74
   *
75
   * @param expression a space-separated list of time fields, following cron
76
   * expression conventions
77
   * @param timeZone a time zone in which the trigger times will be generated
78
   */
79
  public CronTrigger(String expression, TimeZone timeZone) {
2✔
80
    this.expression = CronExpression.parse(expression);
4✔
81
    Assert.notNull(timeZone, "TimeZone must not be null");
3✔
82
    this.zoneId = timeZone.toZoneId();
4✔
83
  }
1✔
84

85
  /**
86
   * Build a {@code CronTrigger} from the pattern provided in the given time zone,
87
   * with the same lenient execution as {@link CronTrigger#CronTrigger(String)}.
88
   * <p>Note that such explicit time zone customization is usually not necessary,
89
   * using {@link infra.scheduling.TaskScheduler#getClock()} instead.
90
   *
91
   * @param expression a space-separated list of time fields, following cron
92
   * expression conventions
93
   * @param zoneId a time zone in which the trigger times will be generated
94
   * @see CronExpression#parse(String)
95
   * @since 4.0
96
   */
97
  public CronTrigger(String expression, ZoneId zoneId) {
×
98
    this.expression = CronExpression.parse(expression);
×
99
    Assert.notNull(zoneId, "ZoneId must not be null");
×
100
    this.zoneId = zoneId;
×
101
  }
×
102

103
  /**
104
   * Return the cron pattern that this trigger has been built with.
105
   */
106
  public String getExpression() {
107
    return this.expression.toString();
4✔
108
  }
109

110
  /**
111
   * Determine the next execution time according to the given trigger context.
112
   * <p>Next execution times are calculated based on the
113
   * {@linkplain TriggerContext#lastCompletion completion time} of the
114
   * previous execution; therefore, overlapping executions won't occur.
115
   */
116
  @Override
117
  public @Nullable Instant nextExecution(TriggerContext triggerContext) {
118
    Instant timestamp = determineLatestTimestamp(triggerContext);
4✔
119
    ZoneId zone = (this.zoneId != null ? this.zoneId : triggerContext.getClock().getZone());
10✔
120
    ZonedDateTime zonedTimestamp = timestamp.atZone(zone);
4✔
121
    ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp);
6✔
122
    return (nextTimestamp != null ? nextTimestamp.toInstant() : null);
7✔
123
  }
124

125
  Instant determineLatestTimestamp(TriggerContext triggerContext) {
126
    Instant timestamp = triggerContext.lastCompletion();
3✔
127
    if (timestamp != null) {
2✔
128
      Instant scheduled = triggerContext.lastScheduledExecution();
3✔
129
      if (scheduled != null && timestamp.isBefore(scheduled)) {
6✔
130
        // Previous task apparently executed too early...
131
        // Let's simply use the last calculated execution time then,
132
        // in order to prevent accidental re-fires in the same second.
133
        timestamp = scheduled;
2✔
134
      }
135
    }
1✔
136
    else {
137
      timestamp = determineInitialTimestamp(triggerContext);
4✔
138
    }
139
    return timestamp;
2✔
140
  }
141

142
  Instant determineInitialTimestamp(TriggerContext triggerContext) {
143
    return triggerContext.getClock().instant();
4✔
144
  }
145

146
  @Override
147
  public boolean equals(@Nullable Object other) {
148
    return (this == other || (other instanceof CronTrigger that &&
14!
149
            this.expression.equals(that.expression)));
4!
150
  }
151

152
  @Override
153
  public int hashCode() {
154
    return this.expression.hashCode();
×
155
  }
156

157
  @Override
158
  public String toString() {
159
    return this.expression.toString();
×
160
  }
161

162
  // Static Factory Methods
163

164
  /**
165
   * Create a {@link CronTrigger} for lenient execution, to be rescheduled
166
   * after every task based on the completion time.
167
   * <p>This variant does not make up for missed trigger firings if the
168
   * associated task has taken too long. As a consequence, original trigger
169
   * firings may be skipped if the previous task is still running.
170
   * <p>This is equivalent to the regular {@link CronTrigger} constructor.
171
   * Note that lenient execution is scheduler-dependent: it may skip trigger
172
   * firings with long-running tasks on a thread pool while executing at
173
   * {@link #forFixedExecution}-like precision with new threads per task.
174
   *
175
   * @param expression a space-separated list of time fields, following cron
176
   * expression conventions
177
   * @see #resumeLenientExecution
178
   * @since 5.0
179
   */
180
  public static CronTrigger forLenientExecution(String expression) {
181
    return new CronTrigger(expression);
5✔
182
  }
183

184
  /**
185
   * Create a {@link CronTrigger} for lenient execution, to be rescheduled
186
   * after every task based on the completion time.
187
   * <p>This variant does not make up for missed trigger firings if the
188
   * associated task has taken too long. As a consequence, original trigger
189
   * firings may be skipped if the previous task is still running.
190
   *
191
   * @param expression a space-separated list of time fields, following cron
192
   * expression conventions
193
   * @param resumptionTimestamp the timestamp to resume from (the last-known
194
   * completion timestamp), with the new trigger calculated from there and
195
   * possibly immediately firing (but only once, every subsequent calculation
196
   * will start from the completion time of that first resumed trigger)
197
   * @see #forLenientExecution
198
   * @since 5.0
199
   */
200
  public static CronTrigger resumeLenientExecution(String expression, Instant resumptionTimestamp) {
201
    return new CronTrigger(expression) {
13✔
202
      @Override
203
      Instant determineInitialTimestamp(TriggerContext triggerContext) {
204
        return resumptionTimestamp;
3✔
205
      }
206
    };
207
  }
208

209
  /**
210
   * Create a {@link CronTrigger} for fixed execution, to be rescheduled
211
   * after every task based on the last scheduled time.
212
   * <p>This variant makes up for missed trigger firings if the associated task
213
   * has taken too long, scheduling a task for every original trigger firing.
214
   * Such follow-up tasks may execute late but will never be skipped.
215
   * <p>Immediate versus late execution in case of long-running tasks may
216
   * be scheduler-dependent but the guarantee to never skip a task is portable.
217
   *
218
   * @param expression a space-separated list of time fields, following cron
219
   * expression conventions
220
   * @see #resumeFixedExecution
221
   * @since 5.0
222
   */
223
  public static CronTrigger forFixedExecution(String expression) {
224
    return new CronTrigger(expression) {
9✔
225
      @Override
226
      protected Instant determineLatestTimestamp(TriggerContext triggerContext) {
227
        Instant scheduled = triggerContext.lastScheduledExecution();
3✔
228
        return (scheduled != null ? scheduled : super.determineInitialTimestamp(triggerContext));
8✔
229
      }
230
    };
231
  }
232

233
  /**
234
   * Create a {@link CronTrigger} for fixed execution, to be rescheduled
235
   * after every task based on the last scheduled time.
236
   * <p>This variant makes up for missed trigger firings if the associated task
237
   * has taken too long, scheduling a task for every original trigger firing.
238
   * Such follow-up tasks may execute late but will never be skipped.
239
   *
240
   * @param expression a space-separated list of time fields, following cron
241
   * expression conventions
242
   * @param resumptionTimestamp the timestamp to resume from (the last-known
243
   * scheduled timestamp), with every trigger in-between immediately firing
244
   * to make up for every execution that would have happened in the meantime
245
   * @see #forFixedExecution
246
   * @since 5.0
247
   */
248
  public static CronTrigger resumeFixedExecution(String expression, Instant resumptionTimestamp) {
249
    return new CronTrigger(expression) {
13✔
250
      @Override
251
      protected Instant determineLatestTimestamp(TriggerContext triggerContext) {
252
        Instant scheduled = triggerContext.lastScheduledExecution();
3✔
253
        return (scheduled != null ? scheduled : super.determineLatestTimestamp(triggerContext));
8✔
254
      }
255

256
      @Override
257
      Instant determineInitialTimestamp(TriggerContext triggerContext) {
258
        return resumptionTimestamp;
3✔
259
      }
260
    };
261
  }
262

263
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc