臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、怎么实现

项目的做法是:开启异步支持,配置线程池,再给后台方法加上 @Async。 调用时,Spring 把这次方法调用交给指定线程池安排执行。

1.1 开启支持、配置线程池

配置在 AsyncConfig.java,主要看这几处:

@EnableAsync  
@Configuration  
public class AsyncConfig implements AsyncConfigurer {  
  
    /**  
     * 创建线程池  
     * @return  
     */  
    @Override  
    @Bean("taskExecutor")  
    public Executor getAsyncExecutor() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        // 核心线程数 - 根据CPU核心数和任务类型调整  
        executor.setCorePoolSize(5);  
        // 最大线程数  
        executor.setMaxPoolSize(10);  
        // 队列容量  
        executor.setQueueCapacity(100);  
        // 线程前缀  
        executor.setThreadNamePrefix("Async-Executor-");  
        // 拒绝策略  
        executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());  
        // 等待所有任务完成再关闭  
        executor.setWaitForTasksToCompleteOnShutdown(true);  
        // 关闭前等待时间  
        executor.setAwaitTerminationSeconds(30);  
        // 使用自定义的TaskDecorator来传递上下文  
        executor.setTaskDecorator(new ContextCopyingDecorator());  
        executor.initialize();  
        return executor;  
    }  
}

getAsyncExecutor() 创建了 ThreadPoolTaskExecutor,名字是 taskExecutor,核心线程数为 5、最大线程数为 10、队列容量为 100,线程名前缀为 Async-Executor-。

1.2 标记后台方法

比如码表导出,注解写在 BasicCodesExportExe.execute 上:

// 原方法结构节选,业务实现省略。
// taskExecutor 是配置类注册的 Bean 名,表示把任务交给这个执行器。
@Async("taskExecutor")
public void execute(BasicCodesExportCmd basicCodesExportCmd) {
    // 查询码表码值、整理数据、上传文件、更新操作状态。
}

1.3 通过注入对象调用

CodeFacadeImpl 注入 BasicCodesExportExe,再调用它:

// 原类中的字段:对象交给 Spring 管理,不是自己 new 出来的。
@Resource
private BasicCodesExportExe basicCodesExportExe;
// exportCodes 方法内的调用节选,参数来自本次导出请求。
basicCodesExportExe.execute(basicCodesExportCmd);
// 正常提交后,当前请求继续设置返回值,不必等文件生成完成。
resultModel.setData(true);
resultModel.setStatus(Status.S.getValue());

看起来还是普通方法调用,但 Spring 会先经过一个代理,把任务提交给 taskExecutor,由它安排执行。默认代理模式下,同一个对象内部用 this.execute(...) 调用自己,通常不会触发异步。

异步决定调用方是否需要等业务做完;线程池负责安排线程、限制数量和排队。 在项目中,它们通过 @Async("taskExecutor") 连在一起。

二、哪些地方用了

当前代码里,以下方法都标记了 @Async("taskExecutor"):

位置用途
BasicCodesExportExe.execute码表码值导出
ExportAreasDataExe.execute地区数据导出
ImportAreasDataExe.execute地区数据导入
SyncCodesCacheByTypesExe.execute按类型刷新缓存
SyncAssociationCodeCacheExe.execute刷新关联关系缓存
AreaCodeGatewayImpl.importAreaPreviewData解析地区导入文件、处理预览数据
BasicAreaPreviewGatewayImpl.deleteByFileId按文件清理地区预览数据、更新操作状态
CodeAssociationGatewayImpl.exportAssociationData关联关系导出
CodeAssociationGatewayImpl.createImportPreviewData生成关联关系导入预览
CodeAssociationGatewayImpl.importAssociationData关联关系导入
CodeAssociationGatewayImpl.confirmImportRelationshipData确认导入关联关系

它们指定的是同一个业务线程池,任务多时会争用这组线程。上游的 queryBatch 查询没有因此自动变成异步调用。

三、导出流程

以码表码值导出为例,入口是 CodeFacadeImpl.exportCodes,真正干活的是 BasicCodesExportExe.execute。

1. 进入后台方法,创建操作记录

@Async("taskExecutor")
public void execute(BasicCodesExportCmd basicCodesExportCmd) {

这次导出由 taskExecutor 安排执行。进入方法后,先创建操作记录(这里会记录谁发起的、导出哪些类型、当前状态是什么。):

OperationHistoryDO operationHistoryDO = new OperationHistoryDO()
    .setOperationId(IdUtil.getSnowflake(1, 1).nextId())
    .setBasicCodeStatus(StatusEnum.ANALYZING.getCode())
    .setErrorMsg(StatusEnum.ANALYZING.getDesc())
    .setCreator(accountId())
    .setOperationType(Short.valueOf(OperationType.CODE_EXPORT.getCode()))
    .setRemark(JSON.toJSONString(basicCodesExportCmd.getTypes()));
 
// 写入一条“正在处理”的导出记录。
basicOperationMapper.insert(operationHistoryDO);

2. 查询码表信息和全部码值 (这里没有分页,也没有每次只取多少条的限制。符合条件的数据越多,这个列表就越大。)

// 查询用户选中的码表类型,后面要用其中的归属域。
List<BasicType> basicTypes = basicTypeGateway.queryBy(
    null, basicCodesExportCmd.getTypes(), null, null);
 
// 查询这些类型下的全部有效码值。
List<BasicCodeDO> basicCodeDOS = basicCodeMapper.selectList(
    Wrappers.lambdaQuery(BasicCodeDO.class)
        .in(BasicCodeDO::getTypeCode, basicCodesExportCmd.getTypes())
        .eq(BasicCodeDO::getIsValid, VALID));
 

3. 转换成 Excel 需要的对象(这一步会创建新的列表和导出对象。所以不是从数据库读一条就写一条,而是先准备好整份导出数据。)

List<CodeModel> codeModels =
    BasicCodeConvertor.INSTANCE.toCodeModelS(basicCodeDOS);
    
    
    
//然后补上归属域
codeModels.stream().forEach(codeModel -> {
    basicTypes.stream().forEach(basicType -> {
        // 根据类型找到对应码表,将归属域填进导出对象。
        if (StringUtils.equals(basicType.getTypeCode(), codeModel.getType())) {
            codeModel.setTypeFrom(basicType.getTypeFrom());
        }
    });
});

4. 将整份列表交给文件服务

UploadFile callBack = externalFileServiceGateway.uploadList(
    codeModels,
    basicCodesExportCmd.getServiceCode(),
    null,
    fileName,
    CodeModel.class);

uploadList 内部先调用 createWriteFileItem 生成 Excel,核心代码是:

// 文件内容超过 16 字节后转存到临时文件,避免完整文件一直保留在堆中。
FileItemFactory factory = new DiskFileItemFactory(16, null);
FileItem item = factory.createItem(fileName, "text/csv", true, fileName);
 
// 根据 CodeModel 的列定义,把整个列表写入 Excel。
EasyExcel.write(item.getOutputStream(), classes)
    .sheet("Sheet1")
    .doWrite(list);

虽然这里创建文件项时写的是 text/csv,实际内容是通过 EasyExcel 生成的 Excel。

临时文件可以减少文件内容的内存占用,但前面准备的全量 Java 对象列表已经存在了。

5. 上传成功,更新操作记录

operationHistoryDO
    .setFileId(callBack.getFileId())
    .setFileName(fileName)
    .setErrorMsg(StatusEnum.SUCCESS.getDesc())
    .setBasicCodeStatus(StatusEnum.SUCCESS.getCode());
 
// 根据本次操作 ID 更新对应记录。
basicOperationMapper.update(
    operationHistoryDO,
    Wrappers.<OperationHistoryDO>lambdaUpdate()
        .eq(OperationHistoryDO::getOperationId,
            operationHistoryDO.getOperationId()));

把当前代码连起来就是:

数据库里的全部符合条件的码值
    ↓ 一次查询
List<BasicCodeDO>
    ↓ 全量转换
List<CodeModel>
    ↓ 补充归属域
EasyExcel 写入临时文件
    ↓
调用文件中心上传
    ↓
保存文件信息、更新导出状态