Java利用工具类提升写报表效率
创始人
2024-01-12 23:02:38

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

为什么使用java代码写报表

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

遇到的问题

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


现在有两个集合

List contractDetails; // 合同明细集合,合同会重复
List 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 result;

通常我们会这么实现

//  setp 1 过滤出 已签订状态的合同编码
Set stopContract = contractInfos.stream().filter(it -> "已签订".equals(it.getStatus())).map(ContractInfo::getContractNo).collect(Collectors.toSet());//step2 根据 step1的合同编码集合过滤出状态正确的contractDetailcontractDetails = contractDetails.stream().filter(it -> stopContract.contains(it.getContractNo())).collect(Collectors.toList());//step3 根据contractNo分别累加对应的moneyTotalMap 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 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 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 data) {Map tableData =Optional.ofNullable(aggregationMap.get(tableName)).orElse(new HashMap());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 {/***将集合转化为数据流,并给一个别名* @param tableName* @param collection* @return*/static CollectionDataStream of(String tableName, Collection collection) {return new CollectionDataStreamImpl(tableName, collection);}/***将 Stream转化为数据流,并给一个别名* @param tableName* @param collection* @return*/static CollectionDataStream of(String tableName, Stream collection) {return new CollectionDataStreamImpl(tableName, collection);}/*** 内连接,可自定义连接条件,使用双循环** @param tableName* @param collection* @param predict* @param * @return*/ CollectionDataStream join(String tableName, Collection collection, JoinPredicate predict);/*** 等值内连接,使用map优化** @param collection* @param tableName* @param aggregationMapper* @param dataValueMapper* @param * @param * @return*///等值条件推荐用法 CollectionDataStream joinUseHashOnEqualCondition(String tableName, Collection collection, Function aggregationMapper, Function dataValueMapper);/*** 左连接,可自定义连接条件,使用双循环** @param tableName* @param collection* @param predict* @param * @return*/ CollectionDataStream leftJoin(String tableName, Collection collection, JoinPredicate predict);/*** 等值左连接,使用map优化** @param collection* @param tableName* @param aggregationMapper* @param dataValueMapper* @param * @param * @return*/ CollectionDataStream leftJoinUseHashOnEqualCondition( String tableName, Collection collection,Function aggregationMapper, Function dataValueMapper);Stream toStream();Stream toStream(String tableName); Stream toStream(String tableName, Class clzz); Stream toStream(Function 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 * @param * @param * @param * @return*/public static >Collector> groupingByLast(Function keyMapper,Function valueMapper) {return Collectors.groupingBy(keyMapper, Collectors.reducing(null, valueMapper, (o1, o2) -> o2));}/*** 传入一个keyMaper和一个比较器* 根据key分组,组内使用比较器进行比较,最终得到一个最大结果* @param keyMapper* @param comparator* @param * @param * @param * @param * @return*/public static >Collector> groupingByMaxComparator(Function keyMapper,Comparator comparator) {return Collectors.groupingBy(keyMapper, Collectors.collectingAndThen(Collectors.maxBy(comparator), it -> it.orElse(null)));}/*** 传入一个keyMaper和一个比较器* 根据key分组,组内使用比较器进行比较,最终得到一个最小结果* @param keyMapper* @param comparator* @param * @param * @param * @param * @return*/public static >Collector> groupingByMinComparator(Function keyMapper,Comparator comparator) {return Collectors.groupingBy(keyMapper, Collectors.collectingAndThen(Collectors.maxBy(comparator), it -> it.orElse(null)));}/*** 分组后组内按照指定字段求和* @param keyMapper* @param * @param * @return*/public static Collector> groupingAndSum(Function keyMapper,Function valueMapper) {return Collectors.groupingBy(keyMapper, Collectors.reducing(BigDecimal.ZERO, valueMapper, NumberUtil::addNumbers));}/*** 根据对象某个字段进行求和* @param mapper* @param * @return*/public static Collector sumByField(Function mapper) {return Collectors.reducing(BigDecimal.ZERO, mapper, NumberUtil::addNumbers);}/*** 求和*/public static Collector sum() {return Collectors.reducing(BigDecimal.ZERO, NumberUtil::addNumbers);}
}

组合使用的实现

 Map 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));

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

优势

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

如果感兴趣,代码仓库地址为https://github.com/404008945/dataStream

相关内容

热门资讯

demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
阿西吧是什么意思 阿西吧相当于... 即使你没有受到过任何外语培训,你也懂四国语言。汉语:你好英语:Shit韩语:阿西吧(아,씨발! )日...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
阿西吧是什么意思 阿西吧相当于... 即使你没有受到过任何外语培训,你也懂四国语言。汉语:你好英语:Shit韩语:阿西吧(아,씨발! )日...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...