news 2026/9/26 18:28:14

Spring Boot整合Quartz实战:从动态调度到持久化集群全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot整合Quartz实战:从动态调度到持久化集群全解析

1. 项目概述:先搞清楚为什么要整合Quartz

先说结论:如果你只是想在Spring Boot里跑个定时任务,@Scheduled注解其实够用,但一旦任务涉及动态调度、持久化、集群部署或者复杂的触发策略,@Scheduled就捉襟见肘了。这时候,Quartz几乎是Java生态里的首选方案。

我这个项目的背景很简单——系统里有大量定时任务,包括报表生成、数据同步、缓存预热、对账批处理等,原来全是@Scheduled硬编码。后来需求变成了“运营人员要在后台页面上随时修改任务执行频率”,甚至要临时暂停某个任务,还要能查看任务的执行历史。@Scheduled显然做不到动态管理,于是调研了一圈,决定把调度层换成Quartz。

为什么是Quartz而不是别的?市面上Java定时任务方案其实不少:@Scheduled、xxl-job、Elastic-Job、Quartz。xxl-job和Elastic-Job都带可视化管理界面,功能很全,但它们的强项是分布式任务调度平台,引入成本偏高,需要额外部署调度中心。Quartz作为老牌调度库,轻量、成熟、和Spring Boot整合方案极其完善,单机场景下够稳,搭配数据库表还能做到任务持久化和简单的高可用。对我这个既要“快速落地”又要“保留扩展空间”的项目来说,Quartz是最稳妥的中间选择。

这篇博文就围绕“Spring Boot整合Quartz”这个主题,从零开始讲清楚两件事:一是整合的基础步骤,二是整合时容易踩的坑和应对方案。不搞花里胡哨的封装,直接给你能落地的代码和配置。

2. 整合前的准备工作:依赖和基础知识

2.1 Maven依赖怎么加

Spring Boot整合Quartz其实非常简单,它提供了官方starter——spring-boot-starter-quartz。这个starter不仅帮你引入了Quartz的核心库,还会自动配置SchedulerFactoryBean和Scheduler,省掉了手工创建工厂的繁琐步骤。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> <version>2.7.18</version> </dependency>

如果你的项目用的是Spring Boot 3.x,spring-boot-starter-quartz的版本号跟着Spring Boot父BOM走即可,不需要显式指定版本。注意Quartz本身版本已经从2.x升级到了3.x,API有一些变化,我们下面主要基于Spring Boot 2.x/Quartz 2.3的稳定组合来讲,兼容性更好。

2.2 Quartz的核心概念,用生活类比讲清楚

Quartz里最核心的接口有三个:Job、Trigger、Scheduler。很多人一开始会被这三个概念绕晕,我打个比方:

  • Job(任务):就是“要做什么”。你写一个类实现Job接口,在execute方法里写业务逻辑。相当于你请了一个跑腿小哥,他负责干什么活。
  • Trigger(触发器):就是“什么时候做”。它定义任务的触发规则,是用Cron表达式、还是固定间隔、还是每天几点。相当于给跑腿小哥安排了一个闹钟。
  • Scheduler(调度器):就是“谁来管这个闹钟”。它负责注册Job和Trigger,让它们按规则跑起来。相当于跑腿公司的调度中心,统筹所有小哥和闹钟。

这三者的协作关系是:Scheduler把JobDetail(Job的详细描述,包含任务名、任务组、具体执行类)和Trigger绑定在一起,注册到调度容器里,到了触发时间就执行Job里的逻辑。

2.3 一个最容易忽略的点:JobDetail和Job的区别

很多新手写Quartz会直接new一个Job去执行,其实Quartz里注册的并不是Job本身,而是JobDetail。JobDetail是Job的描述信息,它持久化了任务的名字、分组、参数等元数据,每次触发时,Quartz会通过反射创建出一个新的Job实例来执行execute方法。

为什么是“每次触发重新创建实例”?因为Quartz的设计目标是任务隔离——不同的触发事件不应该共享同一个Job实例的状态,避免并发执行时出现数据混乱。这个设计也带来了一个常见的坑:Job里无法通过@Autowired直接注入Spring Bean,因为Job实例是由Quartz自己反射创建的,不在Spring容器管理范围内。后面第三章我会专门讲怎么解决这问题。

2.4 配置文件先准备好

依赖加好之后,application.yml里其实什么都不配置也能跑起来。Quartz默认使用RAM存储(内存),重启任务就没了。但既然是整合,我建议至少把线程池和实例名先配好:

spring: quartz: job-store-type: memory scheduler-name: my-scheduler properties: org.quartz.threadPool.threadCount: 8 org.quartz.threadPool.threadPriority: 5 org.quartz.jobStore.misfireThreshold: 60000

threadCount是调度线程池大小,默认是4。如果你的任务数量多、执行时间长,建议调大到8或16。misfireThreshold是“任务错过触发时长的阈值”,单位毫秒,默认60000(1分钟),超过这个阈值会被判定为misfire。这里先记住这两个参数,后面排查问题时你会回来找它们的。

3. 核心细节解析:三种Trigger怎么选

3.1 CronTrigger:最常用,必掌握

CronTrigger基于Cron表达式触发,支持秒级到年级的执行规则。这是项目里用得最多的触发器,几乎所有“每天凌晨2点执行”“每小时的15分执行”这类需求都是它搞定。

Cron表达式格式:秒 分 时 日 月 周,共6位(Spring Boot整合的Quartz里是6位,不是常见的Unix Cron 5位)。举个例子:0 0 2 * * ?表示每天凌晨2点整执行。注意周字段用?表示“不指定”,这也是Quartz Cron和Unix Cron的一个明显区别。

使用示例:

CronTrigger trigger = TriggerBuilder.newTrigger() .withIdentity("report-trigger", "default-group") .withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")) .build();

这里最多人懵的地方就是?和*的区别。*表示“每一刻”都触发,比如日字段写*表示每天都执行;周字段写?表示“不关心星期几”,避免“日和周同时指定导致语义冲突”。Quartz规定,日和周字段必须有一个设置为?,不然会报错。这个规则我项目里第一次写时就踩到过,Cron表达式怎么校验都过不去,后来才发现问题出在“周字段必须为?或者日和周互斥”这个细节上。

3.2 SimpleTrigger:适合固定间隔重复执行

如果你不需要复杂的Cron规则,只是想“每5分钟执行一次”或者“延迟10秒后执行一次”,那SimpleTrigger更合适。它支持指定起始时间、重复次数、重复间隔。

SimpleTrigger trigger = TriggerBuilder.newTrigger() .withIdentity("sync-trigger", "default-group") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(300) .repeatForever()) .build();

repeatForever()表示无限重复,如果你只想固定次数,用.withRepeatCount(10)就行。注意repeatForever和withRepeatCount只能选一个,否则运行时报错。

3.3 CalendarIntervalTrigger:处理“每月”“每周”这类规则

这个触发器用的相对少,但很实用——比如“每三个月执行一次”。Cron表达式表达这种跨月的间隔其实非常别扭,而CalendarIntervalTrigger可以按SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、YEAR等粒度来设置间隔。

CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger() .withIdentity("quarterly-trigger", "default-group") .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule() .withIntervalInMonths(3)) .build();

这个触发器的好处是它排除了夏令时和时区的影响,对“每个月最后一天执行”这类需求也支持得很好。

3.4 选择建议

我整理了一个简表,方便你按需选择:

场景推荐Trigger理由
每天/每周/每月固定时间执行CronTrigger表达式灵活,支持复杂规则
固定间隔重复,如每5分钟SimpleTrigger配置简单,延迟+重复次数直观
每月/每季度/每年间隔执行CalendarIntervalTrigger跨周期表达清晰,免去Cron计算
需要忽略某些特定日期CronTrigger + HolidayCalendar配合Calendar排除节假日

4. 实操过程:从零搭一个可运行的完整案例

4.1 编写Job类

先创建一个任务类,继承QuartzJobBean(Spring Boot整合包提供的基类),或者实现Job接口。

@Component public class SyncDataJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap dataMap = context.getJobDetail().getJobDataMap(); String taskName = dataMap.getString("taskName"); String tableName = dataMap.getString("tableName"); System.out.println("[" + LocalDateTime.now() + "] 执行任务:" + taskName + ",目标表:" + tableName); // 这里写真正的业务逻辑,比如同步数据 } }

这里有个关键点:我在Job里用JobExecutionContext拿JobDataMap中的参数。JobDataMap就是给你放业务参数用的,运行时可动态传入。为什么推荐这种写法?因为一个Job类可能被多个Trigger触发,每次触发的参数可能不同,通过JobDataMap传递既能解耦,又能灵活控制每次执行的行为。

4.2 配置JobDetail和Trigger

接下来创建一个配置类,把JobDetail和Trigger装配成Spring Bean,框架启动时会自动注册定时任务。

@Configuration public class QuartzConfig { @Bean public JobDetail syncDataJobDetail() { JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put("taskName", "sync-order-data"); jobDataMap.put("tableName", "t_order"); return JobBuilder.newJob(SyncDataJob.class) .withIdentity("syncDataJob", "data-group") .usingJobData(jobDataMap) .storeDurably() .build(); } @Bean public Trigger syncDataTrigger() { return TriggerBuilder.newTrigger() .forJob(syncDataJobDetail()) .withIdentity("syncDataTrigger", "data-group") .withSchedule(CronScheduleBuilder.cronSchedule("0 0/30 * * * ?")) .build(); } }

这里有几个细节值得强调:

第一,storeDurably()是必须的。如果你只是创建一个JobDetail不设置storeDurably(true),Quartz会认为该任务没有绑定Trigger就无意义,启动时会报“Jobs added with no trigger must be durable”错误。

第二,forJob(syncDataJobDetail())把Trigger和JobDetail绑定起来,但这里传的是JobDetail对象,不是再构建一个JobKey字符串。这样两个Bean通过方法调用产生关联,Spring容器初始化时不会搞错顺序。

第三,JobDataMap的键值都是字符串,运行时在Job里可以通过context.getMergedJobDataMap()获取,注意是“merged”,它会合并JobDetail和Trigger上绑定的所有数据。

4.3 动态调度服务类

只是配置好定时任务还不够,项目里更大的需求是“运行中随时创建新任务”,比如运营在后台设置一个新任务的执行时间。这时候需要注入Scheduler,自己写一个动态调度服务。

@Service public class QuartzDynamicService { @Autowired private Scheduler scheduler; /** * 动态创建定时任务 */ public void addJob(Class<? extends Job> jobClass, String jobName, String jobGroup, String cron, JobDataMap params, String triggerName, String triggerGroup) throws SchedulerException { JobDetail jobDetail = JobBuilder.newJob(jobClass) .withIdentity(jobName, jobGroup) .usingJobData(params) .storeDurably() .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity(triggerName, triggerGroup) .forJob(jobDetail) .withSchedule(CronScheduleBuilder.cronSchedule(cron)) .build(); // 先移除存在的,避免重复注册 if (scheduler.checkExists(jobDetail.getKey())) { scheduler.deleteJob(jobDetail.getKey()); } scheduler.scheduleJob(jobDetail, trigger); } /** * 暂停任务 */ public void pauseJob(String jobName, String jobGroup) throws SchedulerException { scheduler.pauseJob(JobKey.jobKey(jobName, jobGroup)); } /** * 恢复任务 */ public void resumeJob(String jobName, String jobGroup) throws SchedulerException { scheduler.resumeJob(JobKey.jobKey(jobName, jobGroup)); } /** * 删除任务 */ public void deleteJob(String jobName, String jobGroup) throws SchedulerException { scheduler.deleteJob(JobKey.jobKey(jobName, jobGroup)); } }

这段代码其实就是整个动态调度的核心了。注意几个方法都是SchedulerException向上抛出,业务层需要捕获并转成友好提示。

4.4 调用示例

在接口或Service里调用动态调度服务:

@RestController @RequestMapping("/task") public class TaskController { @Autowired private QuartzDynamicService quartzDynamicService; @PostMapping("/add") public String addTask(@RequestParam String jobName, @RequestParam String cron) { JobDataMap params = new JobDataMap(); params.put("taskName", jobName); params.put("createBy", "admin"); try { quartzDynamicService.addJob(SyncDataJob.class, jobName, "dynamic-group", cron, params, jobName + "-trigger", "dynamic-group"); return "任务添加成功"; } catch (SchedulerException e) { return "任务添加失败:" + e.getMessage(); } } }

这样下来,一个最简化的“Spring Boot整合Quartz + 动态任务管理”骨架就搭好了。启动项目后,控制台会每30分钟输出一次同步日志,同时你也可以通过HTTP接口动态添加新任务。

5. 进阶配置:持久化与集群,生产环境必须考虑的事

5.1 RAM还是JDBC,这是个问题

开发阶段用RAM存储没问题,重启项目任务没了也无所谓。但上到生产环境,如果你的任务因为服务器重启就全部消失,这在业务上不能接受。Quartz提供了JDBC存储模式,把JobDetail、Trigger、Calendar等元数据存到数据库表中,项目重启后任务自动恢复。

配置方式在application.yml里:

spring: quartz: job-store-type: jdbc jdbc: initialize-schema: always properties: org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate org.quartz.jobStore.tablePrefix: QRTZ_

关键是新增了job-store-type: jdbc和对应的数据库表。Quartz官方提供了建表SQL脚本,在Quartz发行包的docs/dbTables目录下,按数据库类型选对应的sql执行即可。如果你不愿意手工建表,Spring Boot还可以在启动时自动执行建表脚本,通过spring.quartz.jdbc.initialize-schema控制,有always(启动时总是初始化)和never(不初始化)两个选项。

要不要自动初始化建表?我建议是生产环境用never,手工执行官方SQL脚本建表,这样表结构可控、不会误删已有数据。always模式有风险——如果项目里已有QRTZ_表,启动时可能因为表已存在而报错。

5.2 集群部署的坑

Quartz还支持集群部署,多个实例共享同一个数据库,通过数据库行锁保证同一时间只有一个实例执行任务。配置上主要是:

spring: quartz: properties: org.quartz.jobStore.isClustered: true org.quartz.jobStore.clusterCheckinInterval: 15000

增加isClustered: true后,所有实例的调度任务会在数据库层做抢占式调度。这里有个大坑:集群模式下,任务执行的并发问题不能靠Quartz自动解决。比如一个任务A,两个实例同时启动,虽然Quartz会保证同一个Trigger只在一个实例上触发,但如果你的任务本身执行时间很长(超过调度间隔),上一次还没跑完,下一次触发也可能被同一实例再次调度,造成重入。

解决办法是在Job类上标注@DisallowConcurrentExecution注解:

@DisallowConcurrentExecution @Component public class SyncDataJob implements Job { // ... }

这个注解的含义是:同一JobDetail的多个触发之间不能并发执行,如果上一次还没执行完,下一次触发会被阻塞或跳过。这是Quartz性能优化和安全运行的关键开关,生产环境务必加上。

5.3 misfire策略怎么选

用了JDBC持久化后,触发机制变成了“触发时间点到了才从库里读取并执行”。如果任务错过了触发时间(比如应用停了一段时间、线程池满了),就会进入“misfire”状态。Quartz对misfire有几种处理策略,常见的有:

  • MISFIRE_INSTRUCTION_SMART_POLICY(默认):根据Trigger类型自动选择合适策略。
  • MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:立即补执行一次。
  • MISFIRE_INSTRUCTION_DO_NOTHING:直接跳过错过的触发,等下一个周期。

对于报表任务、数据同步任务,我一般用FIRE_ONCE_NOW,保证数据不缺失;对于那种“固定时间点必须执行,错过就无意义”的任务,比如定时推送消息,用DO_NOTHING更合理,因为用户已经错过了最佳接收时间,补发反而造成骚扰。

设置方式是:

CronScheduleBuilder cronSchedule = CronScheduleBuilder.cronSchedule("0 0 2 * * ?") .withMisfireHandlingInstructionFireAndProceed();

FireAndProceed就是“立即执行,并按新时间表计算下一次触发时间”。withMisfireHandlingInstructionDoNothing则对应“跳过”。

6. 踩坑实录与排查技巧

6.1 Job中无法注入Spring Bean,出现NPE

前面提到过,QuartzJobBean或者Job实现类的实例由Quartz内部反射创建,不受Spring容器管理,所以@Autowired一个Service进去会得到null。

解决办法有三种:

第一,实现ApplicationContextAware,把ApplicationContext静态化,在Job里直接通过它拿Bean:

@Component public class SpringContextUtils implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext appContext) { applicationContext = appContext; } public static <T> T getBean(Class<T> clazz) { return applicationContext.getBean(clazz); } }

Job里这样用:

public class SyncDataJob implements Job { @Override public void execute(JobExecutionContext context) { UserService userService = SpringContextUtils.getBean(UserService.class); userService.syncUserData(); } }

第二,用SpringBeanJobFactory扩展版。Spring Boot的QuartzAutoConfiguration已经内置了AutowireSpringBeanJobFactory,它会在Job实例化后自动调用autowireBean,所以理论上你直接@Autowired就能生效。但某些自定义配置场景可能覆盖了这个工厂,导致注入失败。所以稳妥做法还是第一种。

第三,使用QuartzJobBean作为基类。QuartzJobBean是Spring提供的抽象类,它重写了execute,先调用BeanFactory对Job进行属性注入,然后调用executeInternal方法。你只需要继承它,在executeInternal里使用注入的属性即可:

@Component public class SyncDataJob extends QuartzJobBean { @Autowired private UserService userService; @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { userService.syncUserData(); } }

这个方法最优雅,推荐优先使用。

6.2 Cron表达式校验报错

常见的错误是“Expression does not conform to Quartz Cron format”,原因基本都是:周字段同时和日字段有值,而没有使用?。比如0 0 2 * * MON就报错,因为“每天2点”和“每周一”冲突了。正确写法是0 0 2 ? * MON,表示“每周一凌晨2点”。

还有一个坑:0 0 2 * * ?里第6位是?,但有人习惯用*,运行时会直接抛RuntimeException。记住:Quartz Cron的周字段如果不用?,就会出现?不能和*共存的矛盾。用在线工具验证时,也要选Quartz格式而不是Linux Cron格式。

6.3 任务明明配了,却不执行

优先级最高的怀疑对象是调度线程池太小。Quartz默认线程数4,如果某个任务执行时间很长,比如数据库查询耗时5分钟,线程全被占满,后面的Trigger虽然触发但排不到线程,看起来就是“没执行”。此时去数据库QRTZ_TRIGGERS表查,状态可能一直处于WAITING或PAUSED之间切换,但不会变COMPLETE。

排查步骤:

  1. 检查spring.quartz.properties.org.quartz.threadPool.threadCount配置。
  2. 在Job执行入口打日志,确认是否被调度。
  3. 查看日志里的QuartzScheduler信息,看线程池是否饱和。
  4. 用JConsole或Arthas查看线程状态。

如果业务上允许,尽量把耗时操作异步化,或者调大线程数。

6.4 重启项目后任务丢了一半

这几乎是100%的RAM存储问题。确认三点:

  • spring.quartz.job-store-type是否配置为jdbc。
  • QRTZ_系列表是否存在(至少11张核心表)。
  • 项目启动时是否成功恢复了Trigger和Job。

排查时可以直接查QRTZ_TRIGGERS表,看里面有没有数据。如果表为空,说明配置没生效,看看initialize-schema是否被误配置成never。

6.5 时区问题:服务器跑起来任务时间不准

Quartz默认使用服务器默认时区。不同机房、不同云厂商的服务器默认时区可能都是UTC,导致Cron表达式表现出的执行时间和你想要的时间差8小时。

解决方式:

CronScheduleBuilder.cronSchedule("0 0 2 * * ?") .inTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));

或者在Trigger上整体配置:

TriggerBuilder.newTrigger() .withSchedule(cronSchedule) .startAt(Date.from(ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).toInstant())) .build();

生产环境建议统一在配置文件中指定时间区:

spring: quartz: properties: org.quartz.scheduler.timeZone: Asia/Shanghai

6.6 动态添加任务时,同名任务被静默覆盖

scheduleJob(jobDetail, trigger)如果jobDetail已存在且不是durable,会抛ObjectAlreadyExistsException。所以我在动态服务类里先checkExists再deleteJob是很有必要的。另外如果重复添加同名Trigger,Quartz也会直接报错。要么先删除再添加,要么用rescheduleJob做更新:

Trigger oldTrigger = scheduler.getTrigger(TriggerKey.triggerKey(triggerName, triggerGroup)); if (oldTrigger != null) { scheduler.rescheduleJob(oldTrigger.getKey(), newTrigger); } else { scheduler.scheduleJob(jobDetail, trigger); }

这比“删除再添加”更稳,因为它保留了任务的历史状态和关联JobDetail,不会丢数据。

7. 最后的一些额外心得

聊几个我在项目里经过几轮迭代才想明白的细节。

一个是关于任务表的设计。Quartz自身有QRTZ_系列表存元数据,但这些表不适合直接做业务查询。我在业务库里额外建了一张sys_job_record执行记录表,每次Job执行开始、结束、异常都往里面插一条记录。这样运营能直观看到任务跑没跑、跑了多久、失败原因是什么。这一层“业务记录”是Quartz官方管理界面给不了你的,得自己补。

另一个是异常处理。Job里抛出异常时,Quartz会根据JobExecutionException判断是继续还是停止任务。如果想让任务失败后下次继续执行,就不要抛出异常,而是捕获后记录日志;如果想让任务在这个Trigger周期内彻底不跑了,可以调用context.getScheduler().pauseJob(context.getJobDetail().getKey())。通常是“失败后告警+下个周期重跑”的组合策略更合理。

第三点是关于线程池调优。Quartz单机模式下,线程池不是越大越好。线程太多会导致任务并发增加,数据库连接、外部接口压力随之上升。我现在的经验是:任务总数在30个以内、单任务耗时5秒以下的场景,threadCount=8足够;单任务耗时长、任务数多时,先拆任务或者考虑分布式调度方案,而不是一味加线程。

最后一个比较实用的小技巧:在Job里通过context.getFireTime()拿到的触发时间,可以用来做“幂等”判断。比如一个同步任务,同一分钟内被重复触发多次,你可以用fireTime的分钟粒度作为幂等键,配合数据库唯一索引,保证数据不会被重复写入。不用太复杂的逻辑,一个小字段就搞定。

Spring Boot整合Quartz这件事,难度其实不在“整合”本身,而在于整合完了之后如何稳定运行、如何动态管理、如何排查问题。希望这篇博文里的经验对你的项目有直接帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 18:26:29

晶振相位噪声如何影响5G光模块误码率

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 18:25:43

【金丹·71】进程间通信:管道、共享内存、信号

【金丹71】进程间通信&#xff1a;管道、共享内存、信号 码农修仙传 金丹期 第71篇 我是玄芯散人&#xff0c;带你从炼气修到大乘。 境界标识 ╔══════════════════════════════════╗ ║ 金丹期 第71篇 ║ ║ 进程…

作者头像 李华
网站建设 2026/9/26 18:25:11

区块链钱包核心解析:从私钥助记词到冷热钱包的安全实践

1. 钱包里没有“币”&#xff1a;先把这个最核心的认知建立起来很多人第一次接触区块链钱包时&#xff0c;脑子里装的是物理钱包的画面——一个皮夹子&#xff0c;里面插着几张钞票、几枚硬币。这个类比在区块链世界里完全是误导。区块链钱包里根本不存在任何“币”&#xff0c…

作者头像 李华
网站建设 2026/9/26 18:25:04

SpringBoot3整合FastJSON2:configureMessageConverters配置实战与避坑指南

SpringBoot 3 项目升级后&#xff0c;原来在 SpringBoot 2 里用得挺顺的 FastJson 突然不干活了&#xff0c;接口要么返回一串 Jackson 的默认格式&#xff0c;要么直接报错&#xff0c;排查几圈下来发现核心问题就出在configureMessageConverters这个配置方法上。这篇内容就围…

作者头像 李华