在日常开发中,我们经常会遇到需要异步处理的场景,比如批量处理患者病历、生成影像报告、同步检查单数据等。SpringBoot 提供的 @Async 注解能快速实现异步任务,但默认情况下,任务仅存储在内存中------一旦系统崩溃或重启,未执行完成的任务会全部丢失,这在医疗、金融等对数据可靠性要求极高的领域是无法接受的。
本文将分享一套工业级的 SpringBoot 异步任务持久化方案,核心解决「任务不丢失、崩溃可恢复、不同任务差异化处理」三大问题,从设计思路到代码实现全程拆解,新手也能轻松落地。
一、核心痛点与设计原则
1. 先明确核心痛点
-
内存任务易丢失:默认异步任务存储在线程池队列中,系统重启/崩溃直接清空;
-
任务状态不可追溯:无法知道任务是待执行、执行中还是失败;
-
不同任务难适配:批量处理、报告生成等任务数据结构不同,需差异化处理;
-
重试与幂等性问题:任务重试易重复执行,影响业务结果。
2. 方案设计核心原则
针对以上痛点,方案设计需遵循 5 大原则:
-
「先落地,后执行」:所有任务提交时先写入持久化存储(数据库),确保任务不会因内存清空丢失;
-
状态全生命周期追踪:为任务标记清晰状态(待执行/执行中/成功/失败),重启后可精准筛选未完成任务;
-
差异化任务解耦:通过策略模式拆分不同类型任务的业务逻辑,便于维护;
-
幂等性保障:通过唯一幂等键防止任务重复执行;
-
失败可重试:区分「执行中崩溃」和「业务失败」,分别处理重启恢复和主动重试。
二、整体架构设计
方案整体分为 5 个核心模块,形成「提交-持久化-执行-恢复-监控」的完整闭环:
用户/业务系统 → 任务提交模块(幂等校验+持久化)→ 异步执行模块(线程池+差异化处理)
↓(崩溃重启后)
任务恢复模块(扫描未完成任务)→ 重新提交至异步执行模块
↓
结果处理模块(更新状态+记录日志)→ (可选)监控告警模块
技术选型(兼顾稳定性与易用性):
-
持久化存储:MySQL(优先,支持事务与复杂查询;小体量场景可选用 Redis);
-
异步框架:Spring
@Async+ 自定义线程池(避免默认线程池缺陷); -
重启恢复:CommandLineRunner(项目启动后自动执行恢复逻辑);
-
差异化处理:策略模式(任务处理器工厂);
-
分布式适配:Redisson(分布式锁,防止多实例重复执行任务)。
三、核心实现步骤(附完整代码)
以下实现基于 SpringBoot 2.7.x + MyBatis-Plus 3.5.x + MySQL 8.0,完整覆盖从表设计到任务恢复的全流程。
步骤 1:设计任务表(核心:持久化与状态追踪)
任务表是方案的基础,需存储任务类型、差异化数据、状态等核心信息,采用 JSON 字段存储不同任务的差异化数据(无需为每种任务建表):
sql
CREATE TABLE `async_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务ID',
`task_type` varchar(50) NOT NULL COMMENT '任务类型(如:PATIENT_DATA、IMAGING_REPORT)',
`task_data` json NOT NULL COMMENT '任务数据(JSON格式,适配不同任务)',
`task_status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态:0-待执行 1-执行中 2-成功 3-失败',
`executor_times` int(11) NOT NULL DEFAULT 0 COMMENT '已执行次数',
`max_retry_times` int(11) NOT NULL DEFAULT 3 COMMENT '最大重试次数',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`exec_start_time` datetime DEFAULT NULL COMMENT '执行开始时间',
`exec_end_time` datetime DEFAULT NULL COMMENT '执行结束时间',
`error_msg` varchar(1000) DEFAULT NULL COMMENT '失败信息',
`task_idempotent_key` varchar(64) NOT NULL COMMENT '幂等键(唯一标识任务)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_idempotent_key` (`task_idempotent_key`), -- 幂等校验索引
KEY `idx_task_status_create_time` (`task_status`, `create_time`) -- 恢复任务查询索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '异步任务表';
步骤 2:定义核心实体与枚举
对应任务表设计实体类,同时通过枚举统一管理任务状态(避免硬编码):
java
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("async_task")
public class AsyncTask {
@TableId(type = IdType.AUTO)
private Long id;
/** 任务类型:PATIENT_DATA/IMAGING_REPORT/TEST_FORM */
private String taskType;
/** 任务数据(JSON字符串) */
private String taskData;
/** 任务状态:0-待执行 1-执行中 2-成功 3-失败 */
private Integer taskStatus;
/** 已执行次数 */
private Integer executorTimes;
/** 最大重试次数 */
private Integer maxRetryTimes;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private LocalDateTime execStartTime;
private LocalDateTime execEndTime;
/** 失败错误信息 */
private String errorMsg;
/** 幂等键(防止重复执行) */
private String taskIdempotentKey;
// 状态枚举(统一管理,避免硬编码)
public enum Status {
PENDING(0, "待执行"),
EXECUTING(1, "执行中"),
SUCCESS(2, "执行成功"),
FAILED(3, "执行失败");
private final int code;
private final String desc;
Status(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
}
}
步骤 3:配置自定义异步线程池
Spring 默认异步线程池核心线程数少、队列无界,易导致 OOM,需自定义线程池适配业务场景:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync // 开启异步支持
public class AsyncConfig {
/**
* 自定义异步线程池
* 核心参数:根据 CPU 核心数 + 业务场景调整
*/
@Bean("asyncTaskExecutor")
public Executor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数(CPU核心数 * 2,兼顾并发与资源)
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
executor.setCorePoolSize(corePoolSize);
// 最大线程数(峰值负载时扩容)
executor.setMaxPoolSize(20);
// 队列容量(缓冲任务,避免线程频繁创建销毁)
executor.setQueueCapacity(1000);
// 线程名称前缀(便于日志排查)
executor.setThreadNamePrefix("async-task-");
// 线程空闲超时时间(60秒,空闲线程自动销毁)
executor.setKeepAliveSeconds(60);
// 拒绝策略:队列满后抛异常(结合任务重试,避免任务静默丢失)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 初始化线程池
executor.initialize();
return executor;
}
}
步骤 4:任务提交模块(先持久化,再执行)
封装任务提交服务,核心逻辑:幂等校验 → 任务数据JSON序列化 → 持久化到数据库 → 提交至异步线程池。确保「先落地,后执行」:
java
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class AsyncTaskSubmitService {
@Resource
private AsyncTaskMapper asyncTaskMapper;
@Resource
private AsyncTaskExecutorService asyncTaskExecutorService;
/**
* 通用任务提交方法(适配所有类型任务)
* @param taskType 任务类型(如 PATIENT_DATA)
* @param taskData 任务数据(任意对象,自动转JSON)
* @param idempotentKey 幂等键(如 任务类型+业务ID)
* @param maxRetryTimes 最大重试次数
* @param <T> 任务数据类型
*/
public <T> void submitTask(String taskType, T taskData, String idempotentKey, int maxRetryTimes) {
// 1. 幂等校验:防止重复提交(如接口重试导致的重复任务)
AsyncTask existTask = asyncTaskMapper.selectByTaskIdempotentKey(idempotentKey);
if (existTask != null) {
log.warn("任务已存在,幂等键:{}", idempotentKey);
return;
}
// 2. 任务数据转JSON(适配不同类型任务的差异化数据)
String taskDataJson = JSON.toJSONString(taskData);
// 3. 构建任务对象并持久化到数据库
AsyncTask task = new AsyncTask();
task.setTaskType(taskType);
task.setTaskData(taskDataJson);
task.setTaskStatus(AsyncTask.Status.PENDING.getCode()); // 初始状态:待执行
task.setExecutorTimes(0);
task.setMaxRetryTimes(maxRetryTimes);
task.setTaskIdempotentKey(idempotentKey);
asyncTaskMapper.insert(task);
log.info("任务持久化成功,任务ID:{},类型:{}", task.getId(), taskType);
// 4. 提交到异步线程池执行
asyncTaskExecutorService.executeTask(task.getId());
}
}
步骤 5:异步执行模块(差异化处理+状态更新)
核心模块:负责任务执行、状态更新、异常捕获与重试判断。通过「任务处理器工厂」实现不同类型任务的差异化处理(策略模式)。
首先定义任务处理器接口(统一规范):
java
/**
* 任务处理器接口(不同类型任务实现此接口)
* @param <T> 任务数据类型
*/
public interface TaskProcessor<T> {
/** 获取任务类型(与提交时的taskType对应) */
String getTaskType();
/** 获取任务数据的Class(用于JSON反序列化) */
Class<T> getTaskDataClass();
/** 核心任务执行逻辑 */
void process(T taskData);
}
实现具体任务处理器(以病历处理为例):
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
// 病历处理任务处理器
@Component
@Slf4j
public class PatientDataProcessor implements TaskProcessor<PatientDataDTO> {
@Override
public String getTaskType() {
return "PATIENT_DATA"; // 与任务提交时的taskType一致
}
@Override
public Class<PatientDataDTO> getTaskDataClass() {
return PatientDataDTO.class; // 任务数据类型
}
@Override
public void process(PatientDataDTO taskData) {
// 这里是核心业务逻辑:如解析病历、同步数据、生成报表等
log.info("开始处理病历任务,患者ID:{},病历编号:{}", taskData.getPatientId(), taskData.getMedicalRecordNo());
// 模拟业务处理(实际场景替换为真实逻辑)
// medicalRecordService.process(taskData);
}
}
实现任务处理器工厂(自动注入所有处理器,根据类型匹配):
java
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component
public class TaskProcessorFactory {
// 存储 任务类型 → 处理器 的映射
private final Map<String, TaskProcessor<?>> processorMap;
// 构造函数自动注入所有TaskProcessor实现类
public TaskProcessorFactory(List<TaskProcessor<?>> processors) {
this.processorMap = processors.stream()
.collect(Collectors.toMap(TaskProcessor::getTaskType, processor -> processor));
}
/** 根据任务类型获取对应的处理器 */
public TaskProcessor<?> getProcessor(String taskType) {
TaskProcessor<?> processor = processorMap.get(taskType);
if (processor == null) {
throw new IllegalArgumentException("未知任务类型:" + taskType);
}
return processor;
}
}
最后实现异步执行服务(核心逻辑):
java
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class AsyncTaskExecutorService {
@Resource
private AsyncTaskMapper asyncTaskMapper;
@Resource
private TaskProcessorFactory taskProcessorFactory;
/**
* 异步执行任务(指定自定义线程池)
*/
@Async("asyncTaskExecutor")
public void executeTask(Long taskId) {
// 1. 查询任务信息(并校验状态)
AsyncTask task = asyncTaskMapper.selectById(taskId);
if (task == null) {
log.error("任务不存在,ID:{}", taskId);
return;
}
// 只有「待执行」状态的任务才能执行(避免重复执行)
if (!AsyncTask.Status.PENDING.getCode().equals(task.getTaskStatus())) {
log.warn("任务状态异常,无法执行。任务ID:{},当前状态:{}", taskId, task.getTaskStatus());
return;
}
// 2. 乐观锁更新状态为「执行中」(防止多线程/多实例并发执行)
int updateCount = asyncTaskMapper.updateStatusToExecuting(taskId, AsyncTask.Status.PENDING.getCode());
if (updateCount == 0) {
log.warn("任务状态更新失败(可能已被其他线程执行),ID:{}", taskId);
return;
}
// 3. 执行任务(核心逻辑)
try {
log.info("开始执行任务,ID:{},类型:{}", taskId, task.getTaskType());
task.setExecStartTime(LocalDateTime.now());
// 3.1 匹配对应的任务处理器
TaskProcessor<?> processor = taskProcessorFactory.getProcessor(task.getTaskType());
// 3.2 JSON反序列化任务数据(适配不同类型任务)
Object taskData = JSON.parseObject(task.getTaskData(), processor.getTaskDataClass());
// 3.3 执行具体业务逻辑
((TaskProcessor<Object>) processor).process(taskData);
// 4. 执行成功:更新状态为「成功」
asyncTaskMapper.updateTaskSuccess(taskId, LocalDateTime.now());
log.info("任务执行成功,ID:{}", taskId);
} catch (Exception e) {
// 5. 执行失败:更新状态+判断是否重试
int currentExecTimes = task.getExecutorTimes() + 1;
String errorMsg = String.format("任务执行失败:%s,已执行次数:%d", e.getMessage(), currentExecTimes);
log.error(errorMsg, e);
// 5.1 确定新状态:未达最大重试次数→待执行(重启/重新提交可重试),否则→失败
Integer newStatus = currentExecTimes >= task.getMaxRetryTimes()
? AsyncTask.Status.FAILED.getCode()
: AsyncTask.Status.PENDING.getCode();
// 5.2 更新任务状态与失败信息
asyncTaskMapper.updateTaskFail(
taskId, newStatus, currentExecTimes, errorMsg, LocalDateTime.now()
);
// 5.3 未达最大重试次数:立即重试(可选,也可仅依赖重启恢复)
if (newStatus == AsyncTask.Status.PENDING.getCode()) {
executeTask(taskId);
}
}
}
}
步骤 6:重启恢复模块(核心:崩溃后不丢任务)
通过 CommandLineRunner 接口,项目启动后自动扫描「待执行」和「执行中」的任务(执行中任务可能是崩溃导致的),重新提交执行:
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class AsyncTaskRecoveryRunner implements CommandLineRunner {
@Resource
private AsyncTaskMapper asyncTaskMapper;
@Resource
private AsyncTaskExecutorService asyncTaskExecutorService;
@Override
public void run(String... args) throws Exception {
log.info("===== 开始恢复未完成的异步任务 =====");
// 1. 查询未完成任务:待执行(0)+ 执行中(1)(执行中可能是崩溃导致的)
List<Integer> unFinishedStatus = Arrays.asList(
AsyncTask.Status.PENDING.getCode(),
AsyncTask.Status.EXECUTING.getCode()
);
List<AsyncTask> unFinishedTasks = asyncTaskMapper.selectUnFinishedTasks(unFinishedStatus);
if (unFinishedTasks.isEmpty()) {
log.info("无未完成任务,恢复结束");
return;
}
log.info("共发现 {} 个未完成任务,开始重新提交", unFinishedTasks.size());
// 2. 逐个恢复任务
for (AsyncTask task : unFinishedTasks) {
// 2.1 若任务状态是「执行中」,先重置为「待执行」(崩溃导致的执行中)
if (AsyncTask.Status.EXECUTING.getCode().equals(task.getTaskStatus())) {
asyncTaskMapper.updateStatusToPending(task.getId());
log.warn("任务 {} 因崩溃处于执行中状态,已重置为待执行", task.getId());
}
// 2.2 重新提交任务执行(避免瞬间提交过多压垮系统,加微小延迟)
asyncTaskExecutorService.executeTask(task.getId());
TimeUnit.MILLISECONDS.sleep(10);
}
log.info("===== 未完成任务恢复提交完成 =====");
}
}
步骤 7:核心 Mapper 方法(MyBatis-Plus)
补充任务表的核心查询与更新方法(基于 MyBatis-Plus 扩展):
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
public interface AsyncTaskMapper extends BaseMapper<AsyncTask> {
/** 根据幂等键查询任务(幂等校验) */
AsyncTask selectByTaskIdempotentKey(@Param("idempotentKey") String idempotentKey);
/** 乐观锁更新状态为执行中(防并发) */
int updateStatusToExecuting(@Param("taskId") Long taskId, @Param("oldStatus") Integer oldStatus);
/** 更新任务为成功状态 */
int updateTaskSuccess(@Param("taskId") Long taskId, @Param("endTime") LocalDateTime endTime);
/** 更新任务为失败状态 */
int updateTaskFail(@Param("taskId") Long taskId,
@Param("newStatus") Integer newStatus,
@Param("execTimes") Integer execTimes,
@Param("errorMsg") String errorMsg,
@Param("endTime") LocalDateTime endTime);
/** 查询未完成任务(待执行+执行中) */
List<AsyncTask> selectUnFinishedTasks(@Param("statusList") List<Integer> statusList);
/** 将执行中任务重置为待执行(崩溃恢复用) */
int updateStatusToPending(@Param("taskId") Long taskId);
}
四、进阶优化(分布式+监控+死信处理)
以上基础方案可满足单体应用需求,若需适配分布式场景或提升可用性,需补充以下优化:
1. 分布式锁(多实例部署必备)
多实例部署时,需防止同个任务被多个实例重复执行,通过 Redisson 分布式锁优化:
java
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
// 在 AsyncTaskExecutorService 的 executeTask 方法中添加锁逻辑
RLock lock = redissonClient.getLock("async_task_lock_" + taskId);
try {
// 尝试加锁(5秒超时,30秒自动释放,防止死锁)
if (!lock.tryLock(5, 30, TimeUnit.SECONDS)) {
log.warn("任务 {} 已被其他实例锁定执行", taskId);
return;
}
// 原有任务执行逻辑...
} finally {
// 释放锁(确保当前线程持有锁时才释放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
2. 任务超时控制
部分任务可能因业务异常卡住(如调用外部接口超时),需添加超时扫描机制:
java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
@Component
@Slf4j
public class AsyncTaskTimeoutScanner {
@Resource
private AsyncTaskMapper asyncTaskMapper;
// 每5分钟扫描一次超时任务(执行中超过30分钟视为超时)
@Scheduled(fixedRate = 5 * 60 * 1000)
public void scanTimeoutTasks() {
List<AsyncTask> timeoutTasks = asyncTaskMapper.selectTimeoutExecutingTasks(30);
for (AsyncTask task : timeoutTasks) {
// 重置为待执行状态,等待重试
asyncTaskMapper.updateStatusToPending(task.getId());
log.warn("任务 {} 执行超时(超过30分钟),已重置为待执行", task.getId());
}
}
}
3. 死信任务处理
达到最大重试次数仍失败的任务(死信任务),需单独存储便于人工介入:
java
-- 死信任务表
CREATE TABLE `async_task_dead_letter` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`task_id` bigint(20) NOT NULL COMMENT '原任务ID',
`task_type` varchar(50) NOT NULL COMMENT '任务类型',
`task_data` json NOT NULL COMMENT '任务数据',
`dead_reason` varchar(1000) NOT NULL COMMENT '失败原因',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_task_id` (`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '异步任务死信表';
在任务执行失败且达到最大重试次数时,插入死信表:
java
// 任务执行失败逻辑中添加
if (newStatus == AsyncTask.Status.FAILED.getCode()) {
// 插入死信表
AsyncTaskDeadLetter deadLetter = new AsyncTaskDeadLetter();
deadLetter.setTaskId(task.getId());
deadLetter.setTaskType(task.getTaskType());
deadLetter.setTaskData(task.getTaskData());
deadLetter.setDeadReason(errorMsg);
asyncTaskDeadLetterMapper.insert(deadLetter);
log.error("任务 {} 已加入死信表,原因:{}", taskId, errorMsg);
}
4. 监控告警
通过 SpringBoot Actuator 暴露任务状态指标,结合 Prometheus+Grafana 可视化,失败任务数超过阈值时触发钉钉/邮件告警:
java
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
// 自定义监控端点,暴露任务状态统计
@Component
@Endpoint(id = "asyncTaskStatus")
public class AsyncTaskStatusEndpoint {
@Resource
private AsyncTaskMapper asyncTaskMapper;
@ReadOperation
public Map<String, Object> getTaskStatus() {
Map<String, Object> statusMap = new HashMap<>();
statusMap.put("pendingCount", asyncTaskMapper.countByStatus(AsyncTask.Status.PENDING.getCode()));
statusMap.put("executingCount", asyncTaskMapper.countByStatus(AsyncTask.Status.EXECUTING.getCode()));
statusMap.put("successCount", asyncTaskMapper.countByStatus(AsyncTask.Status.SUCCESS.getCode()));
statusMap.put("failedCount", asyncTaskMapper.countByStatus(AsyncTask.Status.FAILED.getCode()));
return statusMap;
}
}
五、使用示例(业务层调用)
最后展示业务层如何提交任务(以病历处理为例):
java
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class PatientDataService {
@Resource
private AsyncTaskSubmitService asyncTaskSubmitService;
/**
* 提交病历处理任务
*/
public void processPatientData(PatientDataDTO dto) {
// 1. 构建幂等键(任务类型+患者ID+病历编号,确保唯一)
String idempotentKey = "PATIENT_DATA_" + dto.getPatientId() + "_" + dto.getMedicalRecordNo();
// 2. 提交任务(类型:PATIENT_DATA,最大重试3次)
asyncTaskSubmitService.submitTask("PATIENT_DATA", dto, idempotentKey, 3);
}
}
六、核心总结
本文方案的核心价值在于「把不稳定的内存任务,变成可追溯、可恢复的持久化任务」,关键要点:
-
「先落地,后执行」是任务不丢失的核心;
-
状态追踪+重启扫描是崩溃恢复的基础;
-
策略模式(任务处理器)实现不同任务的差异化解耦;
-
幂等性+分布式锁是分布式场景的必备保障。
该方案已在医疗系统中落地验证,适配批量处理、报告生成等多种异步场景,稳定性拉满。如果你的业务也有异步任务可靠性需求,可直接基于本文代码改造使用~