001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.scheduling.annotation; 018 019import java.lang.reflect.Method; 020import java.time.Duration; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.IdentityHashMap; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.TimeZone; 031import java.util.concurrent.ConcurrentHashMap; 032import java.util.concurrent.ScheduledExecutorService; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036 037import org.springframework.aop.framework.AopInfrastructureBean; 038import org.springframework.aop.framework.AopProxyUtils; 039import org.springframework.aop.support.AopUtils; 040import org.springframework.beans.factory.BeanFactory; 041import org.springframework.beans.factory.BeanFactoryAware; 042import org.springframework.beans.factory.BeanNameAware; 043import org.springframework.beans.factory.DisposableBean; 044import org.springframework.beans.factory.ListableBeanFactory; 045import org.springframework.beans.factory.NoSuchBeanDefinitionException; 046import org.springframework.beans.factory.NoUniqueBeanDefinitionException; 047import org.springframework.beans.factory.SmartInitializingSingleton; 048import org.springframework.beans.factory.config.AutowireCapableBeanFactory; 049import org.springframework.beans.factory.config.ConfigurableBeanFactory; 050import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; 051import org.springframework.beans.factory.config.NamedBeanHolder; 052import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; 053import org.springframework.beans.factory.support.RootBeanDefinition; 054import org.springframework.context.ApplicationContext; 055import org.springframework.context.ApplicationContextAware; 056import org.springframework.context.ApplicationListener; 057import org.springframework.context.EmbeddedValueResolverAware; 058import org.springframework.context.event.ContextRefreshedEvent; 059import org.springframework.core.MethodIntrospector; 060import org.springframework.core.Ordered; 061import org.springframework.core.annotation.AnnotatedElementUtils; 062import org.springframework.core.annotation.AnnotationAwareOrderComparator; 063import org.springframework.core.annotation.AnnotationUtils; 064import org.springframework.lang.Nullable; 065import org.springframework.scheduling.TaskScheduler; 066import org.springframework.scheduling.Trigger; 067import org.springframework.scheduling.config.CronTask; 068import org.springframework.scheduling.config.FixedDelayTask; 069import org.springframework.scheduling.config.FixedRateTask; 070import org.springframework.scheduling.config.ScheduledTask; 071import org.springframework.scheduling.config.ScheduledTaskHolder; 072import org.springframework.scheduling.config.ScheduledTaskRegistrar; 073import org.springframework.scheduling.support.CronTrigger; 074import org.springframework.scheduling.support.ScheduledMethodRunnable; 075import org.springframework.util.Assert; 076import org.springframework.util.StringUtils; 077import org.springframework.util.StringValueResolver; 078 079/** 080 * Bean post-processor that registers methods annotated with @{@link Scheduled} 081 * to be invoked by a {@link org.springframework.scheduling.TaskScheduler} according 082 * to the "fixedRate", "fixedDelay", or "cron" expression provided via the annotation. 083 * 084 * <p>This post-processor is automatically registered by Spring's 085 * {@code <task:annotation-driven>} XML element, and also by the 086 * {@link EnableScheduling @EnableScheduling} annotation. 087 * 088 * <p>Autodetects any {@link SchedulingConfigurer} instances in the container, 089 * allowing for customization of the scheduler to be used or for fine-grained 090 * control over task registration (e.g. registration of {@link Trigger} tasks. 091 * See the @{@link EnableScheduling} javadocs for complete usage details. 092 * 093 * @author Mark Fisher 094 * @author Juergen Hoeller 095 * @author Chris Beams 096 * @author Elizabeth Chatman 097 * @since 3.0 098 * @see Scheduled 099 * @see EnableScheduling 100 * @see SchedulingConfigurer 101 * @see org.springframework.scheduling.TaskScheduler 102 * @see org.springframework.scheduling.config.ScheduledTaskRegistrar 103 * @see AsyncAnnotationBeanPostProcessor 104 */ 105public class ScheduledAnnotationBeanPostProcessor 106 implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor, 107 Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware, 108 SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean { 109 110 /** 111 * The default name of the {@link TaskScheduler} bean to pick up: {@value}. 112 * <p>Note that the initial lookup happens by type; this is just the fallback 113 * in case of multiple scheduler beans found in the context. 114 * @since 4.2 115 */ 116 public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler"; 117 118 119 protected final Log logger = LogFactory.getLog(getClass()); 120 121 private final ScheduledTaskRegistrar registrar; 122 123 @Nullable 124 private Object scheduler; 125 126 @Nullable 127 private StringValueResolver embeddedValueResolver; 128 129 @Nullable 130 private String beanName; 131 132 @Nullable 133 private BeanFactory beanFactory; 134 135 @Nullable 136 private ApplicationContext applicationContext; 137 138 private final Set<Class<?>> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); 139 140 private final Map<Object, Set<ScheduledTask>> scheduledTasks = new IdentityHashMap<>(16); 141 142 143 /** 144 * Create a default {@code ScheduledAnnotationBeanPostProcessor}. 145 */ 146 public ScheduledAnnotationBeanPostProcessor() { 147 this.registrar = new ScheduledTaskRegistrar(); 148 } 149 150 /** 151 * Create a {@code ScheduledAnnotationBeanPostProcessor} delegating to the 152 * specified {@link ScheduledTaskRegistrar}. 153 * @param registrar the ScheduledTaskRegistrar to register @Scheduled tasks on 154 * @since 5.1 155 */ 156 public ScheduledAnnotationBeanPostProcessor(ScheduledTaskRegistrar registrar) { 157 Assert.notNull(registrar, "ScheduledTaskRegistrar is required"); 158 this.registrar = registrar; 159 } 160 161 162 @Override 163 public int getOrder() { 164 return LOWEST_PRECEDENCE; 165 } 166 167 /** 168 * Set the {@link org.springframework.scheduling.TaskScheduler} that will invoke 169 * the scheduled methods, or a {@link java.util.concurrent.ScheduledExecutorService} 170 * to be wrapped as a TaskScheduler. 171 * <p>If not specified, default scheduler resolution will apply: searching for a 172 * unique {@link TaskScheduler} bean in the context, or for a {@link TaskScheduler} 173 * bean named "taskScheduler" otherwise; the same lookup will also be performed for 174 * a {@link ScheduledExecutorService} bean. If neither of the two is resolvable, 175 * a local single-threaded default scheduler will be created within the registrar. 176 * @see #DEFAULT_TASK_SCHEDULER_BEAN_NAME 177 */ 178 public void setScheduler(Object scheduler) { 179 this.scheduler = scheduler; 180 } 181 182 @Override 183 public void setEmbeddedValueResolver(StringValueResolver resolver) { 184 this.embeddedValueResolver = resolver; 185 } 186 187 @Override 188 public void setBeanName(String beanName) { 189 this.beanName = beanName; 190 } 191 192 /** 193 * Making a {@link BeanFactory} available is optional; if not set, 194 * {@link SchedulingConfigurer} beans won't get autodetected and 195 * a {@link #setScheduler scheduler} has to be explicitly configured. 196 */ 197 @Override 198 public void setBeanFactory(BeanFactory beanFactory) { 199 this.beanFactory = beanFactory; 200 } 201 202 /** 203 * Setting an {@link ApplicationContext} is optional: If set, registered 204 * tasks will be activated in the {@link ContextRefreshedEvent} phase; 205 * if not set, it will happen at {@link #afterSingletonsInstantiated} time. 206 */ 207 @Override 208 public void setApplicationContext(ApplicationContext applicationContext) { 209 this.applicationContext = applicationContext; 210 if (this.beanFactory == null) { 211 this.beanFactory = applicationContext; 212 } 213 } 214 215 216 @Override 217 public void afterSingletonsInstantiated() { 218 // Remove resolved singleton classes from cache 219 this.nonAnnotatedClasses.clear(); 220 221 if (this.applicationContext == null) { 222 // Not running in an ApplicationContext -> register tasks early... 223 finishRegistration(); 224 } 225 } 226 227 @Override 228 public void onApplicationEvent(ContextRefreshedEvent event) { 229 if (event.getApplicationContext() == this.applicationContext) { 230 // Running in an ApplicationContext -> register tasks this late... 231 // giving other ContextRefreshedEvent listeners a chance to perform 232 // their work at the same time (e.g. Spring Batch's job registration). 233 finishRegistration(); 234 } 235 } 236 237 private void finishRegistration() { 238 if (this.scheduler != null) { 239 this.registrar.setScheduler(this.scheduler); 240 } 241 242 if (this.beanFactory instanceof ListableBeanFactory) { 243 Map<String, SchedulingConfigurer> beans = 244 ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class); 245 List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values()); 246 AnnotationAwareOrderComparator.sort(configurers); 247 for (SchedulingConfigurer configurer : configurers) { 248 configurer.configureTasks(this.registrar); 249 } 250 } 251 252 if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) { 253 Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type"); 254 try { 255 // Search for TaskScheduler bean... 256 this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false)); 257 } 258 catch (NoUniqueBeanDefinitionException ex) { 259 if (logger.isTraceEnabled()) { 260 logger.trace("Could not find unique TaskScheduler bean - attempting to resolve by name: " + 261 ex.getMessage()); 262 } 263 try { 264 this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true)); 265 } 266 catch (NoSuchBeanDefinitionException ex2) { 267 if (logger.isInfoEnabled()) { 268 logger.info("More than one TaskScheduler bean exists within the context, and " + 269 "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + 270 "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + 271 "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + 272 ex.getBeanNamesFound()); 273 } 274 } 275 } 276 catch (NoSuchBeanDefinitionException ex) { 277 if (logger.isTraceEnabled()) { 278 logger.trace("Could not find default TaskScheduler bean - attempting to find ScheduledExecutorService: " + 279 ex.getMessage()); 280 } 281 // Search for ScheduledExecutorService bean next... 282 try { 283 this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false)); 284 } 285 catch (NoUniqueBeanDefinitionException ex2) { 286 if (logger.isTraceEnabled()) { 287 logger.trace("Could not find unique ScheduledExecutorService bean - attempting to resolve by name: " + 288 ex2.getMessage()); 289 } 290 try { 291 this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true)); 292 } 293 catch (NoSuchBeanDefinitionException ex3) { 294 if (logger.isInfoEnabled()) { 295 logger.info("More than one ScheduledExecutorService bean exists within the context, and " + 296 "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + 297 "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + 298 "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + 299 ex2.getBeanNamesFound()); 300 } 301 } 302 } 303 catch (NoSuchBeanDefinitionException ex2) { 304 if (logger.isTraceEnabled()) { 305 logger.trace("Could not find default ScheduledExecutorService bean - falling back to default: " + 306 ex2.getMessage()); 307 } 308 // Giving up -> falling back to default scheduler within the registrar... 309 logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing"); 310 } 311 } 312 } 313 314 this.registrar.afterPropertiesSet(); 315 } 316 317 private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) { 318 if (byName) { 319 T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType); 320 if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory) { 321 ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean( 322 DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName); 323 } 324 return scheduler; 325 } 326 else if (beanFactory instanceof AutowireCapableBeanFactory) { 327 NamedBeanHolder<T> holder = ((AutowireCapableBeanFactory) beanFactory).resolveNamedBean(schedulerType); 328 if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory) { 329 ((ConfigurableBeanFactory) beanFactory).registerDependentBean(holder.getBeanName(), this.beanName); 330 } 331 return holder.getBeanInstance(); 332 } 333 else { 334 return beanFactory.getBean(schedulerType); 335 } 336 } 337 338 339 @Override 340 public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) { 341 } 342 343 @Override 344 public Object postProcessBeforeInitialization(Object bean, String beanName) { 345 return bean; 346 } 347 348 @Override 349 public Object postProcessAfterInitialization(Object bean, String beanName) { 350 if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler || 351 bean instanceof ScheduledExecutorService) { 352 // Ignore AOP infrastructure such as scoped proxies. 353 return bean; 354 } 355 356 Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean); 357 if (!this.nonAnnotatedClasses.contains(targetClass) && 358 AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) { 359 Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, 360 (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> { 361 Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations( 362 method, Scheduled.class, Schedules.class); 363 return (!scheduledMethods.isEmpty() ? scheduledMethods : null); 364 }); 365 if (annotatedMethods.isEmpty()) { 366 this.nonAnnotatedClasses.add(targetClass); 367 if (logger.isTraceEnabled()) { 368 logger.trace("No @Scheduled annotations found on bean class: " + targetClass); 369 } 370 } 371 else { 372 // Non-empty set of methods 373 annotatedMethods.forEach((method, scheduledMethods) -> 374 scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean))); 375 if (logger.isTraceEnabled()) { 376 logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + 377 "': " + annotatedMethods); 378 } 379 } 380 } 381 return bean; 382 } 383 384 /** 385 * Process the given {@code @Scheduled} method declaration on the given bean. 386 * @param scheduled the @Scheduled annotation 387 * @param method the method that the annotation has been declared on 388 * @param bean the target bean instance 389 * @see #createRunnable(Object, Method) 390 */ 391 protected void processScheduled(Scheduled scheduled, Method method, Object bean) { 392 try { 393 Runnable runnable = createRunnable(bean, method); 394 boolean processedSchedule = false; 395 String errorMessage = 396 "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required"; 397 398 Set<ScheduledTask> tasks = new LinkedHashSet<>(4); 399 400 // Determine initial delay 401 long initialDelay = scheduled.initialDelay(); 402 String initialDelayString = scheduled.initialDelayString(); 403 if (StringUtils.hasText(initialDelayString)) { 404 Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both"); 405 if (this.embeddedValueResolver != null) { 406 initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString); 407 } 408 if (StringUtils.hasLength(initialDelayString)) { 409 try { 410 initialDelay = parseDelayAsLong(initialDelayString); 411 } 412 catch (RuntimeException ex) { 413 throw new IllegalArgumentException( 414 "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long"); 415 } 416 } 417 } 418 419 // Check cron expression 420 String cron = scheduled.cron(); 421 if (StringUtils.hasText(cron)) { 422 String zone = scheduled.zone(); 423 if (this.embeddedValueResolver != null) { 424 cron = this.embeddedValueResolver.resolveStringValue(cron); 425 zone = this.embeddedValueResolver.resolveStringValue(zone); 426 } 427 if (StringUtils.hasLength(cron)) { 428 Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); 429 processedSchedule = true; 430 if (!Scheduled.CRON_DISABLED.equals(cron)) { 431 TimeZone timeZone; 432 if (StringUtils.hasText(zone)) { 433 timeZone = StringUtils.parseTimeZoneString(zone); 434 } 435 else { 436 timeZone = TimeZone.getDefault(); 437 } 438 tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); 439 } 440 } 441 } 442 443 // At this point we don't need to differentiate between initial delay set or not anymore 444 if (initialDelay < 0) { 445 initialDelay = 0; 446 } 447 448 // Check fixed delay 449 long fixedDelay = scheduled.fixedDelay(); 450 if (fixedDelay >= 0) { 451 Assert.isTrue(!processedSchedule, errorMessage); 452 processedSchedule = true; 453 tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay))); 454 } 455 String fixedDelayString = scheduled.fixedDelayString(); 456 if (StringUtils.hasText(fixedDelayString)) { 457 if (this.embeddedValueResolver != null) { 458 fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString); 459 } 460 if (StringUtils.hasLength(fixedDelayString)) { 461 Assert.isTrue(!processedSchedule, errorMessage); 462 processedSchedule = true; 463 try { 464 fixedDelay = parseDelayAsLong(fixedDelayString); 465 } 466 catch (RuntimeException ex) { 467 throw new IllegalArgumentException( 468 "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long"); 469 } 470 tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay))); 471 } 472 } 473 474 // Check fixed rate 475 long fixedRate = scheduled.fixedRate(); 476 if (fixedRate >= 0) { 477 Assert.isTrue(!processedSchedule, errorMessage); 478 processedSchedule = true; 479 tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay))); 480 } 481 String fixedRateString = scheduled.fixedRateString(); 482 if (StringUtils.hasText(fixedRateString)) { 483 if (this.embeddedValueResolver != null) { 484 fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString); 485 } 486 if (StringUtils.hasLength(fixedRateString)) { 487 Assert.isTrue(!processedSchedule, errorMessage); 488 processedSchedule = true; 489 try { 490 fixedRate = parseDelayAsLong(fixedRateString); 491 } 492 catch (RuntimeException ex) { 493 throw new IllegalArgumentException( 494 "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long"); 495 } 496 tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay))); 497 } 498 } 499 500 // Check whether we had any attribute set 501 Assert.isTrue(processedSchedule, errorMessage); 502 503 // Finally register the scheduled tasks 504 synchronized (this.scheduledTasks) { 505 Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4)); 506 regTasks.addAll(tasks); 507 } 508 } 509 catch (IllegalArgumentException ex) { 510 throw new IllegalStateException( 511 "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage()); 512 } 513 } 514 515 /** 516 * Create a {@link Runnable} for the given bean instance, 517 * calling the specified scheduled method. 518 * <p>The default implementation creates a {@link ScheduledMethodRunnable}. 519 * @param target the target bean instance 520 * @param method the scheduled method to call 521 * @since 5.1 522 * @see ScheduledMethodRunnable#ScheduledMethodRunnable(Object, Method) 523 */ 524 protected Runnable createRunnable(Object target, Method method) { 525 Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled"); 526 Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass()); 527 return new ScheduledMethodRunnable(target, invocableMethod); 528 } 529 530 private static long parseDelayAsLong(String value) throws RuntimeException { 531 if (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1)))) { 532 return Duration.parse(value).toMillis(); 533 } 534 return Long.parseLong(value); 535 } 536 537 private static boolean isP(char ch) { 538 return (ch == 'P' || ch == 'p'); 539 } 540 541 542 /** 543 * Return all currently scheduled tasks, from {@link Scheduled} methods 544 * as well as from programmatic {@link SchedulingConfigurer} interaction. 545 * @since 5.0.2 546 */ 547 @Override 548 public Set<ScheduledTask> getScheduledTasks() { 549 Set<ScheduledTask> result = new LinkedHashSet<>(); 550 synchronized (this.scheduledTasks) { 551 Collection<Set<ScheduledTask>> allTasks = this.scheduledTasks.values(); 552 for (Set<ScheduledTask> tasks : allTasks) { 553 result.addAll(tasks); 554 } 555 } 556 result.addAll(this.registrar.getScheduledTasks()); 557 return result; 558 } 559 560 @Override 561 public void postProcessBeforeDestruction(Object bean, String beanName) { 562 Set<ScheduledTask> tasks; 563 synchronized (this.scheduledTasks) { 564 tasks = this.scheduledTasks.remove(bean); 565 } 566 if (tasks != null) { 567 for (ScheduledTask task : tasks) { 568 task.cancel(); 569 } 570 } 571 } 572 573 @Override 574 public boolean requiresDestruction(Object bean) { 575 synchronized (this.scheduledTasks) { 576 return this.scheduledTasks.containsKey(bean); 577 } 578 } 579 580 @Override 581 public void destroy() { 582 synchronized (this.scheduledTasks) { 583 Collection<Set<ScheduledTask>> allTasks = this.scheduledTasks.values(); 584 for (Set<ScheduledTask> tasks : allTasks) { 585 for (ScheduledTask task : tasks) { 586 task.cancel(); 587 } 588 } 589 this.scheduledTasks.clear(); 590 } 591 this.registrar.destroy(); 592 } 593 594}