社区说|浅谈 WorkManager 的设计与实现:系统概述

news2024/11/26 8:31:21

什么是 社区说 ?

反思 系列博客是一种看似 “内卷” ,但却 效果显著 的学习方式,该系列起源和目录请参考 这里 。

困境

作为一名 Android 开发者,即使你没有用过,也一定对 WorkManager 耳熟能详。

自2018年发布以来,作为 Google 官方推出的架构组件,它未像 LiveDataViewModel 一样广泛应用。究其原因,一起来看 官方 当初对 WorkManager 的描述:

WorkManager 用于执行可 延迟异步 的后台任务。它提供了一个 可靠可调度 的后台任务执行环境,可以处理 即使在应用退出或设备重启后仍需要运行 的任务。

快速提炼重点,我们得到了什么?

WorkManager,可以处理后台任务,这些任务哪怕手机重启也一定会执行?

看完简介,我的内心毫无波澜,“关机重启仍会执行” 的确很不错,but who cares ? 我根本用不到这些。

它给人的第一印象并不惊艳,甚至可以说 平平无奇 ,无论是相亲市场还是技术领域,这都非常致命。

经过一系列的实践和研究后,回过头再看 WorkManager,笔者认为 WorkManager 是传统 Android 领域内学院派编程风格的代表作,是沧海遗珠。它为 后台任务的处理和调度 提供了一个优秀的解决方案。

即使如此,WorkManager 仍和 Paging 面临着同样的 困境: 难以推广、默默无闻。简单的项目用不到,复杂的项目经过若干年的沉淀,该领域早已应用了其它方案,学习和迁移成本过高,以至不被需要。

——时至今日,社区内除了若干 使用简介源码分析 的博客单篇,我们仍很难找到其 实战进阶最佳实践 的相关系列。

目的

本文笔者将通过针对 Android 的后台任务管理机制,进行一个系统性的分析和设计。

最终的目的,并非是让读者将 WorkManager 强行引入和使用,而是对 后台任务的管理和调度工具 (后文简称 后台任务库 )有一个清晰的认知——即使从未使用过,在将来的某一天,遇到类似的业务诉求时,也能快速形成一个清晰的思路和方案。

基本概念

想要构建一个优秀的后台任务库,需要依靠不断的迭代、优化和扩展,最终成为一个灵活、完善的工具。

那么 后台任务库 需要提供哪些功能,为什么要设计这些功能?

第一个映入眼帘的概念是:线程调度,顾名思义,它是后台异步任务的基石。

1.线程调度

举个例子,你负责的是一个视频类APP的研发,最初的业务诉求如下:

APP 的日志上报。

需求清晰明了,显然,日志上报是一个 后台异步任务,在子线程进行 IO 操作: Log 文件本地的读写,以及上传到服务器。

这里我们引入 任务执行器(TaskExecutor)的概念:其用于执行后台任务的具体逻辑。通常,我们使用 线程池 来管理任务的线程,并提供线程的 创建销毁调度 等功能:

// androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
public class WorkManagerTaskExecutor implements TaskExecutor {

  final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());

  // 1.可切换主线程
  private final Executor mMainThreadExecutor = new Executor() {
    @Override
    public void execute(@NonNull Runnable command) {
        mMainThreadHandler.post(command);
    }
  };

  // 2.可切换后台工作线程
  private final Executor mBackgroundExecutor = Executors.newFixedThreadPool(
                Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)),
                createDefaultThreadFactory(isTaskExecutor));
}

为提高可读性,本文的示例代码都有 大幅精简 ,读者应尽量避免「只见树木不见森林」,以理解设计理念为主。

这里我们为组件提供了最基础的线程调度的能力,便于内部实现 主线程后台线程 的切换。其中我们为后台线程申请了一个线程池,并根据可用处理器核心数量来设置适当的线程数(通常同时最多执行的任务数为4),以充分利用设备的性能。

2.任务队列和串行化

接下来我们设计一个任务队列,保证可以不断接收新的任务,并及时分发给空闲的后台线程执行:

public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E> {

  public boolean add(E e);

  public boolean offer(E e);

  public E remove();

  public E poll();
}

很经典的一个队列接口,WorkManager 也并未单独造轮子,而是借用了 JavaArrayDeque 类。这是一个非常经典的实现类,在诸多大名鼎鼎的工具库中都有它的身影(比如RxJava),限于篇幅不展开,感兴趣的读者可自行查看源码。

读者可预见到的是,后台任务的创建和添加的时机是无法控制的,加上 ArrayDeque 设计之初都并未考虑线程同步,因此目前的设计将会有 线程安全问题

因此,后台任务的入列和执行,必须借用一个新的角色保证串行,就像单线程一样。

实现方案非常简单,通过代理和加锁,提供一个TaskExecutor的包装类即可:

public class SerialExecutorImpl implements SerialExecutor {

  // 1. 任务队列
  private final ArrayDeque<Task> mTasks;
  // 2. 后台线程的Executor,即上文中数量为4的线程池
  private final Executor mBackgroundExecutor;

  // 3.锁对象
  @GuardedBy("mLock")
  private Runnable mActive;

  @Override
  public void execute(@NonNull Runnable command) {
      // 不断地入列、出列和任务执行
      synchronized (mLock) {
          mTasks.add(new Task(this, command));
          if (mActive == null) {
            if ((mActive = mTasks.poll()) != null) {
                mExecutor.execute(mActive);
            }
          }
      }
  }
}

3.任务状态、类型和结果回调

下一步,我们对所关注的任务状态进行一个罗列,不难得出,我们关注的状态大致有:任务开始(Enqueued)、任务执行中(Running)、任务成功(Successded)、任务失败(Failed)、任务取消(Cancelled)等几种:

请注意,上文中的日志上报我们定义成了 一次性工作,即只执行一次,执行结束即永久结束。

实际上,一次性工作 并不能涵盖所有的业务场景,举例来说,作为一个视频类的 APP,我们需 定期 对用户的播放进度进行一次记录或上报,保证用户即使杀掉APP,下次仍在最后记录的播放位置继续播放。

这里我们引入了 定期任务 的概念,它只有一个终止状态 CANCELLED。这是因为定期工作永远不会结束。每次运行后,无论结果如何,系统都会重新对其进行调度。

最后,我们将后台任务的执行抽象为一个接口,开发者实现 doWork 接口,实现具体后台业务,如日志上报等,并返回具体的结果:

public abstract class Worker {

    // 后台任务的具体实现,`WorkerManager`内部进行了线程调度,执行在工作线程.
    @WorkerThread
    public abstract @NonNull Result doWork();
}

public abstract static class Result {

    @NonNull
    public static Result success() {
        return new Success();
    }

    @NonNull
    public static Result retry() {
        return new Retry();
    }

    @NonNull
    public static Result failure() {
        return new Failure();
    }
}

持久化

目前为止,我们实现了一个简化版、基于内存中队列的后台任务库,接下来我们将针对 后台任务持久化 的必要性,进行进一步的讨论。

1.非即时任务

第一步我们需要引入 非即时任务 的概念。

作为互联网的从业者,读者对类似的提示弹窗应该并不陌生:

您手机/PC的新版本已下载完毕:「立即安装」、「定时安装」、「稍后提醒我」

显然,这是一个常见的功能,用户选择后,应用或系统的后台需在未来的某个时间点,升级或提醒用户。如果和前文中的任务类型进行区分,前者我们可以归纳为 即时任务,后者则可称之为 延时任务非即时任务

非即时任务的诉求,将会使我们现有 后台任务库复杂度呈指数级提升 ,但这是 必要 的,原因如下:

首先,上文中 定期任务 也属于非即时任务的范畴,虽然该任务是立即执行并等待的,但实际上其真正的业务逻辑,仍是未来的某个时间点触发;其次,也是最重要的一点,作为一个健壮的后台任务库,和 即时任务 相比,对 非即时任务 提供支持的优先级要高得多。

——这似乎违反直觉,在日常开发中, 即时任务 似乎才是主流,但我们忽视了一个事实,资源并非无限

在文章的开始,我们构建了基本的线程调度能力,并创建了一个数量为 4 的线程池。但随着业务复杂度的提升,线程池可能会同时执行多个任务,这意味着部分晚入列、或优先级低的任务,会经常性等待前面的任务执行完毕。

严格意义上讲,此时,即时任务都转化为了非即时任务,再进一步抽象,所有即时任务都是非即时任务

万物皆可异步,是异步编程的一个经典概念,该思想在 HandlerRxJava协程 中都有体现。

即时任务被延时执行是合理的吗?对于后台任务而言,是非常合理的,如果开发者有明确的诉求, 必须立即 执行某段业务逻辑,那么就不应该用 后台任务库,而是直接在内存中调用这块代码。

2.持久化存储

当后台任务可能被延时执行,思考下一个问题,如何保证任务执行的可靠性?

终极解决方案必然是 后台任务持久化,通过本地文件或者数据库存储后,即使进程被用户杀掉或系统回收,在合适的时机,APP总是能够将任务恢复并重建。

考虑到安全性,WorkManager 最终选择使用了 Room 数据库,并且设计维护了一个非常复杂的 Database,简单罗列下核心的 WorkSpec 表:

@Entity(indices = [Index(value = ["schedule_requested_at"]), Index(value = ["last_enqueue_time"])])
data class WorkSpec(

    // 1.任务执行的状态,ENQUEUED/RUNNING/SUCCEEDED/FAILED/CANCELLED
    @JvmField
    @ColumnInfo(name = "state")
    var state: WorkInfo.State = WorkInfo.State.ENQUEUED,

    // 2.Worker的类名,便于反射和日志打印
    @JvmField
    @ColumnInfo(name = "worker_class_name")
    var workerClassName: String,

    // 3.输入参数
    @JvmField
    @ColumnInfo(name = "input")
    var input: Data = Data.EMPTY,

    // 4.输出参数
    @JvmField
    @ColumnInfo(name = "output")
    var output: Data = Data.EMPTY,

    // 5.定时任务
    @JvmField
    @ColumnInfo(name = "initial_delay")
    var initialDelay: Long = 0,

    // 6.定期任务
    @JvmField
    @ColumnInfo(name = "interval_duration")
    var intervalDuration: Long = 0,

    // 7.约束关系
    @JvmField
    @Embedded
    var constraints: Constraints = Constraints.NONE,

    // ...
)

设计好字段后,接下来我们设计其操作类 WorkSpecDao :

@Dao
interface WorkSpecDao {
  // ...

  @Insert(onConflict = OnConflictStrategy.IGNORE)
  fun insertWorkSpec(workSpec: WorkSpec)

  @Query("SELECT * FROM workspec WHERE id=:id")
  fun getWorkSpec(id: String): WorkSpec?

  @Query("SELECT id FROM workspec")
  fun getAllWorkSpecIds(): List<String>

  @Query("UPDATE workspec SET state=:state WHERE id=:id")
  fun setState(state: WorkInfo.State, id: String): Int

  @Query("SELECT state FROM workspec WHERE id=:id")
  fun getState(id: String): WorkInfo.State?

  // ...
}

细心的读者会发现,WorkSpecDao 的设计中除了声明常规的 InsertQueryUpdate 等,并没有 DELETE 类型的操作。

读者经过认真考虑后,可得该设计是合理的——由于有 setState() 可更新任务的状态,已完成或取消的工作无需删除,而是通过 SQL 语句,灵活分类按需获取,如:

@Dao
interface WorkSpecDao {
  // ...

  // 获取全部执行中的任务
  @Query("SELECT * FROM workspec WHERE state=RUNNING")
  fun getRunningWork(): List<WorkSpec>

  // 获取全部近期已完成的任务
  @Query(
      "SELECT * FROM workspec WHERE last_enqueue_time >= :startingAt AND state IN COMPLETED_STATES ORDER BY last_enqueue_time DESC"
  )
  fun getRecentlyCompletedWork(startingAt: Long): List<WorkSpec>

  // ...
}

这可称之额外收获,通过 Room 的持久化存储,在保证了任务能够被稳定执行的同时,还可对所有任务进行备份,从而向开发者提供更多额外的能力。

准确来说,WorkManager 内部的 Dao 提供了 Delete 方法,但并未直接暴露给开发者,而是用于内部解决 任务间的冲突 问题,这个后文再提。

优先级管理

下面我们针对任务的 优先级 进一步进行讨论。

虽然上文明确说了,对于需要立即执行的行为,不应该作为后台任务,而是应该直接执行对应的业务代码块——看起来优先级机制并非刚需。

但实际上,这种机制仍然有一定的必要性。

1.约束条件

说到 约束条件,熟悉 JobScheduler 的开发者对此不会感到陌生,

约束条件概念
NetworkType约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED)。
BatteryNotLow如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行。
RequiresCharging如果设置为 true,那么工作只能在设备充电时运行。
DeviceIdle如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。在运行批量操作时,此约束会非常有用;若是不用此约束,批量操作可能会降低用户设备上正在积极运行的其他应用的性能。
StorageNotLow如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。

WorkManager 也提供了类似的概念,实际上内部也正是基于 JobScheduler 实现的,但 WorkManager 并非只是单纯的代理。

首先,当API版本不足时,WorkManager 兼容性使用 AlarmManagerGcmScheduler 作为补充:

// androidx.work.impl.Schedulers.java
static Scheduler createBestAvailableBackgroundScheduler(
        @NonNull Context context,
        @NonNull WorkManagerImpl workManager) {

    Scheduler scheduler;

    if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
        scheduler = new SystemJobScheduler(context, workManager);
    } else {
        scheduler = tryCreateGcmBasedScheduler(context);
        if (scheduler == null) {
            scheduler = new SystemAlarmScheduler(context);
        }
    }
    return scheduler;
}

其次,读者知道,JobScheduleronStartJob 等回调默认运行在主线程,不能直接进行耗时操作。WorkManagerWorker 内部则进行了线程调度,默认实现在 WorkerThread

// androidx.work.impl.background.systemjob.SystemJobService
public class SystemJobService extends JobService {

    public boolean onStartJob(@NonNull JobParameters params) {
      //...
      mWorkManagerImpl.startWork(...);
    }
}

// androidx.work.impl.WorkManagerImpl
public class WorkManagerImpl extends WorkManager {
  public void startWork(...) {
      mWorkTaskExecutor.executeOnTaskThread(new StartWorkRunnable(...));
  }
}

由于 JobScheduler 是由系统服务中的 JobSchedulerService 实现的,因此其自身的高权限,可以在APP被杀或重启后,仍然可以唤起并执行 JobService 及对应的任务。

2.新的保活机制?

Android 官方提供了后台作业的强大支持,国内厂商大多数第一时间却想拿它来做 保活

——举例来说,既然 WorkManager 支持定期任务,且即使 APP 被杀或者重启都能够保证执行;那么我一个 IM APP,每 10 秒拉取下接口看有没有新消息,顺便启动下 APP 某些页面或者通知组件,想必也是非常合理的。

image.png
实际上,对于 保活 的诉求,WorkManager 做不到,其本质是 JobScheduler 做不到:

首先,随着版本的迭代,Android 系统对后台任务的管理愈发严苛,小于 15 分钟的定期任务已经被强制调整为 15 分钟执行,避免频繁的后台定时任务对前台应用的影响,规避了 API 的非法滥用:

WorkManager : 我把你当兄弟,你竟然想睡我?

其次是笔者的猜测,由于用户安全等相关的考量,国内设备厂商对 JobSchedulerService 等相关都有一定的魔改——比如,当用户手动将 APP 强制关闭,这种操作意图拥有最高优先级,即使是系统服务也不应对其再次启动(厂商白名单除外,如微信和QQ?)。

两点结合,WorkManager 的定期任务受到了严格的限制,这也意味着类似保活需求其无法满足,其 “不稳定” 性这也是其国内应用较少的主要原因之一。

3.前台服务

最后,我们讨论下 如何规范地调度系统资源

最经典的场景仍然是后台任务的加急,即使有约束条件,部分后台任务仍需要被 特殊加急 ,比如 用户聊天时发送短视频处理付款或订阅流程 等。这些任务对用户很重要,会在后台快速执行,并需要立即开始执行。

系统对于应用的资源分配非常严格,读者可以通过 这里 简单了解。

由于任务的执行依赖于系统对资源的分配,因此想要提高执行的优先级,势必需要提升 APP 组件自身的优先级,那么实现方案已经非常明显了:使用 前台服务

当你需要通过调用类似 worker.setExpedited(true) 标记为加急任务时,Worker 内部的具体实现,需开发者额外创建前台通知,提升优先级的同时,将你后台的任务行为同步给用户。

小结

小结并不是总结,还有更多内容可以扩展,比如:

  • 1、Android 系统的 JobScheduler 任务调度机制的内部原理是什么样的?

  • 2、WorkManager 组件的初始化机制、持久化机制等,内部的实现细节有哪些需要注意的?

  • 3、WorkManager 中的约束任务和非约束任务在执行上有什么不同?

  • 4、WorkManager 还有哪些其它的亮点?

篇幅原因,这些问题笔者将另起一篇进行更深入性的讨论,敬请期待。


关于我

Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 GitHub。

如果您觉得文章还差了那么点东西,也请通过 关注 督促我写出更好的文章——万一哪天我进步了呢?

  • 我的Android学习体系
  • 关于文章纠错
  • 关于知识付费
  • 关于《反思》系列

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

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

相关文章

设计模式第22讲——访问者模式(Visitor)

目录 一、什么是访问者模式 二、角色组成 三、优缺点 四、 应用场景 4.1 生活场景 4.2 Java场景 五、代码实现 5.0 UML类图 5.1 抽象访问者——Visitor 5.2 具体访问者——Tourist 5.3 抽象元素——Spot 5.4 具体元素——View、Relic 5.5 对象结构——SpotCollecti…

大厂面试打起12万分小心?3轮技术面过,你也可能挂在HR手上!

很多朋友在面试大厂时存在一个误区&#xff0c;认为面试你的那个是最初给你打电话的HR&#xff0c;其实不然&#xff0c;更大可能是业务部门相关的 HRBP导致你面试失败。 1、什么是HRBP? 为了解释清楚这个问题&#xff0c;先说 HRBP 是什么。HRBP全称为 Human Resource Busin…

java对象clone

Object提供了colne方法给我们定义的类&#xff0c;用来进行对象克隆&#xff0c;但是这个clone方法是protected的&#xff0c;所以需要在我们需要使用clone的类中重写Object的clone方法&#xff0c;并且需要实现Cloneable接口&#xff0c;Cloneable接口是一个标记接口&#xff…

「JVS低代码开发平台2.1.8版本」-首页功能介绍

JVS是面向软件开发团队可以快速实现应用的基础开发脚手架&#xff0c;主要定位于企业信息化通用底座&#xff0c;采用微服务分布式框架&#xff0c;提供丰富的基础功能&#xff0c;集成众多业务引擎&#xff0c;它灵活性强&#xff0c;界面化配置对开发者友好&#xff0c;底层容…

【Java】Netty中ByteBuf学习笔记

文章目录 1) ByteBuf创建与自动扩容2&#xff09;直接内存 vs 堆内存3&#xff09;池化 vs 非池化4&#xff09;组成5&#xff09;写入6&#xff09;扩容7&#xff09;读取8&#xff09;retain & release9&#xff09;slice10&#xff09;duplicate11&#xff09;copy12&am…

给LLM装上知识:从LangChain+LLM的本地知识库问答到LLM与知识图谱的结合

第一部分 基于LangChain ChatGLM-6B的本地知识库问答的应用实现 1.1 什么是LangChain&#xff1a;连接本地知识库与LLM的桥梁 作为一个 LLM 应用框架&#xff0c;LangChain 支持调用多种不同模型&#xff0c;提供相对统一、便捷的操作接口&#xff0c;让模型即插即用&#x…

java并发编程 6:java内存模型与volatile(重点)

目录 硬件内存模型Java 内存模型主内存工作内存内存交互的八个原子操作JMM作用 可见性退不出的循环volatile解决同步模式之 Balking 有序性指令重排解决指令重排 volatile 原理volatile如何保证可见性volatile如何保证有序性volatile 不能解决指令交错double-checked locking 问…

2023/7/5总结

JS BOM 是浏览器对象模型 window对象是一个全局对象&#xff0c;也是JavaScript的顶级对象 所以通过var定义在全局作用域中的变量、函数都会变成window对象的属性和方法 定时器-延时函数 setTimeout(回调函数&#xff0c;等待的毫秒数) 延时函数只会执行一次 清除延时函…

Java基础---String的长度限制

目录 典型回答 常量池限制 运行期限制 典型回答 String有长度限制&#xff0c;编译期和运行期不一样编译期需要用 CONSTANT_Utf8_info 结构用于表示字符串常量的值&#xff0c;而这个结构是有长度限制&#xff0c;他的限制是65535运行期&#xff0c;String的length参数是Int…

Jenkins 配置用户角色和权限

1、配置条件&#xff1a; 1&#xff09;已安装Jenkins&#xff0c;当前案例使用版本&#xff1a;2.319.3 2&#xff09;已成功进入Jenkins&#xff0c;并新建用户&#xff1a;dev_java 2、安装插件【系统管理-插件管理-搜索-可选插件】&#xff1a;Role-based Authoriz…

JavaScript 进阶 - 第4天

JavaScript 进阶 - 第4天笔记 文章目录 JavaScript 进阶 - 第4天笔记1 深浅拷贝1.1 浅拷贝1.2 深拷贝&#xff08;面试&#xff09;1.2.1 递归实现深拷贝1.2.2 js库lodash里面cloneDeep内部实现了深拷贝1.2.3 JSON序列化 2 异常处理2.1 throw 抛异常2.2 try ... catch 捕获异常…

Linux之Kafka保姆式详细安装教程

下载Kafka 《Kafka官网下载》 注意&#xff1a;下载的是二进制文件&#xff0c;不要下载源码&#xff01;这里可以采用第三方下载工具加速下载&#xff0c;如&#xff1a;迅雷等 上传到Linux服务器的/data/目录下进行解压 tar -zxvf是解压文件命令&#xff0c;-C表示把解压…

【C++学习笔记】1.6 引用

目录 &#x1f36f;1.6 引用 &#x1f95d;1. 引用的概念 &#x1f95d;2. 引用的特性 1、引用在定义时必须初始化 2、一个变量可以有多个引用 3、引用一旦引用一个实体&#xff0c;再不能引用其他实体 &#x1f95d;3. 常引用 1、取别名的原则&#xff1a;对原引用的…

数据查询大揭秘:收好几个模式化公式

欢迎来到数据查询大揭秘&#xff01;今天&#xff0c;我将与大家分享一些宝贵的秘诀和技巧&#xff0c;让你轻松应对数据查询的挑战。准备好了吗&#xff1f;收起你的笔记本和便签纸&#xff0c;因为我即将为你介绍几个模式化公式&#xff0c;让你事半功倍地完成数据查询任务&a…

湖北文理学院工程能力实训开班!

为深化校企合作&#xff0c;产教融合助力新工科建设&#xff0c;提升学生工程实践能力&#xff0c;电巢工程能力实训班按照不同岗位类别&#xff0c;匹配对应的企业岗位任职能力要求对学生开展分级培养&#xff0c;以产业需求为导向&#xff0c;培养创新型、应用型人才。 7月3…

open3D cmake+win10+vs2019编译

已经采用python版open3D实现和验证了功能&#xff0c;但是在C迁移上却遇到了不少问题&#xff1a; 1、可能是与本地的编译器存在差异&#xff0c;在使用open3D git上的winows版本时&#xff0c;存在地址访问冲突和std::bad_alloc等问题。前者在适用IO读写时必现&#xff0c;后者…

【Git】Windows如何运行.sh脚本文件

在Windows系统中运行.sh脚本需要借助第三方工具&#xff0c;比如Git Bash、Cygwin或WSL&#xff08;Windows Subsystem for Linux&#xff09;等。 以下是使用Git Bash运行.sh脚本的步骤&#xff1a; 安装Git Bash&#xff1a;从Git官方网站&#xff08;https://git-scm.com/…

【Ubuntu学习MySQL——MySQL基本操作命令】

1.创建数据库 2.删除数据库 3.选择数据库 4. 创建数据表 5.删除数据表 6.往数据表中插入数据 7.从数据表中查询数据 SELECT column_name,column_name FROM table_name [WHERE Clause] [LIMIT N][ OFFSET M]""" 查询语句中你可以使用一个或者多个表&#xff0c…

SpringMVC源码-DispatcherServlet

一、SpringMVC请求处理流程 DispatcherServlet&#xff1a;DispatcherServlet是SpringMVC中的前端控制器&#xff0c;负责接收request并将request转发给对应的处理组件。HandlerMapping&#xff1a;HanlerMapping是SpringMVC中完成url到Controller映射的组件。Handler&#xff…

智能汽车时代,产业如何“软硬兼施”

摘要&#xff1a; 智能汽车时代&#xff0c;以车用芯片、基础软件为代表的卡脖子关键技术&#xff0c;牵动着国内整个汽车供应链的安全。“软硬兼施”正成为从企业到汽车全行业的共同重大行动。 汽车产业链、供应链安全问题近两年已经引起全行业前所未有的关注。进入智能汽车时…