Java - 表数据监控(动态SQL执行+表达式匹配)
需求:对指定表数据进行监控,查询是否生成数据,生成视为成功,未生成视为失败。失败后可根据不同的条件类型,进行进一步更具体的判断监控(针对不同的类型,定制化查询SQL)。
目录
一、设计思路
在数据治理和ETL流程监控场景中,经常需要对指定表数据进行监控,查询前置或当前任务的数据生成情况,获取生成状态以便进行后续处理。传统硬编码存在代码冗余、维护大量监控点困难等问题。
本方案选择将监控规则存储在配置表中,通过动态SQL执行和表达式匹配实现灵活的数据监控。
核心:
1.监控规则配置表
2.SQL语句动态执行
3.表达式结果匹配

二、表结构
1.被监控的数据表 data_info
| id(主键) | type(类型) | dataDate(生成日期) |
|---|---|---|
| 1 | type1 | 20251001 |
| 2 | type2 | 20251001 |
| 3 | type3 | 20251001 |
| 4 | type1 | 20251002 |
| 5 | type1 | 20251003 |
| 6 | type3 | 20251004 |
2.监控规则配置表
| id(主键) | type(类型) | content(规则语句) | comp(规则匹配条件) | sort(规则执行顺序) |
|---|---|---|---|---|
| 1 | type1 | select COUNT(1) as num from data_info where dataDate::date = CURRENT_DATE and type = ‘type1’ | {num}<1 | 0 |
| 2 | type2 | select COUNT(1) as num from data_info where dataDate::date = CURRENT_DATE and type = ‘type2’ | {num}<1 | 0 |
| 3 | 0 | select COUNT(1) as num from data_info where dataDate::date = CURRENT_DATE and type = ${type} | {num}<1 | 0 |
配置示例分析:
特定类型监控:type='type1’时,监控type1数据的当日生成情况
通用类型监控:type='0’时,通过参数${type}实现可配置监控
表达式设计:{num}<1 表示"未生成数据"的监控失败条件
三、核心实现原理
3.1.动态SQL执行器(DynamicIntegerSqlExecutor)
因为判断时仅需根据数量进行简单判断,所以仅设置了返回Integer类型的方法;但支持多种查询场景。
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>
* 动态SQL执行器,仅返回Integer结果
* </p>
*/
@Slf4j
@Repository
public class DynamicIntegerSqlExecutor {
private final JdbcTemplate jdbcTemplate;
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public DynamicIntegerSqlExecutor(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
}
/**
* 执行单条SQL并返回Integer结果
* @return
*/
public Integer executeSql(String sql, Map<String, Object> params) {
try {
if (params == null || params.isEmpty()) {
// 如果没有参数,使用普通的jdbcTemplate
log.info("DynamicIntegerSqlExecutor-sql执行语句:{}", JSONUtil.toJsonStr(sql));
return jdbcTemplate.queryForObject(sql, Integer.class);
} else {
//使用命名参数,将${paramName}格式转换为:paramName格式
// return namedParameterJdbcTemplate.queryForObject(sql, params, Integer.class);
// 使用预编译语句,避免SQL注入
SqlParseResult parseResult = convertToPreparedStatement(sql, params);
log.info("DynamicIntegerSqlExecutor-sql执行参数:{}", JSONUtil.toJsonStr(parseResult.getPositionlaParams()));
log.info("DynamicIntegerSqlExecutor-sql执行语句:{}", JSONUtil.toJsonStr(parseResult.getSql()));
return jdbcTemplate.queryForObject(parseResult.getSql(), Integer.class, parseResult.getPositionlaParams());
}
} catch (Exception e) {
throw new RuntimeException("执行SQL失败:" + sql, e);
}
}
/**
* 转换为预编译语句格式(?占位符与按顺序排放的参数)
* 使用预编译语句,避免SQL注入风险
* @param sql
* @param params
* @return
*/
private SqlParseResult convertToPreparedStatement(String sql, Map<String, Object> params) {
Pattern pattern = Pattern.compile("\\$\\{(\\w+)\\}");
Matcher matcher = pattern.matcher(sql);
StringBuffer resultSql = new StringBuffer();
ArrayList<Object> positionalParams = new ArrayList<>();
try {
while (matcher.find()) {
String paramName = matcher.group(1);
Object paramValue = params.get(paramName);
if (paramValue == null) {
throw new IllegalAccessException("参数未提供:" + paramName);
}
// 处理List类型的参数
if (paramValue instanceof List) {
List<?> listValue = (List<?>) paramValue;
if (listValue.isEmpty()) {
// 处理空列表的情况
matcher.appendReplacement(resultSql, "NULL");
} else {
// 生成多个占位符:?,?,?
String placeholders = String.join(",",
Collections.nCopies(listValue.size(), "?"));
matcher.appendReplacement(resultSql, placeholders);
positionalParams.addAll(listValue);
}
} else {
// 替换为 ?占位符
matcher.appendReplacement(resultSql, "?");
positionalParams.add(paramValue);
}
}
} catch (IllegalAccessException e) {
log.error("参数未提供:{}", e);
}
matcher.appendTail(resultSql);
return new SqlParseResult(resultSql.toString(), positionalParams.toArray());
}
/**
* 批量执行多条SQL并返回结果列表
*/
public List<Integer> executeMultipleSql(List<String> sqlList) {
List<Integer> results = new ArrayList<>();
for (String sql : sqlList) {
results.add(executeSql(sql, new HashMap<>()));
}
return results;
}
/**
* 批量执行带参数的SQL
*/
public List<Integer> executeMultipleSqlWithParams(List<SqlWithParams> sqlWithParamsList) {
List<Integer> results = new ArrayList<>();
for (SqlWithParams swp : sqlWithParamsList) {
results.add(executeSql(swp.getSql(), swp.getParams()));
}
return results;
}
/**
* 执行单条SQL并返回Integer结果
* String型入参添加模糊查询通配符
* @param sql
* @param params
* @return
*/
public Integer executeSqlWithLike(String sql, Map<String, Object> params) {
Map<String, Object> processedParams = new HashMap<>();
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (entry.getValue() instanceof String) {
// 自动为字符串参数添加%通配符
processedParams.put(entry.getKey(), "%" + entry.getValue() + "%");
} else {
processedParams.put(entry.getKey(), entry.getValue());
}
}
return executeSql(sql, processedParams);
}
// 参数辅助类
public static class SqlWithParams {
private String sql;
private Map<String, Object> params;
public SqlWithParams(String sql, Map<String, Object> params) {
this.sql = sql;
this.params = params;
}
public SqlWithParams(String sql) {
this(sql, new HashMap<>(0));
}
// getter方法
public String getSql() {return sql;}
public Map<String, Object> getParams() {return params;}
}
// 结果辅助类
public static class SqlParseResult {
private String sql;
private Map<String, Object> params;
private Object[] positionlaParams;
public SqlParseResult(String sql, Map<String, Object> params) {
this.sql = sql;
this.params = params;
}
public SqlParseResult(String sql, Object[] positionalParams) {
this.sql = sql;
this.positionlaParams = positionalParams;
}
public String getSql() {return sql;}
public Map<String, Object> getParams() {return params;}
public Object[] getPositionlaParams() {return positionlaParams;}
}
}
3.2.表达式评估器工具类(ExpressionEvaluatorUtils)
SpEL(Spring Expression Language)应用:用于解析表达式子 {num}>1并获取结果。
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.excel.util.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* <p>
* 表达式评估器工具类
* </p>
*/
@Component
public class ExpressionEvaluatorUtils {
/**
* 替换模板变量
* @param template
* @param variables
* @return
*/
public static String replaceVariables(String template, Map<String, Object> variables) {
String result = template;
for (Map.Entry<String, Object> entry : variables.entrySet()) {
String placeholder = "{" + entry.getKey() + "}";
result = result.replace(placeholder, entry.getValue().toString());
}
return result;
}
/**
* SpEL解析
* 返回表达式执行结果
* @param template
* @param variables
* @return
*/
public static boolean evaluateExpression(String template, Map<String, Object> variables) {
// 替换模板中的变量
String expressionStr = replaceVariables(template, variables);
// 使用SpEl解析表达式
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(expressionStr);
// 执行表达式并返回布尔结果
return Boolean.TRUE.equals(expression.getValue(Boolean.class));
}
public static void main(String[] args) {
String template = "{num}<1";
Map<String, Object> variableMap = new HashMap<>(1);
variableMap.put("num", "20.3");
System.out.printf("表达式解析结果" + evaluateExpression(template, variableMap));
}
}
3.3.监控流程控制
/**
* 根据通用规则获取监控结果
* @param ruleContent 查询SQL eg: select count(1) as num from table ...
* @param ruleComp 匹配条件 eg:{num} < 1
* @param ruleParams 查询SQL入参 eg: ${type}
* @return
*/
private String getRuleCompResult(String ruleContent, String ruleComp, Map<String, Object> ruleParams) {
boolean compResult = false;
String monitorResult = "1"; // 成功
// 动态执行SQL获得替换{num}的数值
Integer num = dynamicIntegerSqlExecutor.executeSql(ruleContent, ruleParams);
if (num != null) {
Map<String, Object> expressionParams = new HashMap<>(1);
expressionParams.put("num", num);
// 解析表达式: {num} > 1,获得最终结果
compResult = ExpressionEvaluatorUtils.evaluateExpression(ruleComp, expressionParams);
log.info("getMonitorResult-获取监控结果:SQL执行结果num:{}, 匹配公式:{},结果:{}"
, num, ruleComp, compResult);
}
if (compResult) {
// 命中匹配结果 -> 失败
monitorResult = "0";
}
return monitorResult;
}
三、高级扩展学习(动态查询分页列表)
当需要动态查询表返回分页列表时,可以将查询SQL存入表中,但是入参必须是必填项。
表结构
| id(主键) | sql_content(sql语句) | sql_path(请求路径) | sql_count_content(sql语句2) |
|---|---|---|---|
| 1 | select * from data_info where type = ${type} | /dataInfo/page | select count(1) from data_info where type = ${type} |
Service层
/**
* 根据动态语句获取分页列表
* @param queryKey 请求路径
* @param params 入参
* @return
*/
public Page<Map<String, Object>> executeQuery(String queryKey, Map<String, Object> params) {
// 根据请求路径获取SQL信息
final DynamicSqlVO dynamicSqlByPath = getDynamicSqlByPath(queryKey);
if(dynamicSqlByPath == null){
throw new RuntimeException("动态接口不存在,请重新测试");
}
final List<Map<String, Object>> maps = sqlMapper.executeQuery(dynamicSqlByPath, params);
final List<Map<String, Object>> countMap = sqlMapper.executeCountQuery(dynamicSqlByPath, params);
Integer total = 0;
if(!CollectionUtils.isEmpty(countMap)){
total = Integer.valueOf(countMap.get(0).get("total").toString());
}
//处理分页参数
Object pageIndex = params.get("pageNo");
if(pageIndex == null){
pageIndex = 1;
}
Object pageSize = params.get("pageSize");
if(pageSize == null){
pageSize = 20;
}
Page<Map<String, Object>> page = new Page<Map<String, Object>>();
page.setRecords(maps);
page.setTotal(total);
page.setSize(Long.valueOf(String.valueOf(pageSize)));
page.setPages(total%(int)pageSize == 0 ? total/(int)pageSize : total/(int)pageSize + 1);
page.setCurrent((int)pageIndex);
return page;
}
Mapper层
@SelectProvider(type = DynamicSqlProvider.class, method = "buildQuery")
List<Map<String, Object>> executeQuery(
@Param("queryKey") DynamicSqlVO queryKey,
@Param("params") Map<String, Object> params
);
@SelectProvider(type = DynamicSqlProvider.class, method = "buildCountQuery")
List<Map<String, Object>> executeCountQuery(
@Param("queryKey") DynamicSqlVO queryKey,
@Param("params") Map<String, Object> params
);
Provider(提供者)
Provider 是 策略模式 的具体实现,负责动态生成 SQL 语句。它通常作为一个 “SQL 工厂”,根据不同的参数和条件生成相应的 SQL。
public class DynamicSqlProvider {
public String buildQuery(Map<String, Object> params) {
DynamicSqlVO queryKey = (DynamicSqlVO) params.get("queryKey");
Map<String, Object> queryParams = (Map) params.get("params");
return DynamicSqlLoader.getDynamicSql(queryKey, queryParams);
}
public String buildCountQuery(Map<String, Object> params) {
DynamicSqlVO queryKey = (DynamicSqlVO) params.get("queryKey");
Map<String, Object> queryParams = (Map) params.get("params");
return DynamicSqlLoader.getDynamicCountSql(queryKey, queryParams);
}
}
Loader(加载器)
Loader 是 资源加载模式 的实现,负责从各种来源(数据库、文件、配置中心)加载配置信息或资源。它更像是数据的提供者和管理者。
@Slf4j
@Component
public class DynamicSqlLoader {
public static String getDynamicSql(DynamicSqlVO config, Map<String, Object> params) {
return buildSql(config, params);
}
public static String getDynamicCountSql(DynamicSqlVO config, Map<String, Object> params) {
return buildCountSql(config, params);
}
private static String buildSql(DynamicSqlVO config, Map<String, Object> params) {
/*动态替换条件
SELECT * FROM user WHERE 1=0 limit 0,10
*/
String sql = config.getSqlContent();
//处理分页参数
Object pageIndex = params.get("pageNo");
if(pageIndex == null){
pageIndex = 1;
}
Object pageSize = params.get("pageSize");
if(pageSize == null){
pageSize = 20;
}
int start = ((int)pageIndex-1)*(int)pageSize;
int end = (int)pageIndex*(int)pageSize;
params.put("start",pageSize);
params.put("end",start);
log.info("执行参数替换前的SQL:"+sql);
//统一处理参数
for (Map.Entry<String, Object> entry : params.entrySet()) {
sql = sql.replaceAll(Pattern.quote("${"+entry.getKey()+"}"),String.valueOf(entry.getValue()));
}
log.info("执行参数替换后的SQL:"+sql);
//替换用户信息参数 ${sys.user.userCode}
for (Map.Entry<String, String> entry : BaseContextHandler.getLocalMap().entrySet()) {
sql = sql.replaceAll(Pattern.quote("${sys.user."+entry.getKey()+"}"),String.valueOf(entry.getValue()));
}
log.info("执行参数替换用户信息后的SQL:"+sql);
// 1.处理无用的/r /n
//sql = sql.replaceAll("[\\r\\n]+", " ");
// 2. 压缩连续空格
sql = sql.replaceAll("\\s+", " ");
//处理不包含分页的内容
if(!sql.toUpperCase(Locale.ROOT).contains("LIMIT")){
sql = String.format("SELECT tab_.* FROM (%s) tab_ LIMIT %s OFFSET %s ",sql,pageSize,start);
log.info("不包含分页,执行参数替换后的SQL:"+sql);
}
return sql;
}
private static String buildCountSql(DynamicSqlVO config, Map<String, Object> params) {
/*动态替换条件
SELECT * FROM user WHERE 1=0 limit 0,10
*/
String sql = config.getSqlContent();
if(!StringUtils.isEmpty(config.getSqlCountContent())){
sql = config.getSqlCountContent();
}
int start = 0;
// 作为统计逻辑 end应该无穷大
int end = 1000000;
params.put("start",start);
params.put("end",end);
log.info("执行参数替换前的SQL:"+sql);
//统一处理参数
for (Map.Entry<String, Object> entry : params.entrySet()) {
sql = sql.replaceAll(Pattern.quote("${"+entry.getKey()+"}"),String.valueOf(entry.getValue()));
}
log.info("执行参数替换后的SQL:"+sql);
//替换用户信息参数 ${sys.user.userCode}
for (Map.Entry<String, String> entry : BaseContextHandler.getLocalMap().entrySet()) {
sql = sql.replaceAll(Pattern.quote("${sys.user."+entry.getKey()+"}"),String.valueOf(entry.getValue()));
}
log.info("执行参数替换用户信息后的SQL:"+sql);
if(StringUtils.isEmpty(config.getSqlCountContent())){
sql = String.format("SELECT count(1) as total FROM (%s) tab_ ",sql);
log.info("不包含分页,执行参数替换后的SQL:"+sql);
}
return sql;
}
public static void main(String[] args) {
String sql = "SELECT * FROM user WHERE 1=0 limit ${start},${end} /r/n";
//统一处理参数
Map<String, Object> params = new HashMap<>();
params.put("start",10);
params.put("end",20);
for (Map.Entry<String, Object> entry : params.entrySet()) {
sql = sql.replaceAll(Pattern.quote("${"+entry.getKey()+"}"),String.valueOf(entry.getValue()));
}
sql = String.format("SELECT tab_.* FROM (%s) tab_ LIMIT %d OFFSET %d ",sql,0,10);
System.out.println(sql.replaceAll("/r"," "));
}
}
DynamicSqlVO
@Data
@ApiModel("动态sql数据对象出参")
public class DynamicSqlVO implements Serializable {
@ApiModelProperty("动态sqlId")
private Long sqlId;
@ApiModelProperty("动态sql内容")
private String sqlContent;
@ApiModelProperty("动态统计sql内容")
private String sqlCountContent;
@ApiModelProperty("动态sql路径")
private String sqlPath;
}

浙公网安备 33010602011771号