首頁 > 軟體

Java工具類實現高效編寫報表

2022-11-21 14:00:57

本文中的工具類和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編寫報表內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com