赞
踩
本次封装引入阿里开源框架EasyExcel,EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。 github地址:GitHub - alibaba/easyexcel: 快速、简洁、解决大文件内存溢出的java处理Excel工具 。64M内存20秒读取75M(46W行25列)的Excel(3.0.2+版本)
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>easyexcel</artifactId>
- <version>3.1.1</version>
- </dependency>
结构图如下:
1.1结构说明:
1.annotation:注解
@ExcelPropertyCheck(自己写的注解用作导入数据校验)
- @Target({ElementType.FIELD, ElementType.TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Inherited
- @Documented
- public @interface ExcelPropertyCheck {
- boolean required() default true; ----是否为空,默认不为空。
-
- boolean checkFormat() default false; ----是否进行格式检验,默认不进行。
-
- int type() default -1; ----格式检验类型,int 已经支持的类型有 0->ip、1->端口、2->时间日期格式
-
- int length() default -1; ----长度校验, int 字符串的长度,-1不进行校验
-
- }
@ExcelProperty(框架自带的,用于标记excel传输类和一些通用的导入导出配置)
- @Target(ElementType.FIELD)
- @Retention(RetentionPolicy.RUNTIME)
- @Inherited
- public @interface ExcelProperty {
-
- String[] value() default {""}; ----导出时对应字段的表头名称
-
- int index() default -1;----排列顺序,最好不要配默认按字段顺序。
-
- int order() default Integer.MAX_VALUE; ----同上
-
- Class<? extends Converter<?>> converter() default AutoConverter.class; ----转换器
-
- String format() default ""; ----格式划输出
- }
特别提醒:!!!
@ExcelPropertyCheck该注解作用类上时只支持required,其余属性无效。字段上的注解配置会覆盖类上的配置。
2.constant:常量类,格式校验的类型定义,如电话号码、日期、IP地址等。
- /**
- * excel导入字段类型常量
- */
- public class ExcelPropertyType {
- //时间日期格式校验
- public static final int DATE_TIME = 0;
- }
3.converter:转换器
读写均可使用,实现Converter重写方法即可。
如将excel 中的日期转为 LocalDateTime 或者将LocalDateTime 转为excel表中的日期
或者将数据库的枚举值0,1,2,3导入到excel文件变成对应的中文汉字
- /**
- * String and string converter
- *
- */
- public class CustomStringStringConverter implements Converter<String> {
- /**
- * 这里读的时候会调用
- *
- * @param context
- * @return
- */
- @Override
- public String convertToJavaData(ReadConverterContext<?> context) {
- return "自定义:" + context.getReadCellData().getStringValue();
- }
-
- /**
- * 这里是写的时候会调用 不用管
- *
- * @return
- */
- @Override
- public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
- return new WriteCellData<>(context.getValue());
- }
-
- }
上述代码中泛型为String 表示此字段经过处理后最后返回的类型为String
4.listener:监听器
ReadListener<T>(EasyExcel提供的)
- ReadListener<T>读监听器,框架提供。提供读取excel不同时期的监听方法。
- 分别为:
- onException()-------------->“读取发生异常时监听”、
- invokeHead()----------------->“读取表头信息监听”、
- invoke()--------------------->“读取每行数据监听”、
- doAfterAllAnalysed()----------->“所有数据读取完毕监听”。
BaseListener<T>我们自己封装的读监听器,在里面结合注解配置完成数据校验。
- BaseListener<T>我们自己封装的读监听器,在里面结合注解配置完成数据校验。
- 对外提供的字段:
- private final List<Map<String, Object>> mapData = new ArrayList<>();
- private final List<T> data = new ArrayList<>();
- private final Map<Integer, String> errorMessageMap = new HashMap<>();
- //非空校验map
- private final Map<String, Boolean> nullAbleFieldMap = new HashMap<>();
- //格式校验map
- private final Map<String, String> checkFormatFieldMap = new HashMap<>();
- //长度校验map
- private final Map<String, Integer> checkLengthFieldMap = new HashMap<>();
- //枚举值校验map
- private final Map<String, String[]> checkEnumFieldMap = new HashMap<>();
字段 | 描述 | 备注 |
mapData | excel读取完毕的Map数据 | Map<String,Object> |
data | excel读取完毕的对于实体List | List<T> |
errorMessageMap | 错误信息map | Map<Integer,String>;key为错误行号,value为描述。 |
nullAbleFieldMap | 非空校验map | Map<String, Boolean> |
checkFormatFieldMap | 格式校验map:电话号码,ip,日期 | Map<String, String> |
checkLengthFieldMap | 长度校验map | Map<String, Integer> |
checkEnumFieldMap | 枚举值检验map | Map<String, String[]> |
结合注解做参数校验或者格式校验的实现思路(反射加泛型)
步骤一:在表头读取的监听方法里利用反射判断检验注解ExcelPropertyCheck加在了那些ExcelDTO哪些属性上,并且ExcelPropertyCheck注解的具体属性是什么将这些存入分门别类的map里面。
步骤二:在读取每行数据的时候,结合步骤一的map,判断数据是否需要校验,并且从map中取出校验的类型是什么,再去完成校验,如果校验失败则将错误信息放入errorMessageMap,当前读取的excel这一行数据全部校验通过则数据放入data 里面
完整的BaseListener<T>代码如下:
- import com.alibaba.excel.context.AnalysisContext;
- import com.alibaba.excel.metadata.data.ReadCellData;
- import com.alibaba.excel.read.listener.ReadListener;
- import com.alibaba.excel.read.metadata.holder.ReadSheetHolder;
- import com.baomidou.mybatisplus.core.toolkit.BeanUtils;
-
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.util.StringUtils;
-
- import java.lang.reflect.Field;
- import java.util.*;
-
-
- public class BaseListener<T> implements ReadListener<T> {
-
- private final Logger logger = LoggerFactory.getLogger(BaseListener.class);
- private Class<?> headClazz;
- //读取数据map形式
- private final List<Map<String, Object>> mapData = new ArrayList<>();
- //读取数据实体类泛型形式
- private final List<T> data = new ArrayList<>();
- //非空校验map
- private final Map<String, Boolean> nullAbleFieldMap = new HashMap<>();
- //格式校验map
- private final Map<String, String> checkFormatFieldMap = new HashMap<>();
- //长度校验map
- private final Map<String, Integer> checkLengthFieldMap = new HashMap<>();
- //枚举值校验map
- private final Map<String, String[]> checkEnumFieldMap = new HashMap<>();
- //数据校验错误信息map key:错误的行号 value:错误信息描述
- private final Map<Integer, String> errorMessageMap = new HashMap<>();
-
- public List<Map<String, Object>> getMapData() {
- return mapData;
- }
-
- public List<T> getData() {
- return data;
- }
-
- public Map<Integer, String> getErrorMessageMap() {
- return errorMessageMap;
- }
-
- public BaseListener() {
- }
-
- /**
- * @param headClazz excel model 类对象
- */
- public BaseListener(Class<?> headClazz) {
- this.headClazz = headClazz;
- }
-
- /**
- * 读取发生异常时的方法
- *
- * @param exception
- * @param context
- * @throws Exception
- */
- @Override
- public void onException(Exception exception, AnalysisContext context) throws Exception {
- logger.debug("发生了异常");
- }
-
- /**
- * 读取头信息
- *
- * @param headMap
- * @param context
- */
- @Override
- public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
- ExcelPropertyCheck clazzHeadAnno = this.headClazz.getAnnotation(ExcelPropertyCheck.class);
- Field[] declaredFields = headClazz.getDeclaredFields();
- if (clazzHeadAnno != null && clazzHeadAnno.required()) {
- for (Field declaredField : declaredFields) {
- nullAbleFieldMap.put(declaredField.getName(), true);
- }
- }
-
- for (Field declaredField : declaredFields) {
- ExcelPropertyCheck annotation = declaredField.getAnnotation(ExcelPropertyCheck.class);
- if (annotation != null) {
- if (annotation.checkFormat()) {
- checkFormatFieldMap.put(declaredField.getName(), annotation.type() + "");
- }
- if (annotation.required()) {
- nullAbleFieldMap.put(declaredField.getName(), true);
- } else {
- nullAbleFieldMap.remove(declaredField.getName());
- }
- if (annotation.required() && annotation.length() != -1) {
- checkLengthFieldMap.put(declaredField.getName(), annotation.length());
- }
- if (annotation.required() && annotation.value().length != 0) {
- checkEnumFieldMap.put(declaredField.getName(), annotation.value());
- }
-
- }
- }
- }
-
- /**
- * 读取每一行数据
- *
- * @param t
- * @param analysisContext
- */
- @Override
- public void invoke(T t, AnalysisContext analysisContext) {
- int rowIndex = ((ReadSheetHolder) analysisContext.currentReadHolder()).getRowIndex() + 1;
- StringBuilder error = new StringBuilder();
- Field[] declaredFields = t.getClass().getDeclaredFields();
- //必填校验和格式校验
- for (Field declaredField : declaredFields) {
- try {
- declaredField.setAccessible(true);
- if (nullAbleFieldMap.get(declaredField.getName()) != null) {
- if (nullAbleFieldMap.get(declaredField.getName())) {
- Object o = declaredField.get(t);
- if (!Objects.nonNull(o)) {
- error.append(declaredField.getName()).append("为空;");
- } else {
- //字段不为空进行长度校验
- if (checkLengthFieldMap.get(declaredField.getName()) != null) {
- if (String.valueOf(o).length() > checkLengthFieldMap.get(declaredField.getName())) {
- error.append(declaredField.getName()).append("长度错误;");
- }
- }
- if (checkEnumFieldMap.get(declaredField.getName()) != null) {
- if (Integer.parseInt(String.valueOf(o)) == -1) {
- error.append(declaredField.getName()).append("枚举值错误;");
- }
- }
- }
- }
- }
- //是否需要进行格式校验
- if (checkFormatFieldMap.get(declaredField.getName()) != null) {
- String res = check(String.valueOf(declaredField.get(t)), Integer.valueOf(checkFormatFieldMap.get(declaredField.getName())));
- if (StringUtils.hasText(res)) {
- error.append(res);
- }
- }
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
- }
- if (StringUtils.hasText(error.toString())) {
- errorMessageMap.put(rowIndex, error.toString());
- }
- mapData.add(BeanUtils.beanToMap(t));
- data.add(t);
- }
-
-
- @Override
- public void doAfterAllAnalysed(AnalysisContext analysisContext) {
- logger.info("excel解析完毕,共解析{}条数据,错误数据{}条,错误详情{}", data.size(), errorMessageMap.size(), errorMessageMap);
- }
-
- private String check(String str, Integer checkType) {
- switch (checkType) {
- case 0:
- return DateUtil.isDateTimeFormat(str) ? "" : "日期格式错误;";
- default:
- return "";
- }
- }
- }
如果导入时除了简单校验还需要特色校验如数据库去重,业务判断等,请另外写一个xxxReadListener 来继承BaseListener<T> 泛型为对应excel传输对象。重写监听方法时请务必super()一下,否则基本校验就没了!
5.model:excel传输对象
注解配置
为了不污染实体类和字段冗余请在这里定义excel传输对象加注解。
- @Data
- @EqualsAndHashCode
- @ExcelPropertyCheck
- public class DemoExcelDto {
- @ExcelProperty("id")
- @ExcelPropertyCheck(required = false)
- private Long id; //记录标识
- @ExcelProperty("导入时间")
- @ExcelPropertyCheck(required = true, checkFormat = true, type = ExcelPropertyType.DATE_TIME)
- private LocalDateTime importTime;
- }
EasyExcelUtils.importExcel(file, IpDepartsExcelDto.class, listener)
- @RequestMapping("importExcel")
- @ResponseBody
- public Result importIps(MultipartFile file) {
- //XXXReadListener listener = new XXXReadListener (XXXExcelDto.class);
- //没有复杂的校验就用BaseListener
- BaseListener<DemoExcelDto> listener = new BaseListener<>();
- //获取校验错误信息
- Map errorMap = EasyExcelUtils.importExcel(file, IpDepartsExcelDto.class, listener);
- //获取读取完毕的数据,即使发生校验错误也会继续读取
- List<IpDeparts> ipDeparts = EasyExcelUtils.dto2model(listener.getData() ,new IpDeparts());
- if (errorMap.size() != 0) {
- //errorMap校验数据不为空就是excel数据校验有不通过的数据。
- return Result.failure(CommonResultStatus.IMPORT_FAIL, errorMap.toString());
- } else {
- //mapper保存数据库。
- mapper.saveAll(ipDeparts);
- return Result.success(CommonResultStatus.OK);
- }
- }
EasyExcelUtils.downloadExcel(response, fileName, ipDepartsExcelDtoList, IpDepartsExcelDto.class)
- @GetMapping("download")
- public void download(HttpServletResponse response) throws UnsupportedEncodingException {
- String fileName = URLEncoder.encode("test", "utf-8");
- List<DemoExcelDto> demoExcelDtos = new ArrayList<>();
- for (int i = 0; i < 5; i++) {
- DemoExcelDto demoExcelDto = new DemoExcelDto(i, "2020-08-01");
- demoExcelDtos.add(demoExcelDto);
- }
- try {
- EasyExcelUtils.downloadExcel(response, fileName, demoExcelDtos, DemoExcelDto.class);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- import com.alibaba.excel.EasyExcel;
- import com.alibaba.excel.util.MapUtils;
- import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
- import com.alibaba.fastjson.JSON;
- import org.springframework.beans.BeanUtils;
- import org.springframework.web.multipart.MultipartFile;
-
- import javax.servlet.http.HttpServletResponse;
- import java.io.IOException;
- import java.util.Collection;
- import java.util.List;
- import java.util.Map;
- import java.util.stream.Collectors;
-
- public class EasyExcelUtils {
- /**
- * 导出excel
- *
- * @param response
- * @param fileName 文件名
- * @param data List<数据集合>
- * @throws IOException
- */
- public static <T> void downloadExcel(HttpServletResponse response, String fileName, List<T> data, Class<?> clazz) throws IOException {
- try {
- response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
- response.setCharacterEncoding("utf-8");
- response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
- // 这里需要设置不关闭流
- EasyExcel.write(response.getOutputStream(), clazz).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).autoCloseStream(Boolean.FALSE).sheet("sheet1")
- .doWrite(data);
- } catch (Exception e) {
- // 重置response
- response.reset();
- response.setContentType("application/json");
- response.setCharacterEncoding("utf-8");
- Map<String, String> map = MapUtils.newHashMap();
- map.put("status", "failure");
- map.put("message", "下载文件失败" + e.getMessage());
- response.getWriter().println(JSON.toJSONString(map));
- }
- }
-
- /**
- * 读取excel 文件
- *
- * @param file 文件
- * @param cl 类对象
- * @param listener 参数校验监听器
- */
- public static <T> Map importExcel(MultipartFile file, Class<T> cl, BaseListener<T> listener) {
- try {
- EasyExcel.read(file.getInputStream(), cl, listener).sheet().doRead();
- return listener.getErrorMessageMap();
- } catch (IOException e) {
- return null;
- }
- }
-
- /**
- * excleDto 转为对应model实体类
- *
- * @param data
- * @param t
- * @param <T>
- * @return
- */
- public static <T> List<T> dto2model(Collection<?> data, T t) {
- return data.stream().map(e -> {
- T t1 = null;
- try {
- t1 = (T) t.getClass().newInstance();
- } catch (Exception exception) {
- exception.printStackTrace();
- }
- BeanUtils.copyProperties(e, t1);
- return t1;
- }).collect(Collectors.toList());
- }
- }
四、总结:
经过简单的封装完成了一个带参数校验的简单使用案例,直接引入项目后在controller直接调用EasyExcelUtils的 "importExcel"方法和"downloadExcel"就能实现excel的导入导出,是不是很方便呢。
有问题留言大家一起交流学习。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。