jdk8函数式编程,10行代码实现 Excel 导出方案

Posted 泉城IT圈子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jdk8函数式编程,10行代码实现 Excel 导出方案相关的知识,希望对你有一定的参考价值。

摘要: excel导出是企业级开发经常遇见的需求,但由于其与业务数据关联度太高很难剥离出公共实现。本文借助java8的函数式编程与接口默认方法的新特性,来实现创造简洁的可直接用于web项目的excel导出工具接口。

jdk8函数式编程,10行代码实现 Excel 导出方案

背景

在面向企业的软件开发中,总会有一些客户提出要将数据导出成excel文件,供以下载查看。这个需求并不难, apache poi 框架可以让我们很方便的生成excel文件,并抽象出了 cell , row ,sheet,workbook等类型,并且能够方便的生成与解析能够被 office 2003,2007解析的excel文件。对于POI框架,在JAVA WEB 开发中确实是大名鼎鼎,就不再详细介绍。如有不懂得同学请移步参考 apache poi 。 我们的大多数的导出excel的需求都是不需要设置excel文件中的样式,而是只需要将我们数据库中的纪录进行筛选汇总查询之后所形成的数据集合,以简单可辨识的的方式写入到目标excel文件,并提供链接从服务器下载该文件资源。这么简单的需求,但我不相信会有人喜欢写导入导出excel相关的程序。

常规处理

先以普通的方式展示如何创建并导出excel文件:

                    /**                    * 将错误的项目生成 excel 并重新导出                    * [@param](https://my.oschina.net/u/2303379) errorCoreList 错误数据集合                    * [@return](https://my.oschina.net/u/556800) 错误详情的excel                    * 生成office2007版本的excel文件                    * 生成的excel保存在 服务器 ${WebRoot}/data/blacklist 文件夹下                    * */
                   [@Override](https://my.oschina.net/u/1162528)
                   public String exportErrorExcel(List<CoreError> errorCoreList) {                       if (StringUtil.isNotEmpty(errorCoreList)){                           try {
                               String url = "data/blacklist/" + System.currentTimeMillis() + ".xlsx";
                               File file = new File(request.getServletContext().getRealPath("/") + url);
                               url = request.getContextPath() +"/" + url;                               if(file.createNewFile());{
                                   Workbook workbook = new XSSFWorkbook();
                                   Sheet sheet = workbook.createSheet();
                                   buildExcelTitle(sheet);
                                   buildExcelContent(sheet,errorCoreList);
                                   workbook.write(new FileOutputStream(file));                                   return url;
                               }
                           }catch (Exception e){
                               logger.error("generate blacklist excel :{} error :{}",System.currentTimeMillis(),e);
                           }
                       }                       return "#";
                   }            
                   /**                    * 创建excel表头                    * */
                    [@Override](https://my.oschina.net/u/1162528)
                    public void buildExcelTitle(Sheet sheet) {
                        Row firstRow = sheet.createRow(0);
                        firstRow.createCell(0,1).
                                setCellValue("代码类型(01:姓名 02:身份证号 03:手机号 04:设备id 05:卡号)\n");
                        firstRow.createCell(1,1).
                                setCellValue("代码值\n");
                        firstRow.createCell(2,1).
                                setCellValue("名单类型(01:黑名单 02:灰名单 03:白名单 )\n");
                        firstRow.createCell(3,1).
                                setCellValue("失败原因 \n");
                    }            
                    /**                     * 将集合输出到sheet里面                     *                     * [@param](https://my.oschina.net/u/2303379) sheet                     * @param collection                     */
                    @Override
                    public void buildExcelContent(Sheet sheet, Collection collection) {                        int rowNum = 1;                        for (CoreError coreError : (List<CoreError>)collection){
                            Row row = sheet.createRow(rowNum++);
                            row.createCell(0,1).setCellValue(coreError.getCodeType());
                            row.createCell(1,1).setCellValue(coreError.getValue());
                            row.createCell(2,1).setCellValue(coreError.getListType());
                            row.createCell(3,1).setCellValue(coreError.getMessage());
                        }
                        sheet.autoSizeColumn(1,false);
                        sheet.autoSizeColumn(3,false);
                    }

在这段代码中实现了一个将错误文件导出到excel文件并提供下载链接的业务逻辑。代码已经经过了许多调节,呈现的思维也足够清晰,但代码现在还有一个非常大的缺点,耦合性太高。依照现在的代码,我们很难将这个业务逻辑与技术实现相互分离,造出一个多种业务需求都能使用的轮子。我们不得不在每个需要导出excel的需求上写数十行甚至几百行导出excel的代码(在我的工作中遇见过很多同事这么做)。

业务分析

 poi 导出excel文件流程图

从创建poi对象到导出excel文件,我们大致进行了创建workbook对象,创建sheet对象。创建一个第一行的row对象,并将标题写入到这一行。然后遍历数据集合,将集合中的每个元素都写入到一个新的行(row)。最后我们再将内存中的workbook对象保存到输出流中。在整个流程中除了两个数据节点,都应该是我们的公共方法,能够拿出来重用。我想做过excel导出的人都知道,这个需求与业务相关性非常高,依赖于特定的业务数据,很难做拆分。java是一个固执且笨重的编程语言,使用java我们该如何设计才能让以后的开发中甚至只需十行代码就能解决呢?这个时候我们就需要借助java8的新特性来实现了。

JAVA 8 新特性

  1. lambda

    Lambda表达式(也称为闭包)是整个Java 8发行版中最受期待的在Java语言层面上的改变,Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据:函数式程序员对这一概念非常熟悉。

  2. 接口的默认方法与静态方法 Java 8用默认方法与静态方法这两个新概念来扩展接口的声明。默认方法使接口有点像Traits(Scala中特征(trait)类似于Java中的Interface,但它可以包含实现代码,也就是目前Java8新增的功能),但与传统的接口又有些不一样,它允许在已有的接口中添加新方法,而同时又保持了与旧版本代码的兼容性。 默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。相反,每个接口都必须提供一个所谓的默认实现,这样所有的接口实现者将会默认继承它(如果有必要的话,可以覆盖这个默认实现)。

使用这两个新特性,我们就能使用函数式编程的方法来解决上面所说的问题。

设计实现

首先,根据上面的流程分析我们对上面的代码进行重构,使其成为一个公用的接口。 ExportExcel.java

       import com.alibaba.fastjson.JSONObject;       import com.br.antifroud.common.http.HttpResultFactory;       import com.br.antifroud.util.UUIDUtils;       import org.apache.poi.ss.usermodel.Row;       import org.apache.poi.ss.usermodel.Sheet;       import org.apache.poi.ss.usermodel.Workbook;       import org.apache.poi.xssf.usermodel.XSSFWorkbook;       
       import javax.servlet.http.HttpServletResponse;       import java.io.ByteArrayOutputStream;       import java.io.IOException;       import java.util.List;       
       /**        * 导出统计分析excel文件接口        * 本文件使用了jdk8的新特性,故最小运行版本为jdk1。8        * @author Wang Weiwei <email>weiwei02@vip.qq.com / weiwei.wang@100credit.com</email>        * @version 1.0        * @sine 2017/4/18        */
       public interface ExportExcel<T> {           /**            * 将字节数组写出到servlet输出流            * @param response  http回应对象,为excel回应的目的地            * @param list 要导出到 excel的数据集合            *  @param titles excel的标题 通常取第一行作为excel的标题            * */
           default void exportExcel(HttpServletResponse response,List<T> list,String[] titles) throws IOException  {               byte[] bytes = selectExcel(list,titles);
               response.setContentType("application/x-msdownload");
               response.setHeader("Content-Disposition", "attachment;filename=" + UUIDUtils.getUUID() + ".xlsx");
               response.setContentLength(bytes.length);
               response.getOutputStream().write(bytes);
               response.getOutputStream().flush();
               response.getOutputStream().close();
           }           /**            * 选择要导出的文件 导出的excel 属于office 2007格式的文件            * @param list excel文件内容            *  @param titles excel 文件的标题            * @return 已经生成excel文件的字节数组            * */
           default byte[] selectExcel(List<T> list, String[] titles) throws IOException {
                   Workbook workbook = new XSSFWorkbook();
                   Sheet sheet = workbook.createSheet();
                   generateExcelTitle(titles,sheet);
                   eachListAndCreateRow(list,sheet);
                   ByteArrayOutputStream out = new ByteArrayOutputStream();
                   workbook.write(out);               return out.toByteArray();
           }       
           /**            * 遍历集合,并创建单元格行            * @param list 数据集合            *  @param sheet 工作簿            * */
          default void eachListAndCreateRow(List<T> list, Sheet sheet){              for (int i = 0,j = 1; i < list.size(); i ++,j++){
                  T t = list.get(i);
                  Row row = sheet.createRow(j);
                  generateExcelForAs(t,row);
              }
          }       
           /**            * 生成excel文件的标题            * */
          default void generateExcelTitle(String[] titles, Sheet sheet){
              Row row = sheet.createRow(0);              for (int i = 0; i < titles.length; i++){
                  row.createCell(i,1).setCellValue(titles[i]);
              }
          }       
           /**            * 创建excel内容文件            * @param t 组装excel 文件的内容            * @param row 当前excel 工作行            * */
           void generateExcelForAs(T t, Row row);       
           /**            * 当发生错误时如此回应信息            * */
           default void errorResponse(HttpServletResponse response){               byte[] message = JSONObject.toJSONString(HttpResultFactory.builderFailtureResult("导出excel文件错误,请重试!")).getBytes();
               response.setContentType("text/json;charset=UTF-8");
               response.setContentLength(message.length);               try {
                   response.getOutputStream().write(message);
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
       }

这个接口只有一个抽象方法(默认方法不是抽象方法),在java8中可称之为函数接口。generateExcelForAs() 方法用于决定我们怎么将我们的对象写入到excel的每个行(row)中,也是我们在以后再处理excel导出业务时唯一需要手动去写的方法。再接口中我们还使用了默认方法,其中默认方法 exportExcel() 为excel文件导出业务的主方法 ,在使用该接口时需要主动执行此方法。

示例

接下来我们拿两个现实excel导出的业务作为示例,详细的说明如何通过10行代码完成excel导出功能. 示例1:在 web 程序中导出excel文件1

    /**根据条件,对规则命中并进行统计 并导出到excel文件         * @param hitRuleQuery 可根据开始时间 结束时间 规则编号 top 进行过滤         * @param response 直接生成文件写入到http请求中         * */
        @RequestMapping(value = "/export.do", method = RequestMethod.GET)        public void export(HitRuleQuery hitRuleQuery, HttpServletResponse response){            try {
                ((ExportExcel<HitRule>)(obj, row) -> {
                    row.createCell(0, Cell.CELL_TYPE_STRING).setCellValue(obj.getRule());
                    row.createCell(1, Cell.CELL_TYPE_STRING).setCellValue(obj.getRuleName());
                    row.createCell(2,Cell.CELL_TYPE_NUMERIC).setCellValue(obj.getMatchTimes());
                }).exportExcel(response,hitRuleService.query(hitRuleQuery),new String[]{"规则编号","规则名称","命中次数"});
            } catch (IOException e) {
               logger.error("export statistics statement rule error",e);
            }
        }

这里有一种新语法也就是上面说过的 lambda 表达式

     (obj, row) -> {
                            row.createCell(0, Cell.CELL_TYPE_STRING).setCellValue(obj.getRule());
                            row.createCell(1, Cell.CELL_TYPE_STRING).setCellValue(obj.getRuleName());
                            row.createCell(2,Cell.CELL_TYPE_NUMERIC).setCellValue(obj.getMatchTimes());
                        }

这段代码其实就是新建了一个上面所定义的 ExportExcel 接口的匿名实现类,因为 ExportExcel 接口只有一个抽象方法,所以我们可以通过面向函数式编程的思想将其看成一个函数 obj, row 是函数的参数,编译器会自动帮助我们推断。(ExportExcel<HitRule>)其实 就是告诉编译器我们所声明的那个函数是ExportExcel类型的,这样编译器就会自动创建这个类型的对象,而不用再使用恶心的匿名内部类。 我们再看一个示例,来演示如何在10行内的代码实现excel的导出 示例2: 在 web 程序中导出excel文件2

         /**根据条件,对接口查询命中并进行统计 并导出到excel文件             * @param interfaceDealQuery 可根据开始时间 结束时间 规则编号 top 进行过滤             * @param response 直接生成文件写入到http请求中             * */
            @RequestMapping(value = "/export.do", method = RequestMethod.GET)            public void export(InterfaceDealQuery interfaceDealQuery, HttpServletResponse response){                try {
                    ((ExportExcel<InterfaceDeal>) (obj, row) -> {
                    row.createCell(0, Cell.CELL_TYPE_STRING).setCellValue(obj.getPartnerName());
                    row.createCell(1, Cell.CELL_TYPE_STRING).setCellValue(obj.getProductionName());
                    row.createCell(2, Cell.CELL_TYPE_STRING).setCellValue(obj.getProdName());
                    row.createCell(3, Cell.CELL_TYPE_NUMERIC).setCellValue(obj.getRequestTimes());
                    row.createCell(4, Cell.CELL_TYPE_NUMERIC).setCellValue(obj.getCloudFoundTimes());
                    row.createCell(5, Cell.CELL_TYPE_NUMERIC).setCellValue(obj.getLocalFoundTimes());
                    row.createCell(6, Cell.CELL_TYPE_NUMERIC).setCellValue(obj.getCloudMissingTimes());
                    row.createCell(7, Cell.CELL_TYPE_STRING).setCellValue(obj.getHitRate());
                }).exportExcel(response,interfaceDealService.query(interfaceDealQuery),                        new String[]{"机构名称","进件产品名称","数据产品名称","请求次数","云端成功请求次数","本地缓存请求次数","失败次数","请求命中率"});
                } catch (IOException e) {
                    logger.error("export statistics statement interface deal error",e);
                }
            }


以上是关于jdk8函数式编程,10行代码实现 Excel 导出方案的主要内容,如果未能解决你的问题,请参考以下文章

函数式编程小分享

编程范式:函数式编程

函数式编程

函数式编程

JDK8系列之使用Function函数式接口实现回调

JDK8系列之使用Function函数式接口实现回调