1. 程式人生 > >Quartz + Tablesaw 報表統計

Quartz + Tablesaw 報表統計

beans cor -a targe quertz entryset 列式存儲 文件導入 else

場景

在12 月份做的報表功能中,直接從 ES 查詢一個月的數據。當數據量特別大時,查詢速度會非常緩慢甚至查詢失敗。解決方案是使用定時任務,在每天淩晨指定時間自動查詢前一天的數據,然後寫入 CSV 文件中,每天追加。生成報表文件時,就不用再查詢 ES,而是讀取 CSV 文件,統計一個月每天數據的總和。

一、定時任務

定時任務使用的是 Quartz 框架。

Quartz 是什麽

Quartz 是一個開源的作業調度框架,由 java 編寫,在.NET 平臺為 Quartz.Net,通過 Quart 可以快速完成任務調度的工作。

Quartz 使用場景

如定時發送郵件、定時統計數據生成報表等等

在項目中使用 Quartz

添加依賴

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.0</version>
</dependency>

spring-quartz.xml 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
    <!-- 要調用的工作類 -->
    <bean id="quartzJob" class="com.devywb.quartzJob"></bean>
    <!-- 定義調用的對象和方法 -->
    <bean id="jobTask" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <!-- 調用的類 -->
        <property name="targetObject">
            <ref bean="quartzJob"></ref>
        </property>
        <!-- 調用的方法 -->
        <property name="targetMethod">
            <value>work</value>
        </property>
    </bean>
    
    <!-- 定義觸發的時間 -->
    <bean id="doTime" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
      <property name="jobDetail">
                <ref bean="jobTask"/>
      </property>
      <!-- cron表達式 -->
      <property name="cronExpression">
        <!-- 每隔20秒鐘執行一次 -->
        <!-- <value>*/20 * * * * ?</value> -->
        <!-- 每天淩晨五點執行 -->
        <value>0 0 5 * * ?</value>
      </property>
    </bean>
    
     <!-- 總管理類 -->
     <bean id="startQuertz" lazy-init="false" autowire="no" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
         <property name="triggers">
             <list>
                 <ref bean="doTime"/>
             </list>
         </property>
     </bean>
</beans>

二、Tablesaw

Tablesaw是一套內存內數據表,其中包含多種數據工具與面向列的存儲格式。其設計思路認為沒人會面向小型任務執行分布式分析,而大家可以在單一服務器上對200萬行級別的表進行交互。
大家能夠利用Tablesaw執行各種規則,從而檢查顯示布局、數據優先級或者針對數據顯示及交互向特定用戶提供擴展控制範圍。在它的幫助下,我們可以利用RDBMS與CSV文件導入數據,添加及刪除列,執行映射與規約操作或者將表保存在經過壓縮的列式存儲格式當中。

添加依賴

<!-- https://mvnrepository.com/artifact/tech.tablesaw/tablesaw-core-->
<dependency>
    <groupId>tech.tablesaw</groupId>
    <artifactId>tablesaw-core</artifactId>
    <version>0.11.2</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-math3</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-math3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-math3</artifactId>
    <version>3.0</version>
</dependency>

在項目中添加 Tablesaw 的依賴後,項目無法啟動,原因是 commons-lang3 和 commons-math3 兩個依賴的版本太高了。後來采用的解決方法是排除依賴再另外引入低版本依賴。

常用 API

// 讀取 csv 文件
Table table = Table.read().csv(String filePath);

// 獲取所有列名
List<String> columnNames = table.columnNames();

// sum
table.sum("列名").get();

// 排序
table.sortDescendingOn("列名"); // 降序
table.sortAscendingOn("列名"); // 升序

// 分組
table.groupBy(columns);

// 前多少條
table.first(nRows)

// 生成 saw 文件
Table table = Table.read().csv(contents, tableName);
table.save(folder);

// 讀取 saw 文件
Table table = Table.readTable(tableNameAndPath)

踩過的坑

寫入數據至 CSV 文件時,當數據中某個字段包含分隔符或其他特殊符號時,程序會報錯。下面列出兩種解決方案。

第一種,使用第三方庫,例如 openCSV,它底層對特殊符號做了處理;

第二種,手動處理字段中的特殊符號;

/**
 * list 轉 CSV 字符串
 * @param header
 * @param data
 * @return
 */
public static String list2CsvStr(Map<String, String> header, List<Map<String, Object>> data) {
    String content = "";
    if (header != null && header.size() > 0) {
        if (data != null && data.size() > 0) {
            StringBuilder sb = new StringBuilder();
            for (Map<String, Object> map : data) {
                Set<Entry<String, String>> entrySet = header.entrySet();
                int i = 0;
                for (Entry<String, String> entry : entrySet) {
                    String key = entry.getKey();
                    if(i > 0) sb.append(SEPARATOR); // 不是第一列時,添加分隔符
                    if(map.containsKey(key)) {
                        Object value = map.get(key);
                        String valueStr = value != null ? value.toString() : "";
                        if(valueStr.contains(SEPARATOR)) { // 如果數據包含分割符
                            if(valueStr.contains("\"")) { // 如果字段中包含雙引號,替換成兩個
                                valueStr.replace("\"", "\"\"");
                            }
                            // 如果字段包含分割符,則用雙引號括起來
                            valueStr = "\"" + valueStr +"\"";
                        }
                        sb.append(valueStr);
                    } else {
                        sb.append("");
                    }
                    i ++ ;
                }
                sb.append("\n");
            }
            content = sb.toString();
        }
    }
    return content;
}

/**
 * map 轉 CSV 字符串
 * @param header
 * @param data
 * @return
 */
public static String map2CsvStr(Map<String, String> header, Map<String, Object> data) {
    String content = "";
    if (header != null && header.size() > 0) {
        if (data != null && data.size() > 0) {
            StringBuilder sb = new StringBuilder();
            int i = 0;
            Set<Entry<String, String>> entrySet = header.entrySet();
            for (Entry<String, String> entry : entrySet) {
                String key = entry.getKey();
                if(i > 0) sb.append(SEPARATOR);
                if(data.containsKey(key)) {
                    Object value = data.get(key);
                    String valueStr = value != null ? value.toString() : "";
                    if(valueStr.contains(SEPARATOR)) { // 如果數據包含分割符
                        if(valueStr.contains("\"")) { // 如果字段中包含雙引號,替換成兩個
                            valueStr.replace("\"", "\"\"");
                        }
                        // 如果字段包含分割符,則用雙引號括起來
                        valueStr = "\"" + valueStr +"\"";
                    }
                    sb.append(valueStr);
                } else {
                    sb.append("");
                }
                i ++;
            }
            sb.append("\n");
            content = sb.toString();
        }
    }
    return content;
}

Quartz + Tablesaw 報表統計