SpringBoot整合EasyExcel实现文件导入导出

作者:JackHorse 时间:2021-11-07 17:14:51 

目录
  • 准备工作

    • 1. 引入pom依赖

    • 2. 实现功能

  • Excel文件下载

    • 3. 日志实体类

    • 4. 接口和具体实现

  • Excel文件导入

    • 5. 文件读取配置

    • 6. 读取测试

    • 7. 附上自定义属性转换器

准备工作

注意:点击查看官网Demo

1. 引入pom依赖


       <!--easyExcel-->
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>easyexcel</artifactId>
       </dependency>

2. 实现功能

  • 结合Vue前端,实现浏览器页面直接导出日志文件

  • 实现文件的导入

Excel文件下载

3. 日志实体类

实体类里有自定义转换器:用于Java类型数据和Excel类型数据的转换,非常使用。结合注解,可以非常方便的进行Excel文件导出。


/**
* <p>
* 操作日志信息
* </p>
*
* @author horse
* @since 2020-09-08
* 注意: 实体类中如果使用@Accessory(chain=true),那么导入的数据无法填充到实例中,导出数据不受影响
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_operational_log")
@ApiModel(value = "OperationalLog对象", description = "操作日志信息")
public class OperationalLog implements Serializable {

private static final long serialVersionUID = 1L;

@ExcelProperty({"操作日志", "日志ID"})
   @ApiModelProperty(value = "日志ID")
   @TableId(value = "id", type = IdType.ASSIGN_ID)
   private String id;

@ExcelProperty({"操作日志", "操作类型"})
   @ApiModelProperty(value = "操作类型")
   private String operType;

@ExcelProperty({"操作日志", "操作描述"})
   @ApiModelProperty(value = "操作描述")
   private String operDesc;

@ExcelProperty({"操作日志", "操作员ID"})
   @ApiModelProperty(value = "操作员ID")
   private String operUserId;

@ExcelProperty({"操作日志", "操作员名称"})
   @ApiModelProperty(value = "操作员名称")
   private String operUserName;

@ExcelProperty({"操作日志", "操作方法"})
   @ApiModelProperty(value = "操作方法")
   private String operMethod;

@ExcelProperty({"操作日志", "请求方法"})
   @ApiModelProperty(value = "请求方法")
   private String operRequWay;

@ExcelProperty(value = {"操作日志", "请求耗时:单位-ms"}, converter = CustomRequestTimeConverter.class)
   @ApiModelProperty(value = "请求耗时:单位-ms")
   private Long operRequTime;

@ExcelProperty({"操作日志", "请求参数"})
   @ApiModelProperty(value = "请求参数")
   private String operRequParams;

@ExcelProperty({"操作日志", "请求Body"})
   @ApiModelProperty(value = "请求Body")
   private String operRequBody;

@ExcelProperty({"操作日志", "请求IP"})
   @ApiModelProperty(value = "请求IP")
   private String operRequIp;

@ExcelProperty({"操作日志", "请求URL"})
   @ApiModelProperty(value = "请求URL")
   private String operRequUrl;

@ExcelProperty(value = {"操作日志", "日志标识"}, converter = CustomLogFlagConverter.class)
   @ApiModelProperty(value = "日志标识: 1-admin,0-portal")
   private Boolean logFlag;

@ExcelProperty({"操作日志", "操作状态"})
   @ApiModelProperty(value = "操作状态:1-成功,0-失败")
   @TableField(value = "is_success")
   private Boolean success;

@ExcelIgnore
   @ApiModelProperty(value = "逻辑删除 1-未删除, 0-删除")
   @TableField(value = "is_deleted")
   @TableLogic(value = "1", delval = "0")
   private Boolean deleted;

@ExcelProperty(value = {"操作日志", "创建时间"}, converter = CustomTimeFormatConverter.class)
   @ApiModelProperty(value = "创建时间")
   private Date gmtCreate;
}

4. 接口和具体实现

4.1 接口


   @OperatingLog(operType = BlogConstants.EXPORT, operDesc = "导出操作日志,写出到响应流中")
   @ApiOperation(value = "导出操作日志", hidden = true)
   @PostMapping("/oper/export")
   public void operLogExport(@RequestBody List<String> logIds, HttpServletResponse response) {
       operationalLogService.operLogExport(logIds, response);
   }

4.2 具体实现

  • 自定义导出策略HorizontalCellStyleStrategy

  • 自定义导出 * CellWriteHandler,更加精确的自定义导出策略


   /**
    * 导出操作日志(可以考虑分页导出)
    *
    * @param logIds
    * @param response
    */
   @Override
   public void operLogExport(List<String> logIds, HttpServletResponse response) {
       OutputStream outputStream = null;
       try {
           List<OperationalLog> operationalLogs;
           LambdaQueryWrapper<OperationalLog> queryWrapper = new LambdaQueryWrapper<OperationalLog>()
                   .orderByDesc(OperationalLog::getGmtCreate);
           // 如果logIds不为null,按照id查询信息,否则查询全部
           if (!CollectionUtils.isEmpty(logIds)) {
               operationalLogs = this.listByIds(logIds);
           } else {
               operationalLogs = this.list(queryWrapper);
           }
           outputStream = response.getOutputStream();

// 获取单元格样式
           HorizontalCellStyleStrategy strategy = MyCellStyleStrategy.getHorizontalCellStyleStrategy();

// 写入响应输出流数据
           EasyExcel.write(outputStream, OperationalLog.class).excelType(ExcelTypeEnum.XLSX).sheet("操作信息日志")
                   // .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 自适应列宽(不是很适应,效果并不佳)
                   .registerWriteHandler(strategy) // 注册上面设置的格式策略
                   .registerWriteHandler(new CustomCellWriteHandler()) // 设置自定义格式策略
                   .doWrite(operationalLogs);
       } catch (Exception e) {
           log.error(ExceptionUtils.getMessage(e));
           throw new BlogException(ResultCodeEnum.EXCEL_DATA_EXPORT_ERROR);
       } finally {
           IoUtil.close(outputStream);
       }
   }

自定义导出策略简单如下:


/**
* @author Mr.Horse
* @version 1.0
* @description: 单元格样式策略
* @date 2021/4/30 8:43
*/

public class MyCellStyleStrategy {

/**
    * 设置单元格样式(仅用于测试)
    *
    * @return 样式策略
    */
   public static HorizontalCellStyleStrategy getHorizontalCellStyleStrategy() {
       // 表头策略
       WriteCellStyle headerCellStyle = new WriteCellStyle();
       // 表头水平对齐居中
       headerCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
       // 背景色
       headerCellStyle.setFillForegroundColor(IndexedColors.SKY_BLUE.getIndex());
       WriteFont headerFont = new WriteFont();
       headerFont.setFontHeightInPoints((short) 14);
       headerCellStyle.setWriteFont(headerFont);
       // 自动换行
       headerCellStyle.setWrapped(Boolean.FALSE);

// 内容策略
       WriteCellStyle contentCellStyle = new WriteCellStyle();
       // 设置数据允许的数据格式,这里49代表所有可以都允许设置
       contentCellStyle.setDataFormat((short) 49);
       // 设置背景色: 需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
       contentCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
       contentCellStyle.setFillForegroundColor(IndexedColors.GREY_40_PERCENT.getIndex());
       // 设置内容靠左对齐
       contentCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
       // 设置字体
       WriteFont contentFont = new WriteFont();
       contentFont.setFontHeightInPoints((short) 12);
       contentCellStyle.setWriteFont(contentFont);
       // 设置自动换行
       contentCellStyle.setWrapped(Boolean.FALSE);
       // 设置边框样式和颜色
       contentCellStyle.setBorderLeft(MEDIUM);
       contentCellStyle.setBorderTop(MEDIUM);
       contentCellStyle.setBorderRight(MEDIUM);
       contentCellStyle.setBorderBottom(MEDIUM);
       contentCellStyle.setTopBorderColor(IndexedColors.RED.getIndex());
       contentCellStyle.setBottomBorderColor(IndexedColors.GREEN.getIndex());
       contentCellStyle.setLeftBorderColor(IndexedColors.YELLOW.getIndex());
       contentCellStyle.setRightBorderColor(IndexedColors.ORANGE.getIndex());

// 将格式加入单元格样式策略
       return new HorizontalCellStyleStrategy(headerCellStyle, contentCellStyle);
   }
}

自定义导出 * 简单如下:


/**
* @author Mr.Horse
* @version 1.0
* @description 实现CellWriteHandler接口, 实现对单元格样式的精确控制
* @date 2021/4/29 21:11
*/
public class CustomCellWriteHandler implements CellWriteHandler {

private static Logger logger = LoggerFactory.getLogger(CustomCellWriteHandler.class);

@Override
   public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
                                Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {

}

/**
    * 单元格创建之后(没有写入值)
    *
    * @param writeSheetHolder
    * @param writeTableHolder
    * @param cell
    * @param head
    * @param relativeRowIndex
    * @param isHead
    */
   @Override
   public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
                               Head head, Integer relativeRowIndex, Boolean isHead) {

}

@Override
   public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
                                      CellData cellData, Cell cell, Head head, Integer relativeRowIndex,
                                      Boolean isHead) {

}

/**
    * 单元格处理后(已写入值): 设置第一行第一列的头超链接到EasyExcel的官网(本系统的导出的excel 0,1两行都是头,所以只设置第一行的超链接)
    * 这里再进行拦截的单元格样式设置的话,前面该样式将全部失效
    *
    * @param writeSheetHolder
    * @param writeTableHolder
    * @param cellDataList
    * @param cell
    * @param head
    * @param relativeRowIndex
    * @param isHead
    */
   @Override
   public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
                                List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex,
                                Boolean isHead) {
       // 设置超链接
       if (isHead && cell.getRowIndex() == 0 && cell.getColumnIndex() == 0) {
           logger.info(" ==> 第{}行,第{}列超链接设置完成", cell.getRowIndex(), cell.getColumnIndex());
           CreationHelper helper = writeSheetHolder.getSheet().getWorkbook().getCreationHelper();
           Hyperlink hyperlink = helper.createHyperlink(HyperlinkType.URL);
           hyperlink.setAddress("https://github.com/alibaba/easyexcel");
           cell.setHyperlink(hyperlink);
       }
       // 精确设置单元格格式
       boolean bool = isHead && cell.getRowIndex() == 1 &&
               (cell.getStringCellValue().equals("请求参数") || cell.getStringCellValue().equals("请求Body"));
       if (bool) {
           logger.info("第{}行,第{}列单元格样式设置完成。", cell.getRowIndex(), cell.getColumnIndex());
           // 获取工作簿
           Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
           CellStyle cellStyle = workbook.createCellStyle();

Font cellFont = workbook.createFont();
           cellFont.setBold(Boolean.TRUE);
           cellFont.setFontHeightInPoints((short) 14);
           cellFont.setColor(IndexedColors.SEA_GREEN.getIndex());
           cellStyle.setFont(cellFont);
           cell.setCellStyle(cellStyle);
       }
   }
}

4.3 前端请求

前端在基于Vue+Element的基础上实现了点击导出按钮,在浏览器页面进行下载。


// 批量导出
   batchExport() {
     // 遍历获取id集合列表
     const logIds = []
     this.multipleSelection.forEach(item => {
       logIds.push(item.id)
     })
      // 请求后端接口
     axios({
       url: this.BASE_API + '/admin/blog/log/oper/export',
       method: 'post',
       data: logIds,
       responseType: 'arraybuffer',
       headers: { 'token': getToken() }
     }).then(response => {
       // type类型可以设置为文本类型,这里是新版excel类型
       const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8' })
       const pdfUrl = window.URL.createObjectURL(blob)
       const fileName = 'HorseBlog操作日志' // 下载文件的名字
       // 对于<a>标签,只有 Firefox 和 Chrome(内核)支持 download 属性
       if ('download' in document.createElement('a')) {
         const link = document.createElement('a')
         link.href = pdfUrl
         link.setAttribute('download', fileName)
         document.body.appendChild(link)
         link.click()
         window.URL.revokeObjectURL(pdfUrl) // 释放URL 对象
       } else {
         // IE 浏览器兼容方法
         window.navigator.msSaveBlob(blob, fileName)
       }
     })
   }

测试结果:还行,基本实现了页面下载的功能

Excel文件导入

5. 文件读取配置

本配置基于泛型的方式编写,可扩展性较强。


/**
* @author Mr.Horse
* @version 1.0
* @description: EasyExcel文件读取配置(不能让spring管理)
* @date 2021/4/27 13:24
*/

public class MyExcelImportConfig<T> extends AnalysisEventListener<T> {

private static Logger logger = LoggerFactory.getLogger(MyExcelImportConfig.class);

/**
    * 每次读取的最大数据条数
    */
   private static final int MAX_BATCH_COUNT = 10;

/**
    * 泛型bean属性
    */
   private T dynamicService;

/**
    * 可接收任何参数的泛型List集合
    */
   List<T> list = new ArrayList<>();

/**
    * 构造函数注入bean(根据传入的bean动态注入)
    *
    * @param dynamicService
    */
   public MyExcelImportConfig(T dynamicService) {
       this.dynamicService = dynamicService;
   }

/**
    * 解析每条数据都进行调用
    *
    * @param data
    * @param context
    */
   @Override
   public void invoke(T data, AnalysisContext context) {
       logger.info(" ==> 解析一条数据: {}", JacksonUtils.objToString(data));
       list.add(data);
       if (list.size() > MAX_BATCH_COUNT) {
           // 保存数据
           saveData();
           // 清空list
           list.clear();
       }
   }

/**
    * 所有数据解析完成后,会来调用一次
    * 作用: 避免最后集合中小于 MAX_BATCH_COUNT 条的数据没有被保存
    *
    * @param context
    */
   @Override
   public void doAfterAllAnalysed(AnalysisContext context) {
       saveData();
       logger.info(" ==> 数据解析完成 <==");
   }

/**
    * 保存数据: 正式应该插入数据库,这里用于测试
    */
   private void saveData() {
       logger.info(" ==> 数据保存开始: {}", list.size());
       list.forEach(System.out::println);
       logger.info(" ==> 数据保存结束 <==");
   }

/**
    * 在转换异常 获取其他异常下会调用本接口。我们如果捕捉并手动抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
    *
    * @param exception
    * @param context
    * @throws Exception
    */
   @Override
   public void onException(Exception exception, AnalysisContext context) throws Exception {
       logger.error(" ==> 数据解析失败,但是继续读取下一行:{}", exception.getMessage());
       //  如果是某一个单元格的转换异常 能获取到具体行号
       if (exception instanceof ExcelDataConvertException) {
           ExcelDataConvertException convertException = (ExcelDataConvertException) exception;
           logger.error("第{}行,第{}列数据解析异常", convertException.getRowIndex(), convertException.getColumnIndex());
       }
   }

}

6. 读取测试


   @ApiOperation(value = "数据导入测试", notes = "操作日志导入测试[OperationalLog]", hidden = true)
   @PostMapping("/import")
   public R excelImport(@RequestParam("file") MultipartFile file) throws IOException {
       EasyExcel.read(file.getInputStream(), OperationalLog.class, new MyExcelImportConfig<>(operationalLogService))
               .sheet().doRead();
       return R.ok().message("文件导入成功");
   }

7. 附上自定义属性转换器

转换器的属性内容转换,需要根据自己的实际业务需求而定,这里仅作为简单示例


/**
* @author Mr.Horse
* @version 1.0
* @description: 自定义excel转换器: 将操作日志的请求耗时加上单位 "ms"
* @date 2021/4/27 10:25
*/

public class CustomRequestTimeConverter implements Converter<Long> {

/**
    * 读取数据时: 属性对应的java数据类型
    *
    * @return
    */
   @Override
   public Class<Long> supportJavaTypeKey() {
       return Long.class;
   }

/**
    * 写入数据时: excel内部的数据类型,因为请求耗时是long类型,对应excel是NUMBER类型,但是加上"ms后对应的是STRING类型"
    *
    * @return
    */
   @Override
   public CellDataTypeEnum supportExcelTypeKey() {
       return CellDataTypeEnum.STRING;
   }

/**
    * 读取回调
    *
    * @param cellData
    * @param contentProperty
    * @param globalConfiguration
    * @return
    * @throws Exception
    */
   @Override
   public Long convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
       // 截取字符串: "ms",转换为long类型
       String value = cellData.getStringValue();
       return Long.valueOf(value.substring(0, value.length() - 2));
   }

@Override
   public CellData<Long> convertToExcelData(Long value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
       // 添加字符串: "ms"
       return new CellData<>(String.valueOf(value).concat("ms"));
   }
}

格式化时间


/**
* @author Mr.Horse
* @version 1.0
* @description: {description}
* @date 2021/4/27 14:01
*/

public class CustomTimeFormatConverter implements Converter<Date> {

@Override
   public Class<Date> supportJavaTypeKey() {
       return Date.class;
   }

@Override
   public CellDataTypeEnum supportExcelTypeKey() {
       return CellDataTypeEnum.STRING;
   }

@Override
   public Date convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
       String value = cellData.getStringValue();
       return DateUtil.parse(value, DatePattern.NORM_DATETIME_PATTERN);
   }

@Override
   public CellData<Date> convertToExcelData(Date value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
       return new CellData<>(DateUtil.format(value, DatePattern.NORM_DATETIME_PATTERN));
   }
}

EasyExcel简单使用,到此结束,打完收功。

来源:https://juejin.cn/post/6956953740158959629

标签:SpringBoot,EasyExcel,文件导入导出
0
投稿

猜你喜欢

  • Java操作redis实现增删查改功能的方法示例

    2022-01-04 07:24:24
  • C#合并BitMap图像生成超大bitmap

    2023-12-08 18:24:11
  • 使用自定义注解实现redisson分布式锁

    2021-09-30 18:31:22
  • 解决mybatis-plus3.1.1版本使用lambda表达式查询报错的方法

    2022-03-19 03:55:09
  • C语言中传值与传指针的介绍与区别

    2023-08-01 15:26:51
  • C#中怎样从指定字符串中查找并替换字符串?

    2023-09-30 14:26:24
  • IntelliJ IDEA各种图标的含义

    2022-08-12 21:50:25
  • Java基础知识精通块作用域与条件及switch语句

    2022-12-21 17:50:07
  • 解决Spring Cloud Feign 请求时附带请求头的问题

    2022-12-11 04:29:46
  • C#中DataGridView操作技巧

    2023-05-03 20:37:30
  • 一文搞懂c# await,async执行流

    2023-07-14 01:22:35
  • Unity3D快速入门教程

    2022-04-03 14:11:14
  • Android Activity生命周期调用的理解

    2023-05-13 14:15:42
  • java实现http的Post、Get、代理访问请求

    2021-10-30 08:13:47
  • Android RetainFragment状态保存的方法

    2023-06-03 03:35:08
  • Java程序控制逻辑—流程控制

    2023-08-28 01:51:18
  • Android支付宝支付开发实例

    2023-01-01 09:15:18
  • Android中的全局变量与局部变量使用小结

    2023-12-07 10:11:49
  • java 分转元与元转分实现操作

    2023-05-13 16:47:22
  • JPype实现在python中调用JAVA的实例

    2022-08-24 03:19:52
  • asp之家 软件编程 m.aspxhome.com