在日常开发中,数据导入是一个高频需求——从 Excel 模板批量导入用户信息、从 CSV 文件同步订单数据、从 JSON 日志解析业务指标等。本文将基于 JDK1.8,结合当前主流技术栈,实现一套高效、可扩展的数据导入方案,涵盖文件解析、数据校验、批量入库全流程。
技术栈选型
首先明确核心技术组件,兼顾成熟度和性能:
技术领域 | 选用组件 | 优势说明 |
---|---|---|
文件解析 | EasyExcel 3.3.0 | 阿里开源,基于 POI 优化,低内存读写 Excel(支持 2003/2007 格式) |
CSV 解析 | OpenCSV 5.6 | 轻量级 CSV 处理库,支持自定义分隔符和编码 |
数据库操作 | MyBatis-Plus 3.5.3.1 | 在 MyBatis 基础上增强,提供批量插入、CRUD 接口,简化 SQL 编写 |
数据校验 | Hibernate Validator 6.2.5 | 实现 JSR380 规范,注解式校验,支持自定义校验规则 |
数据库连接 | Druid 1.2.16 | 阿里开源连接池,支持监控、防-SQL住入,性能优于传统 C3P0/DBCP |
JSON 处理 | FastJSON 2.0.32 | 阿里开源,高性能 JSON 解析,支持复杂对象转换 |
日志 | SLF4J + Logback | 日志门面+实现,统一日志接口,方便后续扩展 |
环境准备
依赖配置(Maven)
在 pom.xml
中引入核心依赖:
<!-- 数据库连接 -->
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.16</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version>
</dependency>
<!-- Excel 解析 -->
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.0</version>
</dependency>
<!-- CSV 解析 -->
<dependency><groupId>com.opencsv</groupId><artifactId>opencsv</artifactId><version>5.6</version>
</dependency>
<!-- 数据校验 -->
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.2.5.Final</version>
</dependency>
<!-- JSON 处理 -->
<dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.32</version>
</dependency>
<!-- 日志 -->
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.4.8</version>
</dependency>
核心实现步骤
1. 数据库设计与实体类定义
以「用户批量导入」为例,假设数据库表 t_user
结构如下:
CREATE TABLE `t_user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',`username` varchar(50) NOT NULL COMMENT '用户名',`phone` varchar(20) NOT NULL COMMENT '手机号',`email` varchar(100) DEFAULT NULL COMMENT '邮箱',`gender` tinyint DEFAULT NULL COMMENT '性别(0-女,1-男)',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_username` (`username`),UNIQUE KEY `uk_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
对应实体类 User
(使用 MyBatis-Plus 注解):
@Data
@TableName("t_user")
public class User {@TableId(type = IdType.AUTO)private Long id;@NotBlank(message = "用户名不能为空")@Size(min = 2, max = 50, message = "用户名长度必须在 2-50 之间")private String username;@NotBlank(message = "手机号不能为空")@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")private String phone;@Email(message = "邮箱格式错误")private String email;@Min(value = 0, message = "性别只能是 0 或 1")@Max(value = 1, message = "性别只能是 0 或 1")private Integer gender;@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;
}
2. 通用导入接口设计
为了支持多格式(Excel/CSV/JSON)导入,抽象出通用导入接口 DataImporter
:
public interface DataImporter<T> {/*** 导入数据* @param inputStream 数据源输入流* @param clazz 目标实体类* @return 导入结果(成功数量、失败数据及原因)*/ImportResult<T> importData(InputStream inputStream, Class<T> clazz);
}
其中 ImportResult
用于封装导入结果:
@Data
public class ImportResult<T> {private int successCount; // 成功数量private List<ErrorData<T>> errorDataList; // 失败数据@Data@AllArgsConstructorpublic static class ErrorData<T> {private T data; // 错误数据private String message; // 错误原因}
}
3. 多格式解析实现
3.1 Excel 导入(基于 EasyExcel)
EasyExcel 的核心优势是「一行一行读取」,避免 POI 全量加载导致的内存溢出,尤其适合大文件(10万行+)。
@Component
public class ExcelImporter<T> implements DataImporter<T> {private static final Logger log = LoggerFactory.getLogger(ExcelImporter.class);@Overridepublic ImportResult<T> importData(InputStream inputStream, Class<T> clazz) {ImportResult<T> result = new ImportResult<>();List<T> dataList = new ArrayList<>();// 读取 ExcelEasyExcel.read(inputStream, clazz, new ReadListener<T>() {@Overridepublic void invoke(T data, AnalysisContext context) {dataList.add(data);}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {log.info("Excel 解析完成,共 {} 条数据", dataList.size());}}).sheet().doRead();// 校验+入库(后续实现)return handleData(dataList, clazz);}
}
3.2 CSV 导入(基于 OpenCSV)
CSV 是纯文本格式,解析效率高于 Excel,适合超大文件(百万行级)。
@Component
public class CsvImporter<T> implements DataImporter<T> {private static final Logger log = LoggerFactory.getLogger(CsvImporter.class);@Overridepublic ImportResult<T> importData(InputStream inputStream, Class<T> clazz) {List<T> dataList = new ArrayList<>();try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);CSVReader csvReader = new CSVReader(reader)) {// 读取表头(假设 CSV 第一行为字段名)String[] headers = csvReader.readNext();// 映射表头到实体类字段(简化处理,实际可通过注解自定义映射)Map<String, String> headerMap = Arrays.stream(headers).collect(Collectors.toMap(h -> h, h -> h.toLowerCase()));// 读取数据行String[] row;while ((row = csvReader.readNext()) != null) {T data = clazz.newInstance();for (int i = 0; i < headers.length; i++) {String fieldName = headerMap.get(headers[i]);if (fieldName != null) {// 通过反射设置字段值(实际可使用 BeanUtils 优化)Field field = clazz.getDeclaredField(fieldName);field.setAccessible(true);field.set(data, convertValue(row[i], field.getType()));}}dataList.add(data);}log.info("CSV 解析完成,共 {} 条数据", dataList.size());} catch (Exception e) {log.error("CSV 解析失败", e);throw new RuntimeException("CSV 解析错误", e);}// 校验+入库return handleData(dataList, clazz);}// 简单类型转换(实际可扩展更多类型)private Object convertValue(String value, Class<?> type) {if (String.class.equals(type)) return value;if (Integer.class.equals(type) || int.class.equals(type)) return Integer.valueOf(value);if (LocalDateTime.class.equals(type)) return LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME);return value;}
}
4. 数据校验与批量入库
4.1 数据校验(基于 Hibernate Validator)
使用 Validator
对解析后的对象进行校验,筛选无效数据:
@Component
public class DataValidator {private final Validator validator;// 初始化 Validator(JDK1.8 支持 Lambda 表达式简化配置)public DataValidator() {ValidatorFactory factory = Validation.buildDefaultValidatorFactory();this.validator = factory.getValidator();}/*** 校验单条数据*/public <T> String validate(T data) {Set<ConstraintViolation<T>> violations = validator.validate(data);if (!violations.isEmpty()) {return violations.stream().map(v -> v.getPropertyPath() + ":" + v.getMessage()).collect(Collectors.joining(";"));}return null;}
}
4.2 批量入库(基于 MyBatis-Plus)
通过 MyBatis-Plus 的 saveBatch
方法实现批量插入,底层会优化为 INSERT INTO ... VALUES (...), (...), (...)
语句,减少数据库交互次数。
@Service
@Transactional
public class UserService extends ServiceImpl<UserMapper, User> {/*** 批量保存用户(每次批量插入 1000 条,避免 SQL 过长)*/public int batchSave(List<User> userList) {final int BATCH_SIZE = 1000;int total = 0;// JDK1.8 流式处理,按批次插入List<List<User>> batches = IntStream.range(0, userList.size()).boxed().collect(Collectors.groupingBy(index -> index / BATCH_SIZE)).values().stream().map(indices -> indices.stream().map(userList::get).collect(Collectors.toList())).collect(Collectors.toList());for (List<User> batch : batches) {boolean success = saveBatch(batch);if (success) total += batch.size();}return total;}
}
4.3 整合校验与入库
在 DataImporter
的实现类中,通过 handleData
方法整合校验和入库逻辑:
public abstract class AbstractDataImporter<T> implements DataImporter<T> {@Autowiredprivate DataValidator validator;@Autowiredprivate UserService userService; // 实际可通过泛型优化为通用 Serviceprotected ImportResult<T> handleData(List<T> dataList, Class<T> clazz) {ImportResult<T> result = new ImportResult<>();List<T> validData = new ArrayList<>();List<ImportResult.ErrorData<T>> errorDataList = new ArrayList<>();// 校验数据for (T data : dataList) {String errorMsg = validator.validate(data);if (errorMsg == null) {validData.add(data);} else {errorDataList.add(new ImportResult.ErrorData<>(data, errorMsg));}}// 批量入库if (!validData.isEmpty()) {int successCount = userService.batchSave(validData);result.setSuccessCount(successCount);}result.setErrorDataList(errorDataList);return result;}
}
5. 接口调用与测试
最后,通过 Controller 暴露导入接口,根据文件类型自动选择对应的解析器:
@RestController
@RequestMapping("/import")
public class ImportController {@Autowiredprivate ExcelImporter<User> excelImporter;@Autowiredprivate CsvImporter<User> csvImporter;@PostMapping("/user")public Result<ImportResult<User>> importUser(@RequestParam("file") MultipartFile file) throws IOException {String fileName = file.getOriginalFilename();DataImporter<User> importer;// 根据文件名后缀选择解析器if (fileName.endsWith(".xlsx") || fileName.endsWith(".xls")) {importer = excelImporter;} else if (fileName.endsWith(".csv")) {importer = csvImporter;} else {return Result.fail("不支持的文件格式");}ImportResult<User> result = importer.importData(file.getInputStream(), User.class);return Result.success(result);}
}
性能优化与注意事项
-
大文件处理:
- 对于 10 万行以上的文件,建议使用「分片上传+异步导入」,避免请求超时
- EasyExcel 可通过
headRowNumber
配置表头位置,通过registerReadListener
实现边读边处理(无需缓存全量数据)
-
数据库优化:
- 批量插入时,设置合理的批次大小(建议 500-1000 条/批,过大可能导致 SQL 执行超时)
- 导入前可关闭表索引,导入后重建(适合全量覆盖场景)
-
异常处理:
- 解析阶段:捕获文件格式错误、编码错误,返回友好提示
- 入库阶段:通过事务保证原子性,失败时回滚并记录错误日志