zoukankan      html  css  js  c++  java
  • 实现Excel文件的上传和解析

    前言

    本文思维导图
    Alt

    一、需求描述

    实现一个页面上传excel的功能,并对excel中的内容做解析,最后存储在数据库中。

    二、代码实现

    需求实现思路:

    1. 先对上传的文件做校验和解析,这里我们通过ExcelUtil工具类来实现;
    2. 解析得到的数据进行批量插入。

    2.1 接口定义

        @PostMapping(path = "/batchMaintainBankBalance")
        @ResponseBody
        public ResultDto<Object> batchMaintainBankBalance(@RequestParam("file") MultipartFile updateFile) {
            try {
                Response response = balanceBankResultBiz.batchMaintainBankBalance(updateFile);
                if (response.isSucc()) {
                    return ResultDto.success(response);
                } else {
                    return ResultDto.fail(response);
                }
            } catch (Exception e) {
                logger.error("excel上传异常", e);
                return ResultDto.fail(ResultStatusEnum.FAIL.getCode(), "excel上传异常");
            }
        }
    

    来看看这一小段代码,我们可以挖掘多少知识点

    2.1.1 web中常见的几个注解(持续更新...)

    1. @Controller:标识一个该类是Spring MVC controller处理器,用来创建处理http请求的对象。
    2. @PostMapping@GetMapping@RequestMapping:三者都是用于将HTTP请求映射到特定处理程序(接口)的方法注解,其中@PostMapping@GetMapping属于组合注解,分别等效于@RequestMapping(method = RequestMethod.POST)@RequestMapping(method = RequestMethod.GET)
    3. @RequestParam:用于将请求中的参数映射到处理程序中的参数。如这里的使用方式:
      @RequestParam("file") MultipartFile updateFile
      
      在调用处,其参数名是"file"(相当于key值),而在处理程序中,file的值映射到updateFile中。

    2.1.2 MultipartFile

    MultipartFile是spring类型,代表HTML中form data方式上传的文件,包含二进制数据+文件名称。在这里,它就代表了上传的excel。

    2.1.3 Response

    public class Response<T> implements Serializable {
    
        private static final Boolean SUCCESS = true;
        private static final Boolean FAILED = false;
    
        private Boolean status;
        private T data;
        private String msg;
    
        public static<T>  Response<T> succ(T data) {
            return new Response<T>().setStatus(true).setData(data);
        }
        public static<T>  Response<T> succMsg(String msg) {
    
            return new Response<T>().setStatus(true).setMsg(msg);
        }
        public static<T>  Response<T> succ() {
    
            return new Response<T>().setStatus(true);
        }
        public static<T>  Response<T> failed(T data) {
    
            return new Response<T>().setStatus(false).setData(data);
        }
        public static<T>  Response<T> failedMsg(String msg) {
    
            return new Response<T>().setStatus(false).setMsg(msg);
        }
        public static<T>  Response<T> failed() {
    
            return new Response<T>().setStatus(false);
        }
        public static<T>  Response<T> build(Boolean status, T data) {
    
            return new Response<T>().setStatus(status).setData(data);
        }
        public static Boolean  isSucc(Response response) {
    
            if(response == null) {
                return false;
            }
            return response.getStatus();
        }
        public  Boolean  isSucc() {
    
            if(this == null) {
                return false;
            }
            return this.getStatus();
        }
    
    }
    

    Response是一个对结果的通用封装,通过泛型来允许对于存放不同类型的返回数据,是一种在程序经常用到的类。这里提一下接口中也用到的ResultDto,其实与Response结构是几乎一样的,只是该代码是实际项目中的代码,实际项目中,对于接口的通用返回类型使用了ResultDto,对于service层的返回值类型使用了Response

    2.1.4 ResultStatusEnum

    public enum ResultStatusEnum {
    	SUCCESS("0000", "成功"), 
    	FAIL("1111", "系统异常"),
    	UNKNOWN("99999", "未知异常");
    
    	private String code;
    	private String message;
    
    	ResultStatusEnum(String code, String message) {
    		this.code = code;
    		this.message = message;
    	}
    
    	public String getCode() {
    		return code;
    	}
    
    	public String getMessage() {
    		return message;
    	}
    
    	public void setMessage(String message) {
    		this.message = message;
    	}
    }
    

    在项目中一般会对错误或异常定义一个枚举类,以上。

    2.2 文件校验

    接下来我们需要对excel文件本身做一些简单的校验

    public void checkExcelFile(MultipartFile multipartFile) throws Exception {
        if (null == multipartFile || multipartFile.isEmpty()) {
            throw new Exception(ResultStatusEnum.FILE_UPLOAD_ERROR.getCode(), ResultStatusEnum.FILE_UPLOAD_ERROR.getMessage());
        }
        if (!multipartFile.getOriginalFilename().endsWith(".xls") &&
                !multipartFile.getOriginalFilename().endsWith(".xlsx")) {
            logger.info(multipartFile.getOriginalFilename() + "不是excel文件");
            throw new Exception(ResultStatusEnum.FILE_UPLOAD_PARAM_ERROR.getCode(), multipartFile.getOriginalFilename() + "不支持的文件格式");
        }
        if (multipartFile.getSize() > 20 * 1024 * 1024) {
            logger.info(multipartFile.getOriginalFilename() + "[" + multipartFile.getSize() + "]超过20M");
            throw new Exception(ResultStatusEnum.FILE_UPLOAD_PARAM_ERROR.getCode(), multipartFile.getOriginalFilename() + "超过20M");
        }
    }
    

    2.3 文件解析

    接下来需要将excel文件中的内容解析出来,映射为我们需要的java对象。

    InputStream inputStream = null;
    inputStream = updateFile.getInputStream();
    // 获取excel列名
    Field[] fields = new BankBalanceExcelDto().getClass().getDeclaredFields();
    List<String> columnName = new ArrayList<>();
    Arrays.stream(fields).forEach(field -> columnName.add(field.getName()));
    List<BankBalanceExcelDto> listList = ExcelUtil.readObjectList(inputStream, BankBalanceExcelDto.class, columnName, checkExcelError);
    

    2.3.1 BankBalanceExcelDto

    BankBalanceExcelDto是与excel中的列名一一对应的一个类,excel中的数据即是映射到该类实例中。

    @Getter
    @Setter
    @ToString
    public class BankBalanceExcelDto {
        /**
         * 交易日期
         */
        @ExcelExportAnnotation(column = 0, excelHeadName = "交易日期",checkNull=true)
        private String transDateFormat;
    
        /**
         * 账号
         */
        @ExcelExportAnnotation(column = 1, excelHeadName = "账号",checkNull=true, checkLength = true,valueLength = 300)
        private String bankAccountNo;
    

    2.3.2 ExcelUtil.readObjectList

    该方法将inputStream按照columnName的格式映射到BankBalanceExcelDto.class,错误放入checkExcelError中。具体实现详见附录:ExcelUtil

    2.4 数据落库

    这一部分就是将2.3中解析得到的数据,插入/更新到数据库中存储。这里我们做的是一个批量上传的任务,下文主要讲讲几个对批量上传操作优化的思路和方法。

    三、批量上传性能优化

    以上实现的功能,在实际使用过程中,导入1000条数据,发现导入时间超过了20秒,这个是用户不能接受的,所以我们必须做性能优化。

    3.1 性能分析工具—Spring StopWatch

    StopWatch 是 Spring 自带的可用于统计程序各模块运行时间的一个类,具体使用方式很简单,如下:

    import org.springframework.util.StopWatch;
    
    // 创建一个 StopWatch 实例
    StopWatch stopWatch = new StopWatch();
    // 参数为 任务名
    stopWatch.start("校验excel文件格式和大小”);
    …校验文件格式和大小模块…
    stopWatch.stop();
    // 同一个监控可监控多个任务
    stopWatch.start("查询库中待维护数据”);
    …查询库中待维护数据…
    stopWatch.stop();
    //打印出所有任务的信息
    logger.info(stopWatch.prettyPrint());
    

    通过该工具,我们可以针对程序中比较慢的模块做优化,做到有的放矢,对症下药。
    接下来分享几个我具体在优化时的几个思路和做法

    3.2 静态数据的查询放在循环外

    对于一些查询固定数据的操作,不要放到循环内,导致反复查询;或者对于同一类数据的查询,可以一次性查出总集合,放入缓存中,在for循环中可以利用lambda表达式进行高效地筛选。
    如:

    for (BankBalanceExcelHasRowDto balanceExcelHasRowDto : bankBalanceExcelHasRowDtos) {
        int row = balanceExcelHasRowDto.getRowNum();
        //校验所有待维护的账号&币种,登录人是否有权限
        //根据账号+币种+交易日期+待维护的状态获取 账号负责人和单据状态
        BalanceBankResult query = new BalanceBankResult();
        query.setBankAccountNo(balanceExcelHasRowDto.getBankAccountNo());
        query.setCurrency(balanceExcelHasRowDto.getCurrency());
        query.setBalanceDate(DateUtil.parseDateStr2Date(balanceExcelHasRowDto.getTransDateFormat(), DateUtil.DATE_FORMAT));
        query.setBalanceStatus(BankBalanceManagementStatusEnum.TO_MAINTAIN.getCode());
        // 根据条件查询该记录---A
        BankBalanceDetailQueryResponseExtend queryResponse = balanceBankResultMapperX.queryBankBalanceDetailByAccount(query);
        if(Objects.isNull(queryResponse)){
            return Response.failedMsg("账号:"+balanceExcelHasRowDto.getBankAccountNo() + "币种"+balanceExcelHasRowDto.getCurrency()
            +"在交易日期"+balanceExcelHasRowDto.getTransDateFormat()+"无待维护数据");
        }
    }
    

    可以看到我们在循环内做了一个条件查询(A),每一条数据都会做一次查询。我们可以优化为:在循环外将数据先一次性查询出来,当然,需要给这个查询尽量多的查询条件,不然一次查询的数据太多,也会导致查询缓慢和占用过多内存的问题。
    优化后如下:

    ate yesterDay = DateUtil.addDays(new Date(), -1, 0, 0, 0);
    Date date = holidayServiceBiz.getPrevWorkDay();
    //获取上一个工作日至上一个自然日的所有存在状态为待维护的数据 --- B
    BankBalanceQueryRequestExtend query = new BankBalanceQueryRequestExtend();
    query.setTransDateStart(date);
    query.setTransDateEnd(yesterDay);
    query.setBalanceStatus(BankBalanceManagementStatusEnum.TO_MAINTAIN.getCode());
    List<BankBalanceQueryResponseExtend> queryResponseExtendList = 
    for (BankBalanceExcelHasRowDto balanceExcelHasRowDto : bankBalanceExcelHasRowDtos) {
        int row = balanceExcelHasRowDto.getRowNum();
        //2.校验所有待维护的账号&币种,登录人是否有权限
        BankBalanceQueryResponseExtend queryResponse = null;
        if (CollectionUtils.isNotEmpty(queryResponseExtendList)) {
            List<BankBalanceQueryResponseExtend> bankBalanceQueryResponseExtends = queryResponseExtendList.stream().filter(b -> b.getBankAccountNo().equals(balanceExcelHasRowDto.getBankAccountNo()))
                    .filter(b -> b.getCurrency().equals(balanceExcelHasRowDto.getCurrency()))
                    .filter(b -> b.getBalanceDate().equals(DateUtil.parseDateStr2Date(balanceExcelHasRowDto.getTransDateFormat(), DateUtil.DATE_FORMAT)))
                    .collect(Collectors.toList());
            if (CollectionUtils.isEmpty(bankBalanceQueryResponseExtends)) {
                return Response.failedMsg("账号:" + balanceExcelHasRowDto.getBankAccountNo() + "币种" + balanceExcelHasRowDto.getCurrency()
                        + "在交易日期" + balanceExcelHasRowDto.getTransDateFormat() + "无待维护数据");
            }
            queryResponse = bankBalanceQueryResponseExtends.get(0);
        }
    }
    

    以上,注释 B 部分。

    3.3 批量插入/更新

    批量插入有多种方式,思路有以下两种:

    1. 在程序中循环遍历逐条更新。
    2. 一次性更新所有数据(更准确的说是一条sql语句来更新所有数据,逐条更新的操作放到数据库端,在业务代码端展现的就是一次性更新所有数据)。

    3.3.1 逐条更新

    1. 逐条更新方法一
      这种方式显然是最简单,也最不容易出错的,即便出错也只是影响到当条出错的数据,而且可以对每条数据都比较可控,更新失败或成功,从什么内容更新到什么内容,都可以在逻辑代码中获取。代码可能像下面这个样子:
    updateBatch(List<MyData> datas){
        for(MyData data : datas){
            try{
                myDataDao.update(data);//更新一条数据,mybatis中如下面的xml文件的update
            }
            catch(Exception e){
                ...//如果更新失败可以做一些其他的操作,比如说打印出错日志等
            }
        }
    }
    
    //mybatis中update操作的实现
    <update>
        update mydata
        set   ...
        where ...
    </update>
    

    这种方式最大的问题就是效率问题,逐条更新,每次都会连接数据库,然后更新,再释放连接资源。即使有数据库连接池的存在,这种损耗在数据量较大的时候也会出现效率问题。因
    2. 逐条更新方法二
    通过循环,依次执行多条update的sql

    <update id="updateBatch"  parameterType="java.util.List">  
        <foreach collection="list" item="item" index="index" open="" close="" separator=";">
            update course
            <set>
                name=${item.name}
            </set>
            where id = ${item.id}
        </foreach>      
    </update>
    

    之所以说这也是逐条更新,是因为它只是将循环从业务代码转移到了sql中,从sql语句中我们也能看到,数据库其实是循环执行的多条update语句。一条记录update一次,性能比较差,容易造成阻塞。

    3.3.2 批量插入/更新

    批量更新

        <update id="updateBankBalances" parameterType="java.util.List">
            update balance_bank_result
            <trim prefix="set" suffixOverrides=",">
                <trim prefix="total_balance =case" suffix="end,">
                    <foreach collection="list" item="item" index="index">
                        <if test="item.totalBalance !=null">
                            when id=#{item.id} then #{item.totalBalance}
                        </if>
                    </foreach>
                </trim>
                <trim prefix="available_balance =case" suffix="end,">
                    <foreach collection="list" item="item" index="index" >
                        <if test="item.availableBalance !=null">
                            when id=#{item.id} then #{item.availableBalance}
                        </if>
                    </foreach>
                </trim>
                <trim prefix="version =case" suffix="end,">
                    <foreach collection="list" item="item" index="index">
                        WHEN id=#{item.id} THEN version + 1
                    </foreach>
                </trim>
            </trim>
            where id in
            <foreach collection="list" index="index" item="item" separator="," open="(" close=")">
                #{item.id,jdbcType=BIGINT}
            </foreach>
            AND balance_status = 1
        </update>
    

    注意:

    1. 被"trim"标签包裹的“foreach”中不需要 “separator、open、close”属性;
    2. 不能省略每句的WHEN id=#{item.id}
      最后,我们看看批量插入的实现,相较于更新来说,更简单:

    批量插入

     <insert id="batchInsertBalanceBankSynTask" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
        insert into balance_bank_syn_task (business_id, syn_status, runtimes,
        bank_account_no, currency, balance_date,
        create_time, last_update_time)
        values
        <foreach item="item" collection="list" separator=",">
          (#{item.businessId,jdbcType=BIGINT}, #{item.synStatus,jdbcType=INTEGER}, #{item.runtimes,jdbcType=INTEGER},
          #{item.bankAccountNo,jdbcType=VARCHAR}, #{item.currency,jdbcType=VARCHAR}, #{item.balanceDate,jdbcType=DATE},
          #{item.createTime,jdbcType=TIMESTAMP}, #{item.lastUpdateTime,jdbcType=TIMESTAMP})
        </foreach>
      </insert>
    

    参考文献

    批量更新-https://www.cnblogs.com/eternityz/p/12284760.html

    附录:
    ExcelUtil

  • 相关阅读:
    磁盘映射命令
    CentOS 配置XWIN/VNC
    生成一个随机数,让用户输入猜这个数字,有三次机会
    自己练习读取写入txt
    python学习笔记:文件操作和集合(转)
    接口测试基础
    nginx_tomcat负载均衡环境
    mysql索引
    shell脚本
    linux基础知识(四)
  • 原文地址:https://www.cnblogs.com/cleverziv/p/13799754.html
Copyright © 2011-2022 走看看