一、CSV文件与Hutool:为什么选择这种组合?
在日常开发中,CSV(Comma-Separated Values,逗号分隔值)文件是一种轻量级、易读易写的数据交换格式,广泛应用于数据导出、报表生成、数据迁移等场景。相比Excel,CSV文件体积更小、解析更快,且无需依赖复杂的Office组件;相比JSON/XML,CSV更适合存储结构化表格数据,便于非技术人员用Excel直接打开编辑。
然而,原生Java处理CSV文件存在诸多痛点:需要手动处理分隔符、引号转义、编码转换,大文件读写容易引发内存溢出,分批处理时难以保证数据顺序。这时,Hutool工具包的CsvUtil模块应运而生——它封装了CSV文件的读写逻辑,提供简洁的API、灵活的配置项和高效的大文件处理能力,让开发者无需重复造轮子,专注于业务逻辑。
1.1 Hutool CsvUtil的核心优势
- API极简:一行代码实现CSV读取/写入,无需关注底层IO细节;
- 配置灵活:支持自定义分隔符(逗号、制表符、竖线等)、编码(UTF-8、GBK等)、表头处理、引号策略;
- 性能优秀:支持逐行读写,避免一次性加载数据到内存,轻松应对GB级大文件;
- 类型适配:自动实现Java Bean与CSV字段的映射,支持日期、枚举、数字等类型的自动转换;
- 资源安全:内置try-with-resources语法支持,自动关闭流资源,避免内存泄漏。
1.2 典型应用场景
- 后台报表导出:用户在系统中点击“导出报表”,后端生成CSV文件并返回下载链接(如订单报表、用户清单);
- 数据迁移导入:从第三方系统接收CSV格式的原始数据,解析后入库(如商品信息、会员数据);
- 日志归档分析:将系统运行日志按CSV格式分批写入文件,便于后续用Excel或Python进行分析;
- 大数据量同步:从数据库查询百万级数据,分批写入CSV文件,传输到数据仓库进行离线计算。
二、前置准备:环境搭建与依赖引入
在使用CsvUtil前,需先完成Hutool的依赖引入。Hutool支持Maven、Gradle等构建工具,也可直接下载JAR包导入项目。
2.1 Maven依赖配置
在pom.xml中添加Hutool的依赖(推荐使用最新稳定版,可从Maven中央仓库查询最新版本):
<!-- Hutool工具包(包含CsvUtil模块) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.28</version> <!-- 替换为最新版本 -->
</dependency>
<!-- 可选:若需处理Excel文件(如CSV与Excel互转),需额外引入POI -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version> <!-- 与Hutool兼容的版本 -->
</dependency>
2.2 核心类与API概览
CsvUtil的核心功能集中在cn.hutool.core.io.file.CsvUtil类中,关键API如下:
| 方法名 | 功能描述 | 适用场景 |
|---|---|---|
read(File file) | 读取CSV文件到CsvData对象 | 小文件读取 |
readByLine(File file) | 逐行读取CSV文件,返回Iterable<String[]> | 大文件读取 |
getReader(...) | 获取CsvReader对象,支持自定义配置 | 复杂配置的读取场景 |
write(...) | 写入数据(列表/Bean)到CSV文件 | 小文件写入 |
getWriter(...) | 获取CsvWriter对象,支持逐行写入 | 大文件写入、分批写入 |
toBeanList(...) | 将CSV数据转换为Java Bean列表 | 数据导入(CSV→Bean→数据库) |
toCsvData(...) | 将Java Bean列表转换为CsvData对象 | 数据导出(Bean→CSV) |
三、基础操作:CSV文件的读写与配置
本节从最简单的场景入手,讲解如何用CsvUtil实现CSV文件的基础读写,包括普通文件读取、Bean映射、自定义配置等。
3.1 场景1:读取CSV文件到内存(小文件适用)
适用于文件体积较小(如10万行以内)、可一次性加载到内存的场景,例如读取配置文件、小型报表。
3.1.1 读取CSV到字符串数组列表
假设存在user.csv文件,内容如下(UTF-8编码,带表头):
id,name,age,register_time
1,张三,25,2024-01-15 09:30:00
2,李四,30,2024-02-20 14:15:00
3,王五,28,2024-03-10 11:20:00
读取代码:
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.io.file.CsvData;
import java.io.File;
import java.util.List;
public class CsvBasicReadDemo {
public static void main(String[] args) {
// 1. 定义CSV文件路径
File csvFile = new File("D:/data/user.csv");
// 2. 读取CSV文件到CsvData对象(包含表头和数据)
CsvData csvData = CsvUtil.read(csvFile);
// 3. 获取表头(第一行数据)
List<String> headers = csvData.getHeader();
System.out.println("CSV表头:" + headers); // 输出:[id, name, age, register_time]
// 4. 获取数据行(除表头外的所有行)
List<List<String>> rows = csvData.getRows();
System.out.println("CSV数据行数:" + rows.size()); // 输出:3
// 5. 遍历数据行
for (int i = 0; i < rows.size(); i++) {
List<String> row = rows.get(i);
System.out.printf("第%d行:id=%s, name=%s, age=%s, register_time=%s%n",
i+1, row.get(0), row.get(1), row.get(2), row.get(3));
}
}
}
3.1.2 读取CSV到Java Bean列表
实际开发中,我们更习惯将CSV数据映射为Java Bean(如User类),便于后续业务处理(如入库)。
第一步:定义User Bean类:
import lombok.Data;
import java.util.Date;
@Data // 使用Lombok简化getter/setter,也可手动编写
public class User {
private Long id; // 对应CSV的id字段
private String name; // 对应CSV的name字段
private Integer age; // 对应CSV的age字段
private Date registerTime;// 对应CSV的register_time字段(下划线转驼峰)
}
第二步:读取CSV并转换为User列表:
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.File;
import java.util.List;
public class CsvReadToBeanDemo {
public static void main(String[] args) {
// 1. 读取CSV文件并直接转换为User列表
// 注意:CSV字段名(下划线)会自动映射到Bean的驼峰字段(如register_time→registerTime)
List<User> userList = CsvUtil.getReader()
.setCharset("UTF-8") // 设置编码(默认UTF-8,可省略)
.setHeader(true) // 表示CSV文件第一行为表头
.read(new File("D:/data/user.csv"), User.class);
// 2. 遍历User列表
for (User user : userList) {
System.out.printf("用户信息:id=%d, name=%s, age=%d, 注册时间=%s%n",
user.getId(), user.getName(), user.getAge(), user.getRegisterTime());
}
}
}
关键说明:
setHeader(true):必须设置为true,否则CsvUtil会将第一行数据当作普通数据行处理;- 字段映射规则:CSV的下划线命名(如
register_time)会自动匹配Bean的驼峰命名(registerTime),无需额外配置; - 类型转换:
CsvUtil会自动将CSV中的字符串转换为Bean的字段类型(如字符串"25"→Integer,字符串"2024-01-15 09:30:00"→Date),支持常见类型(String、Integer、Long、Date、Boolean等)。
3.2 场景2:写入数据到CSV文件(小文件适用)
适用于数据量较小、可一次性写入的场景,例如导出单个用户的订单明细、小型统计报表。
3.2.1 写入字符串列表到CSV
直接将内存中的字符串列表写入CSV文件,包含表头:
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.File;
import java.util.ArrayList;
import java.util.List;
public class CsvWriteStringDemo {
public static void main(String[] args) {
// 1. 定义CSV文件输出路径
File outputFile = new File("D:/data/order.csv");
// 2. 准备数据(第一行为表头,后续为数据行)
List<List<String>> data = new ArrayList<>();
// 表头
data.add(List.of("order_id", "user_id", "amount", "pay_time"));
// 数据行
data.add(List.of("1001", "1", "99.9", "2024-04-01 10:00:00"));
data.add(List.of("1002", "2", "199.5", "2024-04-01 11:30:00"));
data.add(List.of("1003", "1", "49.9", "2024-04-02 09:15:00"));
// 3. 写入CSV文件(默认UTF-8编码,逗号分隔)
CsvUtil.write(outputFile, data);
System.out.println("CSV文件写入完成!路径:" + outputFile.getAbsolutePath());
}
}
3.2.2 写入Java Bean列表到CSV
将内存中的User列表直接写入CSV,自动生成表头(基于Bean的字段名):
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.File;
import cn.hutool.core.date.DateUtil;
import java.util.ArrayList;
import java.util.List;
public class CsvWriteBeanDemo {
public static void main(String[] args) {
// 1. 准备User数据列表
List<User> userList = new ArrayList<>();
userList.add(createUser(4L, "赵六", 22, "2024-04-05 08:45:00"));
userList.add(createUser(5L, "孙七", 35, "2024-04-06 15:20:00"));
userList.add(createUser(6L, "周八", 29, "2024-04-07 16:50:00"));
// 2. 定义输出文件
File outputFile = new File("D:/data/user_export.csv");
// 3. 写入Bean列表到CSV(自动生成表头,字段名对应Bean的属性名)
CsvUtil.getWriter(outputFile, "UTF-8") // 设置输出文件和编码
.setHeader("用户ID", "姓名", "年龄", "注册时间") // 自定义表头(可选,默认用Bean字段名)
.writeBeans(userList); // 写入Bean列表
System.out.println("Bean列表写入CSV完成!");
}
// 辅助方法:创建User对象
private static User createUser(Long id, String name, Integer age, String registerTimeStr) {
User user = new User();
user.setId(id);
user.setName(name);
user.setAge(age);
user.setRegisterTime(DateUtil.parse(registerTimeStr)); // 日期字符串转Date
return user;
}
}
关键说明:
setHeader(...):可选配置,若不设置,默认表头为Bean的字段名(如id、name、age、registerTime);设置后,表头按自定义名称显示(如用户ID、姓名);- 日期格式化:
CsvUtil默认将Date类型转换为yyyy-MM-dd HH:mm:ss格式,若需自定义格式,需通过CsvConfig配置(见3.3节); - 空值处理:若Bean字段为
null,写入CSV时会显示为空字符串(可通过配置自定义空值占位符)。
3.3 场景3:自定义CSV配置(分隔符、编码、格式)
实际场景中,CSV文件可能使用非逗号分隔符(如制表符\t、竖线|)、非UTF-8编码(如GBK),或需要自定义日期格式、引号策略。此时需通过CsvConfig配置CsvReader或CsvWriter。
3.3.1 读取GBK编码、竖线分隔的CSV文件
假设存在product.csv文件,编码为GBK,分隔符为竖线|,内容如下:
product_id|product_name|price|stock
P001|华为手机|3999.0|100
P002|小米平板|2499.0|50
P003|苹果耳机|1299.0|200
读取代码(自定义配置):
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.CsvConfig;
import cn.hutool.core.io.file.CsvReader;
import cn.hutool.core.io.file.File;
import java.util.List;
public class CsvCustomReadDemo {
public static void main(String[] args) {
// 1. 创建CsvConfig对象,配置读取规则
CsvConfig config = new CsvConfig();
config.setCharset("GBK"); // 设置编码为GBK
config.setFieldSeparator('|'); // 设置分隔符为竖线
config.setQuoteChar('"'); // 设置字段引号(默认双引号,可选)
config.setSkipEmptyRows(true); // 跳过空行(默认false)
// 2. 创建CsvReader,应用配置
CsvReader reader = CsvUtil.getReader(config);
reader.setHeader(true); // 第一行为表头
// 3. 读取CSV文件
File csvFile = new File("D:/data/product.csv");
List<List<String>> rows = reader.readList(csvFile);
// 4. 遍历数据
System.out.println("商品列表:");
for (List<String> row : rows) {
System.out.printf("商品ID:%s,名称:%s,价格:%s,库存:%s%n",
row.get(0), row.get(1), row.get(2), row.get(3));
}
}
}
3.3.2 写入CSV时自定义日期格式、空值占位符
将User列表写入CSV时,需将日期格式改为yyyy/MM/dd,空值显示为-:
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.CsvConfig;
import cn.hutool.core.io.file.CsvWriter;
import cn.hutool.core.io.file.File;
import cn.hutool.core.date.DatePattern;
import java.util.ArrayList;
import java.util.List;
public class CsvCustomWriteDemo {
public static void main(String[] args) {
// 1. 准备数据(包含空值)
List<User> userList = new ArrayList<>();
userList.add(createUser(7L, "吴九", null, "2024-04-08 09:00:00")); // age为null
userList.add(createUser(8L, null, 32, "2024-04-09 10:30:00")); // name为null
userList.add(createUser(9L, "郑十", 26, null)); // registerTime为null
// 2. 创建CsvConfig,配置写入规则
CsvConfig config = new CsvConfig();
config.setDatePattern(DatePattern.NORM_DATE_PATTERN); // 日期格式:yyyy/MM/dd
config.setNullStr("-"); // 空值占位符:-
config.setFieldSeparator('\t'); // 分隔符:制表符(适合Excel打开)
// 3. 创建CsvWriter,应用配置
File outputFile = new File("D:/data/user_custom.csv");
try (CsvWriter writer = CsvUtil.getWriter(outputFile, "UTF-8", config)) {
// 自定义表头
writer.writeHeader("用户ID", "姓名", "年龄", "注册时间");
// 写入Bean列表
writer.writeBeans(userList);
}
System.out.println("自定义配置的CSV写入完成!");
}
// 辅助方法:创建User对象(支持null值)
private static User createUser(Long id, String name, Integer age, String registerTimeStr) {
User user = new User();
user.setId(id);
user.setName(name);
user.setAge(age);
if (registerTimeStr != null) {
user.setRegisterTime(DateUtil.parse(registerTimeStr));
}
return user;
}
}
生成的CSV文件内容(制表符分隔,空值显示为-,日期格式为yyyy/MM/dd):
用户ID 姓名 年龄 注册时间
7 吴九 - 2024/04/08
8 - 32 2024/04/09
9 郑十 26 -
四、重点突破:大文件CSV处理(分批读写与内存优化)
当CSV文件体积超过100MB(或数据量超过100万行)时,一次性读取/写入会导致内存溢出(OOM) ——因为JVM堆内存无法容纳全部数据。此时必须采用逐行读写或分批处理策略,而CsvUtil的readByLine和CsvWriter逐行写入能力正是为解决此问题设计的。
4.1 大文件处理的核心原则
- 避免一次性加载:逐行读取/写入,每处理一行释放一行内存;
- 控制分批大小:从数据库查询数据时,按批次(如每批1万行)获取,避免单批数据过大;
- 优化IO性能:使用缓冲流(Hutool内置),减少IO次数;
- 保证数据顺序:分批查询时按唯一键(如主键ID)排序,确保写入顺序与业务逻辑一致;
- 资源及时释放:使用try-with-resources语法,确保流在处理完成后自动关闭。
4.2 场景4:读取GB级大CSV文件(逐行处理)
适用于读取超大CSV文件(如日志文件、数据备份文件),需逐行解析并处理(如过滤无效数据、提取关键字段入库)。
示例:读取1GB的日志CSV文件,过滤出ERROR级别的日志并写入新文件
假设system_log.csv文件包含1000万行日志,格式如下(UTF-8编码,逗号分隔):
log_time,level,content,ip
2024-04-10 00:01:23,INFO,系统启动成功,192.168.1.1
2024-04-10 00:02:56,ERROR,数据库连接超时,192.168.1.2
2024-04-10 00:03:12,WARN,内存使用率过高,192.168.1.1
...
处理代码(逐行读取,过滤ERROR日志):
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.CsvReader;
import cn.hutool.core.io.file.CsvWriter;
import cn.hutool.core.io.file.File;
import java.io.IOException;
public class CsvReadLargeFileDemo {
public static void main(String[] args) {
// 1. 定义输入大文件和输出过滤文件
File inputFile = new File("D:/data/system_log.csv");
File outputFile = new File("D:/data/error_log.csv");
// 2. 创建CsvReader(逐行读取)和CsvWriter(逐行写入)
try (CsvReader reader = CsvUtil.getReader();
CsvWriter writer = CsvUtil.getWriter(outputFile, "UTF-8")) {
// 3. 读取表头并写入输出文件
String[] header = reader.readHeader(inputFile);
writer.writeHeader(header);
System.out.println("表头:" + String.join(",", header));
// 4. 逐行读取数据(核心:每次只加载一行到内存)
String[] row;
long totalRows = 0; // 总读取行数
long errorRows = 0; // ERROR日志行数
long startTime = System.currentTimeMillis();
// readLine():每次读取一行,返回null表示文件结束
while ((row = reader.readLine(inputFile)) != null) {
totalRows++;
// 5. 过滤条件:日志级别为ERROR(第二列,索引1)
if ("ERROR".equals(row[1])) {
errorRows++;
writer.writeRow(row); // 写入ERROR日志到输出文件
// 每处理1000行打印一次进度
if (errorRows % 1000 == 0) {
System.out.printf("已处理ERROR日志:%d行,总读取行数:%d行%n", errorRows, totalRows);
}
}
}
// 6. 打印处理结果
long costTime = (System.currentTimeMillis() - startTime) / 1000;
System.out.printf("处理完成!总读取行数:%d,ERROR日志行数:%d,耗时:%d秒%n",
totalRows, errorRows, costTime);
} catch (IOException e) {
e.printStackTrace();
System.err.println("大文件处理失败:" + e.getMessage());
}
}
}
关键优化点:
reader.readLine(inputFile):每次只读取一行数据到内存,处理完成后立即释放,避免OOM;- try-with-resources:
CsvReader和CsvWriter在try块结束后自动关闭,无需手动调用close(); - 进度监控:每处理1000行打印一次进度,便于观察处理状态;
- 缓冲IO:
CsvUtil内置BufferedReader和BufferedWriter,默认缓冲区大小为8192字节,减少IO次数。
4.3 场景5:导出百万级数据到CSV(分批写入与顺序保证)
实际开发中,大文件CSV导出的数据源通常来自数据库(如MySQL、PostgreSQL)。此时需分批查询数据库,并逐批写入CSV,同时保证数据顺序(如按订单创建时间排序)。
示例:从MySQL数据库分批查询100万条订单数据,写入CSV文件
需求:导出2024年1月1日至2024年4月30日的所有订单,按order_id升序排列,每批查询1万条,避免内存溢出。
第一步:准备数据库连接与Mapper(MyBatis示例)
- 定义
OrderBean类:
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class Order {
private Long orderId; // 订单ID(主键)
private Long userId; // 用户ID
private BigDecimal amount; // 订单金额
private String status; // 订单状态(PAYED:已支付,UNPAID:未支付,REFUNDED:已退款)
private Date createTime; // 创建时间
private Date payTime; // 支付时间
}
- 定义
OrderMapper接口(MyBatis):
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
@Mapper
public interface OrderMapper {
/**
* 分批查询订单数据
* @param startId 起始订单ID(用于分页,避免offset过大导致性能问题)
* @param endTime 结束时间(2024-04-30)
* @param pageSize 每批查询数量
* @return 订单列表
*/
List<Order> selectOrderByPage(
@Param("startId") Long startId,
@Param("endTime") Date endTime,
@Param("pageSize") Integer pageSize);
/**
* 查询符合条件的订单总数(用于进度计算)
* @param endTime 结束时间
* @return 订单总数
*/
Long selectOrderTotalCount(@Param("endTime") Date endTime);
}
- 编写MyBatis XML映射文件(
OrderMapper.xml):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.OrderMapper">
<!-- 分批查询订单:用ID分页(比offset分页更高效) -->
<select id="selectOrderByPage" resultType="com.example.entity.Order">
SELECT
order_id AS orderId,
user_id AS userId,
amount,
status,
create_time AS createTime,
pay_time AS payTime
FROM
`order`
WHERE
create_time >= '2024-01-01 00:00:00'
AND create_time <= #{endTime}
AND order_id > #{startId} <!-- 用ID过滤,避免offset导致的全表扫描 -->
ORDER BY
order_id ASC <!-- 按主键升序,保证分批写入顺序一致 -->
LIMIT #{pageSize}
</select>
<!-- 查询订单总数 -->
<select id="selectOrderTotalCount" resultType="java.lang.Long">
SELECT COUNT(*)
FROM `order`
WHERE
create_time >= '2024-01-01 00:00:00'
AND create_time <= #{endTime}
</select>
</mapper>
第二步:分批导出CSV的核心代码
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.CsvWriter;
import cn.hutool.core.io.file.File;
import cn.hutool.core.date.DateUtil;
import com.example.entity.Order;
import com.example.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Date;
import java.util.List;
@Service
public class OrderCsvExportService {
// 每批查询数量(1万行/批,可根据JVM内存调整,建议5000-20000)
private static final Integer BATCH_SIZE = 10000;
// 导出时间范围:2024-01-01 至 2024-04-30
private static final Date START_TIME = DateUtil.parse("2024-01-01 00:00:00");
private static final Date END_TIME = DateUtil.parse("2024-04-30 23:59:59");
@Autowired
private OrderMapper orderMapper;
/**
* 导出2024年1-4月的订单数据到CSV文件
* @return 导出文件路径
*/
public String exportOrderToCsv() {
// 1. 定义输出文件路径(避免覆盖,添加时间戳)
String fileName = "order_export_20240101_20240430_" + System.currentTimeMillis() + ".csv";
File outputFile = new File("D:/data/export/" + fileName);
// 2. 初始化变量
long totalCount = orderMapper.selectOrderTotalCount(END_TIME); // 总订单数
long exportedCount = 0; // 已导出订单数
Long lastOrderId = 0L; // 上一批最后一条订单的ID(用于分页)
long startTime = System.currentTimeMillis();
System.out.printf("开始导出订单数据:总数量=%d,每批=%d行,输出路径=%s%n",
totalCount, BATCH_SIZE, outputFile.getAbsolutePath());
// 3. 创建CsvWriter(try-with-resources自动关闭)
try (CsvWriter writer = CsvUtil.getWriter(outputFile, "UTF-8")) {
// 3.1 写入表头(自定义中文表头)
writer.writeHeader(
"订单ID", "用户ID", "订单金额", "订单状态",
"创建时间", "支付时间"
);
// 3.2 循环分批查询并写入
while (exportedCount < totalCount) {
// 3.2.1 分批查询数据库(按ID分页,避免offset性能问题)
List<Order> orderList = orderMapper.selectOrderByPage(
lastOrderId, END_TIME, BATCH_SIZE
);
if (orderList.isEmpty()) {
break; // 无数据时退出循环(防止死循环)
}
// 3.2.2 逐行写入CSV(也可批量写入:writer.writeBeans(orderList))
for (Order order : orderList) {
writer.writeBean(order);
// 更新上一批最后一条订单ID(保证下一批查询的起始位置正确)
lastOrderId = order.getOrderId();
}
// 3.2.3 更新导出进度
exportedCount += orderList.size();
// 每导出10批打印一次进度(10万行)
if (exportedCount % (BATCH_SIZE * 10) == 0 || exportedCount == totalCount) {
long costTime = (System.currentTimeMillis() - startTime) / 1000;
double progress = (exportedCount * 100.0) / totalCount;
System.out.printf("导出进度:%.2f%%,已导出:%d/%d行,耗时:%d秒%n",
progress, exportedCount, totalCount, costTime);
}
// 3.2.4 可选:手动触发GC(避免内存碎片累积,谨慎使用)
// System.gc();
}
// 4. 导出完成
long totalCostTime = (System.currentTimeMillis() - startTime) / 1000;
System.out.printf("导出完成!总耗时:%d秒,文件路径:%s%n",
totalCostTime, outputFile.getAbsolutePath());
return outputFile.getAbsolutePath();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("订单CSV导出失败:" + e.getMessage());
}
}
}
4.3.1 关键技术点解析(避免OOM与保证顺序)
-
ID分页替代offset分页:
- 传统
LIMIT offset, size分页在offset较大时(如100万)会导致全表扫描,性能极差; - 采用
order_id > lastOrderId LIMIT size的方式,利用主键索引快速定位,性能稳定; - 需确保
order_id是自增主键或有序唯一键,否则会导致数据遗漏或重复。
- 传统
-
分批写入的顺序保证:
- 数据库查询时按
order_id ASC排序,确保每批数据的顺序正确; - 每批写入后记录
lastOrderId,下一批从lastOrderId之后查询,避免数据重复或遗漏; CsvWriter.writeBean(order)逐行写入,保证内存中只缓存一行Bean数据。
- 数据库查询时按
-
内存优化策略:
- 控制
BATCH_SIZE(建议5000-20000):过小会增加数据库查询次数,过大则可能导致内存溢出; - 避免在循环中创建大量对象:复用
lastOrderId等变量,减少对象创建; - 可选手动触发GC:在每批处理完成后调用
System.gc()(谨慎使用,避免影响性能)。
- 控制
-
异常处理与容错:
- 捕获
IOException并抛出业务异常,便于上层处理; - 检查
orderList.isEmpty(),防止因数据库无数据导致死循环; - 文件名添加时间戳,避免多用户同时导出时文件覆盖。
- 捕获
4.4 场景6:超大文件CSV的压缩导出(减少存储与传输成本)
对于GB级CSV文件,直接存储或传输会占用大量磁盘和带宽。可在写入CSV时同时进行压缩(如GZIP格式),减少文件体积(压缩率通常可达10:1)。
Hutool的CsvWriter支持写入压缩流,只需将File对象替换为GZIPOutputStream即可。
示例:导出百万级订单并压缩为GZIP格式
import cn.hutool.core.io.file.CsvWriter;
import cn.hutool.core.io.file.File;
import cn.hutool.core.util.ZipUtil;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;
public class CsvGzipExportDemo {
public static void main(String[] args) {
// 1. 定义压缩文件路径(后缀为.gz)
File gzipFile = new File("D:/data/order_export_2024.gz");
// 2. 创建GZIP输出流(包装FileOutputStream)
try (OutputStream out = new FileOutputStream(gzipFile);
GZIPOutputStream gzipOut = new GZIPOutputStream(out);
// 3. 创建CsvWriter,写入GZIP流
CsvWriter writer = CsvUtil.getWriter(gzipOut, "UTF-8")) {
// 4. 写入表头
writer.writeHeader("订单ID", "用户ID", "金额", "状态", "创建时间");
// 5. 分批查询并写入(逻辑同4.3节,此处省略数据库查询代码)
// ...(分批查询订单列表,逐行写入)
System.out.println("GZIP压缩CSV导出完成!原体积约1GB,压缩后约100MB");
} catch (IOException e) {
e.printStackTrace();
}
}
}
关键说明:
GZIPOutputStream:Java原生压缩流,Hutool的CsvWriter可直接写入;- 压缩率:文本类CSV文件的GZIP压缩率通常为8-15倍,1GB的CSV文件压缩后约70-120MB;
- 解压读取:读取压缩后的CSV文件时,需用
GZIPInputStream包装FileInputStream,再交给CsvReader。
五、实战进阶:CSV与其他数据格式的互转
在实际项目中,CSV常需与Excel、JSON、数据库等格式互转。Hutool结合POI等工具,可实现灵活的格式转换。
5.1 场景7:CSV文件转换为Excel文件(XLSX)
适用于需要将CSV数据用Excel打开并进行复杂编辑的场景(如添加公式、格式化单元格)。
需额外引入POI依赖(见2.1节),代码示例:
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.File;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import java.util.List;
public class CsvToExcelDemo {
public static void main(String[] args) {
// 1. 读取CSV文件
File csvFile = new File("D:/data/user.csv");
List<List<String>> csvData = CsvUtil.read(csvFile).getRows();
// 2. 创建ExcelWriter(XLSX格式,支持大数据量)
File excelFile = new File("D:/data/user.xlsx");
try (ExcelWriter writer = ExcelUtil.getWriter(excelFile)) {
// 3. 写入CSV数据到Excel
writer.write(csvData);
// 4. 可选:美化Excel(设置表头样式、列宽)
writer.setHeaderAlias("id", "用户ID")
.setHeaderAlias("name", "姓名")
.setColumnWidth(0, 10) // 第0列(id)宽度10
.setColumnWidth(1, 15) // 第1列(name)宽度15
.autoSizeColumn(2) // 第2列(age)自动调整宽度
.autoSizeColumn(3); // 第3列(register_time)自动调整宽度
System.out.println("CSV转换为Excel完成!路径:" + excelFile.getAbsolutePath());
}
}
}
5.2 场景8:Excel文件转换为CSV文件
适用于将Excel中的报表数据转换为CSV,便于导入其他系统(如数据库、大数据平台)。
代码示例:
import cn.hutool.core.io.file.CsvUtil;
import cn.hutool.core.io.file.File;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelReader;
import java.util.List;
public class ExcelToCsvDemo {
public static void main(String[] args) {
// 1. 读取Excel文件(第一行为表头)
File excelFile = new File("D:/data/user.xlsx");
try (ExcelReader reader = ExcelUtil.getReader(excelFile)) {
// 2. 读取所有数据(包含表头)
List<List<Object>> excelData = reader.read();
// 3. 转换为String列表(CSV要求字符串格式)
List<List<String>> csvData = excelData.stream()
.map(row -> row.stream()
.map(obj -> obj == null ? "" : obj.toString())
.toList())
.toList();
// 4. 写入CSV文件
File csvFile = new File("D:/data/user_from_excel.csv");
CsvUtil.write(csvFile, csvData);
System.out.println("Excel转换为CSV完成!路径:" + csvFile.getAbsolutePath());
}
}
}
六、常见问题与解决方案
在使用CsvUtil处理CSV文件时,可能会遇到编码乱码、字段分隔错误、内存溢出等问题。本节整理了高频问题及解决方案。
6.1 问题1:CSV文件打开后中文乱码
现象:用Excel打开CSV文件时,中文显示为乱码(如“张三”→“ÕÅÈý”)。
原因:CSV文件编码与Excel读取编码不一致(Hutool默认UTF-8,Excel默认GBK)。
解决方案:
- 方案1:写入CSV时指定编码为GBK:
// 写入时用GBK编码(Excel默认读取GBK) CsvUtil.getWriter(new File("D:/data/user.csv"), "GBK") .writeBeans(userList); - 方案2:用Excel手动选择编码打开:
- 打开Excel → 数据 → 自文本/CSV → 选择CSV文件 → 编码选择“UTF-8” → 完成。
6.2 问题2:读取CSV时字段分隔符错误
现象:CSV文件用竖线|分隔,但CsvUtil仍按逗号解析,导致字段错乱。
原因:未配置CsvConfig的fieldSeparator,默认使用逗号分隔。
解决方案:
// 创建配置,设置分隔符为竖线
CsvConfig config = new CsvConfig();
config.setFieldSeparator('|');
// 应用配置读取
CsvReader reader = CsvUtil.getReader(config);
List<List<String>> rows = reader.readList(new File("D:/data/product.csv"));
6.3 问题3:大文件导出时内存溢出(OOM)
现象:导出百万级数据时,JVM抛出java.lang.OutOfMemoryError: Java heap space。
原因:一次性将所有数据加载到内存,超过JVM堆内存限制。
解决方案:
- 采用分批查询+逐行写入(见4.3节),避免一次性加载数据;
- 调整JVM堆内存大小(如
-Xms2g -Xmx4g),增加可用内存; - 减少单批查询数量(如将
BATCH_SIZE从20000调整为5000); - 避免在循环中创建大量临时对象(如字符串拼接用
StringBuilder)。
6.4 问题4:CSV字段包含分隔符或引号导致解析错误
现象:CSV字段中包含逗号(如“上海,浦东”)或双引号(如“"华为"手机”),导致字段被拆分。
原因:未启用引号包裹机制,CsvUtil默认将逗号视为分隔符。
解决方案:
- 写入时自动用引号包裹含特殊字符的字段(Hutool默认启用):
CsvConfig config = new CsvConfig(); config.setQuoteChar('"'); // 字段包含分隔符时自动添加双引号(默认启用) CsvUtil.getWriter(outputFile, "UTF-8", config) .writeRow(List.of("1", "上海,浦东", "2024-05-01")); // 生成的CSV字段:1,"上海,浦东",2024-05-01 - 读取时配置引号处理:
CsvConfig config = new CsvConfig(); config.setQuoteChar('"'); // 识别引号包裹的字段 CsvReader reader = CsvUtil.getReader(config);
6.5 问题5:分批写入时数据顺序错乱
现象:导出的CSV文件中,数据顺序与数据库中的顺序不一致(如订单ID不连续)。
原因:
- 数据库查询未排序(未加
ORDER BY); - 分页方式错误(用
offset分页,而非ID分页); - 多线程写入时未同步(多线程写入会打乱顺序)。
解决方案: - 查询时必须按唯一键排序(如
ORDER BY order_id ASC); - 采用ID分页(
order_id > lastOrderId LIMIT size); - 大文件导出时使用单线程写入(多线程会导致顺序错乱,且IO性能提升有限)。
七、总结与最佳实践
7.1 Hutool CsvUtil的核心优势回顾
- 低学习成本:API简洁直观,无需掌握复杂的CSV解析原理;
- 高性能:逐行读写支持GB级大文件,避免OOM;
- 高灵活性:支持自定义分隔符、编码、日期格式、引号策略;
- 强兼容性:无缝对接Java Bean、数据库、Excel等,降低开发成本。
7.2 大文件处理最佳实践
- 优先使用逐行读写:小文件(<10万行)可一次性读写,大文件必须逐行处理;
- 数据库分批查询用ID分页:避免
offset分页的性能问题,保证数据顺序; - 控制批处理大小:
BATCH_SIZE建议设置为5000-20000,平衡IO次数和内存占用; - 使用压缩减少存储:大文件导出时用GZIP压缩,降低存储和传输成本;
- 资源安全管理:始终用try-with-resources包裹
CsvReader和CsvWriter,避免流泄漏。
7.3 业务场景适配建议
| 场景 | 推荐API | 注意事项 |
|---|---|---|
| 小文件读取(<10万行) | CsvUtil.read(file) | 直接加载到内存,便捷高效 |
| 大文件读取(>10万行) | CsvReader.readLine(file) | 逐行读取,避免OOM |
| 小文件写入 | CsvUtil.write(file, data) | 一次性写入,代码简洁 |
| 大文件写入 | CsvWriter.writeBean(bean) | 逐行写入,分批查询数据库 |
| Bean与CSV互转 | CsvReader.read(file, User.class)/CsvWriter.writeBeans(beans) | 确保字段映射正确,日期格式匹配 |
| 自定义格式CSV | CsvConfig+CsvReader/CsvWriter | 配置分隔符、编码、引号策略 |
通过本文的讲解,相信你已掌握Hutool CsvUtil的核心用法,能够轻松应对从基础CSV读写到百万级大文件处理的各类场景。
在实际开发中,建议结合业务需求选择合适的API和配置,兼顾开发效率和系统性能。
1万+

被折叠的 条评论
为什么被折叠?



