Spring的任务调度

news2024/9/22 3:40:20

Spring的任务调度

1.概述

Spring框架为任务调度提供了专门的解决方案。在Spring框架的org.springframework.scheduling包中,通过对JDK 的ScheduledExecutorService接口的实例进行封装,对外提供了一些注解和接口,为开发者处理定时任务提供了统一的配置方式。          

Spring框架借助了JDK的能力实现了任务调度,但是开发者通过Spring来实现定时任务调度是非常简单和便捷的。如果需要单机环境下要实现任务调度的功能,使用Spring 的任务调度方式无疑是首选。

2.Spring任务调度的使用

我们使用Spring来进行定时任务调度的话,主要是两种方式。1. @Scheduled注解的方式。2. 基于SchedulingConfigurer接口的方式。这两种方式都需要使用@EnableScheduling注解来开启Spring框架的定时任务调度功能。

2.1 @Scheduled注解的方式

@Scheduled注解的方式比较简单,我们只要在需要调度方法上加上@Scheduled注解,然后通过配置@Scheduled注解的元素值,来控制任务调度的频率。这种配置任务调度的频率方式有点硬编码的味道。若后期若需要调整任务调度的频率,则需要修改代码后重启服务才能生效。

2.1.1 @Scheduled注解

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
   String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;
   String cron() default "";
   String zone() default "";
   long fixedDelay() default -1;
   String fixedDelayString() default "";
   long fixedRate() default -1;
   String fixedRateString() default "";
   long initialDelay() default -1;
   String initialDelayString() default "";
}

@Scheduled注解的元素值说明:

cron

一个类似cron的任务执行表达式

zone

cron表达时解析使用的时区,默认为服务器的本地时区。

fixedDelay

上一次任务执行结束到下一次任务执行开始的固定时间延迟,单位为ms。

fixedDelayString

同fixedDelay,返回的是延迟的字符串形式

fixedRate

以固定的时间间隔来执行Scheduled注释的任务,单位为ms。使用fixedRate执行任务调度时,若上一次任务还未执行完毕,则将下一次任务加入worker队列,等上一次任务执行完成后,才能执行下一次任务。

fixedRateString

同fixedRate,返回的是时间间隔的字符串形式。

initialDelay

首次执行Scheduled注释任务的延迟时间,单位ms。

initialDelayString

同initialDelay,返回的是延迟的字符串形式。

TimeUnit

执行任务的时间单位,默认是ms。

要使用@Scheduled注解方式进行任务调度的话,我们只要在需要进行调度的方法上加上@Scheduled注解,再通过注解的属性值对任务调度的频率进行设置。最后增加一个@EnableScheduling注解来启用Spring定时任务的执行功能。在Spring中只需要通过以上的几步设置,就可以完成一个任务调度的功能。

2.1.2 示例代码

@Component
@EnableScheduling
public class MyTask {
    @Scheduled(cron = "0/5 * * * * ?")
    public void cronFun(){
       System.out.println("Thread "+Thread.currentThread().getName()+" execute  cronFun! "+DateUtil.DateToString(new Date()));
    }  
   
    @Scheduled(fixedDelay = 1000 * 3,initialDelay = 1000 * 5)
    public void fixedDelayFun(){
       System.out.println("Thread "+Thread.currentThread().getName()+" execute fixedDelayFun! "+DateUtil.DateToString(new Date()));
    }
    
    @Scheduled(fixedRate = 1000 * 6, initialDelay = 1000 * 5)
    public void fixedRateFun(){
       System.out.println("Thread "+Thread.currentThread().getName()+" execute fixedRateFun! "+DateUtil.DateToString(new Date()));
    }
}

在以上的示例代码中,分别使用了三种方式来模拟进行了任务的调度。

  • cron任务执行表达式的方式:我们定义了一个每5秒钟一次频率的cron表达式。cron表达式是由若干数字、空格、符号按一定的规则,组成的一组字符串,用来表达时间的信息。该字符串由6个空格分为7个域,每一个域代表一个时间含义。

  • fixedDelay是以固定的延迟来执行下一个任务。在示例代码中设置了每延迟3秒钟来执行下一个任务。初次执行调度任务会延迟5秒。在每一个任务执行完成后,都会停顿3秒钟,才会再执行下一个任务。

                     

  •  fixedRate以固定的时间间隔来执行调度任务。在示例代码中设置了每6秒钟的时间间隔频率来执行下一个任务。初次执行调度任务会延迟5秒。

  • 使用fixedRate进行任务调度时需要注意的是,若任务的执行时间超过了时间间隔,也就是上一次任务还未执行完毕,而又有执行下一次任务的调度。此时程序会将下一次任务暂时加入worker队列进行等待,直到上一次任务执行完成后,再执行队列中任务。在示例代码中,我们使用线程休眠的方式来模拟任务的执行时间,我们设置任务的执行时间为10秒,超过了调度的时间间隔6秒。那么此时任务调度就会变成每次会过10秒后,才会再执行下次任务。

       

2.2  SchedulingConfigurer接口方式

在Spring中,虽然使用@Scheduled注解的方式来进行任务调度确实简单易用,但是这种相当于硬编码的方式,一旦设定了任务的执行频率,在任务的执行过程中就无法改变。

Spring还提供了通过实现SchedulingConfigurer接口的方式来配置定时任务。SchedulingConfigurer是一个函数式接口,通过实现SchedulingConfigurer接口来配置任务调度,我们可以在任务运行过程中动态的调整任务的执行时间或频率,而无需修改代码和重启服务。这种方式特别适合于需要根据不同环境和需求,来实现定时任务执行频率差异化的场景。

SchedulingConfigurer接口

@FunctionalInterface
public interface SchedulingConfigurer {
   void configureTasks(ScheduledTaskRegistrar taskRegistrar);
}

示例代码:

@Component
public class CronTriggerSchedulingConfigurer implements SchedulingConfigurer {
   @Value("${schedul.config.trigger.cron}")
   private String cron;
   
   @Override
   public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
      taskRegistrar.addTriggerTask(() ->{
         System.out.println("this is cron triggerTask! "+DateUtil.DateToString(new Date()));          
      },
      trigger ->{          
         return new CronTrigger(cron).nextExecutionTime(trigger);
      });
   }
}

上述的示例代码中CronTriggerSchedulingConfigurer类实现了SchedulingConfigurer接口,在重写接口的configureTasks方法时,我们向ScheduledTaskRegistrar实例注册了一个TriggerTask类型的任务。TriggerTask类的构造函数需要两个参数,一个是代表任务业务逻辑的Runnable实例。另一个是触发任务执行频率的Trigger实例。这里Trigger我们使用了CronTrigger,这就意味着任务调度将以cron表达式定义的方式来执行。

代码中使用了@Value注解来注入cron表达式,在实际开发中,我们就可以使用Apollo配置或者从数据库读取的方式来获取cron表达式。这样当我们修改了cron表达式,程序在下次执行任务时,就读到新的cron表达式,那么任务调度就会以新的频率来执行任务。在示例中cron表达式配置为schedul.config.trigger.cron = 0/6 * * * * ?,表示定时任务会以6秒钟一次的频率来执行。

在configureTasks方法中,如果触发任务执行频率使用了PeriodicTrigger的实例,那么任务调度将会以周期形式来触发执行。PeriodicTrigger是可以用周期时间间隔fixedRate或者周期时间延迟fixedDelay这两种具体形式来触发任务。我们可以通过设置PeriodicTrigger实例的属性fixedRate来具体决定使用哪种方式。

period

long类型

表示执行任务的周期时长,fixedRate模式下表示执行任务的时间间隔,fixedDelay模式下表示延迟多长时间后才执行下一个任务。

timeUnit

TimeUnit类型

表示period周期的时间单位,默认是毫秒。

initialDelay

long类型

表示延迟多长时间后,才开始执行第一次任务。

fixedRate

boolean类型

表示是以fixedRate固定时间间隔模式,还是fixedDelay固定延迟模式来执行任务,默认为false也就是fixedDelay模式。

@Component
public class PeriodicTriggerSchedulingConfigurer implements SchedulingConfigurer {
   @Value("${schedul.config.trigger.periodic}")
   private long periodic; 
   
   @Override
   public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
      System.out.println("Periodic triggerTask start at "+DateUtil.DateToString(new Date()));
      
      taskRegistrar.addTriggerTask(() ->{
         ConcurrentUtil.Sleep(5);
         System.out.println("Thread "+Thread.currentThread().getName()+" execute periodic triggerTask! "+DateUtil.DateToString(new Date()));          
      },
      trigger ->{          
         PeriodicTrigger periodicTrigger = new PeriodicTrigger(periodic);
         periodicTrigger.setInitialDelay(6000);
         return periodicTrigger.nextExecutionTime(trigger);
      });    
   }
}

上述代码中的PeriodicTriggerSchedulingConfigurer使用了PeriodicTrigger这个周期性任务的执行触发器。触发器的initialDelay属性设置为6秒,表示第一次任务会延迟6秒后才执行。由于fixedRate属性默认为false, 那么任务将以fixedDelay固定延迟的模式来执行。schedul.config.trigger.periodic设置是的3000毫秒,所以任务在执行完成后,会延迟个3秒钟才能执行下一个的任务。任务执行时间是5秒加上延迟的3秒,所以每执行一次任务的周期是8秒钟。

当我们把PeriodicTrigger触发器的fixedRate属性默认为true时, 那么任务调度就会以fixedRate固定时间间隔的模式来执行。由于任务的周期periodic设置是的3秒,而任务的执行时间是5秒,任务执行时间比周期时间长,最终每次执行任务的时间周期就是5秒钟。

@Component
public class PeriodicTriggerSchedulingConfigurer implements SchedulingConfigurer {
   @Value("${schedul.config.trigger.periodic}")
   private long periodic; 
   
   @Override
   public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
      System.out.println("Periodic triggerTask start at "+DateUtil.DateToString(new Date()));
      
      taskRegistrar.addTriggerTask(() ->{
         System.out.println("Thread "+Thread.currentThread().getName()+" execute periodic triggerTask! "+DateUtil.DateToString(new Date()));          
         ConcurrentUtil.Sleep(5);
      },
      trigger ->{          
         PeriodicTrigger periodicTrigger = new PeriodicTrigger(periodic);
         periodicTrigger.setInitialDelay(6000);
         periodicTrigger.setFixedRate(true);
         return periodicTrigger.nextExecutionTime(trigger);
      });    
   }
}

3.Spring任务调度的原理

3.1 @EnableScheduling注解

我们在使用Spring进行定时任务调度的时候,需要使用@EnableScheduling注解来开启任务调度的功能。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}

在@EnableScheduling注解的源码中,我们发现@EnableScheduling注解又通过@Import注解引入了SchedulingConfiguration这个配置类。

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {
   @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
   @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
   public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
      return new ScheduledAnnotationBeanPostProcessor();
   }
}

在SchedulingConfiguration配置类中会使用Bean注解的方式来注入一个ScheduledAnnotationBeanPostProcessor类型的实例。ScheduledAnnotationBeanPostProcessor是一个Spring中典型的后置处理器,它是任务调度的核心类。Spring定时任务调度的功能主要是在这个类中实现的。

其实Spring中有许多类似的@EnableXXX+@Import的组合,这种组合可以实现以可插拔的方式来开启和关闭某项功能,可拓展性极强。

3.2 ScheduledAnnotationBeanPostProcessor

ScheduledAnnotationBeanPostProcessor类间接继承了BeanPostProcessor接口,这个后置处理器实现了BeanPostProcessor接口的postProcessAfterInitialization方法。postProcessAfterInitialization方法是在Bean实例化以后会执行的回调方法。

在ScheduledAnnotationBeanPostProcessor类的postProcessAfterInitialization方法中,Spring会找出所有带有@Scheduled注解的方法,然后再根据@Scheduled注解中配置的任务调度信息,将这些方法注册成一个个的定时任务。

3.2.1调度任务的注册

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
   //对一些特殊类型的Bean不做处理,直接返回。
if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
         bean instanceof ScheduledExecutorService) {
      // Ignore AOP infrastructure such as scoped proxies.
      return bean;
   }

//获取bean的最终目标类
   Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
   //判断目标类中是否含有@Scheduled注解
if (!this.nonAnnotatedClasses.contains(targetClass) &&
         AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
         //获取目标类标记了Scheduled注解方法
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
            (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
               Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                     method, Scheduled.class, Schedules.class);
               return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
            });
      //如果目标类中没有@Scheduled注解方法
      //就把这个目标类缓存到nonAnnotatedClasses的Set集合中
if (annotatedMethods.isEmpty()) {
         this.nonAnnotatedClasses.add(targetClass);
         if (logger.isTraceEnabled()) {
            logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
         }
      }
      else {
         // Non-empty set of methods
//遍历所有Scheduled注解方法,通过processScheduled方法把@Scheduled注解
//方法注册成定时任务
         annotatedMethods.forEach((method, scheduledMethods) ->
               scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
         if (logger.isTraceEnabled()) {
            logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
                  "': " + annotatedMethods);
         }
      }
   }
   return bean;
}

在postProcessAfterInitialization方法中,通过反射的方式获取到所有@Scheduled注解的方法,然后再遍历这些方法,把这些注解和方法作为参数来调用processScheduled方法,processScheduled方法中会根据注解信息的不同,把方法注册成不同类型的定时任务。

protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
   try {
      //通过参数bean和method来创建一个Runnable的实例
Runnable runnable = createRunnable(bean, method);
      boolean processedSchedule = false;
      String errorMessage =
            "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";

      Set<ScheduledTask> tasks = new LinkedHashSet<>(4);

      //从Scheduled注解解析获取注解中initialDelay属性值
//initialDelay有数值和字符串两种配置方式,这两方式只能取其一
long initialDelay = scheduled.initialDelay();
      String initialDelayString = scheduled.initialDelayString();
      if (StringUtils.hasText(initialDelayString)) {
         Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
         if (this.embeddedValueResolver != null) {
            initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
         }
         if (StringUtils.hasLength(initialDelayString)) {
            try {
               initialDelay = parseDelayAsLong(initialDelayString);
            }
            catch (RuntimeException ex) {
               throw new IllegalArgumentException(
                     "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
            }
         }
      }

      //从Scheduled注解中获取cron的任务执行表达式
//如果存在cron表达式,则向ScheduledTaskRegistrar注册一个CronTask类型的任务
//注意Scheduled注解中使用了cron表达式,就不能使用initialDelay
      String cron = scheduled.cron();
      if (StringUtils.hasText(cron)) {
         String zone = scheduled.zone();
         if (this.embeddedValueResolver != null) {
            cron = this.embeddedValueResolver.resolveStringValue(cron);
            zone = this.embeddedValueResolver.resolveStringValue(zone);
         }
         if (StringUtils.hasLength(cron)) {
            Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
            processedSchedule = true;
            if (!Scheduled.CRON_DISABLED.equals(cron)) {
               TimeZone timeZone;
               if (StringUtils.hasText(zone)) {
                  timeZone = StringUtils.parseTimeZoneString(zone);
               }
               else {
                  timeZone = TimeZone.getDefault();
               }
               tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
            }
         }
      }

      // At this point we don't need to differentiate between initial delay set or not anymore
      if (initialDelay < 0) {
         initialDelay = 0;
      }

      //从Scheduled注解中获取fixedDelay属性值
//如果配置了fixedDelay属性值,
//则向ScheduledTaskRegistrar注册一个FixedDelayTask类型的任务
      long fixedDelay = scheduled.fixedDelay();
      if (fixedDelay >= 0) {
         Assert.isTrue(!processedSchedule, errorMessage);
         processedSchedule = true;
         tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
      }
      String fixedDelayString = scheduled.fixedDelayString();
      if (StringUtils.hasText(fixedDelayString)) {
         if (this.embeddedValueResolver != null) {
            fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
         }
         if (StringUtils.hasLength(fixedDelayString)) {
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;
            try {
               fixedDelay = parseDelayAsLong(fixedDelayString);
            }
            catch (RuntimeException ex) {
               throw new IllegalArgumentException(
                     "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
            }
            tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
         }
      }

      //从Scheduled注解中获取fixedRate属性值
//如果配置了fixedRate属性值,
//则向ScheduledTaskRegistrar注册一个FixedRateTask类型的任务
      long fixedRate = scheduled.fixedRate();
      if (fixedRate >= 0) {
         Assert.isTrue(!processedSchedule, errorMessage);
         processedSchedule = true;
         tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
      }
      String fixedRateString = scheduled.fixedRateString();
      if (StringUtils.hasText(fixedRateString)) {
         if (this.embeddedValueResolver != null) {
            fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
         }
         if (StringUtils.hasLength(fixedRateString)) {
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;
            try {
               fixedRate = parseDelayAsLong(fixedRateString);
            }
            catch (RuntimeException ex) {
               throw new IllegalArgumentException(
                     "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
            }
            tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
         }
      }

      // Check whether we had any attribute set
      Assert.isTrue(processedSchedule, errorMessage);

      // Finally register the scheduled tasks
      synchronized (this.scheduledTasks) {
         Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
         regTasks.addAll(tasks);
      }
   }
   catch (IllegalArgumentException ex) {
      throw new IllegalStateException(
            "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
   }
}

processScheduled方法共有三个参数:1. Scheduled注解。2.带有Scheduled注解method方法。3.包含了调度任务method方法Bean。

processScheduled方法中会对Scheduled注解中的属性值进行解析,根据注解中属性值的不同,则向ScheduledTaskRegistrar实例注册不同类型的任务。

  1. 若Scheduled注解中配置了cron的任务执行表达式,则向ScheduledTaskRegistrar注册一个CronTask类型的任务。
  2. 若Scheduled注解配置了fixedDelay属性,则向ScheduledTaskRegistrar注册一个FixedDelayTask类型的任务。
  3. 若Scheduled注解配置了fixedRate属性值,则向ScheduledTaskRegistrar注册一个FixedRateTask类型的任务
  4. 若Scheduled注解中配置了initialDelay属性,那么FixedDelayTask和FixedRateTask的任务首次执行会进行延迟。

在配置Scheduled注解的属性值时,需要注意的是:

  1. cron表达式和initialDelay属性不能同时配置。
  2. cron表达式,fixedDelay,fixedRate这三种任务类型每次只能选择其中的一种类型进行配置。

3.2.2调度任务的执行

程序中把Scheduled注解的方法注册成调度任务后,还需要对这些注册的任务进行执行的操作。

ScheduledAnnotationBeanPostProcessor类继承了ApplicationListener<ContextRefreshed-

Event>接口,实现了接口的onApplicationEvent(E event)方法。所以这个后置处理器类也是一个事件监听器,该监听器主要用于监听ContextRefreshedEvent类型的事件。ContextRefreshedEvent事件类是Spring内部为我们提供的一个事件。该事件类的一个参数就是ApplicationContext对象,即当前的容器。Spring是通过容器的refresh()刷新方法来创建一个IOC容器时,容器创建过程的最后一步finishRefresh()方法会调用publishEvent(new ContextRefreshedEvent(this))方法发布了ContextRefreshedEvent事件。此时Spring容器中的bean都已经创建完成,容器也已经初始化完成了。publishEvent方法会触发ScheduledAnnotationBeanPostProcessor监听器所实现的onApplicationEvent方法。在onApplicationEvent方法中又调用了finishRegistration()方法。在finishRegistration()方法中,就会把已经解析注册任务给调度执行起来。

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
   if (event.getApplicationContext() == this.applicationContext) {
      finishRegistration();
   }
}
private void finishRegistration() {
   if (this.scheduler != null) {
      this.registrar.setScheduler(this.scheduler);
   }
   //这里主要是对SchedulingConfigurer接口方式配置的调度任务进行处理
   if (this.beanFactory instanceof ListableBeanFactory) {
      //获取容器中所有SchedulingConfigurer类型的实例
Map<String, SchedulingConfigurer> beans =
            ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
      List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
      AnnotationAwareOrderComparator.sort(configurers);
      //遍历容器中的每个SchedulingConfigurer实例,依次调用实例的configureTasks方法
//通过configureTasks方法,就把用户自定义的任务注册到了ScheduledTaskRegistrar实例中
for (SchedulingConfigurer configurer : configurers) {
         configurer.configureTasks(this.registrar);
      }
   }

    //当ScheduledTaskRegistrar中没有任务调度的线程池时, 
//就会在容器中去查找线程池,如果容器中存在线程池的话,就通过setTaskScheduler方法
//把这个线程池用于执行要进行调度的任务
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
      Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
      try {
         // Search for TaskScheduler bean...
//通过resolveSchedulerBean方法按类型在容器中查找TaskScheduler类型的Bean实例
         this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
      }
      catch (NoUniqueBeanDefinitionException ex) {
         logger.trace("Could not find unique TaskScheduler bean", ex);
//如果容器中存在多个TaskScheduler类型的Bean实例
//就再通过resolveSchedulerBean方法按名称的方式
//来确定一个TaskScheduler类型的Bean实例
         try {
            this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
         }
         catch (NoSuchBeanDefinitionException ex2) {
            if (logger.isInfoEnabled()) {
               logger.info("More than one TaskScheduler bean exists within the context, and " +
                     "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
                     "(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
                     "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
                     ex.getBeanNamesFound());
            }
         }
      }
      catch (NoSuchBeanDefinitionException ex) {
         logger.trace("Could not find default TaskScheduler bean", ex);
         // Search for ScheduledExecutorService bean next...
//若容器中不存在TaskScheduler类型的实例
//就再去查找ScheduledExecutorService类型的实例。
//也是先按类型再容器中查找ScheduledExecutorService类型的实例
         try {
            this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
         }
         catch (NoUniqueBeanDefinitionException ex2) {
            logger.trace("Could not find unique ScheduledExecutorService bean", ex2);
//同样当容器中存在多个ScheduledExecutorService类型的实例的话
//就会再按名称的方式来确定一个
            try {
               this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
            }
            catch (NoSuchBeanDefinitionException ex3) {
               if (logger.isInfoEnabled()) {
                  logger.info("More than one ScheduledExecutorService bean exists within the context, and " +
                        "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
                        "(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
                        "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
                        ex2.getBeanNamesFound());
               }
            }
         }
         catch (NoSuchBeanDefinitionException ex2) {
            logger.trace("Could not find default ScheduledExecutorService bean", ex2);
            // Giving up -> falling back to default scheduler within the registrar...
            logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");
         }
      }
   }
  //最后调用ScheduledTaskRegistrar实例的afterPropertiesSet方法
//在该方法中,程序会通过线程池把注册的调度任务给执行起来
   this.registrar.afterPropertiesSet();
}

通过分析以上finishRegistration()方法的源码,发现finishRegistration()方法主要做了三件事情:

  1. 把SchedulingConfigurer接口类型调度任务注册到ScheduledTaskRegistrar实例。
  2. 给ScheduledTaskRegistrar实例设置一个用于执行调度任务的线程池。
  3. 调用ScheduledTaskRegistrar实例的afterPropertiesSet()方法,把注册的调度任务给执行起来。
3.2.1.1 SchedulingConfigurer接口调度任务的注册

SchedulingConfigurer接口中就声明了一个方法configureTasks(ScheduledTaskRegistrar taskRegistrar),方法中需要传入一个ScheduledTaskRegistrar类型的实例参数。

从代码中可以看出,Spring会首先从容器中查找所有SchedulingConfigurer类型的Bean实例,对这些Bean实例进行排序,然后依次遍历每个实例的并调用其configureTasks方法。在configureTasks方法中就可以把用户自定义的任务,注册到ScheduledTaskRegistrar实例中。

3.2.1.2 设置线程池

Spring首先将@Scheduled注解的调度任务解析出来后,就会注册到ScheduledTaskRegistrar的实例。接下来就是对这些调度的任务的执行,对于任务的执行,Spring会把这些任务交给线程池来执行,所以到目前为止我们就需要一个线程池。那么执行任务的调度线程池从何而来,Spring是做了如下的处理:

1. Spring会首先在容器中取查找TaskScheduler类型的实例,如果容器中有且只存在一个TaskScheduler类型的实例,就把这个TaskScheduler类型实例的线程池设置到ScheduledTaskRegistrar实例中,以后调度任务的执行就使用这个线程池。

2. 如果容器中存在多个TaskScheduler类型的实例,程序就会抛出NoUniqueBeanDefinitionException异常。程序会捕捉NoUniqueBeanDefinitionException异常,在对NoUniqueBeanDefinitionException异常进行处理时,会再按名称来确定一个TaskScheduler实例。Bean的名称会默认为taskScheduler。

3.如果容器中不存在TaskScheduler类型的实例,程序会抛出NoSuchBeanDefinitionException异常。程序在捕捉到NoSuchBeanDefinitionException异常进行处理的时候,又会在容器中取查找ScheduledExecutorService类型的线程池。

4. 程序查找ScheduledExecutorService类型线程池的步骤和以上查询TaskScheduler线程池步骤大致相似。如果容器中有且只存在一个ScheduledExecutorService实例,就直接返回这个实例。如果存在多个也是抛出NoUniqueBeanDefinitionException异常,在处理NoUniqueBeanDefinitionException这个异常时,同样是再按名称taskScheduler确定一个。若容器不存在ScheduledExecutorService类型的实例,在处理NoSuchBeanDefinitionException异常的时候,只是记录一下日志不做其他任务的处理,表明此时容器中用户没有配置线程池。

 在以上对线程池查找的每个步骤中,其实都调用了resolveSchedulerBean的方法。resolveSchedulerBean的方法是通过布尔参数的byName,来确定是按类型查找还是按名称来查找。

private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) {
   if (byName) {
      T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType);
      if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory) {
         ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean(
               DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName);
      }
      return scheduler;
   }
   else if (beanFactory instanceof AutowireCapableBeanFactory) {
      NamedBeanHolder<T> holder = ((AutowireCapableBeanFactory) beanFactory).resolveNamedBean(schedulerType);
      if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory) {
         ((ConfigurableBeanFactory) beanFactory).registerDependentBean(holder.getBeanName(), this.beanName);
      }
      return holder.getBeanInstance();
   }
   else {
      return beanFactory.getBean(schedulerType);
   }
}

线程池设置的流程图:

3.2.1.3 执行任务

现在是Spring已经将@Scheduled注解的调度任务解析出来并注册了,执行调度任务线程池也有了(如果用户没有配置线程池,Spring 接下来会自己默认创建一个),接下来就是任务的执行了。

调度任务的执行Spring是调用了ScheduledTaskRegistrar类的afterPropertiesSet方法。在afterPropertiesSet方法中又调用了scheduleTasks方法进行任务的执行。

this.registrar.afterPropertiesSet();

public void afterPropertiesSet() {
   scheduleTasks();
}
protected void scheduleTasks() {
   //如果程序中没有用户自定义配置线程池,
   //程序就会默认创建一个单线程的线程池
   if (this.taskScheduler == null) {
      this.localExecutor = Executors.newSingleThreadScheduledExecutor();
      this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
   }
   //对TriggerTask 触发类型的任务进行执行
   if (this.triggerTasks != null) {
      for (TriggerTask task : this.triggerTasks) {
         addScheduledTask(scheduleTriggerTask(task));
      }
   }
   //对CronTask Cron表达式类型的任务进行执行
   if (this.cronTasks != null) {
      for (CronTask task : this.cronTasks) {
         addScheduledTask(scheduleCronTask(task));
      }
   }
   //对固定时间间隔的任务进行执行
   if (this.fixedRateTasks != null) {
      for (IntervalTask task : this.fixedRateTasks) {
         addScheduledTask(scheduleFixedRateTask(task));
      }
   }
   //对固定延迟任务进行执行
   if (this.fixedDelayTasks != null) {
      for (IntervalTask task : this.fixedDelayTasks) {
         addScheduledTask(scheduleFixedDelayTask(task));
      }
   }
}

从scheduleTasks方法的源码可以看出,Spring会先判断用户有没有配置自定义的线程池,如果没有的话,就会默认创建一个单线程的线程池来执行调度任务。

Spring在解析调度任务的时候,是按照cron表达式,fixedDelay,fixedRate,TriggerTask这几种不同的任务类型进行注册。所以在调度任务执行的时,也是分别遍历各种类型任务的集合,依次来执行各种的调度任务。

3.2.1.3.1 CronTask和TriggerTask任务执行

从scheduleTasks方法的代码可以看出,Spring是通过调用scheduleTriggerTask方法和scheduleCronTask方法来分别执行CronTask和TriggerTask类型的任务。

public ScheduledTask scheduleCronTask(CronTask task) {
   ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
   boolean newTask = false;
   if (scheduledTask == null) {
      scheduledTask = new ScheduledTask(task);
      newTask = true;
   }
   if (this.taskScheduler != null) {
      scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
   }
   else {
      addCronTask(task);
      this.unresolvedTasks.put(task, scheduledTask);
   }
   return (newTask ? scheduledTask : null);
}
public ScheduledTask scheduleTriggerTask(TriggerTask task) {
   ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
   boolean newTask = false;
   if (scheduledTask == null) {
      scheduledTask = new ScheduledTask(task);
      newTask = true;
   }
   if (this.taskScheduler != null) {
      scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
   }
   else {
      addTriggerTask(task);
      this.unresolvedTasks.put(task, scheduledTask);
   }
   return (newTask ? scheduledTask : null);
}

TaskScheduler线程池实例调用了schedule方法来执行CronTask和TriggerTask任务。在Spring的TaskScheduler线程池类中组合封装了JDK的ScheduledExecutorService实例。TaskScheduler线程池对任务的执行最终都是委托给了ScheduledExecutorService实例来执行的。

我们知道ScheduledExecutorService实例执行schedule方法是一次性的,执行一次后就不会再次执行了,但是实际中的CronTask和TriggerTask任务都是周期循环执行的。这里面其实是Spring自己做了一些工作来支持循环周期执行。原理很简单,简单概括就是Spring在进行schedule任务调度时,每次会根据当前调度的时间信息,计算出下一次任务的执行时间,然后不断的重复进行任务调度。下面我们通过源码来具体分析实现的细节。

public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
   //获取JDK调度线程池的实例用于执行任务
   ScheduledExecutorService executor = getScheduledExecutor();
   try {
      ErrorHandler errorHandler = this.errorHandler;
      if (errorHandler == null) {
         errorHandler = TaskUtils.getDefaultErrorHandler(true);
      }
      //实例化一个ReschedulingRunnable的实例,并调用实例schedule方法来调度任务
      return new ReschedulingRunnable(task, trigger, executor, errorHandler).schedule();
   }
   catch (RejectedExecutionException ex) {
      throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
   }
}

我们以TaskScheduler接口其中的一个实现类ThreadPoolTaskScheduler为例来进行分析。在schedule方法中,有一行非常关键的代码是用任务,触发器,线程池等参数构建了一个ReschedulingRunnable的对象,然后再调用这个对象的schedule方法。

public ScheduledFuture<?> schedule() {
   synchronized (this.triggerContextMonitor) {
      //计算出下一次任务的执行时间
      this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
      if (this.scheduledExecutionTime == null) {
         return null;
      }
      //现在下次任务的执行时间减去当前的时间
//计算出距离下次任务的时间间隔
      long initialDelay = this.scheduledExecutionTime.getTime() - System.currentTimeMillis();
      //将ReschedulingRunnable自身对象作为任务提交给线程池执行
      this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
      return this;
   }
}

在ReschedulingRunnable类中的schedule方法中,Spring会根据CronTask或者TriggerTask任务的触发机制计算出下一次任务的执行时间。然后用计算出的下一次任务的执行时间减去系统当前时间得到距离下一次任务的时间间隔。源码中用这个时间间隔作为任务延迟的参数,把ReschedulingRunnable对象本身this作为任务,调用了ScheduledExecutorService实例的schedule方法来提交任务进行执行。ReschedulingRunnable之所以能够作为任务,是因为这个类实现了Runnable接口。

Runnable接口中定义了一个run方法,当Runnable接口的实例提交到线程池后,其定义的run方法就会被执行。ReschedulingRunnable作为Runnable接口的实现类,自然也给run方法给出了自己的实现。

public void run() {
   //记录调度任务实际开始执行的时间
   Date actualExecutionTime = new Date();
   //这里是执行用户自定义的调度任务
   super.run();
   //记录调度任务实际执行结束的时间
   Date completionTime = new Date();
   synchronized (this.triggerContextMonitor) {
      Assert.state(this.scheduledExecutionTime != null, "No scheduled execution");
      //在TriggerContext的上下文中更新任务执行的时间信息
      this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
      if (!obtainCurrentFuture().isCancelled()) {
         //再一次调用schedule方法,触发下一次的任务调度
         schedule();
      }
   }
}

在run方法中,首先调用父类的run方法,这里通过super.run()方法来执行用户自定义的调度任务,也就是@Scheduled注解的方法,并会记录下执行这些任务实际开始和结束时间。然后用任务的调度时间,执行任务实际开始和结束时间,在TriggerContext的上下文中更新任务执行的时间信息。在做完这一切以后,Spring 会再次调用schedule方法触发下一次的任务调度。而这个schedule方法又是把ReschedulingRunnable对象本身作为任务提交,那么ReschedulingRunnable对象作为任务时,其自身的run()方法又会被调用,这样ReschedulingRunnable类的schedule()方法会触发run()方法,而run()方法又会schedule()方法,这样就达到了循环调用的目的。

3.2.1.3.2 FixedRate任务执行

在ScheduledTaskRegistrar实例中,通过调用scheduleFixedRateTask方法来执行FixedRate类型的调度任务。

public ScheduledTask scheduleFixedRateTask(FixedRateTask task) {
   ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
   boolean newTask = false;
   if (scheduledTask == null) {
      scheduledTask = new ScheduledTask(task);
      newTask = true;
   }
   if (this.taskScheduler != null) {
      if (task.getInitialDelay() > 0) {
         Date startTime = new Date(System.currentTimeMillis() + task.getInitialDelay());
         scheduledTask.future =
               this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), startTime, task.getInterval());
      }
      else {
         scheduledTask.future =
               this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), task.getInterval());
      }
   }
   else {
      addFixedRateTask(task);
      this.unresolvedTasks.put(task, scheduledTask);
   }
   return (newTask ? scheduledTask : null);
}

在scheduleFixedRateTask方法中,对FixedRate类型调度任务的执行其实又是调用了TaskScheduler线程池实例的scheduleWithFixedDelay方法。

本例中我们以TaskScheduler线程池实例之一的ThreadPoolTaskScheduler类来进行分析。ThreadPoolTaskScheduler类的scheduleWithFixedDelay方法的源码如下。

public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period) {
   //获取JDK的ScheduledExecutorService实例线程池 
   ScheduledExecutorService executor = getScheduledExecutor();
   long initialDelay = startTime.getTime() - System.currentTimeMillis();
   try {
      //调用JDK线程池的scheduleAtFixedRate方法来执行固定间隔的任务
      return executor.scheduleAtFixedRate(errorHandlingTask(task, true), initialDelay, period, TimeUnit.MILLISECONDS);
   }
   catch (RejectedExecutionException ex) {
      throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
   }
}

从以上源码中可以出,FixedRate类型的任务执行比较简单,由于JDK中的ScheduledExecutorService线程池本身就支持执行FixedRate这种固定时间间隔的任务。所以ThreadPoolTaskScheduler类中会先实例化一个ScheduledExecutorService类型的线程池,把任务封装成了一个DelegatingErrorHandlingRunnable类型的任务,然后程序就直接调用ScheduledExecutorService线程池实例的scheduleAtFixedRate方法来执行调度任务。

3.2.1.3.2 FixedDelay任务执行

FixedDelay和FixedRate任务的实现原理十分相似,由于JDK中的ScheduledExecutorService线程池也支持执行FixedDelay这种固定时间延迟的任务。所以程序中对FixedDelay任务,最终是委托给了ScheduledExecutorService线程池实例来执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2154009.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网安面试会问到的:http的长连接和短连接

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 《Java代码审…

探索 Python 的火焰:Fire 库的神秘力量

文章目录 &#x1f525; 探索 Python 的火焰&#xff1a;Fire 库的神秘力量第一部分&#xff1a;背景介绍第二部分&#xff1a;Fire 库是什么&#xff1f;第三部分&#xff1a;如何安装 Fire&#xff1f;第四部分&#xff1a;简单库函数使用方法第五部分&#xff1a;场景应用第…

32.递归、搜索、回溯之floodfill算法

0.简介 1.图像渲染 . - 力扣&#xff08;LeetCode&#xff09; 题目解析 算法原理 代码 class Solution {int[] dx { 0, 0, 1, -1 };int[] dy { 1, -1, 0, 0 };int m, n;int prev;public int[][] floodFill(int[][] image, int sr, int sc, int color) {if (image[sr][sc]…

yolov5足球运动分析-速度分析-足球跟踪

足球分析项目 引言 在现代体育分析领域&#xff0c;利用先进的计算机视觉技术和机器学习模型对比赛视频进行深入解析已成为一种趋势。本项目旨在通过YOLO&#xff08;You Only Look Once&#xff09;这一顶级的人工智能目标检测模型来识别并跟踪足球比赛中的球员、裁判以及足球…

【每日一题】LeetCode 2374.边积分最高节点(图、哈希表)

【每日一题】LeetCode 2374.边积分最高节点&#xff08;图、哈希表&#xff09; 题目描述 给定一个有向图&#xff0c;图中包含 n 个节点&#xff0c;节点编号从 0 到 n - 1。每个节点都有一个出边&#xff0c;指向图中的另一个节点。图由一个长度为 n 的整数数组 edges 表示…

江协科技STM32学习- P15 TIM输出比较

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

6张图掌握提示词工程师工作范围与工作技巧(提示词原理篇)

在人工智能的疆域中&#xff0c;提示词工程师扮演着至关重要的角色。他们精心设计的话语&#xff0c;是引导AI模型理解人类需求、激发创造力的关键。如同指挥官的号令&#xff0c;提示词工程师的每一个提问&#xff0c;都让AI的潜力得到释放&#xff0c;让技术与智慧的对话更加…

如何有效检测住宅IP真伪?

在当今的互联网时代&#xff0c;住宅IP&#xff08;即家庭用户通过宽带服务提供商获得的IP地址&#xff09;在跨境电商、广告投放、网络安全等多个领域扮演着重要角色。然而&#xff0c;随着网络环境的复杂化和欺诈行为的增多&#xff0c;如何有效检测和辨别住宅IP的真伪成为了…

2024年csp-j 初赛 真题+答案解析

恭喜CSP-J组考生完成第一轮认证&#xff01; 今天是CSP-J/S初赛的考试日&#xff0c;首先要祝贺所有参加CSP-J组考试的同学顺利完成第一轮认证&#xff01;

C++中的new与delete

目录 1.简介 2.底层 1.简介 new是升级版的malloc&#xff0c;它会先开空间再去调用构造函数。 delete是升级版的free&#xff0c;它会先调用析构函数再free掉空间。 class A { public:A(int a10, int b10){a a1;b b1;}private:int a;int b; };int main() {//new会先开空间…

大数据Flink(一百二十三):五分钟上手Flink MySQL连接器

文章目录 五分钟上手Flink MySQL连接器 一、创建数据库表 二、​​​​​​创建session集群 三、源表查询 四、​​​​​窗口计算 五、​​​​​​结果数据写回数据库 五分钟上手Flink MySQL连接器 MySQL Connector可以将本地或远程的MySQL数据库连接到Flink中&#x…

以太网接口MII 和 RMII

媒体独立接口 (MII) 是连接以太网MAC (媒体访问控制) 设备和PHY (物理层) 设备的标准化方法。其主要目的是促进以太网系统这两个基本组件之间的通信。 媒体独立接口 (MII) 介质独立接口 (MII) 是由 IEEE 802.3 标准定义的并行接口。MII 的管理接口允许配置和控制多个 PHY 设备…

安装Win11 24H2如何跳过BitLocker加密

今天来给大家分享一下在安装 Windows11 24H2 时&#xff0c;如何跳过BitLocker加密的小方法&#xff0c;Windows11 24H2版本默认自动为用户开启Bitlocker 加密&#xff0c;很多用户不知道自己设备已被偷偷加密&#xff0c;这样也可能会导致磁盘性能严重损失&#xff0c;特别是4…

计算机毕业设计 数字化农家乐管理平台的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

使用Diskgenius系统迁移

使用Diskgenius系统迁移 1、使用系统迁移2、注意点3、新备份的系统盘装在电脑上可能出现盘符错乱导致开机不进入桌面情况 1、使用系统迁移 参考视频&#xff1a; DiskGenius无损系统迁移&#xff0c;换硬盘无需重装系统和软件 2、注意点 1&#xff09;新的硬盘里面的所有资料…

linux安装Anaconda3

先将Anaconda3安装包下载好&#xff0c;然后在主文件夹里新建一个文件夹&#xff0c;将Anaconda3安装包拖进去。 打开终端未来不出现缺东西的异常情况&#xff0c;我们先安装 yum install -bzip2然后进入根目录下&#xff0c;在进入Anaconda3文件夹下 sh包安装方式 sh Anac…

为什么用迭代器调用不了对象中的函数

没加const可以 加了const就不行 我懂了 加了const v的值就不能修改&#xff0c;我的那些函数都可以修改值 应该是 好像不对 有大佬会吗

vue 实现tab菜单切换

1、目标&#xff1a; 实现切换tab菜单&#xff0c;激活状态&#xff0c;按钮高亮&#xff0c;显示对应的菜单内容 2、实现 <template><div class"tan_menu"><ul class"container"><liclass"item"v-for"item in tab…

常用工具推荐!分享7款AI论文修改软件工具网站

在当今学术研究和写作领域&#xff0c;AI论文修改软件工具已经成为了不可或缺的助手。这些工具不仅能够帮助研究人员提高写作效率&#xff0c;还能确保论文的质量和原创性。以下是七款值得推荐的AI论文修改软件工具网站&#xff0c;其中特别推荐千笔-AIPassPaper。 1. 千笔-AI…

C++编程语言:基础设施:命名空间(Bjarne Stroustrup)

第 14 章 命名空间(Namespaces) 目录 14.1 组成问题(Composition Problems) 14.2 命名空间(Namespaces) 14.2.1 显式修饰(Explicit Qualification) 14.2.2 使用using 声明 14.2.3 using 指令 14.2.4 参数依赖查询 14.2.5 命名空间的开放性 14.3 模块化和接口 …