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" }