提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
Spring Boot上Excel的导入、导出与模版的下载
前言
项目编写的过程上,要实现对excel文件内容上的数据,进行插入到相对应的数据库上,同时还要对导入的excel某些字段进行校验,校验不通过进行返回前段异常数据与所在异常的位置
提示:以下是本篇文章正文内容,下面案例可供参考
一、引入EasyExcel的Maven依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>
二、excel文件的上传并插入到数据库
对于上传的excel文件,在传输过程中存在一些问题需要我们注意:
1、数据的插入的时候,采用批量插入方式,减少网络开销,提升插入效率。
2、文件上传过程中,需要进行校验上传文件的类型,只允许上传指定类型的文件,以提高安全性。
3、文件上传较大的excel数据,需要分批进行处理操作,避免全部进行加载占用过多的内存,避免发生OOM。
4、当excel文件上传的数据发生异常时,要进行数据的回滚操作,避免产生一些不必要的麻烦。
基于以上的声明,弄了一个小案例,废话不多说,如下所示:
三、文件的导入
3.1、pojo
3.1.1、user
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
@ApiModel(value = "user", description = "学生信息表")
public class user implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主键id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "姓名")
@TableField("user_name")
private String userName;
@ApiModelProperty(value = "密码")
@TableField("password")
private String password;
@ApiModelProperty(value = "创建时间")
@TableField("create_time")
private LocalDateTime createTime;
@ApiModelProperty(value = "修改时间")
@TableField("update_time")
private LocalDateTime updateTime;
}
3.1.2、UserExcelRecordDTO
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserExcelRecordDTO{
@ExcelProperty(value = "ID", index = 0)
private Integer id;
@ExcelProperty(value = "姓名", index = 1)
private String userName;
@ExcelProperty(value = "密码", index = 2)
private String password;
/**
@DateTimeFormat(value = "yyyy-MM-dd")
@ExcelProperty(value = "过期时间", index = 3)
private String expireTime;
**/
//注意在excel存在导入时间的字段的话,要使用上面的样式,要不然会出现时间转换失败
//表示第几行
private Integer rows;
}
3.1.3、UserExcelRecordDTO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserExcelRecordDTO {
@ExcelProperty(value = "ID", index = 0)
private Integer Id;
@ExcelProperty(value = "姓名", index = 1)
private String userName;
@ExcelProperty(value = "密码", index = 2)
private String password;
//表示第几行
private Integer rows;
}
3.2、Controller
/**
* easyExcel上传文件
* 参数必须为file
*/
@PostMapping("/upload")
@ApiOperation("学生数据的导入")
public ResponseResult upload(MultipartFile file) throws Exception {
return userService.upload(file);
}
3.3、service & serviceImpl
3.3.1、service
ResponseResult upload(MultipartFile file) throws Exception;
3.3.2、serviceImpl
/**
* 文件的导入
* @param file
* @return
*/
public ResponseResult upload(MultipartFile file) throws Exception {
if (file == null) {
log.error("学生信息导入操作,上传文件为空");
//AppHttpCodeEnum.UPLOAD_NOT_PRESENCE只是代表返回异常的数据,自己可以定义,问题不大
return ResponseResult.errorResult(AppHttpCodeEnum.UPLOAD_NOT_PRESENCE);
}
//获取文件名
String fileName = file.getOriginalFilename();
//验证文件名是否合格(xlsx, xls, xlsm,xlt)
//文件后缀名校验
if (!(fileName.endsWith("xls") || fileName.endsWith("xlsx") || fileName.endsWith("xlsm") || fileName.endsWith("xlt"))) {
log.error("学生信息文件的导入,文件上传格式错误");
return ResponseResult.errorResult(AppHttpCodeEnum.UPLOAD_DATA_FORMAT_ERROR);
}
Map returnMap = new HashMap<>();
//验证导入工时的标题头是否合法
Workbook wb = null;
String originalFilename = file.getOriginalFilename();
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
if (!"xls".equals(suffix) && !"xlsx".equals(suffix)) {
//return ResultEx.error(Constant.ERROR_CODE, "上传文件只支持xls和xlsx文件后缀;", "");
log.error("学生数据EXCEL导入,上传文件只支持xls和xlsx文件后缀");
return ResponseResult.errorResult(AppHttpCodeEnum.UPLOAD_DATA_SUFFIX);
}
InputStream fin = file.getInputStream();
if ("xls".equals(suffix)) {
wb = new HSSFWorkbook(fin);
} else if ("xlsx".equals(suffix)) {
wb = new XSSFWorkbook(fin);
}
String[] columnName =
{"ID", "姓名", "密码"};
//校验表头是否正确
boolean resultVali = verificationStudentExcelHeadLine(wb, columnName);
if (!resultVali) {
log.error("学生文件的导入数据错误,导入Excel表头与模板不一致,请核对文件");
return ResponseResult.errorResult(AppHttpCodeEnum.UPLOAD_TABLE_DATA_ERROR);
}
EasyExcel.read(file.getInputStream(), UserExcelRecordDTO.class, new UserDataListener(userService)).sheet().doRead();
return null;
}
/**
* excel表格上数据的校验
* 校验表头是否正确
*
* @param wb
* @param columnName
* @return
* @throws Exception
*/
public static boolean verificationStudentExcelHeadLine(Workbook wb, String[] columnName) throws Exception {
String result = null;
try {
Sheet sheet = wb.getSheetAt(0);
Row row = sheet.getRow(0);
//6
System.err.println("columnName.length:" + columnName.length);
if (row != null && row.getLastCellNum() == columnName.length) {
int lastCellNum = row.getLastCellNum();
System.err.println("lastCellNum:" + lastCellNum);
for (int idx = 0; idx < lastCellNum; idx++) {
String value = getCellValue(row.getCell(idx));
if (idx < 11) {
if (org.apache.commons.lang3.StringUtils.isBlank(value) || !columnName[idx].equals(value)) {
result = "标题行第" + (idx + 1) + "列名称错误!";
return false;
}
}
}
} else {
result = "上传文件首行不能为空或与工时导入表格表头不一致!";
return false;
}
} catch (Exception ex) {
return false;
}
return true;
}
3.3.3、UserDataListener
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelAnalysisException;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
public class UserDataListener extends AnalysisEventListener<UserExcelRecordDTO> {
private final UserService userService;
public UserDataListener(UserService userService) {
this.userService= userService;
}
/**
* 每隔1000条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
List<UserExcelRecordDTO> list = new ArrayList<UserExcelRecordDTO>();
@Override
public void invoke(UserExcelRecordDTO data, AnalysisContext context) {
log.info("invoke---开始校验表格数据");
validChoiceInfo(data, context);
log.info("invoke---开始解析表格数据");
log.info("解析到一条数据:{}", JSON.toJSONString(data));
list.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
list.clear();
}
}
private void validChoiceInfo(UserExcelRecordDTO data, AnalysisContext context) {
if (data.getId() == null){
log.info("上传失败:第{}行ID信息为空",context.readRowHolder().getRowIndex());
throw new UploadExcelException("上传失败:第" + (context.readRowHolder().getRowIndex() + 1) +"行ID信息为空");
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
@Transactional
private ResponseResult saveData() {
//获取数据库上所有的学生数据
List<UserDevice> allUserList = userService.getAllUser();
log.info("{}条数据,开始存储数据库!", list.size());
List<UserExcelRecordDTO> userList = new ArrayList<>(); //创建需要保存的实体类User集合
for (int i = 0; i < list.size(); i++) {
try {
UserExcelRecordDTO user = UserExcelRecordDTO
.builder()
//ID
.id(list.get(i).getId())
//姓名
.userName(list.get(i).getUserName())
//密码
.password(list.get(i).getPassword())
.build();
//添加到集合
userList.add(user);
} catch (Exception e) {
log.error(e.getMessage());
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_REQUIRE);
}
}
/**
* 数据库上的所有学生数据
*/
//收集集合上的所有的学生ID数据信息
List<Integer> collectUserIdSet = allUserList.stream().map(User::getId).collect(Collectors.toList());
/**
* 进行excel导入的所有学生ID
*/
//收集所有的要进行导入的ID,为了校验一致性
List<Integer> SetId = userList.stream().map(UserExcelRecordDTO::getId).collect(Collectors.toList());
/**
* 校验导入的学生ID数据是否存在
*/
if (collectUserIdSet.containsAll(SetId)) {
log.error("导入的excel上存在重复的学生ID,请检查!!!");
return ResponseResult.errorResult(AppHttpCodeEnum.UPLOAD_INSERT_DECODE);
}
if (!CollectionUtils.isEmpty(userList)) {
userService.saveUser(userList); //集合不为空之后存储用户
}else{
log.error("数据存储失败,请检查数据格式!!!");
return ResponseResult.errorResult(AppHttpCodeEnum.UPLOAD_DATA_ERROR);
}
log.info("存储数据库成功!");
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
}
四、数据的导出
4.1、UserExcel
@Data
@AllArgsConstructor
@NoArgsConstructor
@ExcelTarget("UserExcel")
public class UserExcel implements Serializable {
/**
* id
*/
@Excel(name = "id", width = 35, orderNum = "1")
private Integer id;
/**
* 姓名
*/
@Excel(name = "姓名", width = 35, orderNum = "2")
private String userName;
/**
* 设备名称
*/
@Excel(name = "密码", width = 35, orderNum = "3")
private String password;
}
4.2、controller
@GetMapping("/export/{Id}")
@ApiOperation("学生数据的导出操作")
public void exportUserDate(HttpServletResponse response, @PathVariable Integer Id) {
log.info("当前传递的项目ID:{}", Id);
userService.exportUserDate(response, Id);
}
4.3、service & serviceImpl
4.3.1、service
void exportUserDate(HttpServletResponse response,Integer Id);
4.3.2、serviceImpl
public void exportUserDate(HttpServletResponse response, Integer Id) {
// 从数据库获取需要导出的数据
LambdaQueryWrapper<user> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(user::getId, Id);
List<user> userList = userMapper.selectList(wrapper);
List<userExcel> userExcelList = BeanUtil.copyList(userList, userExcel.class);
//导出操作
OfficeExportUtil.exportExcel(OfficeExportUtil.getWorkbook("学生数据表", "Sheet", userExcel.class, userExcelList), "学生数据", response);
}
4.4、utils
4.4.1、OfficeExportUtil
/**
* Office导出工具类
**/
@Slf4j
public class OfficeExportUtil {
/**
* 允许导出的最大条数
*/
private static final Integer EXPORT_EXCEL_MAX_NUM = 10000;
/**
* 获取导出的 Workbook对象
*
* @param title 大标题
* @param sheetName 页签名
* @param object 导出实体
* @param list 数据集合
* @return Workbook
*/
public static Workbook getWorkbook(String title, String sheetName, Class<?> object, List<?> list) {
//判断导出数据是否为空
if (list == null) {
list = new ArrayList<>();
}
//判断导出数据数量是否超过限定值
if (list.size() > EXPORT_EXCEL_MAX_NUM) {
title = "导出数据行数超过:" + EXPORT_EXCEL_MAX_NUM + "条,无法导出、请添加导出条件!";
list = new ArrayList<>();
}
//获取导出参数
ExportParams exportParams = new ExportParams(title, sheetName, ExcelType.XSSF);
//设置导出样式
exportParams.setStyle(ExcelStyleUtil.class);
//设置行高
exportParams.setHeight((short) 8);
//输出Workbook流
return ExcelExportUtil.exportExcel(exportParams, object, list);
}
/**
* 获取导出的 Workbook对象
*
* @param path 模板路径
* @param map 导出内容map
* @return Workbook
*/
public static Workbook getWorkbook(String path, Map<String, Object> map) {
//获取导出模板
TemplateExportParams params = new TemplateExportParams(path);
//设置导出样式
params.setStyle(ExcelStyleUtil.class);
//输出Workbook流
return ExcelExportUtil.exportExcel(params, map);
}
/**
* 导出Excel
*
* @param workbook workbook流
* @param fileName 文件名
* @param response 响应
*/
public static void exportExcel(Workbook workbook, String fileName, HttpServletResponse response) {
//给文件名拼接上日期
fileName = fileName + StrUtil.UNDERLINE + DateUtil.today();
//输出文件
try (OutputStream out = response.getOutputStream()) {
//获取文件名并转码
String name = URLEncoder.encode(fileName, "UTF-8");
//编码
// 设置强制下载不打开
response.setContentType("application/force-download");
// 下载文件的默认名称
response.setHeader("Content-Disposition", "attachment;filename=" + name + ".xlsx");
//输出表格
workbook.write(out);
} catch (IOException e) {
log.error("文件导出异常,详情如下:", e);
} finally {
try {
if (workbook != null) {
//关闭输出流
workbook.close();
}
} catch (IOException e) {
log.error("文件导出异常,详情如下:", e);
}
}
}
}
五、excel模版下载
5.1、controller
@GetMapping(value = "/download")
public void download(HttpServletResponse response) throws FileNotFoundException {
userService.downloadExcel(response);
}
5.2、service & serviceImpl
5.2.1、service
void downloadExcel(HttpServletResponse response);
5.2.2、serviceImpl
public void downloadExcel(HttpServletResponse response) {
//下载模板
//方法一:直接下载路径下的文件模板(这种方式貌似在SpringCloud和Springboot中,打包成JAR包时,无法读取到指定路径下面的文件,不知道记错没,你们可以自己尝试下!!!)
try {
//文件名称
String fileName = "学生数据表.xlsx";
//设置要下载的文件的名称
response.setHeader("Content-disposition", "attachment;fileName=" + fileName);
//通知客服文件的MIME类型
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
//获取文件的路径
String filePath = this.getClass().getResource("/template/" + fileName).getPath();
FileInputStream input = new FileInputStream(filePath);
OutputStream out = response.getOutputStream();
byte[] b = new byte[2048];
int len;
while ((len = input.read(b)) != -1) {
out.write(b, 0, len);
}
//修正 Excel在“xxx.xlsx”中发现不可读取的内容。是否恢复此工作薄的内容?如果信任此工作簿的来源,请点击"是"
response.setHeader("Content-Length", String.valueOf(input.getChannel().size()));
input.close();
} catch (Exception ex) {
log.error("下载模版失败!!!");
ex.printStackTrace();
}
}
excel
模版所放置的位置
六、全局异常处理
6.1、GlobalExceptionHandler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
}
6.2、BaseException
/**
* 业务异常
*/
public class BaseException extends RuntimeException {
public BaseException() {
}
public BaseException(String msg) {
super(msg);
}
}
6.3、UploadExcelException
public class UploadExcelException extends BaseException {
public UploadExcelException(String msg) {
super(msg);
}
}
总结
本文利用EasyExcel
实现了Excel文件的导入功能。本文的主要目的是,从Controller
层到Dao
层全流程演示Excel文件的导入导出以及excel
模版的下载,以后遇到相似的业务需求,复制即可运行。