Java定时任务技术分析

news2024/11/27 16:41:01

《从零打造项目》系列文章

工具

  • 比MyBatis Generator更强大的代码生成器

ORM框架选型

  • SpringBoot项目基础设施搭建
  • SpringBoot集成Mybatis项目实操
  • SpringBoot集成MybatisPlus项目实操
  • SpringBoot集成Spring Data JPA项目实操

数据库变更管理

  • 数据库变更管理:Liquibase or Flyway

  • SpringBoot结合Liquibase实现数据库变更管理

定时任务框架

  • Java定时任务技术分析

缓存

  • 待更新

安全框架

  • 待更新

开发规范

  • 待更新

常见的业务场景:

  • 某博客平台,支持定时发送文章。
  • 某学习平台,定时发送学习任务通知用户
  • 定时进行数据抓取等等

在项目中要求我们在某个时刻去做某件事情,下面我们就来看看有哪些方法可以实现定时任务。

JDK内置类

Timer

java.util.Timer是 JDK 1.3 开始就已经支持的一种定时任务的实现方式。

Timer 内部使用一个叫做 TaskQueue 的类存放定时任务,它是一个基于最小堆实现的优先级队列。TaskQueue 会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!由于某个任务的执行时间可能较长,则后面的任务运行的时间会被延迟,所以执行的时间和你预期的时间可能不一致。延迟的任务具体开始的时间,就是依据前面任务的结束时间。

核心方法:

//启动任务之后,延迟多久时间执行
void schedule(TimerTask task, long delay);
//在指定的时间执行任务
void schedule(TimerTask task, Date time);
//启动任务后,延迟多久时间执行,执行之后指定间隔多久重复执行任务
void schedule(TimerTask task, long delay, long period);
//指定时间启动任务,执行后间隔指定时间重复执行任务
void schedule(TimerTask task, Date firstTime, long period);

代码案例:

public class TimerUse {

  public static void main(String[] args) {
    System.out.println("当前时间: " + new Date() + "n" +
        "线程名称: " + Thread.currentThread().getName());

    testTimer1();
//    testTimer2();
//    testTimer3();
//    testTimer4();
  }

  // 方法一:设定指定任务task在指定时间time执行 schedule(TimerTask task, long delay)
  public static void testTimer1() {
    Timer timer = new Timer("Timer");
    timer.schedule(new TimerTask() {
      public void run() {
        System.out.println("当前时间: " + new Date() + "n" +
            "线程名称: " + Thread.currentThread().getName());
      }
    }, 3500);
    // 设定指定的时间time为3500毫秒
  }

  /**
   * 方法二:设定指定任务task在指定延迟delay后间隔指定时间peroid执行 schedule(TimerTask task, long delay, long period)
   */
  public static void testTimer2() {
    Timer timer = new Timer("Timer");
    timer.schedule(new TimerTask() {
      public void run() {
        System.out.println("当前时间: " + new Date() + "n" +
            "线程名称: " + Thread.currentThread().getName());
      }
    }, 2000, 3500);
  }


  /**
   * 方法三:在指定的时间执行任务 schedule(TimerTask task, Date time)
   */

  public static void testTimer3() {
    Date date = new Date();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.add(Calendar.MINUTE, 1); // 往后推一分钟

    Date time = calendar.getTime();    //获取当前系统时间

    Timer timer = new Timer("Timer");
    timer.schedule(new TimerTask() {
      public void run() {
        System.out.println("当前时间: " + new Date() + "n" +
            "线程名称: " + Thread.currentThread().getName());
      }
    }, time);
  }

  /**
   * 方法四:安排指定的任务task在指定的时间firstTime开始进行重复的固定速率period执行. schedule(TimerTask task, Date firstTime,
   * long period)
   */
  public static void testTimer4() {
    Calendar calendar = Calendar.getInstance();
    calendar.set(Calendar.HOUR_OF_DAY, 12); // 控制小时
    calendar.set(Calendar.MINUTE, 0);    // 控制分钟
    calendar.set(Calendar.SECOND, 0);    // 控制秒

    Date time = calendar.getTime();    //获取当前系统时间

    Timer timer = new Timer("Timer");
    timer.schedule(new TimerTask() {
      public void run() {
        System.out.println("当前时间: " + new Date() + "n" +
            "线程名称: " + Thread.currentThread().getName());
      }
    }, time, 1000 * 60 * 60 * 24);// 这里设定将延时每天固定执行
  }
}

注意事项

1、创建一个 Timer 对象相当于新启动了一个线程,但是这个新启动的线程,并不是守护线程。它一直在后台运行,通过如下代码将新启动的 Timer 线程设置为守护线程。

Timer timer = new Timer(true);

变为守护线程,则意味着主线程执行结束,则程序就结束了,定时任务也就不会执行。

2、当计划时间早于当前时间,则任务立即被运行。

ScheduledExecutorService

ScheduledExecutorService 是一个接口,有多个实现类,比较常用的是 ScheduledThreadPoolExecutor

public class ScheduledThreadPoolExecutor 
  extends ThreadPoolExecutor implements ScheduledExecutorService {}

scheduledthreadexecutor

ScheduledThreadPoolExecutor 的状态管理、入队操作、拒绝操作等都是继承于 ThreadPoolExecutorScheduledThreadPoolExecutor 主要是提供了周期任务和延迟任务相关的操作;

schedule(Runnable command, long delay, TimeUnit unit) // 无返回值的延迟任务
schedule(Callable callable, long delay, TimeUnit unit) // 有返回值的延迟任务
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) // 固定频率周期任务
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) // 固定延迟周期任务

代码示例:

TimerTask repeatedTask = new TimerTask() {
  @SneakyThrows
  public void run() {
    System.out.println("当前时间: " + new Date() + "n" +
                       "线程名称: " + Thread.currentThread().getName());
  }
};
System.out.println("当前时间: " + new Date() + "n" +
                   "线程名称: " + Thread.currentThread().getName());
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
long delay = 1L;
long period = 2L;
// 延迟1s,周期2s
executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.SECONDS);
// 定时任务重复执行3个周期
Thread.sleep((delay + period * 3) * 1000);
executor.shutdown();

执行结果为:

当前时间: Wed Nov 23 09:52:35 CST 2022n线程名称: main
当前时间: Wed Nov 23 09:52:36 CST 2022n线程名称: pool-1-thread-1
当前时间: Wed Nov 23 09:52:38 CST 2022n线程名称: pool-1-thread-1
当前时间: Wed Nov 23 09:52:40 CST 2022n线程名称: pool-1-thread-1
当前时间: Wed Nov 23 09:52:42 CST 2022n线程名称: pool-1-thread-1

注意事项

1、scheduleAtFixedRatescheduleWithFixedDelay 是我们最常用的两个方法,两者略有区别,前者为固定频率周期任务,如果任务执行时间超出周期时,下一次任务会立刻运行;后者为固定延迟周期任务,无论执行时间是多少,其结果都是在执行完毕后,停顿固定的时间,然后执行下一次任务

2、ScheduledThreadPoolExecutor 线程最多为核心线程,最大线程数不起作用,因为 DelayedWorkQueue 是无界队列。

更多内容推荐阅读:并发系列(7)之 ScheduledThreadPoolExecutor 详解

小结

在 JDK 中,内置了两个类,可以实现定时任务的功能:

  • java.util.Timer :可以通过创建 java.util.TimerTask 调度任务,在同一个线程中串行执行,相互影响。也就是说,对于同一个 Timer 里的多个 TimerTask 任务,如果一个 TimerTask 任务在执行中,其它 TimerTask 即使到达执行的时间,也只能排队等待。因为 Timer 是串行的,同时存在 坑坑 ,所以后来 JDK 又推出了 ScheduledExecutorService ,Timer 也基本不再使用。
  • java.util.concurrent.ScheduledExecutorService :在 JDK 1.5 新增,基于线程池设计的定时任务类,每个调度任务都会被分配到线程池中并发执行,互不影响。这样,ScheduledExecutorService 就解决了 Timer 串行的问题。

在日常开发中,我们很少直接使用 Timer 或 ScheduledExecutorService 来实现定时任务的需求。主要有几点原因:

  • 它们仅支持按照指定频率,不直接支持指定时间的定时调度,需要我们结合 Calendar 自行计算,才能实现复杂时间的调度。例如说,每天、每周五、2019-11-11 等等,不支持 Cron 表达式。
  • 它们是进程级别,而我们为了实现定时任务的高可用,需要部署多个进程。此时需要等多考虑,多个进程下,同一个任务在相同时刻,不能重复执行。
  • 项目可能存在定时任务较多,需要统一的管理,此时不得不进行二次封装。

所以,一般情况下,我们会选择专业的调度任务中间件

中间件

Spring Task

由于 SpringTask 已经存在于 Spring 框架中,所以无需添加依赖。

下面我们弄个小 Demo 测试一下,新建一个项目,引入依赖。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.6.3</version>
  <relativePath/>
</parent>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
  </dependency>
</dependencies>

添加 SpringTask 的配置类

@Configuration
@EnableScheduling
public class SpringTaskConfig {

}

在 application.yml 添加关于 Spring Task 的配置,如下:

spring:
  task:
    # Spring Task 调度任务的配置,对应 TaskSchedulingProperties 配置类
    scheduling:
      thread-name-prefix: job- # 线程池的线程名的前缀。默认为 scheduling- ,建议根据自己应用来设置
      pool:
        size: 10 # 线程池大小。默认为 1 ,根据自己应用来设置
      shutdown:
        await-termination: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
        await-termination-period: 60 # 等待任务完成的最大时长,单位为秒。默认为 0 ,根据自己应用来设置

spring.task.scheduling.shutdown 配置项,是为了实现 Spring Task 定时任务的优雅关闭。

定时任务测试类

@Service
@Slf4j
public class ScheduledTaskService {

  private final AtomicInteger counts = new AtomicInteger();

  //  @Scheduled(cron = "0 0/10 * ? * ?")//每10分钟执行一次
  @Scheduled(fixedRate = 3000) // 每 3秒执行一次
  public void pushMessage() {
    log.info("[execute]定时第({})给用户发送通知", counts.incrementAndGet());
  }

}

最后创建一个启动类,启动项目,控制台输出如下:

SpringTask定时任务

Spring Task 支持 Cron 表达式 。Cron 表达式主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作。

Cron 格式中每个时间元素的说明

Cron参数说明

平时可以找一个 Cron 表达式生成器在线网站按需生成想要的表达式。

SpringTask 功能小结:

1、SpringTask 内置于 Spring 框架,相比于Quartz更加简单方便,不需要引入其他依赖。

2、Spring Task 底层是基于 JDK 的 ScheduledThreadPoolExecutor 线程池来实现的。

3、支持 Cron 表达式

4、只支持单机,功能单一

Quartz

Github:https://github.com/quartz-scheduler/quartz

Quartz 作为一个优秀的开源调度框架,Quartz 具有以下特点:

  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  3. 分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。

另外,作为 Spring 默认的调度框架,Quartz 很容易与 Spring 集成实现灵活可配置的调度功能。

在 Quartz 体系结构中,有三个组件非常重要:

  • Scheduler :调度器。Scheduler启动Trigger去执行Job。
  • Trigger :触发器。用来定义 Job(任务)触发条件、触发时间,触发间隔,终止时间等。四大类型:SimpleTrigger(简单的触发器)、CornTrigger(Cron表达式触发器)、DateIntervalTrigger(日期触发器)、CalendarIntervalTrigger(日历触发器)。
  • Job :任务。具体要执行的业务逻辑,比如:发送短信、发送邮件、访问数据库、同步数据等。

Quartz 应用分为单机模式和集群模式。实际应用中,我们都会选择集群模式,关于 Quartz 的使用,后续会单独出一篇文章进行介绍。

Quartz 框架出现的比较早,后续不少定时框架,或多或少都基于 Quartz 研发的,比如当当网的elastic-job就是基于quartz二次开发之后的分布式调度解决方案。

并且,Quartz 并没有内置 UI 管理控制台,不过你可以使用 quartzui 这个开源项目来解决这个问题。

Quartz 虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。

Quartz 优缺点:

  • 可以与 Spring 集成,并且支持动态添加任务和集群。
  • 分布式支持不友好,没有内置 UI 管理控制台、使用麻烦(相较于其他框架)。

XXL-JOB

官方说明:XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

通俗来讲:XXL-JOB 是一个任务调度框架,通过引入 XXL-JOB 相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的启动执行中止以及包含了日志记录与查询任务状态监控

官方文档:https://www.xuxueli.com/xxl-job/

GitHub:https://github.com/xuxueli/xxl-job

Gitee:http://gitee.com/xuxueli0323/xxl-job

特性:

xxl-job特性

Xxl-job 解决了很多 Quartz 的不足。

Xxl-job 解决了很多 Quartz 的不足

XXL-JOB 的架构设计如下图所示:

xxl-job架构图

从上图可以看出,XXL-JOB调度中心执行器 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。

关于 xxl-job 的使用,下篇文章会详细介绍的。

xxl-job 的优点相对于 Quartz 非常明显,使用更加简单,而且内置了 UI 管理控制台。

Elastic-Job

Github:https://github.com/apache/shardingsphere-elasticjob

官方文档:https://shardingsphere.apache.org/elasticjob/current/cn/overview/

ElasticJob 是面向互联网生态和海量任务的分布式调度解决方案,基于QuartzZooKeeper 开发,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 它通过弹性调度、资源管控、以及作业治理的功能,打造一个适用于互联网场景的分布式调度解决方案,并通过开放的架构设计,提供多元化的作业生态。 它的各个产品使用统一的作业 API,开发者仅需一次开发,即可随意部署。

ElasticJob-Lite 的架构设计如下图所示:

ElasticJob-Lite Architecture

从上图可以看出,Elastic-Job 没有调度中心这一概念,而是使用 ZooKeeper 作为注册中心,注册中心负责协调分配任务到不同的节点上。

Elastic-Job 中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。

@Component
@ElasticJobConf(name = "dayJob", cron = "0/10 * * * * ?", shardingTotalCount = 2,
        shardingItemParameters = "0=AAAA,1=BBBB", description = "简单任务", failover = true)
public class TestJob implements SimpleJob {
    @Override
    public void execute(ShardingContext shardingContext) {
        log.info("TestJob任务名:【{}】, 片数:【{}】, param=【{}】", shardingContext.getJobName(), shardingContext.getShardingTotalCount(),
                shardingContext.getShardingParameter());
    }
}

Elastic-Job 支持的功能:

Elastic-Job 支持的功能

关于 Elastic-Job 的使用,未来会抽时间出一篇文章的。

Elastic-Job 相较于 XXL-JOB 缺点也比较明显,就是需要引入额外的中间件,比如 Zookeeper,增加了操作难度。

总结

由于本人目前接触到的框架有限,除了上述四种中间件,还有不少大公司自研的中间件,如果从个人开发的角度来看,推荐大家使用 XXL-JOB,开箱即用,且配有可视化界面。如果想对其他中间件有所了解,这里推荐阅读如下两篇文章:

  • 《Spring Job?Quartz?XXL-Job?年轻人才做选择,艿艿全莽~》
  • Java 定时任务框架大揭秘!| JavaGuide

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

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

相关文章

内存 管理

内存管理c/c中内存分布sizeof 与 strlenc 语言中动态内存管理方式malloccallocreallocc 中动态内存管理new 与 delete自定义类型空间的动态分配new 与 delete 的实现operator new 与 operator delete基本概念辨识malloc/free 与 new/delete 区别 *****内存泄漏c/c中内存分布 c…

手把手教你:CSS + JS实现文本交替

1. Koa 中间件 Koa 的中间件通过一种更加传统的方式进行级联&#xff0c;摒弃了以往 node 频繁的回调函数造成的复杂代码逻辑。使用异步函数&#xff0c;我们可以实现"真正" 的中间件。与之不同&#xff0c;当执行到 yield next 语句时&#xff0c;Koa 暂停了该中间…

Qt 在linux上检测内存泄漏,用valgrind的问题

我在ubuntu上装了Qt5.15.2, 打开我的项目后&#xff0c;准备检测内存泄漏问题。 此时&#xff1a; 内存检测工具都是可用状态&#xff0c;但点击内存检测后&#xff0c;进度条走完后&#xff0c;就结束了。项目都没启动起来&#xff0c;这怎么检测内存问题&#xff1f; 然后&…

11月30日(第二天)

序列化&#xff1a;implements Serializable&#xff0c;public final static Long SeriaVersionUid 1L;MP的使用步骤&#xff1a;在BookBO类上使用TableName(“表名")去关联&#xff0c;在字段上使用TableId&#xff0c;TableField进行字段关联。(最好新建一个BookVO类,…

嫦娥五号探测器详细介绍

嫦娥五号&#xff08;Change 5&#xff09;&#xff0c;即嫦娥五号探测器&#xff0c;是由中国空间技术研究院研制的中国首个实施无人月面取样返回的航天器&#xff0c;是完成中国探月工程重大科技专项“绕、落、回”三步走发展战略最后一步的关键任务。 中国探月工程三步走 嫦…

Linux 主机间ssh相互免密

Linux 主机间ssh相互免密两步实现ssh主机免密详细教程请往下看主机间ssh相互免密 —— 方法一主备两台未配置密钥主机作为测试生成公私密钥拷贝公钥到目标主机ssh免密测试主机间ssh相互免密——方法二实验准备分发密钥对免密测试MobaXterm免密登录服务器&#xff08;以ecs-4207…

SAP中的PI接口

文章目录1 Pi overall2 Create pi process3 Inbound interface4 Outbound interface5 Matter need attention6 Pi test7 View log8 Transport CTS1 Pi overall What is the pi ? Pi is Sap’s middle software . it is interacted with SAP an external system. 2 Create p…

【Python】Python 中实现数据序列化

文章目录一、前言二、为什么要进行序列化三、Python 中的数据序列化1. json模块2. pickle模块3. shelve模块4. 总结参考链接一、前言 首先&#xff0c;要了解什么是序列化&#xff0c;请参考我的另一篇文章&#xff1a;序列化与反序列化介绍 本文主要介绍 Python 中的数据序列…

i.MX 6ULL 驱动开发 二十七:块设备

参考&#xff1a;【块设备】通用块层 struct bio 详解 | zzm (aliez22.github.io) 一、Linux 中块设备驱动框架 二、块设备基本概念 1、扇区的概念来自硬件&#xff0c;扇区是硬件最小操作单位。 2、块的概念来自文件系统&#xff0c;是文件系统数据处理的最小单位。 3、段…

django 开启CSRFtoken校验,以及postman实现问题

1.0 Django默认的CSRFtoken 表现&#xff1a; 后端使用的是Django的表单验证 post请求携带参数的问题 2.0 先处理post请求携带数据的csrfmiddlewaretoken 在登录界面 在input输入框中隐藏&#xff0c;所以需要提取input的value值&#xff0c;在【tests】脚本中进行提取&…

小样本关系分类(原型学习):Better Few-Shot Relation Extraction with Label Prompt Dropout

Better Few-Shot Relation Extraction with Label Prompt Dropout core idea 在小样本关系分类中&#xff0c;prompt信息是relation name是信息&#xff0c;这篇文章为了保持train和test的一致性&#xff0c;将train中的一些relation name信息删除掉了。&#xff08;相反&…

linux应用移植问题

背景 公司设备降成本&#xff0c;设备运行平台从armv7架构mpu换成了armv5架构的mpu&#xff0c;应用移植过程都挺顺利的&#xff0c;只是牵涉到一个引用外部库的应用时&#xff0c;运行该应用到引用库中的函数时&#xff0c;应用抛出illegal instrution异常。 问题分析 初步…

HTML(超文本标记语言)

HTML(超文本标记语言) 网页的基本信息 、等成对的标签&#xff0c;分别叫**开放标签**和**闭合标签**&#xff0c;单独呈现的标签(空元素)&#xff0c;如&#xff1b;意为用 / 来关闭空元素 DOCTYPE&#xff1a;告诉浏览器&#xff0c;我们要使用什么规范 head&#xff1a;代…

Jenkins安装与配置Windows11系统

目录 Jenkins安装 一、下载 官网地址&#xff1a;Jenkins download and deployment 点击LTS&#xff08;长期稳定版&#xff09;下的Windows 二、安装 基本就是一路Next即可 三、配置 访问地址 http://localhost:8080/&#xff0c;会出现如下界面&#xff1a; 安装 注&…

Redis实战——优惠券秒杀(一人一单业务)

需求&#xff1a;修改秒杀业务&#xff0c;要求同一个优惠券&#xff0c;一个用户只能下一单 我们只需要在增加订单之前&#xff0c;拿用户id和优惠券id判断订单是否已经存在&#xff0c;如果存在&#xff0c;说明用户已经购买。 代码实现&#xff1a; package com.hmdp.serv…

狂神说Go语言学习笔记(二)

一、匿名变量 匿名变量的特点是一个下划线 “_”&#xff0c;它本身就是一个特殊的标识符。它可以像其他标识符那样用于变量的声明或赋值&#xff08;任何类型都可以赋值给它&#xff09;&#xff0c;但任何赋给这个标识符的值都将被抛弃&#xff0c;因此这些值不能在后续的代…

共享雨伞app开发,提高资源利用率

共享经济无疑是当下热门行业之一&#xff0c;生活中随处可见的共享自行车、共享汽车、共享充电宝等一系列设备都是共享经济的产物。互联网时代下的共享经济已经呈现出多元化的发展趋势&#xff0c;其中在日常出行方面&#xff0c;共享雨伞的出现可以很好的解决用户偶遇的突发天…

Centos7 + kubenetes 一键安装实战

1. kubernetes部署环境要求 一台或多台机器&#xff0c;操作系统CentOS7.x-86_x64硬件配置&#xff1a;内存2GB或2G&#xff0c;CPU 2核或CPU 2核集群内各个机器之间能相互通信集群内各个机器可以访问外网&#xff0c;保证网络畅通&#xff08;最好能访问谷歌&#xff09;,需要…

【STM32笔记】HAL库中的SPI传输(可利用中断或DMA进行连续传输)

【STM32笔记】HAL库中的SPI传输&#xff08;可利用中断或DMA进行连续传输&#xff09; SPI 是英语Serial Peripheral interface的缩写&#xff0c;顾名思义就是串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。 SPI&#xff0c;是一种高速的&…

优秀软件测试报告

Document number 文档编号 Confidentiality level 密级 Test Report-01 [绝密/秘密/内部公开] Document version 文档版本 Total 10 pages 共 10 页 V1.0 测试报告 Prepared by 拟制 Date 日期 yyyy-mm-dd Reviewed by 评审人 Date 日期 yyyy-mm-dd Approved b…