把 Excel 当作一种协议来看,代码实现起来就简单多了

把 Excel 当作一种协议来看,代码实现起来就简单多了,第1张

把 Excel 当作一种协议来看,代码实现起来就简单多了 提出问题

日常工作中,我们经常会遇到使用 Excel 批量导入的需求,正常情况下,我们会在 Controller 中接收前端传过来的文件流,并进行解析

@RequestMapping("importExcel")
public ResultDTO> importExcel(@RequestParam("file") MultipartFile file) {
    try (InputStream in = file.getInputStream()) {
        // TODO 解析 Excel
        List EasyExcel.read(in)...
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

这样做看起来也挺好的,常规也是这样做的。

但是在团队中,会遇到这些问题

    对 EasyExcel 的 API 不熟悉,容易出错解析 Excel 放在 Controller 层?还是 Service 层?支持 Excel 导入,又要支持在页面上的批量 *** 作,如何复用?

做过多次类似需求的同学会发现,导入其实是一种 批量 *** 作 的手段,而 Excel 是一种数据交互格式,就像 JSON 和 XML 一样。若直接在前端进行Excel解析成 JSON,对于后端来说,就只要提供一个批量 *** 作的接口就可以完成批量 *** 作和 Excel 导入两个需求了。

话外音:前端的很多限制,请不要在前端做 Excel 解析

因此,我们可以得出一个洞察:Excel 导入本质上是一种通过 Excel 这种数据交互格式进行批量 *** 作的手段

有了这个认识,我们比较容易想到,

应该在 Controller 层将 Excel 解析成批量 *** 作所使用的数据对象然后复用批量 *** 作的 Service 方法即可

然而,很多人在先接到了一个 Excel 导入的需求时,没多想,把很多业务逻辑与 EasyExcel 耦合在一起,结果再来一个批量 *** 作的需求,因为难以解耦(可能也不是同一个人做),就重起一个方法,把相同的业务逻辑再写了一遍,增加开发和维护成本。

那么,如何优雅地实现 Excel 导入呢?

既然是一种协议,那就应该像 JSON 那样,我们在写后端代码时是不需要感知到 JSON 解析的过程的

所以要看一下在 SpringMVC 中 JSON 是如何被解析的?

解决方案

经过研究,SpringMVC 是通过一系列实现 HttpMessageConverter 接口的类来支持不同三方包来解析 JSON 请求体的。
我们要实现批量 *** 作,要接收一个 List,因为是泛型,我们要实现针对泛型的接口 GenericHttpMessageConverter 来实现这个解析过程

模仿 AbstractJsonHttpMessageConverter 的写法,我们实现 canRead 和 read 两个方法:

@Component // 让 Spring 扫描可以扫到
public class ExcelHttpMessageConverter implements GenericHttpMessageConverter {
    private final static List SUPPORTED_MEDIA_TYPES = Arrays.asList(
            // xlsx 格式
            MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
            MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"),
            //  xls 格式
            MediaType.parseMediaType("application/vnd.ms-excel"),
            MediaType.parseMediaType("application/vnd.ms-excel;charset=UTF-8"));

    
    @Override
    public boolean canRead(Type type, Class contextClass, MediaType mediaType) {
        if (!SUPPORTED_MEDIA_TYPES.contains(mediaType)) {
            return false;
        }
        if (!(type instanceof ParameterizedTypeImpl)) {
            return false;
        }
        // 导入只能用 List 接收
        if (!List.class.isAssignableFrom(((ParameterizedTypeImpl) type).getRawType())) {
            return false;
        }
        // GenericTypeResolver 是 SpringCore 提供用来解析泛型参数的强大工具类
        Type[] types = ((ParameterizedTypeImpl) GenericTypeResolver.resolveType(type, contextClass)).getActualTypeArguments();
        if (types == null || types.length != 1) {
            return false;
        }
        Class clazz = (Class) types[0];
        ExcelBody excelBody = clazz.getAnnotation(ExcelBody.class);
        return excelBody != null;
    }

    @Override
    public Object read(Type type, @Nullable Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        // 解析出参数的具体类型
        Class clazz = (Class) ((ParameterizedTypeImpl) GenericTypeResolver.resolveType(type, contextClass)).getActualTypeArguments()[0];
        // 获取 ExcelBody 注解
        ExcelBody excelBody = clazz.getAnnotation(ExcelBody.class);
        try (InputStream in = inputMessage.getBody()) {
            // 通过 EasyExcel 解析输入流并返回
            return EasyExcel.read(in)
                    .headRowNumber(excelBody.headRowNumber())
                    .sheet(excelBody.sheetNo() > 0 ? excelBody.sheetNo() : null, "".equals(excelBody.sheetName()) ? null : excelBody.sheetName())
                    .head(clazz).doReadSync();
        }
    }

    @Override
    public List getSupportedMediaTypes() {
        return SUPPORTED_MEDIA_TYPES;
    }

   // 该接口剩下的方法留空即可...
}
 

因为 Excel 解析需要指定一些配置参数,我们约定 用于接收 Excel 的类必须打上 ExcelBody 这个注解。

@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelBody {
    
    String sheetName() default "";

    
    int sheetNo() default -1;

    
    int headRowNumber() default 1;
}
用一下试试

首先,我们写一个批量更新的接口,注意在参数前加上 @RequestBody 像普通接收 JSON 格式数据的接口一样

@PostMapping("batchUpdateUser")
public List batchUpdateUser(@RequestBody List user) {
     return user;
 }

然后在请求体的定义中加上 ExcelBody 注解即可

@Data
@ExcelBody
public class UpdateUserByIdDTO {
    @ExcelProperty("id")
    private Long id;
    @ExcelProperty("用户名")
    private String username;
    @ExcelProperty("地址")
    private String address;
}

我们建一个 Excel 文件,根据 UpdateUserByIdDTO 定义的字段名称,造几条测试数据

使用 PostMan 调用一下,看看效果

可以看见,Excel 的解析对于业务代码就完全透明了

看起来这个接口跟普通的接口一样,那我完全不改代码,可以像常规的接口那样用 JSON 格式数据调用吗?试试就知道了

答案是,可以!!

通过这个办法,我们就可以用一个接口,即实现页面上普通的批量 *** 作,又支持 Excel 导入进行批量 *** 作了!

待优化

本文提供的演示代码只实现了最简单的解析,若要完善起来还有很多可以做的。例如:

    数据格式校验(Excel 是用户直接上传的,无法要求他们一定按模板提交,内容填错会读取失败,生成程序抛异常,应支持数据验证并提供友好的提示给用户)多 Sheet 读取。目前我们直接限定接收对象只能是 List,其实可以支持一个普通类,拥有多个 List 字段,每个 List 代表一个 Sheet自定义转换器,以应对复杂的模板解析

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5717175.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)