一、EasyExcel 简介二、读写 Excel
2.1 创建项目2.2 写2.3 读 三、数据字典
3.1 数据字典的设计3.2 Excel 数据批量导入3.3 Excel数据批量导出3.4 数据字典列表展示
一、EasyExcel 简介Java 领域解析 Excel、生成 Excel 比较有名的框架有 Apache poi、jxl 等。但他们都存在一个严重的问题就是非常的耗内存。如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会 OOM 或者 JVM 频繁的 full gc。
EasyExcel 是阿里巴巴开源的一个 excel 处理框架,以使用简单、节省内存著称。EasyExcel 能大大减少占用内存的主要原因是在解析 Excel 时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。
Excel导入导出的应用场景:
数据导入:减轻录入工作量数据导出:统计信息归档数据传输:异构系统之间数据传输
官方网站:https://github.com/alibaba/easyexcel
快速开始:https://www.yuque.com/easyexcel/doc/easyexcel
① 创建一个普通的maven项目
② 引入依赖
com.alibaba easyexcel 2.1.7 org.slf4j slf4j-simple 1.7.5 org.apache.xmlbeans xmlbeans 3.1.0 org.projectlombok lombok 1.18.12 junit junit 4.12
③ 创建实体类
@ExcelProperty标注 excel 文件中表头
@Data
public class ExcelStudentDTO {
@ExcelProperty("姓名")
private String name;
@ExcelProperty("生日")
private Date birthday;
@ExcelProperty("薪资")
private Double salary;
}
2.2 写
① 最简单的写
public class ExcelWriteTest {
@Test
public void simpleWriteXlsx() {
String fileName = "d:/excel/simpleWrite.xlsx"; //需要提前新建目录
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, ExcelStudentDTO.class).sheet("模板").doWrite(data());
}
//辅助方法
private List data(){
List list = new ArrayList<>();
//算上标题,做多可写65536行
//超出:java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)
for (int i = 0; i < 65535; i++) {
ExcelStudentDTO data = new ExcelStudentDTO();
data.setName("Helen" + i);
data.setBirthday(new Date());
data.setSalary(123456.1234);
list.add(data);
}
return list;
}
}
② 不同版本的写
@Test
public void simpleWriteXls() {
String fileName = "d:/excel/simpleWrite.xls";
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, ExcelStudentDTO.class).excelType(ExcelTypeEnum.XLS).sheet("模板").doWrite(data());
}
③ 写入大数据量
xls 版本的Excel最多一次可写0 …65535行;xlsx 版本的Excel最多一次可写0…1048575行
2.3 读参考文档:https://www.yuque.com/easyexcel/doc/read
① 创建监听器
@Slf4j public class ExcelStudentDTOListener extends AnalysisEventListener{ @Override public void invoke(ExcelStudentDTO data, AnalysisContext context) { log.info("解析到一条数据:{}", data); } @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("所有数据解析完成!"); } }
② 测试用例
public class ExcelReadTest {
@Test
public void simpleReadXlsx() {
String fileName = "d:/excel/simpleWrite.xlsx";
// 这里默认读取第一个sheet
EasyExcel.read(fileName, ExcelStudentDTO.class, new ExcelStudentDTOListener()).sheet().doRead();
}
@Test
public void simpleReadXls() {
String fileName = "d:/excel/simpleWrite.xls";
EasyExcel.read(fileName, ExcelStudentDTO.class, new ExcelStudentDTOListener()).excelType(ExcelTypeEnum.XLS).sheet().doRead();
}
}
三、数据字典
3.1 数据字典的设计
何为数据字典?数据字典负责管理系统常用的分类数据或者一些固定数据,例如:省市区三级联动数据、民族数据、行业数据、学历数据等,数据字典帮助我们方便的获取和适用这些通用数据。
数据字典的设计:
parent_id:上级id,通过id与parent_id构建上下级关系,例如:我们要获取所有行业数据,那么只需要查询parent_id=20000的数据name:名称,例如:填写用户信息,我们要select标签选择民族,“汉族”就是数据字典的名称value:值,例如:填写用户信息,我们要select标签选择民族,“1”(汉族的标识)就是数据字典的值dict_code:编码,编码是我们自定义的,全局唯一,例如:我们要获取行业数据,我们可以通过parent_id获取,但是parent_id是不确定的,所以我们可以根据编码来获取行业数据 3.2 Excel 数据批量导入
① 添加依赖
com.alibaba easyexcel org.apache.xmlbeans xmlbeans
② 创建Excel实体类
@Data
public class ExcelDictDTO {
@ExcelProperty("id")
private Long id;
@ExcelProperty("上级id")
private Long parentId;
@ExcelProperty("名称")
private String name;
@ExcelProperty("值")
private Integer value;
@ExcelProperty("编码")
private String dictCode;
}
③ 创建监听器
@Slf4j //@AllArgsConstructor //全参 @NoArgsConstructor //无参 public class ExcelDictDTOListener extends AnalysisEventListener{ private static final int BATCH_COUNT = 5; List list = new ArrayList(); private DictMapper dictMapper; //传入mapper对象 public ExcelDictDTOListener(DictMapper dictMapper) { this.dictMapper = dictMapper; } @Override public void invoke(ExcelDictDTO data, AnalysisContext context) { log.info("解析到一条记录: {}", data); list.add(data); // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if (list.size() >= BATCH_COUNT) { saveData(); // 存储完成清理 list list.clear(); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 saveData(); log.info("所有数据解析完成!"); } private void saveData() { log.info("{}条数据,开始存储数据库!", list.size()); dictMapper.insertBatch(list); //批量插入 log.info("存储数据库成功!"); } }
④ Mapper层批量插入
接口:DictMapper
void insertBatch(Listlist);
xml:DictMapper.xml
insert into dict ( id , parent_id , name , value , dict_code ) values ( #{item.id} , #{item.parentId} , #{item.name} , #{item.value} , #{item.dictCode} )
⑤ Service 层创建监听器实例
接口 DictService
void importData(InputStream inputStream);
实现:DictServiceImpl
@Transactional(rollbackFor = {Exception.class})
@Override
public void importData(InputStream inputStream) {
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(inputStream, ExcelDictDTO.class, new ExcelDictDTOListener(baseMapper)).sheet().doRead();
log.info("importData finished");
}
⑥ Controller层接收客户端上传
@ApiOperation("Excel批量导入数据字典")
@PostMapping("/import")
public R batchimport(
@ApiParam(value = "Excel文件", required = true)
@RequestParam("file") MultipartFile file) {
try {
InputStream inputStream = file.getInputStream();
dictService.importData(inputStream);
return R.ok().message("批量导入成功");
} catch (Exception e) {
//UPLOAD_ERROR(-103, "文件上传错误"),
throw new BusinessException(ResponseEnum.UPLOAD_ERROR, e);
}
}
⑦ 添加 mapper 发布配置
src/main/java **/*.xml false
⑧ 前端实现
3.3 Excel数据批量导出导入Excel 点击上传
① Service层解析Excel数据
接口:DictService
ListlistDictData();
实现:DictServiceImpl
@Override public ListlistDictData() { List dictList = baseMapper.selectList(null); //创建ExcelDictDTO列表,将Dict列表转换成ExcelDictDTO列表 ArrayList excelDictDTOList = new ArrayList<>(dictList.size()); dictList.forEach(dict -> { ExcelDictDTO excelDictDTO = new ExcelDictDTO(); BeanUtils.copyProperties(dict, excelDictDTO); excelDictDTOList.add(excelDictDTO); }); return excelDictDTOList; }
② Controller层接收客户端请求
@ApiOperation("Excel数据的导出")
@GetMapping("/export")
public void export(HttpServletResponse response){
try {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("mydict", "UTF-8").replaceAll("\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), ExcelDictDTO.class).sheet("数据字典").doWrite(dictService.listDictData());
} catch (IOException e) {
//EXPORT_DATA_ERROR(104, "数据导出失败"),
throw new BusinessException(ResponseEnum.EXPORT_DATA_ERROR, e);
}
}
③ 前端实现
3.4 数据字典列表展示导出Excel
① 需求
element-ui 树形数据的两种加载方案
- 非延迟加载
需要后端返回的数据结构中包含嵌套数据,并且嵌套数据放在children属性中延迟加载
不需要后端返回数据中包含嵌套数据,并且要定义布尔属性hasChildren,表示当前节点是否包含子数据。如果hasChildren为true,就表示当前节点包含子数据;如果hasChildren为false,就表示当前节点不包含子数据。 如果当前节点包含子数据,那么点击当前节点的时候,就需要通过load方法加载子数据
② 非延迟加载实现
- 实体类添加属性
Dict中添加属性
@ApiModelProperty(value = "子节点") @TableField(exist = false) private List关键实现children = new ArrayList<>();
@Override
public Dict getTreeDataList() {
List dictList = baseMapper.selectList(null);
Dict root = null;
Map parentDictMap = new HashMap<>();
//第一次遍历找到根节点
for (Dict dict : dictList) {
if (dict.getParentId() == 0L) {
root = dict;
}
parentDictMap.put(dict.getId(), dict);
}
//第二次遍历组装数据
for (Dict dict : dictList) {
Long parentId = dict.getParentId();
if (parentId != 0L) {
Dict parentDict = parentDictMap.get(parentId);
parentDict.getChildren().add(dict);
}
}
return root;
}
Controller层接收前端请求@ApiOperation("加载数据字段数据")
@GetMapping("/tree")
public R list() {
try {
Dict rootDict = dictService.getTreeDataList();
return R.ok().data("data", rootDict.getChildren());
} catch (Exception e) {
throw new BusinessException(ResponseEnum.ERROR, e);
}
}
前端实现非延迟加载
③ 延迟加载实现
- 实体类添加属性
Dict中添加属性
@ApiModelProperty(value = "是否包含子节点") @TableField(exist = false)//在数据库表中忽略此列 private boolean hasChildren;关键实现
@Override public ListController层接收前端请求listByParentId(long parent_id) { List dictList = dictMapper.selectList(new QueryWrapper ().eq("parent_id", parent_id)); for (Dict dict : dictList) { dict.setHasChildren(hasChildren(dict.getId())); } return dictList; } private boolean hasChildren(long id) { Integer count = dictMapper.selectCount(new QueryWrapper ().eq("parent_id", id)); return count.intValue() != 0; }
@ApiOperation("根据上级id获取子节点数据列表")
@GetMapping("/listByParentId/{parentId}")
public R listByParentId(
@ApiParam(value = "上级节点id", required = true)
@PathVariable Long parentId) {
List dictList = dictService.listByParentId(parentId);
return R.ok().data("list", dictList);
}
前端实现延迟加载



