目录
背景
编辑
实现思路
包结构
规则引擎调用入口
调用执行策略
规则引擎详细代码
TimeConditionExpression.java
TimeConditionType.java
TimeConditionRule.java
TimeConditionRuleEngine.java
MergeRank.java
TimeConditionInvocationHandler.java
接下来是5个规则实现类
DayConditionRule.java
WeekConditionRule.java
MonthConditionRule.java
QuarterConditionRule.java
YearConditionRule.java
最终输出的数据结构如下:
背景
工作中常常需要对某一个或者多个指标进行日期维度的统计分析。通常简单的思路是从sql中实现,直接查询当前维度所有日期的指标值,这种写法sql会比较复杂,扩展性不强。
现在介绍一种JAVA代码中基于规则引擎的设计思路进行封装的 年、季、月、周、日五个维度的统计查询实现思路,整体代码简洁易读,易于扩展。分享给各位参考,一起学习!
常见的场景如下:
实现思路
接下来会直接贴代码,并简单描述。
包结构
规则引擎调用入口
参数即时间维度,这是策略中定义的枚举值。
@GetMapping("/clean-data-trend")
@ApiOperation(value = "已清洗数据表/已清洗覆盖率", 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);
}
调用执行策略
最终经过规则处理后返回一组当前维度的指标值。
// 统计清洗数据趋势
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);
规则引擎详细代码
接下来是重点
TimeConditionExpression.java
维度规则表达式,可以理解成引擎中执行的入参。
range 时间范围
className 引擎最终需要执行的 Mapper 类名method 需要执行的方法名字符串
paramTypes 参数类型 (额外参数的类型)
params 参数对象(与参数类型对应)
sqlSession 对应连接源的sqlSession,用于执行单粒度统计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
时间维度枚举
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
时间维度规则配置.
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;
/**
* 融合数据总量
*/
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;
}
/**
* 修复 jdk 的 WEEK_OF_YEAR 跨年陷阱
*
* @param date
* @return {@link Integer}
* @author chentl
* @version v1.0.0
* @since 3:21 下午 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 && week <= 1) {
week += 52;
}
return week;
}
}
TimeConditionRuleEngine.java
时间维度规则引擎对象,由它执行具体的规则计算。
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 matches any Rule"));
return rule.getResult();
}
}
MergeRank.java
合并统计结果对象,用于接收细粒度统计结果。最终将当前时间维度的所有日期数据按顺序封装成集合返回。
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 = "单个接口上次调用时间", 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的动态代理类
/**
* 动态代理类
*/
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
这个是底层最细粒度的统计sql实现,由自己根据业务编写。查询出时间维度下,某个时间节点的指标值即可。是不是非常简单!
<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>
接下来是5个规则实现类
DayConditionRule.java
按天维度,默认近7天
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;
/**
* 一个星期每天统计
*/
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
按周维度,默认近4周
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.vo.MergeRank;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/**
* 一个月每周统计
*/
public class WeekConditionRule extends TimeConditionRule{
private List<MergeRank> mergeRankList = new ArrayList<MergeRank>();
public static void main(String[] args) {
System.out.println(DateUtil.beginOfWeek(new Date()));
System.out.println(DateUtil.endOfWeek(new Date()));
}
@Override
public boolean evaluate(TimeConditionExpression expression) {
if (expression.getRange() == TimeConditionType.WEEK) {
initCalendar();
List<MergeRank> mergeRankList = new ArrayList<MergeRank>();
startCalendar.setTime(DateUtil.beginOfWeek(new Date()));
endCalendar.setTime(DateUtil.endOfWeek(new Date()));
for (int i = 0; i < 4; i++) {
System.out.println("startCalendar: " + format.format(startCalendar.getTime()));
System.out.println("endCalendar: " + format.format(endCalendar.getTime()));
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}年第{1}周", String.valueOf(endCalendar.get(Calendar.YEAR)), getWeekOfYear(endCalendar.getTime())));
mergeRank.setMillionTime(endCalendar.getTime().getTime());
startCalendar.add(Calendar.DATE, -7);
endCalendar.add(Calendar.DATE, -7);
mergeRankList.add(mergeRank);
}
//排序
mergeRankList.sort(MergeRank::compare);
this.mergeRankList = mergeRankList;
return true;
}
return false;
}
@Override
public List<MergeRank> getResult() {
return mergeRankList;
}
}
MonthConditionRule.java
按月统计,默认12月
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;
/**
* 一个季度每月周统计
*/
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}月", 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
按季统计,默认近4季度
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;
/**
* 一年每个季度统计
*/
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");
}
//计算季度
int currentMonth = endCalendar.get(Calendar.MONTH);
int currentQuarter = (currentMonth % 4) + 1;
mergeRank.setTime(MessageFormat.format("{0}年第{1}季度", 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
按年维度,默认近7年。
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;
/**
* 最近7年统计
*/
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}年", String.valueOf(endCalendar.get(Calendar.YEAR))));
mergeRank.setMillionTime(endCalendar.getTime().getTime());
endCalendar.add(Calendar.YEAR, -1);
mergeRankList.add(mergeRank);
}
//排序
mergeRankList.sort(MergeRank::compare);
this.mergeRankList = mergeRankList;
return true;
}
return false;
}
@Override
public List<MergeRank> getResult() {
return mergeRankList;
}
}
最终输出的数据结构如下:
{
"code": 200,
"success": true,
"data": {
"apiDataSumTrend": [
{
"dayCount": "177246",
"time": "2023年第46周"
},
{
"dayCount": "177036",
"time": "2023年第45周"
},
{
"dayCount": "991",
"time": "2023年第44周"
},
{
"dayCount": "381",
"time": "2023年第43周"
}
]
},
"msg": "操作成功"
}