Statistical universal package for data query of day, week, month, quarter and year in JAVA code

Table of Contents

background

?edit

Implementation ideas

Package structure

Rule engine call entry

Call execution strategy

Rule engine detailed code

TimeConditionExpression.java

TimeConditionType.java

TimeConditionRule.java

TimeConditionRuleEngine.java

MergeRank.java

TimeConditionInvocationHandler.java

Next are 5 rule implementation classes

DayConditionRule.java

WeekConditionRule.java

MonthConditionRule.java

QuarterConditionRule.java

YearConditionRule.java

The final output data structure is as follows:


Background

In work, it is often necessary to perform statistical analysis on one or more indicators in the date dimension. Usually the simple idea is to implement it from SQL and directly query the indicator values of all dates in the current dimension. This way of writing SQL will be more complicated and not very scalable.

Now we introduce an implementation idea of statistical query in five dimensions of year, quarter, month, week and day encapsulated in JAVA code based on the design idea of rule engine. The overall code is concise, easy to read and easy to expand. Share it with you for reference and learn together!

Common scenarios are as follows:

Implementation Ideas

Next, I will post the code directly and briefly describe it.

Package structure

Rule engine call entrance

The parameter is the time dimension, which is an enumeration value defined in the policy.

@GetMapping("/clean-data-trend")
    @ApiOperation(value = "Cleaned data table/Cleaned coverage", notes = "day week month season year")
    public R<Object> cleanDataTrend(@ApiParam(value = "day week month season year", required = true) String dataType) {
        Map<String, Object> resultMap = dataGovernService.cleanDataTrend(dataType);
        return R.data(resultMap);
    }

Calling execution strategy

Finally, after rule processing, a set of indicator values of the current dimension is returned.

// Statistics cleaning data trends
TimeConditionExpression ex = new TimeConditionExpression(
                TimeConditionType.getCode(dataType),
                DataGovernMapper.class.getName(),
                new String[]{"cleanDataTableTrend"},
                null,
                null,
                sqlSession);
TimeConditionRuleEngine engine = new TimeConditionRuleEngine();
List<MergeRank> tableTrendList = engine1.process(ex);

Rule engine detailed code

The next thing is the important point

TimeConditionExpression.java

Dimension rule expressions can be understood as input parameters for execution in the engine.

range time range
className is the Mapper class name that the engine ultimately needs to execute.

method is the method name string that needs to be executed

paramTypes parameter types (types of additional parameters)

params parameter object (corresponding to parameter type)

sqlSession corresponds to the sqlSession of the connection source, used to execute single-granularity statistical sql

package com.dsj.prod.backend.biz.utils.tcr;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.ibatis.session.SqlSession;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TimeConditionExpression {

        private TimeConditionType range;
        private String className;
        private String[] method;
        private Class<?>[] paramTypes;
        private Object[] params;
        private SqlSession sqlSession;


}

TimeConditionType.java

time dimension enumeration

package com.dsj.prod.backend.biz.utils.tcr;

import com.baomidou.mybatisplus.annotation.EnumValue;

public enum TimeConditionType {
    DAY(1, "day"),
    WEEK(2, "week"),
    MONTH(3, "month"),
    SEASON(4, "season"),
    YEAR(5, "year"),
    ;

    TimeConditionType(int code, String descp){
        this.code = code;
        this.descp = descp;
    }

    @EnumValue
    private final int code;
    private final String descp;

    public static TimeConditionType getEnum(int value){
        for (TimeConditionType e:TimeConditionType.values()) {
            if(e.ordinal() == value) {
                return e;
            }
        }
        return null;
    }

    public static TimeConditionType getCode(String descp) {
        for (TimeConditionType e : TimeConditionType.values()) {
            if (e.getDescp().equalsIgnoreCase(descp)) {
                return e;
            }
        }
        return TimeConditionType.DAY;
    }

    public int getCode() {
        return code;
    }

    public String getDescp() {
        return descp;
    }
}

TimeConditionRule.java

Time dimension rule configuration.

import com.dsj.prod.backend.api.vo.MergeRank;
import org.apache.ibatis.session.SqlSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/**
 *Total amount of fused data
 */
public abstract class TimeConditionRule {
    private static final Logger logger = LoggerFactory.getLogger(TimeConditionRule.class);

    public static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static Calendar startCalendar = Calendar.getInstance(Locale.CHINA);
    public static Calendar endCalendar = Calendar.getInstance(Locale.CHINA);

    protected void initCalendar() {
        startCalendar = Calendar.getInstance(Locale.CHINA);
        endCalendar = Calendar.getInstance(Locale.CHINA);
        startCalendar.set(Calendar.MILLISECOND, 0);
        startCalendar.set(Calendar.SECOND, 0);
        startCalendar.set(Calendar.MINUTE, 0);
        startCalendar.set(Calendar.HOUR_OF_DAY, 0);
        endCalendar.set(Calendar.MILLISECOND, 999);
        endCalendar.set(Calendar.SECOND,59);
        endCalendar.set(Calendar.MINUTE, 59);
        endCalendar.set(Calendar.HOUR_OF_DAY, 23);
    }

    abstract boolean evaluate(TimeConditionExpression expression);

    abstract List<MergeRank> getResult();

    public Object getObject(String className, String methon, SqlSession sqlSession, Class<?>[] parameterTypes, Object[] args) {
        try {
            Class interfaceImpl = Class.forName(className);
            Object instance = Proxy.newProxyInstance(
                    interfaceImpl.getClassLoader(),
                    new Class[]{interfaceImpl},
                    new TimeConditionInvocationHandler(sqlSession.getMapper(interfaceImpl))
            );

            Method method = instance.getClass().getMethod(methon, parameterTypes);

            return method.invoke(instance, args);
        } catch (Exception e) {
            logger.error("getObject error is : ", e);
        }
        return null;
    }

    /**
     * Fix WEEK_OF_YEAR New Year's Eve trap in jdk
     *
     * @param date
     * @return {@link Integer}
     * @author chentl
     * @version v1.0.0
     * @since 3:21 pm 2022/1/6
     **/
    public static Integer getWeekOfYear(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setFirstDayOfWeek(Calendar.MONDAY);
        calendar.setTime(date);
        int week = calendar.get(Calendar.WEEK_OF_YEAR);
        // JDK think 2021-12-31 as 2022 1th week
        int mouth = calendar.get(Calendar.MONTH);
        if (mouth >= 11 & amp; & amp; week <= 1) {
            week + = 52;
        }
        return week;
    }
}

TimeConditionRuleEngine.java

Time dimension rule engine object, which performs specific rule calculations.

package com.dsj.prod.backend.biz.utils.tcr;

import com.dsj.prod.backend.api.vo.MergeRank;
import java.util.ArrayList;
import java.util.List;

public class TimeConditionRuleEngine {

    private static List<TimeConditionRule> rules = new ArrayList<>();

    static {
        rules.add(new DayConditionRule());
        rules.add(new WeekConditionRule());
        rules.add(new MonthConditionRule());
        rules.add(new QuarterConditionRule());
        rules.add(new YearConditionRule());
    }

    public List<MergeRank> process(TimeConditionExpression expression) {

        TimeConditionRule rule = rules.stream()
            .filter(r -> r.evaluate(expression))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("Expression does not match any Rule"));
        return rule.getResult();
    }
}

MergeRank.java

Merge statistical result object, used to receive fine-grained statistical results. Finally, all date data in the current time dimension are encapsulated into a set in order and returned.

package com.dsj.prod.backend.api.vo;

import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.Date;

@Data
public class MergeRank {

    private String dayCount;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM")
    @JSONField(format = "yyyy-MM")
    @ApiModelProperty(value = "Last call time of a single interface", required = true)
    private String time;
    private long millionTime;

    public static int compare(MergeRank o1,MergeRank o2){
        if (o1.getMillionTime() > o2.getMillionTime()) {
            return 1;
        }
        if (o1.getMillionTime() < o2.getMillionTime()) {
            return -1;
        }
        return 0;
    }
}

TimeConditionInvocationHandler.java

SQLMapper’s dynamic proxy class

/**
 * Dynamic proxy class
 */
public class TimeConditionInvocationHandler implements InvocationHandler {

    private Object target;

    public TimeConditionInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

XXXXMapper.xml

This is the most fine-grained statistical SQL implementation at the bottom, written by myself based on the business. Just query the indicator value of a certain time node in the time dimension. Isn’t it very simple!

 <sql id="timeRangeConditions">
        <if test="startDate!=null and endDate!=null">
            <![CDATA[
                    AND create_time >= #{startDate}
                    AND create_time <= #{endDate}
                ]]>
        </if>
    </sql>



<select id="cleanDataTableTrend" resultType="com.dsj.prod.backend.api.vo.MergeRank">
        SELECT
        count( DISTINCT t1.source_table_id ) dayCount
        FROM
        t_inspect_clean_task_table t1
        WHERE
        t1.is_deleted = 0
        <include refid="timeRangeConditions"/>

    </select>

Next are 5 rule implementation classes

DayConditionRule.java

By day dimension, the default is the last 7 days

package com.dsj.prod.backend.biz.utils.tcr;

import cn.hutool.core.util.ObjectUtil;
import com.dsj.prod.backend.api.vo.MergeRank;
import com.dsj.prod.common.utils.DateUtils;

import java.util.*;
import java.util.stream.Stream;

/**
 * Statistics every day for a week
 */
public class DayConditionRule extends TimeConditionRule {
    private List<MergeRank> mergeRankList = new ArrayList<>();

    @Override
    public boolean evaluate(TimeConditionExpression expression) {
        if (expression.getRange() == TimeConditionType.DAY) {
            initCalendar();

            List<MergeRank> mergeRankList = new ArrayList<>();

            for (int i = 0; i < 7; i + + ) {
                Class<?>[] defaultParamTypes = new Class<?>[]{String.class, String.class};
                Object[] defaultParams = new Object[]{format.format(startCalendar.getTime()), format.format(endCalendar.getTime())};

                Class<?>[] paramTypes = Stream.concat(
                        Arrays.stream(defaultParamTypes),
                        Objects.isNull(expression.getParamTypes()) ? Stream.empty() : Arrays.stream(expression.getParamTypes())
                ).toArray(Class[]::new);

                Object[] combinedParams = Stream.concat(
                        Arrays.stream(defaultParams),
                        Objects.isNull(expression.getParams()) ? Stream.empty() : Arrays.stream(expression.getParams())
                ).toArray();

                MergeRank mergeRank = (MergeRank) getObject(
                        expression.getClassName(),
                        expression.getMethod()[0],
                        expression.getSqlSession(),
                        paramTypes,
                        combinedParams
                );


                startCalendar.add(Calendar.DATE, -1);
                endCalendar.add(Calendar.DATE, -1);

                if (ObjectUtil.isNull(mergeRank)) {
                    mergeRank = new MergeRank();
                    mergeRank.setDayCount("0");
                }
                mergeRank.setTime(DateUtils.format(endCalendar.getTime(), "yyyy-MM-dd"));
                mergeRankList.add(mergeRank);
            }
            this.mergeRankList = mergeRankList;

            return true;
        }
        return false;
    }

    @Override
    public List<MergeRank> getResult() {
        return mergeRankList;
    }
}
WeekConditionRule.java

By week dimension, the default is the past 4 weeks

package com.dsj.prod.backend.biz.utils.tcr;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.dsj.prod.backend.api.enums.ProcessType;
import com.dsj.prod.backend.api.vo.MergeRank;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

/**
 * Weekly statistics for a month
 */
public class WeekConditionRule extends TimeConditionRule{
    private List<MergeRank> mergeRankList = new ArrayList<MergeRank>();

    @Override
    public boolean evaluate(TimeConditionExpression expression) {

        if (expression.getRange() == TimeConditionType.WEEK) {
            initCalendar();

            List<MergeRank> mergeRankList = new ArrayList<MergeRank>();
            startCalendar.setTime(DateUtil.beginOfMonth(new Date()));
            endCalendar.setTime(DateUtil.beginOfMonth(new Date()));

            for (int i = 0; i < 4; i + + ) {
                endCalendar.add(Calendar.DATE, + 6);

                MergeRank mergeRank = (MergeRank) getObject(expression.getClassName(), expression.getMethod()[0], expression.getSqlSession(),
                        ArrayUtil.addAll( expression.getParamTypes(),new Class[]{String.class, String.class}),
                        ArrayUtil.addAll(expression.getParams(),new Object[]{format.format(startCalendar.getTime()), format.format(endCalendar.getTime())}));

                if (ObjectUtil.isNull(mergeRank)) {
                    mergeRank = new MergeRank();
                    mergeRank.setDayCount("0");
                }
                mergeRank.setTime(MessageFormat.format("{1}th week of {0} year", String.valueOf(endCalendar.get(Calendar.YEAR)), getWeekOfYear(endCalendar.getTime())));
                mergeRank.setMillionTime(endCalendar.getTime().getTime());
                startCalendar.add(Calendar.DATE, + 6);
                mergeRankList.add(mergeRank);
            }

            //Sort
            mergeRankList.sort(MergeRank::compare);
            this.mergeRankList = mergeRankList;

            return true;
        }
        return false;
    }

    @Override
    public List<MergeRank> getResult() {
        return mergeRankList;
    }

}
MonthConditionRule.java

Statistics by month, default is December

package com.dsj.prod.backend.biz.utils.tcr;

import cn.hutool.core.util.ObjectUtil;
import com.dsj.prod.backend.api.enums.ProcessType;
import com.dsj.prod.backend.api.vo.MergeRank;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * Monthly and weekly statistics for a quarter
 */
public class MonthConditionRule extends TimeConditionRule{
    private List<MergeRank> mergeRankList = new ArrayList<MergeRank>();

    @Override
    public boolean evaluate(TimeConditionExpression expression) {

        if (expression.getRange() == TimeConditionType.MONTH) {
            initCalendar();

            List<MergeRank> mergeRankList = new ArrayList<MergeRank>();

            for (int i = 0; i < 12; i + + ) {
                startCalendar.add(Calendar.MONTH, -1);

                MergeRank mergeRank = (MergeRank) getObject(expression.getClassName(), expression.getMethod()[0], expression.getSqlSession(),
                        new Class[] {String.class, String.class},
                        new Object[] {format.format(startCalendar.getTime()), format.format(endCalendar.getTime())});


                if (ObjectUtil.isNull(mergeRank)) {
                    mergeRank = new MergeRank();
                    mergeRank.setDayCount("0");
                }
                mergeRank.setTime(MessageFormat.format("{0}年{1}month", String.valueOf(endCalendar.get(Calendar.YEAR)), endCalendar.get(Calendar.MONTH) + 1));
                endCalendar.add(Calendar.MONTH, -1);

                mergeRankList.add(mergeRank);
            }


            this.mergeRankList = mergeRankList;

            return true;
        }
        return false;
    }

    @Override
    public List<MergeRank> getResult() {
        return mergeRankList;
    }
}
QuarterConditionRule.java

Quarterly statistics, the default is the last 4 quarters

package com.dsj.prod.backend.biz.utils.tcr;

import cn.hutool.core.util.ObjectUtil;
import com.dsj.prod.backend.api.enums.ProcessType;
import com.dsj.prod.backend.api.vo.MergeRank;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * Statistics for each quarter of the year
 */
public class QuarterConditionRule extends TimeConditionRule{
    private List<MergeRank> mergeRankList = new ArrayList<MergeRank>();

    @Override
    public boolean evaluate(TimeConditionExpression expression) {

        if (expression.getRange() == TimeConditionType.SEASON) {
            initCalendar();

            List<MergeRank> mergeRankList = new ArrayList<MergeRank>();

            for (int i = 0; i < 4; i + + ) {
                startCalendar.add(Calendar.MONTH, -3);

                MergeRank mergeRank = (MergeRank) getObject(expression.getClassName(), expression.getMethod()[0], expression.getSqlSession(),
                        new Class[] {String.class, String.class},
                        new Object[] {format.format(startCalendar.getTime()), format.format(endCalendar.getTime())});


                if (ObjectUtil.isNull(mergeRank)) {
                    mergeRank = new MergeRank();
                    mergeRank.setDayCount("0");
                }

                //Calculate quarter
                int currentMonth = endCalendar.get(Calendar.MONTH);
                int currentQuarter = (currentMonth % 4) + 1;
                mergeRank.setTime(MessageFormat.format("{1} quarter of {0} year", String.valueOf(endCalendar.get(Calendar.YEAR)), currentQuarter));


                endCalendar.add(Calendar.MONTH, -3);
                mergeRankList.add(mergeRank);
            }


            this.mergeRankList = mergeRankList;

            return true;
        }
        return false;
    }

    @Override
    public List<MergeRank> getResult() {
        return mergeRankList;
    }
}
YearConditionRule.java

By year dimension, the default is the past 7 years.

package com.dsj.prod.backend.biz.utils.tcr;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.dsj.prod.backend.api.enums.ProcessType;
import com.dsj.prod.backend.api.vo.MergeRank;
import org.apache.commons.collections.ListUtils;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * Statistics for the last 7 years
 */
public class YearConditionRule extends TimeConditionRule{
    private List<MergeRank> mergeRankList = new ArrayList<MergeRank>();

    @Override
    public boolean evaluate(TimeConditionExpression expression) {

        if (expression.getRange() == TimeConditionType.YEAR) {
            initCalendar();

            List<MergeRank> mergeRankList = new ArrayList<MergeRank>();

            for (int i = 0; i < 7; i + + ) {
                startCalendar.add(Calendar.YEAR, -1);



                MergeRank mergeRank = (MergeRank) getObject(expression.getClassName(), expression.getMethod()[0], expression.getSqlSession(),
                        ArrayUtil.addAll(expression.getParamTypes(),new Class[]{String.class, String.class}),
                        ArrayUtil.addAll(expression.getParams(),new Object[]{format.format(startCalendar.getTime()), format.format(endCalendar.getTime())}));

                if (ObjectUtil.isNull(mergeRank)) {
                    mergeRank = new MergeRank();
                    mergeRank.setDayCount("0");
                }
                mergeRank.setTime(MessageFormat.format("{0}year", String.valueOf(endCalendar.get(Calendar.YEAR))));
                mergeRank.setMillionTime(endCalendar.getTime().getTime());
                endCalendar.add(Calendar.YEAR, -1);
                mergeRankList.add(mergeRank);
            }
            //Sort
            mergeRankList.sort(MergeRank::compare);
            this.mergeRankList = mergeRankList;

            return true;
        }
        return false;
    }

    @Override
    public List<MergeRank> getResult() {
        return mergeRankList;
    }
}

The final output data structure is as follows:

{
  "code": 200,
  "success": true,
  "data": {
    "apiDataSumTrend": [
      {
        "dayCount": "177246",
        "time": "46th week of 2023"
      },
      {
        "dayCount": "177036",
        "time": "45th week of 2023"
      },
      {
        "dayCount": "991",
        "time": "44th week of 2023"
      },
      {
        "dayCount": "381",
        "time": "43rd week of 2023"
      }
    ]
  },
  "msg": "Operation successful"
}