• SpringBoot定时任务的动态配置处理(动态获取数据库配置的定时任务)


    1. 前言

    2. SpringBoot的动态定时任务

    2.1 准备工作——数据库表和数据

    • 关于 corn 表达式的了解:
      cron表达式的详细介绍(各域说明以及举例说明).
    • 数据如图:
      在这里插入图片描述
    • 建表语句:
      SET NAMES utf8mb4;
      SET FOREIGN_KEY_CHECKS = 0;
      
      -- ----------------------------
      -- Table structure for my_task_corn
      -- ----------------------------
      DROP TABLE IF EXISTS `my_task_corn`;
      CREATE TABLE `my_task_corn`  (
        `corn_id` int(10) NOT NULL AUTO_INCREMENT COMMENT '主键id',
        `corn` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'corn表达式',
        `execute_cycle` varchar(4) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '定时任务执行的配置的周期(01:每天一次 02:每周一次 03:每月一次)',
        `corn_desc` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '解释说明',
        `off_state` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '关闭状态',
        PRIMARY KEY (`corn_id`) USING BTREE
      ) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '定时任务配置表' ROW_FORMAT = Dynamic;
      
      -- ----------------------------
      -- Records of my_task_corn
      -- ----------------------------
      INSERT INTO `my_task_corn` VALUES (1, '0/5 * * * * ?', '00', '每5秒执行一次', 'on');
      INSERT INTO `my_task_corn` VALUES (2, '0 0 0 * * ? ', '01', '每天一次,每天晚上凌晨24点执行(0 0 0 * * ? *)', 'on');
      INSERT INTO `my_task_corn` VALUES (3, '0 0 0 ? * 5', '02', '每周一次,每周五凌晨24点执行', 'off');
      INSERT INTO `my_task_corn` VALUES (4, '0 0 0 1 * ?', '03', '每月1号执行凌晨24点执行', 'off');
      INSERT INTO `my_task_corn` VALUES (6, '0 0 0 1 7 ?', '04', '每年一次,每年的7月1日执行凌晨24点执行', 'on');
      
      SET FOREIGN_KEY_CHECKS = 1;
      
      • 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

    2.2 Java代码实现

    2.2.1 实体

    • JPA工具自动生成的,没改,看着不舒服自己改,如下:
      package com.liu.susu.pojo;
      
      import javax.persistence.*;
      import java.util.Objects;
      
      /**
       * @FileName MyTaskCornEntity
       * @Description  动态定时任务配置表
       * @Author susu
       * @date 2022-06-21
       **/
      @Entity
      @Table(name = "my_task_corn", schema = "liu", catalog = "")
      public class MyTaskCornEntity {
      
          private int cornId;
          private String corn;
          private String executeCycle;
          private String cornDesc;
          private String offState;
      
          @Id
          @Column(name = "corn_id", nullable = false)
          public int getCornId() {
              return cornId;
          }
      
          public void setCornId(int cornId) {
              this.cornId = cornId;
          }
      
          @Basic
          @Column(name = "corn", nullable = true, length = 32)
          public String getCorn() {
              return corn;
          }
      
          public void setCorn(String corn) {
              this.corn = corn;
          }
      
          @Basic
          @Column(name = "execute_cycle", nullable = true, length = 4)
          public String getExecuteCycle() {
              return executeCycle;
          }
      
          public void setExecuteCycle(String executeCycle) {
              this.executeCycle = executeCycle;
          }
      
          @Basic
          @Column(name = "corn_desc", nullable = true, length = 100)
          public String getCornDesc() {
              return cornDesc;
          }
      
          public void setCornDesc(String cornDesc) {
              this.cornDesc = cornDesc;
          }
      
          @Basic
          @Column(name = "off_state", nullable = true, length = 10)
          public String getOffState() {
              return offState;
          }
      
          public void setOffState(String offState) {
              this.offState = offState;
          }
      
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null || getClass() != o.getClass()) return false;
              MyTaskCornEntity that = (MyTaskCornEntity) o;
              return cornId == that.cornId && Objects.equals(corn, that.corn) && Objects.equals(executeCycle, that.executeCycle) && Objects.equals(cornDesc, that.cornDesc) && Objects.equals(offState, that.offState);
          }
      
          @Override
          public int hashCode() {
              return Objects.hash(cornId, corn, executeCycle, cornDesc, offState);
          }
      }
      
      
      • 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

    2.2.2 查询数据库

    • 如下:
      在这里插入图片描述
      package com.liu.susu.thread.task.scheduled.dynamic;
      
      import com.liu.susu.pojo.Dog;
      import org.springframework.stereotype.Service;
      
      import javax.persistence.EntityManager;
      import javax.persistence.PersistenceContext;
      import javax.persistence.Query;
      
      /**
       * @FileName CornUtils
       * @Description
       * @Author susu
       * @date 2022-06-21
       **/
      @Service
      public class CornUtils {
      
          @PersistenceContext
          private EntityManager entityManager;
      
          public String getCorn(String executeCycle){
              String sql = "select t.corn from my_task_corn t where t.execute_cycle=?1";
              Query query = entityManager.createNativeQuery(sql);
              query.setParameter(1,executeCycle);
              return (String) query.getSingleResult();
          }
      
      }
      
      
      • 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

    2.2.3 动态定时任务实现代码1

    2.2.3.1 代码

    • 如下:
      在这里插入图片描述

      package com.liu.susu.thread.task.scheduled.dynamic;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.scheduling.annotation.Async;
      import org.springframework.scheduling.annotation.SchedulingConfigurer;
      import org.springframework.scheduling.config.ScheduledTaskRegistrar;
      import org.springframework.scheduling.support.CronTrigger;
      import org.springframework.stereotype.Component;
      
      import java.time.LocalDateTime;
      
      /**
       * @FileName DynamicScheduledTest
       * @Description  动态定时任务(从数据库获取配置)
       * @Author susu
       * @date 2022-06-21
       **/
      @Component
      @Slf4j
      public class DynamicScheduledTest implements SchedulingConfigurer {
      
          @Autowired
          private CornUtils cornUtils;
      
          @Override
          public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
      
              scheduledTaskRegistrar.addTriggerTask(
                      //1.添加任务内容(Runnable)
                      () -> log.info("这里动态执行定时任务--->"+Thread.currentThread().getName()),
      
                      //2.设置执行周期(Trigger)
                      triggerContext -> {
                          //2.1 从数据库获取 corn 表达式(执行周期)
                          String cron = cornUtils.getCorn("00");
                          log.info("cron表达式--->: " + cron);
      
                          //2.3 返回执行周期(Date)
                          return new CronTrigger(cron).nextExecutionTime(triggerContext);
                      }
              );
          }
      
      }
      
      
      • 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

    2.2.3.2 效果

    • 如图:
      在这里插入图片描述

    2.2.4 动态定时任务实现代码2

    • 与上面是一样的,就是拆分一下
      package com.liu.susu.thread.task.scheduled.dynamic;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.scheduling.Trigger;
      import org.springframework.scheduling.TriggerContext;
      import org.springframework.scheduling.annotation.SchedulingConfigurer;
      import org.springframework.scheduling.config.ScheduledTaskRegistrar;
      import org.springframework.scheduling.support.CronTrigger;
      import org.springframework.stereotype.Component;
      
      import java.time.LocalDateTime;
      import java.util.Date;
      
      /**
       * @FileName DynamicScheduledTest
       * @Description  动态定时任务(从数据库获取配置)
       * @Author susu
       * @date 2022-06-21
       **/
      @Component
      @Slf4j
      public class DynamicScheduledTest2 implements SchedulingConfigurer {
      
          @Autowired
          private CornUtils cornUtils;
      
      
          @Override
          public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
              scheduledTaskRegistrar.addTriggerTask(myTask(),trigger());
          }
      
          /**
           * 要执行的定时任务内容
           * @return
           */
          private Runnable myTask() {
              return new Runnable() {
                  @Override
                  public void run() {
                      //业务逻辑部分
                      log.info("这里动态执行定时任务--->"+Thread.currentThread().getName());
                  }
              };
          }
      
          /**
           * 从数据库读取执行周期
           * @return
           */
          private Trigger trigger() {
              return new Trigger() {
                  @Override
                  public Date nextExecutionTime(TriggerContext triggerContext) {
                      //每一次任务触发,都会执行这里的方法一次,重新获取下一次的执行时间。所以它是下下次才生效的,即不是实时生效
                      //1.从数据库获取 corn 表达式(执行周期)
                      String cron = cornUtils.getCorn("00");
                      log.info("cron表达式--->: " + cron);
      
                      //2 返回执行周期(Date)
                      CronTrigger cronTrigger = new CronTrigger(cron);
                      Date date = cronTrigger.nextExecutionTime(triggerContext);
                      return date;
                  }
              };
          }
      
      }
      
      
      • 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

    3. 对于配置多个定时任务的优化

    • 对于配置多任务的情况,在上面代码的基础上可以进行优化

    3.1 优化代码1

    代码

    • 如下:
      在这里插入图片描述
      在这里插入图片描述
      package com.liu.susu.thread.task.scheduled.dynamic;
      
      import com.liu.susu.pojo.MyTaskCornEntity;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.scheduling.Trigger;
      import org.springframework.scheduling.TriggerContext;
      import org.springframework.scheduling.annotation.SchedulingConfigurer;
      import org.springframework.scheduling.config.ScheduledTaskRegistrar;
      import org.springframework.scheduling.support.CronTrigger;
      import org.springframework.stereotype.Component;
      import org.springframework.util.ReflectionUtils;
      
      import java.lang.reflect.Method;
      import java.util.Date;
      import java.util.List;
      
      /**
       * @FileName DynamicScheduledTest
       * @Description  动态定时任务(从数据库获取配置)——多个任务
       * @Author susu
       * @date 2022-06-21
       **/
      @Component
      @Slf4j
      public class DynamicScheduledTest3 implements SchedulingConfigurer {
      
          @Autowired
          private CornUtils cornUtils;
      
          @Override
          public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
              //1.获取需要执行的所有的任务
              List<MyTaskCornEntity> cronList = cornUtils.getAllCornsList();
      
              //2.根据配置情况执行对应的任务
              for (MyTaskCornEntity cornEntity : cronList){
                  scheduledTaskRegistrar.addTriggerTask(myTask(cornEntity),myTrigger(cornEntity));
              }
          }
      
          /**
           * 这里执行所有的定时任务
           * @param cornEntity
           * @return
           */
          private Runnable myTask(MyTaskCornEntity cornEntity) {
              return new Runnable() {
                  @Override
                  public void run() {
                      //业务逻辑部分
                      if ("00".equals(cornEntity.getExecuteCycle())){
                          log.info("这里动态执行的定时任务是--->"+cornEntity.getCorn()+"-->"+Thread.currentThread().getName());
                      }else if ("05".equals(cornEntity.getExecuteCycle())){
                          log.info("这里动态执行的定时任务是--->"+cornEntity.getCorn()+"-->"+Thread.currentThread().getName());
                      }
                  }
              };
          }
      
          /**
           * 根据每一个配置的cron表达式,返回执行周期
           * @param cornEntity
           * @return
           */
          private Trigger myTrigger(MyTaskCornEntity cornEntity){
              return new Trigger() {
                  @Override
                  public Date nextExecutionTime(TriggerContext triggerContext) {
                      CronTrigger trigger = new CronTrigger(cornEntity.getCorn());
                      return trigger.nextExecutionTime(triggerContext);
                  }
              };
          }
      
      }
      
      
      • 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

    效果

    • 如图:
      在这里插入图片描述

    3.2 优化代码2——线程池

    • 上面 3.1 优化后的代码,我们可以看到虽然是读取了多个任务,但是是一个线程在跑这些任务,实际开发一般用线程池,所以我们接下来的优化是在3.1的基础上使用线程池。
    • 在 3.1 代码的基础上只加一行代码,如图:
      在这里插入图片描述
    • 效果如图:
      在这里插入图片描述

    3.2 优化代码3——自定义线程池

    3.2.1 自定义线程池(ThreadPoolTaskScheduler)

    • 如下:
      在这里插入图片描述
      package com.liu.susu.thread.task.scheduled.dynamic;
      
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.scheduling.TaskScheduler;
      import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
      import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
      
      import java.util.concurrent.Executor;
      import java.util.concurrent.ThreadPoolExecutor;
      
      /**
       * @FileName DynamicScheduledPoolConfig
       * @Description  动态定时任务的线程池
       * @Author susu
       * @date 2022-03-09
       **/
      @Configuration
      public class DynamicScheduledPoolConfig {
          private static int poolSize = 5;//核心线程
          private static int awaitTermination = 60;//挂壁时间
          private static String threadNamePrefix = "task-job-";
          private static volatile ThreadPoolTaskScheduler taskScheduler;
          @Bean
          public static Executor getMySchedulerPool(){
              if(taskScheduler == null){
                  synchronized (ThreadPoolTaskScheduler.class){
                      if(taskScheduler == null){
                          taskScheduler = new ThreadPoolTaskScheduler();
                          taskScheduler.setThreadNamePrefix(threadNamePrefix);
                          taskScheduler.setPoolSize(poolSize);
      //                    taskScheduler.setThreadFactory(Executors.defaultThreadFactory());
                          taskScheduler.setAwaitTerminationSeconds(awaitTermination);//演示挂壁的时间为60s
                          /**设置为false,关闭线程池中的任务时,直接执行shutdownNow() 延时关闭 开启*/
                          taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
                          taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//线程池对拒绝任务(无线程可用的)的处理策略
                          taskScheduler.initialize();//初始化
                      }
                  }
              }
              return taskScheduler;
          }
      
      }
      
      
      • 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

    3.2.2 使用自定义的线程池

    • 如图,代码只改一处,所以直接上图
      在这里插入图片描述
      • 放上完整代码也行
      package com.liu.susu.thread.task.scheduled.dynamic;
      
      import com.liu.susu.pojo.MyTaskCornEntity;
      import com.liu.susu.thread.task.async.async2.config.LiuTaskExecutorConfig2;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.context.annotation.Bean;
      import org.springframework.core.task.TaskExecutor;
      import org.springframework.scheduling.TaskScheduler;
      import org.springframework.scheduling.Trigger;
      import org.springframework.scheduling.TriggerContext;
      import org.springframework.scheduling.annotation.SchedulingConfigurer;
      import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
      import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
      import org.springframework.scheduling.config.ScheduledTaskRegistrar;
      import org.springframework.scheduling.support.CronTrigger;
      import org.springframework.stereotype.Component;
      
      import java.util.Date;
      import java.util.List;
      import java.util.concurrent.Executors;
      import java.util.concurrent.ThreadPoolExecutor;
      
      /**
       * @FileName DynamicScheduledTest
       * @Description  动态定时任务(从数据库获取配置)——多个任务
       * @Author susu
       * @date 2022-06-21
       **/
      @Component
      @Slf4j
      public class DynamicScheduledTest4 implements SchedulingConfigurer {
      
          @Autowired
          private CornUtils cornUtils;
          
          @Override
          public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
              //0-1: 配置线程池
      //        scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
      
              //0-2: 用自定义的线程池
              scheduledTaskRegistrar.setScheduler(DynamicScheduledPoolConfig.getMySchedulerPool());
      
              //1.获取需要执行的所有的任务
              List<MyTaskCornEntity> cronList = cornUtils.getAllCornsList();
      
              //2.根据配置情况执行对应的任务
              for (MyTaskCornEntity cornEntity : cronList){
                  scheduledTaskRegistrar.addTriggerTask(myTask(cornEntity),myTrigger(cornEntity));
              }
          }
      
          /**
           * 这里执行所有的定时任务
           * @param cornEntity
           * @return
           */
          private Runnable myTask(MyTaskCornEntity cornEntity) {
              return new Runnable() {
                  @Override
                  public void run() {
                      //业务逻辑部分
      //                log.info("这里动态执行的定时任务是--->"+cornEntity.getCorn()+"-->"+Thread.currentThread().getName());
      
                      if ("00".equals(cornEntity.getExecuteCycle())){
                          log.info("任务1:这里动态执行的定时任务是--->"+cornEntity.getCorn()+"-->"+Thread.currentThread().getName());
      
                      }else if ("05".equals(cornEntity.getExecuteCycle())){
                          log.info("任务2:这里动态执行的定时任务是--->"+cornEntity.getCorn()+"-->"+Thread.currentThread().getName());
                      }
      
                  }
              };
          }
      
          /**
           * 根据每一个配置的cron表达式,返回执行周期
           * @param cornEntity
           * @return
           */
          private Trigger myTrigger(MyTaskCornEntity cornEntity){
              return new Trigger() {
                  @Override
                  public Date nextExecutionTime(TriggerContext triggerContext) {
                      CronTrigger trigger = new CronTrigger(cornEntity.getCorn());
                      return trigger.nextExecutionTime(triggerContext);
                  }
              };
          }
      
      }
      
      
      
      • 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

    3.2.3 效果

    • 如图:
      在这里插入图片描述

    4. 代码

  • 相关阅读:
    什么是RabbitMQ
    【Python】pyecharts 数据可视化模块
    Node.js -- http模块
    准备好抛弃 HTML 了吗?Dart 3.1 和 Flutter 3.13 发布
    600w播放,80w涨粉,B站UP主恰饭B站粉丝竟刷屏感谢甲方!
    UE4 Sequence添加基础动画效果 (03-主序列的使用)
    【【萌新的riscV的学习之关于risc指令集的学习使用总五】】
    Nginx实战
    Unity 之 定时调用函数的方法
    【Java】PAT(Basic Level) 1016 部分A+B
  • 原文地址:https://blog.csdn.net/suixinfeixiangfei/article/details/125398999