Springboot集成Quartz
Quartz简介
Job 表示一个工作,要执行的具体业务内容。
JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。
Trigger 代表一个调度参数的配置,什么时候去调。
Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。
案例
背景
系统中有一张自定义的定时任务表,需要实现动态的添加、修改、删除、启停定时任务等功能,定时任务里包含了业务需要执行的按设置周期执行的代码
由于不想使用Quartz的数据存储功能,所以下面实现里直接使用了这张自定义的表,以及使用了Quartz的任务调度和触发功能
实现步骤
1. 引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId><version>2.7.18</version>
</dependency>
2. 添加配置类
@Configuration
public class ScheduleQuartzConfig {@Beanpublic Scheduler scheduler() throws SchedulerException {Scheduler scheduler = schedulerFactoryBean().getScheduler();return scheduler;}@Beanpublic SchedulerFactoryBean schedulerFactoryBean() {Properties properties = new Properties();properties.setProperty("org.quartz.threadPool.threadCount", "10");properties.setProperty("org.quartz.threadPool.threadNamePrefix","quartz_worker");SchedulerFactoryBean factory = new SchedulerFactoryBean();factory.setSchedulerName("QUARTZ_SCHEDULER");factory.setQuartzProperties(properties);return factory;}
}
3. 编写Job类
该类就是将来要定时执行的业务代码,具体代码路径根据实际情况规划即可,重点是继承 QuartzJobBean
,重写 executeInternal
方法
//锁定机制,以确保在同一时间只有一个任务实例运行
@DisallowConcurrentExecution
public class MyJob extends QuartzJobBeanprivate final static Logger logger = LoggerFactory.getLogger(PushPvImitateDataJob.class);@Overrideprotected void executeInternal(JobExecutionContext context) throws JobExecutionException {//业务代码logger.info("开始执行业务代码了。。。");try {Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();} logger.info("业务代码执行完成了!!!!");}
}
4. 编写定时任务表的实体类
ScheduleLog.java
@Data
public class ScheduleLog {private Integer id;private String name; //任务名称private String jobClassName; //任务的实现类 如:xxx.xxx.xx.MyJobprivate String cronExpression; //触发时机表达式,比如每5秒执行一次 0/5 * * * * ?private Integer status; //状态:0 启动 1 禁用private String remark;private Long createTime;private Long updateTime;private Long lastTime; //上一次执行时间private Long nextTime; //下一次执行时间private String lastTimeText;private String nextTimeText;
}
5. 创建管理定时任务的工具类
@Component
public class ScheduleQuartzManage {private static final Logger logger = LoggerFactory.getLogger(ScheduleQuartzManage.class);/*** 创建定时任务 定时任务创建之后默认启动状态** @param scheduler: 调度器* @param scheduleLog: 报告订阅对象* @return: void**/public void createScheduleJob(Scheduler scheduler, ScheduleLog scheduleLog) throws SchedulerException {//获取到定时任务的执行类 必须是类的绝对路径名称//定时任务类需要是job类的具体实现 QuartzJobBean是job的抽象类。
// Class<? extends Job> jobClass = PushPvImitateDataJob.class;Class<? extends Job> jobClass = null;try {jobClass = (Class<? extends Job>) Class.forName(scheduleLog.getJobClassName());} catch (ClassNotFoundException e) {e.printStackTrace();}// 构建定时任务信息JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(scheduleLog.getId().toString(),"jobGroup").usingJobData("id",scheduleLog.getId()).build();// 设置定时任务执行方式CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleLog.getCronExpression());// 构建触发器trigger// 如果已经有下一次时间,就设置为下一次时间为触发时间CronTrigger trigger;if (!Objects.isNull(scheduleLog.getNextTime())) {Date date = new Date(scheduleLog.getNextTime());trigger = TriggerBuilder.newTrigger().startAt(date).withIdentity(scheduleLog.getId().toString(), "jobGroup").withSchedule(scheduleBuilder).build();} else {trigger = TriggerBuilder.newTrigger().withIdentity(scheduleLog.getId().toString(),"jobGroup").withSchedule(scheduleBuilder).build();}scheduler.scheduleJob(jobDetail, trigger);// 设置下次执行时间//long nextTime = trigger.getNextFireTime().getTime();//LocalDateTime nextTime = DateUtil.dateToLocalDate(trigger.getNextFireTime());//logger.info("下次一执行时间:{}",DateUtil.formatDateTime(new Date(nextTime)));//scheduleLog.setNextTime(nextTime);}/*** 根据任务名称暂停定时任务** @param scheduler 调度器* @param jobName 定时任务名称(这里直接用ReportSubscribePO的Id)* @throws SchedulerException*/public void pauseScheduleJob(Scheduler scheduler, String jobName) throws SchedulerException {JobKey jobKey = JobKey.jobKey(jobName, "jobGroup");scheduler.pauseJob(jobKey);}/*** 根据任务名称恢复定时任务** @param scheduler 调度器* @param scheduleLog 定时任务名称(这里直接用ReportSubscribePO的Id)* @throws SchedulerException*/public void resumeScheduleJob(Scheduler scheduler, ScheduleLog scheduleLog) throws SchedulerException {// 判断当前任务是否在调度中Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.groupEquals("jobGroup"));List<JobKey> thisNameJobs = jobKeys.stream().filter(jobKey -> Objects.equals(scheduleLog.getId().toString(), jobKey.getName())).collect(Collectors.toList());if (thisNameJobs.size() > 0){JobKey jobKey = JobKey.jobKey(scheduleLog.getId().toString(), "jobGroup");scheduler.resumeJob(jobKey);// 下一次执行时间设置回去Trigger trigger = scheduler.getTrigger(TriggerKey.triggerKey(scheduleLog.getId().toString(), "jobGroup"));long nextTime = trigger.getNextFireTime().getTime();scheduleLog.setNextTime(nextTime);}else {createScheduleJob(scheduler, scheduleLog);}}/*** 更新定时任务** @param scheduler 调度器* @param scheduleLog 报告订阅对象* @throws SchedulerException*/public void updateScheduleJob(Scheduler scheduler, ScheduleLog scheduleLog) throws SchedulerException {//获取到对应任务的触发器TriggerKey triggerKey = TriggerKey.triggerKey(scheduleLog.getId().toString(),"jobGroup");//设置定时任务执行方式CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleLog.getCronExpression());//重新构建任务的触发器triggerCronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);if (trigger == null){return;}// trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();trigger = TriggerBuilder.newTrigger().startNow().withIdentity(scheduleLog.getId().toString(), "jobGroup").withSchedule(scheduleBuilder).build();//重置对应的jobscheduler.rescheduleJob(triggerKey, trigger);scheduleLog.setNextTime(trigger.getNextFireTime().getTime());}/*** 根据定时任务名称从调度器当中删除定时任务** @param scheduler 调度器* @param jobName 定时任务名称 (这里直接用 ScheduleLog 的Id)* @throws SchedulerException*/public void deleteScheduleJob(Scheduler scheduler, String jobName) throws SchedulerException {JobKey jobKey = JobKey.jobKey(jobName,"jobGroup");scheduler.deleteJob(jobKey);}
}
6. 项目启动时做任务初始化
这里我直接让service实现了CommandLineRunner,然后在run()方法中,将初始化逻辑写入进来,让数据库中的持久化的任务全部添加进内存中。
@Service
public class ScheduleLogServiceImpl implements ScheduleLogService, CommandLineRunnerprivate final static Logger logger = LoggerFactory.getLogger(ScheduleLogServiceImpl.class);@Autowiredprivate ScheduleLogDao scheduleLogDao;@Autowiredprivate ScheduleQuartzManage scheduleQuartzManage;@Autowiredprivate Scheduler scheduler; //这里的scheduler来源于配置@Overridepublic void run(String... args) throws Exception {// 初始化所有的已经启用的订阅List<ScheduleLog> enableSubs = getEnableScheduleList();logger.info("需要初始化的任务个数:{}", enableSubs.size());for (ScheduleLog sub : enableSubs) {try {logger.info("开始初始化订阅任务,任务name:{}", sub.getName());scheduleQuartzManage.createScheduleJob(scheduler, sub);} catch (Exception e) {logger.error("启动时初始化订阅任务失败:{}", e.getMessage());}}}@Overridepublic List<ScheduleLog> getEnableScheduleList() {HashMap<String, Object> param = new HashMap<>();param.put("status",1);List<ScheduleLog> list = scheduleLogDao.getList(param);return list;}@Overridepublic boolean add(Map data) {data.put("createTime", DateUtil.currentSeconds());Integer add = scheduleLogDao.add(data);if(add > 0){//创建调度器data.put("id",add);ScheduleLog scheduleLog = BeanUtil.mapToBean(data, ScheduleLog.class,false,null);if(scheduleLog.getStatus() < 1){return true;}try {scheduleQuartzManage.createScheduleJob(scheduler, scheduleLog);} catch (SchedulerException e) {e.printStackTrace();}return true;}return false;}@Overridepublic boolean del(Integer id) {Integer del = scheduleLogDao.del(id);if(del > 0){try {scheduleQuartzManage.deleteScheduleJob(scheduler,String.valueOf(id));} catch (SchedulerException e) {e.printStackTrace();}return true;}return false;}@Overridepublic boolean edit(Map data) {data.put("updateTime",DateUtil.currentSeconds());Integer save = scheduleLogDao.save(data);if(save > 0){try {ScheduleLog scheduleLog = getDetail((Integer) data.get("id"));if(data.get("status").equals(0)){//关闭scheduleQuartzManage.deleteScheduleJob(scheduler,data.get("id").toString());}else{//开启scheduleQuartzManage.deleteScheduleJob(scheduler,data.get("id").toString());scheduleQuartzManage.createScheduleJob(scheduler,scheduleLog);}} catch (SchedulerException e) {e.printStackTrace();}return true;}return false;}
}
至此基本就实现了定时任务的管理了,controller 里的内容包含了对定时任务进行管理的接口,就不写了
总结
- 同一个任务是否可以并行执行,可参考第3步设置
- 每次项目重新部署后,自动加载数据库中的定时任务
- Quartz在发生异常时会重试一次,注意异常处理,可在第3步中处理
参考链接:https://juejin.cn/post/7054762566035193869#heading-7
Cron表达式
不管是Spring自带的定时任务实现,还是SpringBoot整合Quartz的定时任务实现,其触发器都支持用corn表达式来表示。
corn表达式是一个字符串,有6或7个域,域之间是用空格进行间隔。
从左到右每个域表示的含义如下:
第几个域 | 英文释义 | 允许值 | 备注 |
---|---|---|---|
一 | Seconds | 0~59 | 秒 |
二 | Minutes | 0~59 | 分 |
三 | Hours | 0~23 | 时 |
四 | DayOfMonth | 1-31 | 天 |
五 | Month | 1-12或月份简写 | 月 |
六 | DayOfWeek | 1-7或星期简写 | 星期,1表示SUN,在day-of-week字段用”6#3”指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发 |
七 | Year | 1970~2099 | 年 |
然后,某些域还支持部分特殊字符,特殊字符的含义如下:
特殊字符 | 含义及注意事项 |
---|---|
* | 任意值 |
? | 占位符,只能在第四域和第六域中使用,表示未说明的值,即不关心它为何值 |
- | 区间,表示区间内有效 |
/ | 固定间隔,符号前表示开始时间,符号后表示每次递增的值; |
, | 枚举有效值的间隔符,表示附加一个可能值 |
L | 表示该区间的最后一个有效值,只能在第四域和第六域中使用, L(“last”) (“last”) “L” 用在day-of-month字段意思是 “这个月最后一天”;用在 day-of-week字段, 它简单意思是 “7” or “SAT”。 如果在day-of-week字段里和数字联合使用,它的意思就是 “这个月的最后一个星期几” – 例如: “6L” means “这个月的最后一个星期五”. 当我们用“L”时,不指明一个列表值或者范围是很重要的,不然的话,我们会得到一些意想不到的结果。 |
W | 表示离指定日期的最近的有效工作日,(周一-周五为工作日),W(“weekday”) 只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发,注意一点:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日。 |
常用corn表达式例子含义说明:
corn表示式 | 表达式含义 |
---|---|
*/10 * * * * ? * | 每隔10秒执行一次 |
0 30 1 * * ? * | 每天凌晨1点30分0秒开始执行 |
0 0 10,14,16 * * ? | 每天10点、14点、16点执行一次 |
0 15 10 L * ? | 每个月最后一天的10点15分执行一次 |
0 15 10 ? * 6L | 每月的最后一个星期五上午10:15触发 |
0 15 10 15 * ? | 每月15日上午10:15触发 |
0 15 10 ? * 6#3 | 每月的第三个星期五上午10:15触发 |
0 15 10 ? * 6L 2018-2020 | 2018年到2020年每个月最后一个星期五的10:15执行 |
校验地址:https://www.bejson.com/othertools/cronvalidate/
相关文章:

Springboot集成Quartz
Quartz简介 Job 表示一个工作,要执行的具体业务内容。 JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。 Trigger 代表一个调度参数的配置…...

Android面试题之Kotlin Jetpack组件LifecycleScope
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点 在Kotlin中,LifecycleScope是Android Jetpack架构组件的一部分,主要用于简化与生命周期相关的协程管理。 它属于android…...
MySQL深分页优化
MySQL中的深分页问题通常是指当我们通过LIMIT语句查询数据,尤其是在翻到较后面的页码时,性能会急剧下降。例如,查询第1000页的数据,每页10条,系统需要跳过前9990条数据,然后才能获取到所需的记录࿰…...

问题:律师会见委托人的方式包括团体会见和( )。 #职场发展#笔记#学习方法
问题:律师会见委托人的方式包括团体会见和( )。 参考答案如图所示...

Spring Boot中整合Jasypt 使用自定义注解+AOP实现敏感字段的加解密
😄 19年之后由于某些原因断更了三年,23年重新扬帆起航,推出更多优质博文,希望大家多多支持~ 🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志 🎐 个人CSND主页——Mi…...
pytorch中的维度变换操作性质大总结:view, reshape, transpose, permute
在深度学习中,张量的维度变换是很重要的操作。在pytorch中,有四个用于维度变换的函数,view, reshape, transpose, permute。其中view, reshape都用于改变张量的形状,transpose, permute都用于重新排列张量的维度,但它们…...

LeetCode刷题 | Day 4 分割等和子集(Partition Equal Subset Sum)自底向上动态规划
LeetCode刷题 | Day 4 分割等和子集(Partition Equal Subset Sum)自底向上动态规划 文章目录 LeetCode刷题 | Day 4 分割等和子集(Partition Equal Subset Sum)自底向上动态规划前言一、题目概述二、解题方法2.1 一维表格的自底向上动态规划2.1.1 思路讲解2.1.2 伪代码 + 逐…...

基于工业互联网打造敏捷供应链的实现方式:创新路径与实践应用
引言 工业互联网和敏捷供应链是当今制造业发展中的两个重要概念。工业互联网以数字化、网络化和智能化为核心,致力于将传统工业生产与互联网技术相融合,从而实现生产过程的高效、智能和灵活。而敏捷供应链则强调快速响应市场需求、灵活调整生产和供应计划…...

碳化硅柱式膜的广泛应用
碳化硅柱式膜是一种高性能的过滤材料,以其独特的性质和广泛的应用领域在现代工业中占据着重要地位。以下是对碳化硅柱式膜的详细介绍: 一、基本概述 碳化硅柱式膜是以碳化硅超滤膜为过滤单元构成的,其过滤精度高达0.1微米。这种膜材料具有耐化…...
【QT】QFont字体设置
设置字体大小 f.setPointSize(12); // 设置字体大小为12点设置字体加粗 f.setBold(true); // 使字体加粗设置字体斜体 f.setItalic(true); // 使字体斜体设置字体下划线 f.setUnderline(true); // 给字体添加下划线设置字体删除线 f.setStrikeOut(true); // 给字体添加删除…...

Vue3+vite部署nginx的二级目录,使用hash模式
修改router访问路径 import { createRouter, createWebHashHistory } from vue-routerconst router createRouter({history: createWebHashHistory (/mall4pc-bbc/),routes: [XXX,] })配置package.json文件 "build:testTwo": "vite build --mode testing --ba…...

云南区块链商户平台发票助手成品
目录 1 概述2 功能对比3 项目演示图4 核心逻辑4.1智能赋码4.2 解密方法4.3 登录与检测4.4 发票金额大写转换4.5 检查登录是否失效4.6 验证码识别5 演示效果6 项目部署6.1 Web站点部署6.1.1 环境6.1.2 前端6.1.3 后端6.2 Docker部署6.2.1 构建镜像6.2.2 创建容器6.3.3 访问项目域…...

AI图书推荐:检索增强生成RAG赋能大语言模型
本书《检索增强生成RAG赋能大型语言模型》(Retrieval-Augmented Generation - Dr. Ray Islam :Mohammad Rubyet )深入探讨了如何通过结合检索系统与神经语言模型,提升人工智能在自然语言处理领域的能力。 以下是各章节内容的概要&…...

高效学习LabVIEW的方法
学习LabVIEW可以通过系统化课程、在线资源、自学实验、参与论坛、结合实际项目等多角度进行。系统课程提供全面基础,在线资源便于查漏补缺,自学实验强化理解,论坛互动解决疑难,结合实际项目应用提高实践技能。结合项目学习是最高效…...

C语言 | Leetcode C语言题解之第136题只出现一次的数字
题目: 题解: class Solution { public:vector<int> singleNumbers(vector<int>& nums) {int eor 0;for (int num:nums)eor ^ num;int rightOne eor & (~eor 1); // 提取出最右的1int onlyOne 0;for (int cur : nums) {if ((cur…...

如何利用Varjo混合现实技术改变飞机维修训练方式
自2017年以来,总部位于休斯顿的HTX实验室一直在推进混合现实技术,与美国空军密切合作,通过其EMPACT平台提供可扩展的沉浸式飞机维护虚拟现实培训。 虚拟和混合现实对维修训练的好处: l 实践技能:提供一个非常接近真实场…...
C++:按指定字符分割字符串
按指定字符分割字符串 [C]对string按指定分隔符分割(split) #include <iostream> #include <vector> #include <string> #include <sstream> using namespace std; int main() {string origin_str "hello world !"; // 需要进行分割的字符串…...

网络网络层之(6)ICMPv4协议
网络网络层之(6)ICMPv4协议 Author: Once Day Date: 2024年6月2日 一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦… 漫漫长路,有人对你微笑过嘛… 全系列文章可参考专栏: 通信网络技术_Once-Day的博客-CS…...
Opengrok代码在线查看平台
OpenGrok 是一个基于 Web 的源代码搜索引擎和交叉引用工具,它可以用来浏览和搜索代码库。虽然 OpenGrok 提供了代码搜索、查看文件和历史等功能,但它本身不是一个完整的在线集成开发环境(IDE)。然而,OpenGrok 可以作为…...

济南适宜地提取
题目: 网上下载中国的DEM、土地利用地图(1980、2000、2015年的)和一张最新济南市行政区划 图(要求:莱芜市并入济南后的区划图); 2.网上下载中国2015年年平均降水空间插值数据;3..网上下载中国2015年年平均气温空间插值数据; (注:以上数据可到资源环境科学与数据中心下载http…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...

Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...

Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...

AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...
HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散
前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说,在叠衣服的过程中,我会带着团队对比各种模型、方法、策略,毕竟针对各个场景始终寻找更优的解决方案,是我个人和我司「七月在线」的职责之一 且个人认为,…...

倒装芯片凸点成型工艺
UBM(Under Bump Metallization)与Bump(焊球)形成工艺流程。我们可以将整张流程图分为三大阶段来理解: 🔧 一、UBM(Under Bump Metallization)工艺流程(黄色区域ÿ…...
[特殊字符] Spring Boot底层原理深度解析与高级面试题精析
一、Spring Boot底层原理详解 Spring Boot的核心设计哲学是约定优于配置和自动装配,通过简化传统Spring应用的初始化和配置流程,显著提升开发效率。其底层原理可拆解为以下核心机制: 自动装配(Auto-Configuration) 核…...