马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
一、阿里巴巴EasyExcel的上风
首先说下EasyExcel相对 Apache poi的上风:
EasyExcel也是阿里研发在poi基础上做了封装,改进产物。它替开发者做了注解列表分析,表格添补等一系列代码编写工作,并将此抽象成通用和可扩展的框架。相对poi,在数据量比较大的时候,它有着更精良的性能表现。导出的时候,easyexcel使用优化的反射技术,制止poi频仍的去创建cell和row对象;导入的时候,它的分析器AnalysisEventListener,可设置批量阈值 BATCH_COUNT,达到阈值就往数据库插入数据,然后清空分析器内部缓存,相同的表格,easyexcel导入所占用的内存要比poi节流90%,制止了大数据量导入的时候,造成的内存占用井喷(这使得stop the world的时间可能会被集中,而体系可能会出现短暂的停摆。),而GC不能均衡变动垃圾回收。同时也制止堆积数据后,sql的巨量数据的批量插入,导致超出mybatis批量插入语句能蒙受的最大长度限制。
二、EasyExcel核心util类
- @Slf4j
- public class EasyExcels {
- public static final String EXT_NAME_XLSX = "xlsx";
- public static final String EXT_NAME_XLS = "xls";
- /**
- *
- * @param response
- * @param data
- * @param filename
- * @param sheetName
- * @param selectMap 自定义下拉列,但是既然数据都导出了,下拉用处何在?这个需求比较少
- * @param <T>
- * @throws IOException
- */
- public static <T> void write(HttpServletResponse response, List<T> data, String filename, String sheetName,
- List<KeyValue<ExcelColumn, List<String>>> selectMap) throws IOException {
- setResponse(response, filename);
- if (StringUtils.isBlank(sheetName)) {
- sheetName = filename;
- }
- // 输出 Excel
- try {
- EasyExcel.write(response.getOutputStream(), data != null && !data.isEmpty() ? data.get(0).getClass() : null)
- .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
- .registerWriteHandler(new CustomCellWriteWeightConfig()) // Excel 列宽自适应
- .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy) //内容样式
- .registerWriteHandler(new SelectWriteHandler(selectMap)) // 基于固定 sheet 实现下拉框
- .sheet(sheetName).doWrite(data);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- response.getOutputStream().close();
- }
- }
- // 简单导入读取,不做解析,不做校验
- public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
- return EasyExcel.read(file.getInputStream(), head, null)
- .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
- .doReadAllSync();
- }
- // 需要配合监听器解析数据
- public static <T> void read(MultipartFile file, Class<T> head, ReadListener<T> listener) throws IOException {
- EasyExcel.read(file.getInputStream(), head, listener)
- .sheet()
- .doRead();
- }
- // 不带下拉列的导出,用的比较多
- public static <T> void export(HttpServletResponse response, List<T> data, String filename, String sheetName) throws IOException {
- setResponse(response, filename);
- if (StringUtils.isBlank(sheetName)) {
- sheetName = filename;
- }
- EasyExcel.write(response.getOutputStream(), data != null && !data.isEmpty() ? data.get(0).getClass() : null)
- .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
- .registerWriteHandler(new CustomCellWriteWeightConfig())
- .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy)
- .sheet(sheetName).doWrite(data);
- }
- // 用于合并单元格列的导出
- public static <T> void export(HttpServletResponse response, List<T> data, String filename, String sheetName, RowWriteHandler handler) throws IOException {
- setResponse(response, filename);
- EasyExcel.write(response.getOutputStream(), data != null && !data.isEmpty() ? data.get(0).getClass() : null)
- .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
- .registerWriteHandler(new CustomCellWriteWeightConfig())
- .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy)
- .registerWriteHandler(handler)
- .sheet(sheetName).doWrite(data);
- }
- // 用于导出表头模板,填充导入数据用的excel模板,因为是模板,所以肯定会有下拉列的需求
- public static <T> void export(HttpServletResponse response, Class<T> clazz, String filename) throws IOException {
- setResponse(response, filename);
- Map<Integer, ExcelSelectedResolve> selectedMap = resolveSelectedAnnotation(clazz);
- EasyExcel.write(response.getOutputStream(), clazz)
- .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
- // .registerWriteHandler(new CustomCellWriteHeightConfig())
- .registerWriteHandler(new CustomCellWriteWeightConfig())
- .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy)
- .registerWriteHandler(new SelectSheetWriteHandler(selectedMap))
- .sheet(filename).doWrite(Collections.emptyList());
- }
- /**
- * 解析表头类中的下拉注解
- * @param head 表头类
- * @param <T> 泛型
- * @return Map<下拉框列索引, 下拉框内容> map
- */
- private static <T> Map<Integer, ExcelSelectedResolve> resolveSelectedAnnotation(Class<T> head) {
- Map<Integer, ExcelSelectedResolve> selectedMap = new HashMap<>();
- // getDeclaredFields(): 返回全部声明的属性;getFields(): 返回public类型的属性
- Field[] fields = head.getDeclaredFields();
- for (int i = 0; i < fields.length; i++){
- Field field = fields[i];
- // 解析注解信息
- ExcelSelected selected = field.getAnnotation(ExcelSelected.class);
- ExcelProperty property = field.getAnnotation(ExcelProperty.class);
- if (selected != null) {
- ExcelSelectedResolve excelSelectedResolve = new ExcelSelectedResolve();
- String[] source = excelSelectedResolve.resolveSelectedSource(selected);
- if (source != null && source.length > 0){
- excelSelectedResolve.setSource(source);
- excelSelectedResolve.setFirstRow(selected.firstRow());
- excelSelectedResolve.setLastRow(selected.lastRow());
- if (property != null && property.index() >= 0){
- selectedMap.put(property.index(), excelSelectedResolve);
- } else {
- selectedMap.put(i, excelSelectedResolve);
- }
- }
- }
- }
- return selectedMap;
- }
- public static void setResponse(HttpServletResponse response, String filename) throws IOException {
- setResponse(response, filename, EXT_NAME_XLSX);
- }
- public static void setResponse(HttpServletResponse response, String filename, String extName) throws IOException {
- String exportFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.name());
- response.setContentType("application/vnd.ms-excel");
- response.setCharacterEncoding("utf-8");
- response.setHeader("Access-Control-Expose-Headers", "token,Content-Type,Content-disposition");
- response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie, token,Content-Type,Content-disposition");
- response.setHeader("Content-disposition", exportFilename + "." + extName);
- }
- }
复制代码
三、导入分析监听器
那要使用easyexcel,首先要解决分析器抽象类的实现:
当时第一次使用easyexcel的时候,对这个工具框架不熟悉,项目时间被催的紧,没时间去做设计,当时修改每个类的字段注解index属性,每个字段单独写校验语句,简直苦不堪言。我只想说,磨刀不误砍柴工,不注重设计的公司,只会被延误更多的时间。
- /**
- * @Title: ExcelImportReadListener
- * @Description: 其他人如果觉得invoke()方法不满足其需求,可以自己实现一下
- * @Author: wenrong
- * @Date: 2024/4/25 17:08
- * @Version:1.0
- */
- @Data
- public abstract class ExcelImportReadListener<T extends ValidateBaseBo> extends AnalysisEventListener<T> {
- private static final Logger log = LoggerFactory.getLogger("excelReadListener");
- public static int BATCH_COUNT = 1000;
- private AtomicLong successNum = new AtomicLong();
- private final Class<T> clazz;
- private Validator<T> validator;
- private List<T> successData = new ArrayList<>();
- private List<T> failureData = new ArrayList<>();
- public ExcelImportReadListener(Class<T> clazz) {
- this.clazz = clazz;
- }
- @Override
- public void invoke(T data, AnalysisContext context) {
- log.info("解析到一条数据:{}", JSONObject.toJSON(data));
- StringBuilder errMsg = new StringBuilder();
- try {
- //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
- errMsg.append(EasyExcelValidateHandler.validateEntity(data));
- // 额外自定义校验,以及设置数据属性的逻辑
- if (validator != null) {
- errMsg.append(validator.validate(data));
- }
- } catch (NoSuchFieldException e) {
- log.error(e.getMessage());
- }
- if (StringUtils.isNotEmpty(errMsg.toString())) {
- data.setErrMsg(errMsg.toString());
- failureData.add(data);
- } else {
- successData.add(data);
- successNum.incrementAndGet();
- }
- if (BATCH_COUNT != 0 && successData.size() >= BATCH_COUNT) {
- try {
- saveData();
- } catch (Exception e) {
- log.error(e.getMessage(), e);
- }
- successData.clear();
- }
- }
- @Override
- public final void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
- // 验证表头
- if (headMap.isEmpty()) {
- throw new ServiceException("无表头");
- }
- if (clazz != null) {
- try {
- Map<Integer, String> indexNameMap = getIndexNameMap(clazz);
- for (Integer index : indexNameMap.keySet()) {
- log.info("表头字段:{}", headMap.get(index));
- if (StringUtils.isEmpty(headMap.get(index))) {
- throw new ServiceException("未设置index");
- }
- // 对比excel表头和解析数据的java实体类的,看是否匹配
- if (!headMap.get(index).equals(indexNameMap.get(index))) {
- throw new ServiceException("导入模板错误");
- }
- }
- } catch (NoSuchFieldException e) {
- log.error(e.getMessage(), e);
- }
- }
- }
- @Override
- public final void doAfterAllAnalysed(AnalysisContext context) {
- log.info("所有数据解析完成!共校验成功{}条数据,校验失败{}条数据", successNum.get(), failureData.size());
- try {
- saveData();
- } catch (Exception e) {
- log.error(e.getMessage(), e);
- }
- }
- /**
- * 将该类做成抽象类,在各service中实现saveDate方法,
- * 不侵入业务,同时不会让解析占用内存
- */
- public void saveData() throws Exception {
- log.info("开始往数据库插入数据");
- }
- private Map<Integer, String> getIndexNameMap(Class<T> clazz) throws NoSuchFieldException {
- Map<Integer, String> excelPropertyMap = new HashMap<>();
- Field field;
- Field[] fields = clazz.getDeclaredFields();
- int sequence = 0;
- for (Field item : fields) {
- field = clazz.getDeclaredField(item.getName());
- field.setAccessible(true);
- ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
- if (excelProperty != null) {
- // 避免每个列都要写index,插入或删除一个字段,所有的index都需要修改。默认为java实体类中字段的顺序。
- int index = excelProperty.index() == -1 ? sequence : excelProperty.index();
- String[] values = excelProperty.value();
- StringBuilder value = new StringBuilder();
- for (String v : values) {
- value.append(v);
- }
- excelPropertyMap.put(index, value.toString());
- sequence++;
- }
- }
- return excelPropertyMap;
- }
- }
复制代码 3.1、分析成功的数据直接落库,错误数据导出
ValidateBaseBo:用在导入的时候,将校验的错误保留下来,然后再把有问题的数据过滤出来,再导出,或者表现在前端的导入效果里,操纵者可以按照错误信息把表格里的数据修改好后,再次导入,而且只将导入失败的数据导出,不用去原表中大片的数据中去找有错误信息的数据,目标是方便操纵者快速定位表格里的问题数据。
- /**
- * @Title: ValidateBaseBo
- * @Description:
- * @Author: wenrong
- * @Date: 2024/10/17 上午11:02
- * @Version:1.0
- */
- @Data
- public abstract class ValidateBaseBo {
- @ExcelProperty(value = "错误信息")
- @TableField(exist = false)
- @ApiModelProperty(hidden = true)
- private String errMsg;
- }
复制代码 3.2、分析过程中校验数据精确性
除了javax.validation,基础的注解校验之外,假如还必要额外的校验,就自界说校验器作增补。
- /**
- * @Title: ValidData
- * @Description: javax.validation 以外校验函数
- * @Author: wenrong
- * @Date: 2024/4/26 19:51
- * @Version:1.0
- */
- public interface Validator<T> {
- /**
- * 这里的实现方法,最后返回的如果为null,一定要返回"",否则会被转化为"null"
- *
- * @param T t
- * @return ""
- */
- String validate(T t);
- }
复制代码
3.3、导入/导出 Convertor
excel导入数据对应的实体类:要注意表格中的汉字和现实存入到数据库中数值的转换:Convertor
- /**
- * @author wenrong
- * @date 2024-11-25 17:38:26
- */
- @Data
- @Builder
- @NoArgsConstructor
- @AllArgsConstructor
- @TableName("····")
- public class YourClass extends ValidateBaseBo implements Serializable {
- private static final long serialVersionUID = 1L;
- public static Map<String, DyzProvinceSchool> provinceMap = new HashMap<>();
- public static Map<String, DyzProvinceSchool> schoolMap = new HashMap<>();
- @ExcelIgnore
- @TableId(type = IdType.AUTO)
- private Long id;
- @ApiModelProperty(value = "省份ID")
- @ExcelProperty(value = "省份", converter = ProvinceConvertor.class)
- @NotNull(message = "不能为空")
- private Integer provinceId;
- @ApiModelProperty(value = "学校ID")
- @ExcelProperty(value = "学校", converter = SchoolConvertor.class)
- @NotNull(message = "不能为空")
- private Integer schoolId;
- @ApiModelProperty(value = "节目代码")
- @ExcelProperty(value = "节目代码")
- @NotNull(message = "不能为空")
- private String worksNumber;
- @ApiModelProperty(value = "节目/项目名称")
- @ExcelProperty(value = "节目/项目名称")
- @NotNull(message = "不能为空")
- private String ``````;
- public String validate(Map<Integer, DyzProvinceSchool> provinceMap, Map<Integer, DyzProvinceSchool> schoolMap) {
- StringBuilder sb = new StringBuilder(this.getErrMsg() == null ? "" : this.getErrMsg());
- if (provinceMap.get(this.provinceId) == null) {
- sb.append("省份不存在: ").append(provinceId);
- }
- if (schoolMap.get(this.schoolId) == null) {
- sb.append("学校不存在: ").append(schoolId);
- }
- return sb.toString();
- }
- public static class GroupTypeConvertor implements Converter<Integer> {
- //导入的时候,将表格的汉字转换成java对应数据库的字段
- @Override
- public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
- GlobalConfiguration globalConfiguration) {
- switch (cellData.getStringValue()) {
- default:
- return 2;
- case "小学组":
- return 0;
- case "中学组":
- return 1;
- }
- }
-
- // 导出的时候,将数据库中存储的值,转换为用户能看懂的汉字
- @Override
- public WriteCellData<?> convertToExcelData(Integer value,
- ExcelContentProperty excelContentProperty,
- GlobalConfiguration globalConfiguration) {
- switch (value) {
- default:
- return new WriteCellData<>("其他组");
- case 0:
- return new WriteCellData<>("小学组");
- case 1:
- return new WriteCellData<>("中学组");
- }
- }
- }
- public static class PresentConvertor implements Converter<Integer> {
- @Override
- public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
- GlobalConfiguration globalConfiguration) {
- if (cellData.getStringValue().equals("否")) {
- return 0;
- } else {
- return 1;
- }
- }
-
- @Override
- public WriteCellData<?> convertToExcelData(Integer value,
- ExcelContentProperty excelContentProperty,
- GlobalConfiguration globalConfiguration) {
- switch (value) {
- default:
- return new WriteCellData<>("-");
- case 0:
- return new WriteCellData<>("否");
- case 1:
- return new WriteCellData<>("是");
- }
- }
- }
- public static class ProvinceConvertor implements Converter<Integer> {
- @Override
- public Integer convertToJavaData(ReadCellData<?> cellData,
- ExcelContentProperty contentProperty,
- GlobalConfiguration globalConfiguration) throws Exception {
- if (provinceMap.isEmpty()) {
- throw new Exception("省份配置数据为空");
- }
- return StringUtils.isBlank(cellData.getStringValue()) ? null : provinceMap.get(cellData.getStringValue()).getId();
- }
- }
- public static class SchoolConvertor implements Converter<Integer> {
- @Override
- public Integer convertToJavaData(ReadCellData<?> cellData,
- ExcelContentProperty contentProperty,
- GlobalConfiguration globalConfiguration) throws Exception {
- if (schoolMap.isEmpty()) {
- throw new Exception("学校配置数据为空");
- }
- return StringUtils.isBlank(cellData.getStringValue()) ? null : schoolMap.get(cellData.getStringValue()).getId();
- }
-
- // 导出转换省略掉
- }
- }
复制代码
3.4、实现导入分析监听器
上面的分析监听器是个抽象类,是一种模板模式的设计头脑应用,我们根据不同的业务,自己扩展invoke方法和saveData方法,但其实saveData也可以做成模板方法,只是必要依靠内部绑定一个数据层dao接口,Mapper,对于有的人来说,会耦合dao层,但我以为假如dao层取一个接口,那么也没什么耦合的问题。节流不必要的重复代码,还是值得的。
那么上述的谁人模板抽象分析监听器可以改为:
- /**
- * @Title: ExcelImportReadListener
- * @Description: 其他人如果觉得invoke()方法不满足其需求,可以自己实现一下
- * @Author: wenrong
- * @Date: 2024/4/25 17:08
- * @Version:1.0
- */
- @Data
- public abstract class ExcelImportReadListener<T extends ValidateBaseBo, S extends IService<T>> extends AnalysisEventListener<T> {
- private static final Logger log = LoggerFactory.getLogger("excelReadListener");
- public static int BATCH_COUNT = 1000;
- private AtomicLong successNum = new AtomicLong();
- private final Class<T> clazz;
- private S service;
- private Validator<T> validator;
- private List<T> successData = new ArrayList<>();
- private List<T> failureData = new ArrayList<>();
- public ExcelImportReadListener(Class<T> clazz, S service) {
- this.clazz = clazz;
- this.service = service;
- }
- @Override
- public void invoke(T data, AnalysisContext context) {
- log.info("解析到一条数据:{}", JSONObject.toJSON(data));
- StringBuilder errMsg = new StringBuilder();
- try {
- //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
- errMsg.append(EasyExcelValidateHandler.validateEntity(data));
- // 额外自定义校验,以及设置数据属性的逻辑
- if (validator != null) {
- errMsg.append(validator.validate(data));
- }
- } catch (NoSuchFieldException e) {
- log.error(e.getMessage());
- }
- if (StringUtils.isNotEmpty(errMsg.toString())) {
- data.setErrMsg(errMsg.toString());
- failureData.add(data);
- } else {
- successData.add(data);
- successNum.incrementAndGet();
- }
- if (BATCH_COUNT != 0 && successData.size() >= BATCH_COUNT) {
- try {
- saveData();
- } catch (Exception e) {
- log.error(e.getMessage(), e);
- }
- successData.clear();
- }
- }
- @Override
- public final void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
- // 验证表头
- if (headMap.isEmpty()) {
- throw new ServiceException("无表头");
- }
- if (clazz != null) {
- try {
- Map<Integer, String> indexNameMap = getIndexNameMap(clazz);
- for (Integer index : indexNameMap.keySet()) {
- log.info("表头字段:{}", headMap.get(index));
- if (StringUtils.isEmpty(headMap.get(index))) {
- throw new ServiceException("未设置index");
- }
- if (!headMap.get(index).equals(indexNameMap.get(index))) {
- throw new ServiceException("导入模板错误");
- }
- }
- } catch (NoSuchFieldException e) {
- log.error(e.getMessage(), e);
- }
- }
- }
- @Override
- public final void doAfterAllAnalysed(AnalysisContext context) {
- log.info("所有数据解析完成!共校验成功{}条数据,校验失败{}条数据", successNum.get(), failureData.size());
- try {
- saveData();
- } catch (Exception e) {
- log.error(e.getMessage(), e);
- }
- }
- /**
- * 将该类做成抽象类,在各service中实现saveDate方法,
- * 不侵入业务,同时不会让解析占用内存
- */
- public void saveData() throws Exception {
- log.info("开始往数据库插入数据");
- List<T> successData = this.getSuccessData();
- List<T> failureData = this.getFailureData();
- boolean saved = service.saveBatch(successData);
- if (!saved) {
- successData.forEach(work -> work.setErrMsg("保存失败"));
- failureData.addAll(successData);
- } else {
- this.setSuccessData(successData);
- }
- }
- private Map<Integer, String> getIndexNameMap(Class<T> clazz) throws NoSuchFieldException {
- Map<Integer, String> excelPropertyMap = new HashMap<>();
- Field field;
- Field[] fields = clazz.getDeclaredFields();
- int sequence = 0;
- for (Field item : fields) {
- field = clazz.getDeclaredField(item.getName());
- field.setAccessible(true);
- ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
- if (excelProperty != null) {
- int index = excelProperty.index() == -1 ? sequence : excelProperty.index();
- String[] values = excelProperty.value();
- StringBuilder value = new StringBuilder();
- for (String v : values) {
- value.append(v);
- }
- excelPropertyMap.put(index, value.toString());
- sequence++;
- }
- }
- return excelPropertyMap;
- }
- }
复制代码 业务代码中实现模板分析监听器的代码示例:
- @Override
- @Transactional
- public ExcelImportReadListener<BasicWorks> importExcel(MultipartFile file) throws IOException {
- List<DyzProvinceSchool> schoolList = dyzProvinceSchoolService.getSchoolList();
- List<DyzProvinceSchool> provinceList = dyzProvinceSchoolService.getProvinceList();
- Map<String, DyzProvinceSchool> schoolMap = schoolList.stream().collect(Collectors.toMap(DyzProvinceSchool::getSchoolName, s -> s));
- Map<Integer, DyzProvinceSchool> schoolMap1 = schoolList.stream().collect(Collectors.toMap(DyzProvinceSchool::getId, s -> s));
- Map<String, DyzProvinceSchool> provinceMap = provinceList.stream().collect(Collectors.toMap(DyzProvinceSchool::getProvinceName, s -> s));
- Map<Integer, DyzProvinceSchool> provinceMap1 = provinceList.stream().collect(Collectors.toMap(DyzProvinceSchool::getId, s -> s));
- BasicWorks.schoolMap = schoolMap;
- BasicWorks.provinceMap = provinceMap;
- // 匿名内部类扩展模板监听器
- ExcelImportReadListener<BasicWorks> readListener = new ExcelImportReadListener<BasicWorks>(BasicWorks.class) {
- @Override
- public void invoke(BasicWorks data, AnalysisContext context) {
- Set<String> allDataExistInExcel = new HashSet<>();
- Set<String> allDataExistInDataSource = list().stream().map(BasicWorks::getWorksNumber).collect(Collectors.toSet());
- List<BasicWorks> failureData = this.getFailureData();
- StringBuilder errMsg = new StringBuilder(data.getErrMsg() == null ? "" : data.getErrMsg());
- if (StringUtils.isBlank(data.getWorksNumber())) {
- errMsg.append("节目代码不能为空,");
- data.setErrMsg(errMsg.toString());
- failureData.add(data);
- } else {
- if (allDataExistInExcel.contains(data.getWorksNumber())) {
- errMsg.append("Excel表格中存在重复的数据,")
- .append("节目代码:").append(data.getWorksNumber());
- data.setErrMsg(errMsg.toString());
- failureData.add(data);
- allDataExistInExcel.add(data.getWorksNumber());
- }
- if (allDataExistInDataSource.contains(data.getWorksNumber())) {
- errMsg.append("数据库中存在重复的数据,")
- .append("节目代码:").append(data.getWorksNumber());
- data.setErrMsg(errMsg.toString());
- failureData.add(data);
- allDataExistInExcel.add(data.getWorksNumber());
- }
- }
- allDataExistInExcel.add(data.getWorksNumber());
- super.invoke(data, context);
- }
-
- // 设置javax.validation以外校验器,将会在invoke方法里执行校验
- readListener.setValidator(work -> work.validate(provinceMap1, schoolMap1));
- // 导入 Excel
- EasyExcels.read(file, BasicWorks.class, readListener);
- return readListener;
- }
复制代码
另外还有必要将表格中图片导入后主动上传到文件服务,然后将url生存在数据库的需求:
- public ExcelImportReadListener<BasicHotel> importExcel(MultipartFile file) throws IOException {
- //获取图片,联合Apache 的ExcelUtil,ExcelPicUtil工具类,获取图片数据对象PictureData
- ExcelReader reader = ExcelUtil.getReader(file.getInputStream());
- Map<String, PictureData> picMap = ExcelPicUtil.getPicMap(reader.getWorkbook(), 0);
- ExcelImportReadListener<BasicHotel> readListener = new ExcelImportReadListener<BasicHotel>(BasicHotel.class) {
- @Override
- public void invoke(BasicHotel data, AnalysisContext context) {
- Set<String> allDataExistInDataSource = list().stream().map(BasicHotel::getHotelName).collect(Collectors.toSet());
- Set<String> allDataExistInExcel = new HashSet<>();
- List<BasicHotel> failureData = this.getFailureData();
- StringBuilder errMsg = new StringBuilder(data.getErrMsg() == null ? "" : data.getErrMsg());
- if (StringUtils.isEmpty(data.getErrMsg())) {
- errMsg.append("酒店名称不能为空,");
- data.setErrMsg(errMsg.toString());
- failureData.add(data);
- } else {
- if (allDataExistInExcel.contains(data.getHotelName())) {
- errMsg.append("Excel表格中存在重复的数据,")
- .append("酒店名称:").append(data.getHotelName());
- data.setErrMsg(errMsg.toString());
- failureData.add(data);
- allDataExistInExcel.add(data.getHotelName());
- }
- if (allDataExistInDataSource.contains(data.getHotelName())) {
- errMsg.append("数据库中存在重复的数据,")
- .append("酒店名称:").append(data.getHotelName());
- data.setErrMsg(errMsg.toString());
- failureData.add(data);
- allDataExistInExcel.add(data.getHotelName());
- }
- }
- allDataExistInExcel.add(data.getHotelName());
- String err = "";
- int rowIndex = context.readRowHolder().getRowIndex() + 1;
- PictureData pictureData = picMap.get(rowIndex + "_0");
- if (pictureData == null) {
- err = String.format(data.getErrMsg() + "第%s行,%s", rowIndex, "酒店照片为空");
- }
- try {
- // 上传图片
- String fileUrl = ossFileController.ftpUploadFile(pictureData.getData(), "", data.getHotelName());
- data.setPicture(fileUrl);
- } catch (IOException ex) {
- err = String.format(data.getErrMsg() + "第%s行,%s", rowIndex, "酒店照片为空上传失败");
- }
- data.setErrMsg(err);
- super.invoke(data, context);
- }
- };
- // 导入 Excel
- EasyExcels.read(file, BasicHotel.class, readListener);
- return readListener;
- }
复制代码 这是导入部分,导出部分,五花八门的需求就比较多了。
四、导出
4.1、导出 数据导入模板
模板一样平常会有下拉选项列的需求,下拉列一样平常用注解枚举几个就行了:
- import java.lang.annotation.*;
- /**
- * 标注导出的列为下拉框类型,并为下拉框设置内容
- */
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.FIELD)
- public @interface ExcelSelected {
- /**
- * 固定下拉内容
- */
- String[] source() default {};
- /**
- * 设置下拉框的起始行,默认为第二行
- */
- int firstRow() default 1;
- /**
- * 设置下拉框的结束行,默认为最后一行
- */
- int lastRow() default 0x10000;
- }
复制代码 下拉注解分析器:
- @Data
- @Slf4j
- public class ExcelSelectedResolve {
- /**
- * 下拉内容
- */
- private String[] source;
- /**
- * 设置下拉框的起始行,默认为第二行
- */
- private int firstRow;
- /**
- * 设置下拉框的结束行,默认为最后一行
- */
- private int lastRow;
- public String[] resolveSelectedSource(ExcelSelected excelSelected) {
- if (excelSelected == null) {
- return null;
- }
- // 获取固定下拉框的内容
- String[] source = excelSelected.source();
- if (source.length > 0) {
- return source;
- }
- // // 获取动态下拉框的内容
- // Class<? extends ExcelDynamicSelect>[] classes = excelSelected.sourceClass();
- // if (classes.length > 0) {
- // try {
- // ExcelDynamicSelect excelDynamicSelect = classes[0].newInstance();
- // String[] dynamicSelectSource = excelDynamicSelect.getSource();
- // if (dynamicSelectSource != null && dynamicSelectSource.length > 0) {
- // return dynamicSelectSource;
- // }
- // } catch (InstantiationException | IllegalAccessException e) {
- // log.error("解析动态下拉框数据异常", e);
- // }
- // }
- return null;
- }
- }
复制代码 下拉handler:
- public class SelectSheetWriteHandler implements SheetWriteHandler {
- private final Map<Integer, ExcelSelectedResolve> selectedMap;
- public SelectSheetWriteHandler(Map<Integer, ExcelSelectedResolve> selectedMap) {
- this.selectedMap = selectedMap;
- }
- /**
- * Called before create the sheet
- */
- @Override
- public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
- }
- /**
- * Called after the sheet is created
- */
- @Override
- public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
- // 这里可以对cell进行任何操作
- Sheet sheet = writeSheetHolder.getSheet();
- DataValidationHelper helper = sheet.getDataValidationHelper();
- selectedMap.forEach((k, v) -> {
- // 设置下拉列表的行: 首行,末行,首列,末列
- CellRangeAddressList rangeList = new CellRangeAddressList(v.getFirstRow(), v.getLastRow(), k, k);
- // 设置下拉列表的值
- DataValidationConstraint constraint = helper.createExplicitListConstraint(v.getSource());
- // 设置约束
- DataValidation validation = helper.createValidation(constraint, rangeList);
- // 阻止输入非下拉选项的值
- validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
- validation.setShowErrorBox(true);
- validation.setSuppressDropDownArrow(true);
- validation.createErrorBox("提示", "请输入下拉选项中的内容");
- sheet.addValidationData(validation);
- });
- }
- }
复制代码 其实假如是动态的下拉列表,不能固定枚举的话,直接从配置数据表中拉出业务配置列表,将列表作为传参,使用util类EasyExcels第一个方法就好。
- @ExcelProperty(value = "组别")
- @ExcelSelected(source = {"小学组", "初中组"})
- @ApiModelProperty(value = "组别:0-小学组,1-初中组,2-其他组")
- @NotNull(message = "不能为空")
- private String groupType;
- @ExcelProperty(value = "是否出席")
- @ExcelSelected(source = {"是", "否"})
- @ApiModelProperty(value = "是否出席:0-否,1是")
- @NotNull(message = "不能为空")
- private String present;
复制代码
4.2、图片导出convertor:
- public class UrlPictureConverter implements Converter<String> {
- public static int urlConnectTimeout = 2000;
- public static int urlReadTimeout = 6000;
- @Override
- public Class<?> supportJavaTypeKey() {
- return String.class;
- }
- @Override
- public WriteCellData<?> convertToExcelData(String url, ExcelContentProperty contentProperty,
- GlobalConfiguration globalConfiguration) throws IOException {
- InputStream inputStream = null;
- try {
- URL value = new URL(url);
- if (ObjectUtils.isEmpty(value)) {
- return new WriteCellData<>("");
- }
- URLConnection urlConnection = value.openConnection();
- urlConnection.setConnectTimeout(urlConnectTimeout);
- urlConnection.setReadTimeout(urlReadTimeout);
- inputStream = urlConnection.getInputStream();
- byte[] bytes = IoUtils.toByteArray(inputStream);
- return new WriteCellData<>(bytes);
- } catch (Exception e) {
- log.info("图片获取异常", e);
- return new WriteCellData<>("图片获取异常");
- } finally {
- if (inputStream != null) {
- inputStream.close();
- }
- }
- }
- }
复制代码 4.3、有归并单元格导出:
- @Data
- @Builder
- @AllArgsConstructor
- @NoArgsConstructor
- @ExcelIgnoreUnannotated
- @Slf4j
- public class WorkJudgesStatisticsVo implements Serializable {
- private static final long serialVersionUID = 1L;
- @ExcelProperty(value = "序号", index = 0)
- private String sequence;
- public static class MergeStrategy implements RowWriteHandler {
- private int totalRowNum;
- public MergeStrategy(int totalRowNum) {
- this.totalRowNum = totalRowNum;
- }
- public static MergeStrategy build(int totalRowNum) {
- return new MergeStrategy(totalRowNum);
- }
- @Override
- public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
- if (isHead) {
- // 处理表头
- return;
- }
- log.info("当前行号:{}", row.getRowNum());
- log.info("总行数:{}", totalRowNum);
- // 合并
- if (row.getRowNum() != totalRowNum + 1) {
- return;
- }
- writeSheetHolder.getSheet().addMergedRegion(new CellRangeAddress(writeSheetHolder.getLastRowIndex(), writeSheetHolder.getLastRowIndex(), 5, 6));
- }
- }
- }
复制代码
4.4、行转列,并使用模板的方式导出:
- @Override
- public void selectWorksJudgesResultReview(HttpServletResponse response) throws IOException {
- List<Map<String, Object>> views = scoreReviewWorksJudgesMapper.selectWorksJudgesResultReview();
- List<DyzScoreReviewWorksJudges> reviewWorksJudges = scoreReviewWorksJudgesMapper.selectList();
- List<DyzScoreWorksFiles> scoreWorksFiles = worksFilesMapper.selectList();
- HashMap<Long, List<DyzScoreWorksFiles>> fileMap = new HashMap<>();
- scoreWorksFiles.forEach(f -> {
- List<DyzScoreWorksFiles> files = fileMap.computeIfAbsent(f.getWorksId(), k -> new ArrayList<>());
- files.add(f);
- });
- HashMap<Long, List<DyzScoreReviewWorksJudges>> scoreMap = new HashMap<>();
- reviewWorksJudges.forEach(judge -> {
- List<DyzScoreReviewWorksJudges> judges = scoreMap.computeIfAbsent(judge.getWorksId(), k -> new ArrayList<>());
- judges.add(judge);
- });
- AtomicInteger sequence = new AtomicInteger(0);
- views.forEach(map -> {
- map.put("sequence", String.valueOf(sequence.incrementAndGet()));
- Long workId = Long.valueOf(map.get("workId").toString());
- List<DyzScoreReviewWorksJudges> judges = scoreMap.get(workId);
- for (int i = 0; i < 15; i++) {
- map.put("score" + (i + 1), "");
- map.put("correctness" + (i + 1), "");
- }
- map.put("avgScore", "");
- map.put("avgScore1", "");
- if (judges != null && judges.size() > 0) {
- AtomicInteger serialNo = new AtomicInteger(0);
- AtomicInteger serialNo1 = new AtomicInteger(0);
- judges.forEach(j -> {
- map.put("score" + serialNo.incrementAndGet(), j.getScore());
- map.put("correctness" + serialNo1.incrementAndGet(), j.getRemark());
- });
- judges.sort(Comparator.comparing(DyzScoreReviewWorksJudges::getScore));
- BigDecimal sum = judges.stream().map(DyzScoreReviewWorksJudges::getScore).reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal avg = sum.divide(BigDecimal.valueOf(judges.size()), 2, BigDecimal.ROUND_HALF_UP);
- map.put("avgScore", avg);
- if (judges.size() > 3) {
- judges.remove(0);
- judges.remove(judges.size() - 1);
- BigDecimal sum1 = judges.stream().map(DyzScoreReviewWorksJudges::getScore).reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal avg1 = sum1.divide(BigDecimal.valueOf(judges.size()), 2, BigDecimal.ROUND_HALF_UP);
- map.put("avgScore1", avg1);
- }
- }
- for (int i = 0; i < 4; i++) {
- map.put("fileName" + (i + 1), "");
- }
- List<DyzScoreWorksFiles> files = fileMap.get(workId);
- if (files != null && files.size() > 0) {
- AtomicInteger serialNo = new AtomicInteger(0);
- files.forEach(f -> map.put("fileName" + serialNo.incrementAndGet(), f.getUrl()));
- }
- });
- ConcurrentHashSet<String> columns = views.stream().flatMap(map -> map.keySet().stream()).collect(Collectors.toCollection(ConcurrentHashSet::new));
- List<String> scoreColumns = columns.stream().filter(c -> c.contains("score") || c.contains("avgScore")).collect(Collectors.toList());
- List<String> correctnessColumns = columns.stream().filter(c -> c.contains("correctness")).collect(Collectors.toList());
- //输入流
- InputStream inputStream = null;
- ServletOutputStream outputStream = null;
- ExcelWriter excelWriter = null;
- try {
- org.springframework.core.io.Resource templateFile = resourceLoader.getResource("classpath:templates\\XXXX报表.xlsx");
- inputStream = templateFile.getInputStream();
- // 获取文件名并转码
- response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
- response.setCharacterEncoding("utf-8");
- outputStream = response.getOutputStream();
- // 创建填充配置
- FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
- // 创建写对象
- excelWriter = EasyExcel.write(outputStream)
- .withTemplate(inputStream).build();
- // 创建Sheet对象
- WriteSheet sheet = EasyExcel.writerSheet(0, "报名数量统计").build();
- excelWriter.fill(views, fillConfig, sheet);
- excelWriter.fill(new FillWrapper("scoreColumns", scoreColumns), sheet);
- excelWriter.fill(new FillWrapper("correctnessColumns", correctnessColumns), sheet);
- } catch (Exception e) {
- log.error("导出失败={}", e.getMessage());
- } finally {
- if (excelWriter != null) {
- excelWriter.finish();
- }
- //关闭流
- if (outputStream != null) {
- try {
- outputStream.close();
- } catch (IOException e) {
- log.error("关闭输出流失败", e);
- }
- }
- if (inputStream != null) {
- try {
- inputStream.close();
- } catch (IOException e) {
- log.error("关闭输入流失败", e);
- }
- }
- }
- }
复制代码 模板内里的取值占位符写法

还有一些表格宽度,高度自顺应策略,美化风格的代码就不贴了,必要的话到我的资源中去下载。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |