Java工具类实现高效编写报表

目录
  • 为什么使用java代码写报表
  • 遇到的问题
  • 工具类
    • CollectionDataStream
    • 如何使用
    • MyCollectors
    • 组合使用的实现
    • 优势

本文中的工具类和demo的代码仓库

为什么使用java代码写报表

对于报表数据大部分情况下使用写sql的方式为大屏/报表提供数据来源,但是对于某些复杂情况下仅仅使用sql无法实现,或者实现起来困难的时候,会采取通过代码实现复杂的逻辑最终将结果返回。

遇到的问题

对于相对复杂的报表,经常需要做数据的连接即表与表的join,分组,计算等操作。sql天然支持这些操作,实现起来很轻松。但是当我们在java代码中需要对数据进行连接时,原生支持的就并不那么友好,我们常常会这么实现

现在有两个集合

List<ContractDetail> contractDetails; // 合同明细集合,合同会重复
List<ContractInfo> contractInfos; // 合同主要信息,不会有重复合同

对应数据结构

public class ContractDetail {
    /**
     * 合同编号
     */
    private String contractNo;
    /**
     * 总金额
     */
    private BigDecimal moneyTotal;
}
public class ContractInfo {
    /**
     * 合同编号
     */
    private String contractNo;
    /**
     * 状态
     */
    private String status;
}

需求

contractDetails 根据 contractNo关联 contractInfos,过滤出status = '已签订’的数据

再根据 contractDetails 中的contractNo分组,分别求每个 contractNo对应的moneyTotal之和

最终输出的应该为一个map

Map<String /* 合同编码 */, BigDecimal /* 对应moneyTotal之和 */> result;

通常我们会这么实现

//  setp 1 过滤出 已签订状态的合同编码
Set<String> stopContract = contractInfos.stream()
                .filter(it -> "已签订".equals(it.getStatus()))
                .map(ContractInfo::getContractNo).collect(Collectors.toSet());
//step2 根据 step1的合同编码集合过滤出状态正确的contractDetail
  contractDetails = contractDetails.stream()
                .filter(it -> stopContract.contains(it.getContractNo()))
                .collect(Collectors.toList());
//step3 根据contractNo分别累加对应的moneyTotal
 Map<String, BigDecimal> result = new HashMap<>();
 contractDetails.stream().forEach(it -> {
            BigDecimal moneyTotal = Optional.ofNullable(result.get(it.getContractNo()))
                    .orElse(BigDecimal.ZERO);
            moneyTotal = moneyTotal.add(it.getMoneyTotal() != null ? it.getMoneyTotal() : BigDecimal.ZERO);
            result.put(it.getContractNo(), moneyTotal);
        });

显然这个实现时比较复杂的,因为使用sql的话无非就是 join 连接之后加上group by分组。求和。就可以轻易解决这个问题。那么看看后面这个工具类,再思考有没有更简单的办法实现。

工具类

CollectionDataStream

集合数据流CollectionDataStream的功能是通过接口对集合之间做关联,实现了类似sql join和left join两个操作

并且实现和java中的Stream相互转换的功能。

聚合数据结构将集合转换成类似表结构的数据结构,包含表名,数据

public class AggregationData {
    Map<String, Map> aggregationMap;
    private AggregationData(){
        aggregationMap = new HashMap<>();
    }
    //key 为别名,value为对应对象
    public AggregationData(String tableName, Object data) {
        aggregationMap = new HashMap<>();
        aggregationMap.put(tableName, BeanUtil.beanToMap(data));
    }
    public Map<String, Map> getRowAllData() {
        return aggregationMap;
    }
    public Map getTableData(String tableName) {
        if (!aggregationMap.containsKey(tableName)) {
            throw new DataStreamException(tableName + ".not.exists");
        }
        return aggregationMap.get(tableName);
    }
    public void setTableData(String tableName, Object data) {
        if(aggregationMap.containsKey(tableName)){
            throw new DataStreamException(tableName+".has.been.exists!");
        }
        aggregationMap.put(tableName, BeanUtil.beanToMap(data));
    }
    private void setTableData(String tableName, Map<String, Object> data) {
        Map<String, Object> tableData =
                Optional.ofNullable(aggregationMap.get(tableName)).orElse(new HashMap<String, Object>());
        tableData.putAll(data);
        aggregationMap.put(tableName, tableData);
    }
    public AggregationData copyAggregationData() {
        AggregationData aggregationData = new AggregationData();
        for (String tableName : this.getRowAllData().keySet()) {
            aggregationData.setTableData(tableName, this.getRowAllData().get(tableName));
        }
        return aggregationData;
    }
}

AggregationData代表一行数据,aggregationMap的key为表名,value为对应的数据

来详细看看这个接口

import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
public interface CollectionDataStream<T> {
    /**
     *将集合转化为数据流,并给一个别名
     * @param tableName
     * @param collection
     * @return
     */
    static CollectionDataStream<AggregationData> of(String tableName, Collection<?> collection) {
        return new CollectionDataStreamImpl(tableName, collection);
    }
    /**
     *将 Stream转化为数据流,并给一个别名
     * @param tableName
     * @param collection
     * @return
     */
    static CollectionDataStream<AggregationData> of(String tableName, Stream<?> collection) {
        return new CollectionDataStreamImpl(tableName, collection);
    }
    /**
     * 内连接,可自定义连接条件,使用双循环
     *
     * @param tableName
     * @param collection
     * @param predict
     * @param <T1>
     * @return
     */
    <T1> CollectionDataStream<T> join(String tableName, Collection<T1> collection, JoinPredicate<T, T1> predict);
    /**
     * 等值内连接,使用map优化
     *
     * @param collection
     * @param tableName
     * @param aggregationMapper
     * @param dataValueMapper
     * @param <T1>
     * @param <R>
     * @return
     */
    //等值条件推荐用法
    <T1, R> CollectionDataStream<T> joinUseHashOnEqualCondition(String tableName, Collection<T1> collection, Function<T, R> aggregationMapper, Function<T1, R> dataValueMapper);
    /**
     * 左连接,可自定义连接条件,使用双循环
     *
     * @param tableName
     * @param collection
     * @param predict
     * @param <T1>
     * @return
     */
    <T1> CollectionDataStream<T> leftJoin(String tableName, Collection<T1> collection, JoinPredicate<T, T1> predict);
    /**
     * 等值左连接,使用map优化
     *
     * @param collection
     * @param tableName
     * @param aggregationMapper
     * @param dataValueMapper
     * @param <T1>
     * @param <R>
     * @return
     */
    <T1, R> CollectionDataStream<T> leftJoinUseHashOnEqualCondition( String tableName, Collection<T1> collection,Function<T, R> aggregationMapper, Function<T1, R> dataValueMapper);
    Stream<T> toStream();
    Stream<Map> toStream(String tableName);
    <R> Stream<R> toStream(String tableName, Class<R> clzz);
    <R> Stream<R> toStream(Function<AggregationData, R> mapper);
}

注意joinUseHashOnEqualCondition和join两个方法的区别。

如果集合之间的连接时某个字段等值连接,那么使用joinUseHashOnEqualCondition,其内部使用的是map分组之后进行连接。而直接使用join的话连接条件可自定义,但是是通过双重循环进行条件判断,效率较低。因此等值情况下,使用joinUseHashOnEqualCondition效率更高。

如何使用

还是已上面的需求为例

先进行两个集合之间的连接

 CollectionDataStream.of("t1", contractDetails) .joinUseHashOnEqualCondition(
                        contractInfos.stream().filter(it -> "已签订".equals(it.getStatus())).collect(Collectors.toList()),
                        "t2",
                        agg -> agg.getTableData("t1").get("contractNo"),
                        ContractInfo::getContractNo
                );

代码解析

CollectionDataStream.of("t1", contractDetails)

是将集合contractDetails转换为表名为t1的数据流,

 .joinUseHashOnEqualCondition(
                        contractInfos.stream().filter(
                          "t2",
                            it -> "已签订".equals(it.getStatus())).collect(Collectors.toList()),
                        agg -> agg.getTableData("t1").get("contractNo"),
                        ContractInfo::getContractNo
                );

内连接contractInfos,同时给contractInfos起别名t2,连接条件是等值连接 t1的contractNo和contractInfos的contractNol连接之后得到新的聚合数据流

当然也可以使用自定义的连接实现

CollectionDataStream.of("t1", contractDetails)
                .join("t2",
                        contractInfos.stream().filter(it -> "已签订".equals(it.getStatus())).collect(Collectors.toList()),
                        (agg, data) -> agg.getTableData("t1").get("contractNo").equals(data.getContractNo())
                )

这里通过内连接,那么也起到了一个过滤的作用。连接完成之后我们还要分组进行计算,那么就需要用到下一个工具类

MyCollectors

是对stram中原生Collectors的一个扩展,实现了更多做报表常用分组的一些操作,

MyCollectorspackage collector;
import utils.NumberUtil;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
public class MyCollectors {
    /**
     * 返回一个Collector用于对集合进行分组并且,对于组内有多个元素,只返回最后一个,其他的忽略
     * 适用于明确分组key唯一的情况,value可为空
     * 谨慎使用,如果分组有多条,会丢失数据!!!
     * @param keyMapper
     * @param <T>
     * @param <K>
     * @param <U>
     * @param <M>
     * @return
     */
    public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, Map<K, U>> groupingByLast(Function<? super T, ? extends K> keyMapper,
                                               Function<? super T, ? extends U> valueMapper) {
        return Collectors.groupingBy(keyMapper, Collectors.reducing(null, valueMapper, (o1, o2) -> o2));
    }
    /**
     * 传入一个keyMaper和一个比较器
     * 根据key分组,组内使用比较器进行比较,最终得到一个最大结果
     * @param keyMapper
     * @param comparator
     * @param <T>
     * @param <K>
     * @param <U>
     * @param <M>
     * @return
     */
    public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, Map<K, T>> groupingByMaxComparator(Function<? super T, ? extends K> keyMapper,
                                                      Comparator<T> comparator) {
        return Collectors.groupingBy(keyMapper, Collectors.collectingAndThen(Collectors.maxBy(comparator), it -> it.orElse(null)));
    }
    /**
     * 传入一个keyMaper和一个比较器
     * 根据key分组,组内使用比较器进行比较,最终得到一个最小结果
     * @param keyMapper
     * @param comparator
     * @param <T>
     * @param <K>
     * @param <U>
     * @param <M>
     * @return
     */
    public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, Map<K, T>> groupingByMinComparator(Function<? super T, ? extends K> keyMapper,
                                                       Comparator<T> comparator) {
        return Collectors.groupingBy(keyMapper, Collectors.collectingAndThen(Collectors.maxBy(comparator), it -> it.orElse(null)));
    }
    /**
     * 分组后组内按照指定字段求和
     * @param keyMapper
     * @param <T>
     * @param <K>
     * @return
     */
    public static <T, K>
    Collector<T, ?, Map<K, BigDecimal>> groupingAndSum(Function<? super T, ? extends K> keyMapper,
                                                       Function<? super T, BigDecimal> valueMapper) {
        return Collectors.groupingBy(keyMapper, Collectors.reducing(BigDecimal.ZERO, valueMapper, NumberUtil::addNumbers));
    }
    /**
     * 根据对象某个字段进行求和
     * @param mapper
     * @param <T>
     * @return
     */
    public static <T>
    Collector<T, ?, BigDecimal> sumByField(Function<? super T, ? extends BigDecimal> mapper) {
        return Collectors.reducing(BigDecimal.ZERO, mapper, NumberUtil::addNumbers);
    }
    /**
     * 求和
     */
    public static Collector<BigDecimal, ?, BigDecimal> sum() {
        return Collectors.reducing(BigDecimal.ZERO, NumberUtil::addNumbers);
    }
}

组合使用的实现

 Map<String /* 合同变么 */, BigDecimal /* 对应moneyTotal之和 */> result = CollectionDataStream.of("t1", contractDetails)
                .joinUseHashOnEqualCondition(
                        contractInfos.stream().filter(it -> "60".equals(it.getStatus())).collect(Collectors.toList()),
                        "t2",
                        agg -> agg.getTableData("t1").get("contractNo"),
                        ContractInfo::getContractNo
                ).toStream("s1", ContractDetail.class)//将数据流转换为 java原生Stream
                .collect(MyCollectors.groupingAndSum(ContractDetail::getContractNo, ContractDetail::getMoneyTotal));

这样的实现显然更加简单,也减少了出错的的概率,减少了代码量,提升了效率。

优势

  • 实现了集合之间的连接操作,并且是流式操作,可以一口气不断连接多个集合。
  • 实现了与Stream之间的相互转换。利用stream的功能可以实现各种复杂操作,例如过滤,转换,分组等。
  • 效率上有一定的保证,对于等值连接采用了Map优化,并且在内连接时,考虑使用后小表连大表进行优化,在一些情况下减少循环次数,在bean转换为行聚合数据时使用cglib下的BeanMap减少内存的占用和性能的消耗

如果感兴趣,代码仓库地址为传送门

到此这篇关于Java工具类实现高效编写报表的文章就介绍到这了,更多相关Java编写报表内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java生成excel报表文件示例

    此次简单的操作将数据从数据库导出生成excel报表以及将excel数据导入数据库 首先建立数据库的连接池: package jdbc; import java.io.FileInputStream; import java.sql.Connection; import java.util.Properties; import org.apache.commons.dbcp.BasicDataSource; public class BaseDAO { private static BasicDa

  • java防盗链在报表中的应用实例(推荐)

    今天我们来聊聊Java防盗链,多说无用,直接上应用案例. 这里所用的工具是报表软件FineReport,搭配有决策系统(一个web前端展示系统,主要用于权限控制),可以采用java防盗链的方式来实现页面权限. 浏览器中直接输入报表URL的时候,它的头文件是空的,因此,可以在访问的时候做两个判断:头文件是否为空以及以什么页面进行跳转,如果不符合跳到错误页面即可. 什么是Referer? 这里的 Referer 指的是HTTP头部的一个字段,也称为HTTP来源地址(HTTP Referer),用来表

  • Java根据模板导出Excel报表并复制模板生成多个Sheet页

    因为最近用报表导出比较多,所有就提成了一个工具类,本工具类使用的场景为  根据提供的模板来导出Excel报表 并且可根据提供的模板Sheet页进行复制 从而实现多个Sheet页的需求, 使用本工具类时,如果需求是每个Sheet页中的数据都不一致,但是表格样式和模板都一样 那么只需要在实际情况中根据 sql 来查询要添加的数据源 (只需更改数据源即可) 采用的技术为 POI 导出,因为类的缘故,目前只支持2003版本的Excel. 使用前请先下载相应jar包! 后期有时间的话会进行进一步完善,初次

  • Java导出Excel统计报表合并单元格的方法详解

    目录 前言 示例 注意事项 总结 前言 Apache POI是一种流行的API,允许程序员使用Java程序创建,修改和显示MS Office文件. 它是由Apache Software Foundation开发和分发的开源库,用于使用Java程序设计或修改Microsoft Office文件. 它包含将用户输入数据或文件解码为MS Office文档的类和方法. HSSF - 用于读取和写入MS-Excel文件的xls格式 示例 类似上面的需要合并表头的报表在日常的开发中也是经常遇到,这里总结下关

  • Java FineReport报表工具导出EXCEL的四种方式

    在实际的应用中会经常需要将数据导出成excel,导出的方式除原样导出还有分页导出.分页分sheet导出和大数据量导出.对于excel2003版,限制了每个sheet的最大行数和列数,大数据量导出时会默认时分多个sheet,而excel2007不会出现这样的问题.这些导出方式在JAVA程序中分别有不同的接口来实现: 1. 原样导出 原样导出就是不预览直接导出excel 其程序接口代码如下: outputStream = new FileOutputStream(new File("E:\\Exce

  • JavaWeb实现图形报表折线图的方法

    本文实例讲述了JavaWeb实现图形报表折线图的方法.分享给大家供大家参考,具体如下: 步骤说明: 1. 导入log4j.jar,jfreechart-0.9.18.jar,jdom.jar,jcommon-0.9.3.jar四个jar包 2. 在某个包中写入Tuxin.Java类 package com.mengya.util; import java.awt.Color; import java.awt.Font; import java.io.PrintWriter; import jav

  • android自动生成dimens适配文件的图文教程详解(无需Java工具类)

    在编写ui界面时因为手机分辨率大小不同,所以展现出来的效果也是不同的,这个时候就需要考虑适配器,让根据手机分辨率自动适配相应尺寸来展示界面,可以提高用户的体验感. 1.首先安装插件ScreenMatch,安装成功之后android studio会提示重启 2.在res->values下创建一个dimens文件,将以下代码复制进去.如果不创建这个文件一下操作会不成功导致项目重启. <dimen name="common_margin">@dimen/dp_15</

  • Java 工具类总结目录(分享)

    如下所示: 工具类 功能 BeanUtils 提供了对于JavaBean进行各种操作,克隆对象,属性等等 Betwixt XML与Java对象之间相互转换 Codec 处理常用的编码方法的工具类包,例如DES,SHA1,MD5,Base64等 Collections Java集合框架操作 Compress Java提供文件打包,压缩类库 Configuration Java应用程序的配置管理类库 DBCP,druid 提供数据库连接池服务 DbUtils 提供对JDBC的操作封装来简化数据查询和

  • Java工具类DateUtils实例详解

    本文实例为大家分享了Java工具类DateUtils的具体代码,供大家参考,具体内容如下 import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; /** * 描述:公共日期工具类 */ public class DateUtils { public static String DATE_FORMAT = "yyyy-M

  • 16 个有用的的Java工具类(小结)

    在Java中,工具类定义了一组公共方法,这篇文章将介绍Java中使用最频繁及最通用的Java工具类.以下工具类.方法按使用流行度排名,参考数据来源于Github上随机选取的5万个开源项目源码. 一. org.apache.commons.io.IOUtils closeQuietly:关闭一个IO流.socket.或者selector且不抛出异常,通常放在finally块 toString:转换IO流. Uri. byte[]为String copy:IO流数据复制,从输入流写到输出流中,最大支

  • Java工具类之@RequestMapping注解

    一.前言 问题阐述:在某一场景下,我们的代码在 Service 实现相同,但却在 Controller 层访问时却希望不同的前缀可以访问.如下 :/say/hello.我们这里希望在不借助任何外部服务的情况下 通过 /a/say/hello 和 /b/say/hello 都可以访问到该接口,同时不想在 Controller 中写两个方法. @RestController @RequestMapping("say") public class SayController { @Autow

  • Java 工具类实现音频音量提升

    目录 前言 Maven依赖 代码 验证一下 前言 本文提供将音频提升音量的java工具类代码,一如既往的实用主义分享. Maven依赖 <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.15</version> </dependency> <dependency> &l

  • Java 工具类实现音频音量提升

    目录 前言 Maven依赖 代码 验证一下 前言 本文提供将音频提升音量的java工具类代码,一如既往的实用主义分享. Maven依赖 <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.15</version> </dependency> <dependency> &l

  • 利用Java工具类Hutool实现验证码校验功能

    目录 Hutool工具类介绍 Hutool实现验证码生成 测试验证码生成 其他样式的验证码 第一篇是纯利用现有JDK提供的绘图类(ImageIO)类制作,这个过程比较复杂且需要了解ImageIO类. 这一篇文章是利用Hutool工具类来实现的,该工具类已经封装验证码所需的相关类等,使用起来较为简单和方便. Hutool工具类介绍 Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”.

  • Java工具类BeanUtils库介绍及实例详解

    BeanUtils工具由Apache软件基金组织编写,提供给我们使用,主要解决的问题是:把对象的属性数据封装到对象中. 在整个J2EE的编程过程中,我们经常会从各种配置文件中读取相应的数据,需要明白的一点是从配置文件中读取到的数据都是String,但是很显然我们的应用程序中不仅仅有String一种数据类型,比如:基本数据类型(int.double.char.float等),还有自定义数据类型(引用数据类型), 那么我们必须面临的一个问题就是讲字符串类型转换为各种具体的数据类型,该怎么办呢? 有两

  • java工具类之实现java获取文件行数

    工具类代码,取得当前项目中所有java文件总行数,代码行数,注释行数,空白行数 复制代码 代码如下: import java.io.BufferedReader;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;import java.io.Reader;import

随机推荐

其他