如何使用 Java 灵活读取 Excel 内容 ?
The following article is from 日拱一兵 Author tan日拱一兵
写在前面
Java 后端程序员应该会遇到读取 Excel 信息到 DB 等相关需求,脑海中可能突然间想起 Apache POI 这个技术解决方案,但是当 Excel 的数据量非常大的时候,你也许发现,POI 是将整个 Excel 的内容全部读出来放入到内存中,所以内存消耗非常严重,如果同时进行包含大数据量的 Excel 读操作,很容易造成内存溢出问题。
本博文源码在公众号:Java后端,后台回复 excel 获取。
另外 EasyExcel 在上层做了模型转换的封装,不需要 cell 等相关操作,让使用者更加简单和方便,且看
简单读
假设我们 excel 中有以下内容:
我们需要新建 User 实体,同时为其添加成员变量
@Data
public class User {
@ExcelProperty(index = 0)
private String name;
@ExcelProperty(index = 1)
private Integer age;
}
你也许关注到了
@ExcelProperty
注解,同时使用了 index 属性 (0 代表第一列,以此类推),该注解同时支持以「列名」name 的方式匹配,比如:
@ExcelProperty("姓名")
private String name;
按照 github 文档的说明:
不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
如果读取的 Excel 模板信息列固定,这里建议以 index 的形式使用,因为如果用名字去匹配,名字重复,会导致只有一个字段读取到数据,所以 index 是更稳妥的方式 如果 Excel 模板的列 index 经常有变化,那还是选择 name 方式比较好,不用经常性修改实体的注解 index 数值 所以大家可以根据自己的情况自行选择
new UserExcelListener()
异常醒目,这也是 EasyExcel 逐行读取 Excel 内容的关键所在,自定义 UserExcelListener
继承 AnalysisEventListener
@Slf4j
public class UserExcelListener extends AnalysisEventListener<User> {
private static final int BATCH_COUNT = 2;
List<User> list = new ArrayList<User>(BATCH_COUNT);
@Override
public void invoke(User user, AnalysisContext analysisContext) {
log.info("解析到一条数据:{}", JSON.toJSONString(user));
list.add(user);
if (list.size() >= BATCH_COUNT) {
saveData();
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
saveData();
log.info("所有数据解析完成!");
}
private void saveData(){
log.info("{}条数据,开始存储数据库!", list.size());
log.info("存储数据库成功!");
}
}
自定义转换器
性别信息转换
@ExcelProperty(index = 2)
private Integer gender;
public class GenderConverter implements Converter<Integer> {
public static final String MALE = "男";
public static final String FEMALE = "女";
@Override
public Class supportJavaTypeKey() {
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
String stringValue = cellData.getStringValue();
if (MALE.equals(stringValue)){
return 1;
}else {
return 2;
}
}
@Override
public CellData convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
return null;
}
}
上面程序的 Converter 接口的泛型是指要转换的 Java 数据类型,与 supportJavaTypeKey 方法中的返回值类型一致@ExcelProperty
查看,该注解是支持自定义 Converter 的,所以我们为 User 实体添加 gender 成员变量,并指定 converter@ExcelProperty(index = 2, converter = GenderConverter.class)
private Integer gender;
日期信息转换
@DateTimeFormat
注解进行格式化@DateTimeFormat
注解,按照要求做格式化@ExcelProperty(index = 3)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private String birth;
web 读
简单 Web
UserController
,在其添加 upload
方法@RestController
@RequestMapping("/users")
@Slf4j
public class UserController {
@PostMapping("/upload")
public String upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener()).sheet().doRead();
return "success";
}
}
匿名内部类方式
public interface IUser {
public boolean saveData(List<User> users);
}
@Service
@Slf4j
public class UserServiceImpl implements IUser {
@Override
public boolean saveData(List<User> users) {
log.info("UserService {}条数据,开始存储数据库!", users.size());
log.info(JSON.toJSONString(users));
log.info("UserService 存储数据库成功!");
return true;
}
}
@Autowired
private IUser iUser;
@PostMapping("/uploadWithAnonyInnerClass")
public String uploadWithAnonyInnerClass(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), User.class, new AnalysisEventListener<User>(){
private static final int BATCH_COUNT = 2;
List<User> list = new ArrayList<User>();
@Override
public void invoke(User user, AnalysisContext analysisContext) {
log.info("解析到一条数据:{}", JSON.toJSONString(user));
list.add(user);
if (list.size() >= BATCH_COUNT) {
saveData();
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
saveData();
log.info("所有数据解析完成!");
}
private void saveData(){
iUser.saveData(list);
}
}).sheet().doRead();
return "success";
}
构造器传参
@Slf4j
public class UserExcelListener extends AnalysisEventListener<User> {
private IUser iUser;
public UserExcelListener(IUser iUser){
this.iUser = iUser;
}
private void saveData(){
iUser.saveData(list);
}
@PostMapping("/uploadWithConstructor")
public String uploadWithConstructor(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener(iUser)).sheet().doRead();
return "success";
}
Lambda 传参
Consumer<T>
,将其作为构造 listener 的参数。Consumer<List<T>>
的参数,这样下面代码被调用时,我们的业务逻辑也就会被相应的执行了:consumer.accept(linkedList);
batchInsert
方法中:满足 Controller RESTful API 的简洁性 listener 更加通用和灵活,它更多是扮演了抽象类的角色,具体的逻辑交给抽象方法的实现来完成 业务逻辑可扩展性也更好,逻辑更加清晰