基于Hutool工具包的CSV文件全方位操作指南:从基础到百万级数据处理

一、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 典型应用场景

  1. 后台报表导出:用户在系统中点击“导出报表”,后端生成CSV文件并返回下载链接(如订单报表、用户清单);
  2. 数据迁移导入:从第三方系统接收CSV格式的原始数据,解析后入库(如商品信息、会员数据);
  3. 日志归档分析:将系统运行日志按CSV格式分批写入文件,便于后续用Excel或Python进行分析;
  4. 大数据量同步:从数据库查询百万级数据,分批写入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),支持常见类型(StringIntegerLongDateBoolean等)。

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的字段名(如idnameageregisterTime);设置后,表头按自定义名称显示(如用户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配置CsvReaderCsvWriter

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堆内存无法容纳全部数据。此时必须采用逐行读写分批处理策略,而CsvUtilreadByLineCsvWriter逐行写入能力正是为解决此问题设计的。

4.1 大文件处理的核心原则

  1. 避免一次性加载:逐行读取/写入,每处理一行释放一行内存;
  2. 控制分批大小:从数据库查询数据时,按批次(如每批1万行)获取,避免单批数据过大;
  3. 优化IO性能:使用缓冲流(Hutool内置),减少IO次数;
  4. 保证数据顺序:分批查询时按唯一键(如主键ID)排序,确保写入顺序与业务逻辑一致;
  5. 资源及时释放:使用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:CsvReaderCsvWriter在try块结束后自动关闭,无需手动调用close()
  • 进度监控:每处理1000行打印一次进度,便于观察处理状态;
  • 缓冲IO:CsvUtil内置BufferedReaderBufferedWriter,默认缓冲区大小为8192字节,减少IO次数。

4.3 场景5:导出百万级数据到CSV(分批写入与顺序保证)

实际开发中,大文件CSV导出的数据源通常来自数据库(如MySQL、PostgreSQL)。此时需分批查询数据库,并逐批写入CSV,同时保证数据顺序(如按订单创建时间排序)。

示例:从MySQL数据库分批查询100万条订单数据,写入CSV文件

需求:导出2024年1月1日至2024年4月30日的所有订单,按order_id升序排列,每批查询1万条,避免内存溢出。

第一步:准备数据库连接与Mapper(MyBatis示例)
  1. 定义Order Bean类:
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;      // 支付时间
}
  1. 定义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);
}
  1. 编写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与保证顺序)
  1. ID分页替代offset分页

    • 传统LIMIT offset, size分页在offset较大时(如100万)会导致全表扫描,性能极差;
    • 采用order_id > lastOrderId LIMIT size的方式,利用主键索引快速定位,性能稳定;
    • 需确保order_id是自增主键或有序唯一键,否则会导致数据遗漏或重复。
  2. 分批写入的顺序保证

    • 数据库查询时按order_id ASC排序,确保每批数据的顺序正确;
    • 每批写入后记录lastOrderId,下一批从lastOrderId之后查询,避免数据重复或遗漏;
    • CsvWriter.writeBean(order)逐行写入,保证内存中只缓存一行Bean数据。
  3. 内存优化策略

    • 控制BATCH_SIZE(建议5000-20000):过小会增加数据库查询次数,过大则可能导致内存溢出;
    • 避免在循环中创建大量对象:复用lastOrderId等变量,减少对象创建;
    • 可选手动触发GC:在每批处理完成后调用System.gc()(谨慎使用,避免影响性能)。
  4. 异常处理与容错

    • 捕获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. 方案1:写入CSV时指定编码为GBK:
    // 写入时用GBK编码(Excel默认读取GBK)
    CsvUtil.getWriter(new File("D:/data/user.csv"), "GBK")
            .writeBeans(userList);
    
  2. 方案2:用Excel手动选择编码打开:
    • 打开Excel → 数据 → 自文本/CSV → 选择CSV文件 → 编码选择“UTF-8” → 完成。

6.2 问题2:读取CSV时字段分隔符错误

现象:CSV文件用竖线|分隔,但CsvUtil仍按逗号解析,导致字段错乱。
原因:未配置CsvConfigfieldSeparator,默认使用逗号分隔。
解决方案

// 创建配置,设置分隔符为竖线
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堆内存限制。
解决方案

  1. 采用分批查询+逐行写入(见4.3节),避免一次性加载数据;
  2. 调整JVM堆内存大小(如-Xms2g -Xmx4g),增加可用内存;
  3. 减少单批查询数量(如将BATCH_SIZE从20000调整为5000);
  4. 避免在循环中创建大量临时对象(如字符串拼接用StringBuilder)。

6.4 问题4:CSV字段包含分隔符或引号导致解析错误

现象:CSV字段中包含逗号(如“上海,浦东”)或双引号(如“"华为"手机”),导致字段被拆分。
原因:未启用引号包裹机制,CsvUtil默认将逗号视为分隔符。
解决方案

  1. 写入时自动用引号包裹含特殊字符的字段(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
    
  2. 读取时配置引号处理:
    CsvConfig config = new CsvConfig();
    config.setQuoteChar('"'); // 识别引号包裹的字段
    CsvReader reader = CsvUtil.getReader(config);
    

6.5 问题5:分批写入时数据顺序错乱

现象:导出的CSV文件中,数据顺序与数据库中的顺序不一致(如订单ID不连续)。
原因

  1. 数据库查询未排序(未加ORDER BY);
  2. 分页方式错误(用offset分页,而非ID分页);
  3. 多线程写入时未同步(多线程写入会打乱顺序)。
    解决方案
  4. 查询时必须按唯一键排序(如ORDER BY order_id ASC);
  5. 采用ID分页(order_id > lastOrderId LIMIT size);
  6. 大文件导出时使用单线程写入(多线程会导致顺序错乱,且IO性能提升有限)。

七、总结与最佳实践

7.1 Hutool CsvUtil的核心优势回顾

  • 低学习成本:API简洁直观,无需掌握复杂的CSV解析原理;
  • 高性能:逐行读写支持GB级大文件,避免OOM;
  • 高灵活性:支持自定义分隔符、编码、日期格式、引号策略;
  • 强兼容性:无缝对接Java Bean、数据库、Excel等,降低开发成本。

7.2 大文件处理最佳实践

  1. 优先使用逐行读写:小文件(<10万行)可一次性读写,大文件必须逐行处理;
  2. 数据库分批查询用ID分页:避免offset分页的性能问题,保证数据顺序;
  3. 控制批处理大小BATCH_SIZE建议设置为5000-20000,平衡IO次数和内存占用;
  4. 使用压缩减少存储:大文件导出时用GZIP压缩,降低存储和传输成本;
  5. 资源安全管理:始终用try-with-resources包裹CsvReaderCsvWriter,避免流泄漏。

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)确保字段映射正确,日期格式匹配
自定义格式CSVCsvConfig+CsvReader/CsvWriter配置分隔符、编码、引号策略

通过本文的讲解,相信你已掌握Hutool CsvUtil的核心用法,能够轻松应对从基础CSV读写到百万级大文件处理的各类场景。
在实际开发中,建议结合业务需求选择合适的API和配置,兼顾开发效率和系统性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值