Java - 表数据监控(动态SQL执行+表达式匹配)

需求:对指定表数据进行监控,查询是否生成数据,生成视为成功,未生成视为失败。失败后可根据不同的条件类型,进行进一步更具体的判断监控(针对不同的类型,定制化查询SQL)。

一、设计思路

在数据治理和ETL流程监控场景中,经常需要对指定表数据进行监控,查询前置或当前任务的数据生成情况,获取生成状态以便进行后续处理。传统硬编码存在代码冗余、维护大量监控点困难等问题。

本方案选择将监控规则存储在配置表中,通过动态SQL执行和表达式匹配实现灵活的数据监控。

核心
1.监控规则配置表
2.SQL语句动态执行
3.表达式结果匹配
在这里插入图片描述

二、表结构

1.被监控的数据表 data_info

id(主键)type(类型)dataDate(生成日期)
1type120251001
2type220251001
3type320251001
4type120251002
5type120251003
6type320251004

2.监控规则配置表

id(主键)type(类型)content(规则语句)comp(规则匹配条件)sort(规则执行顺序)
1type1select COUNT(1) as num from data_info where dataDate::date = CURRENT_DATE and type = ‘type1’{num}<10
2type2select COUNT(1) as num from data_info where dataDate::date = CURRENT_DATE and type = ‘type2’{num}<10
30select COUNT(1) as num from data_info where dataDate::date = CURRENT_DATE and type = ${type}{num}<10

配置示例分析:
特定类型监控: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)
1select * from data_info where type = ${type}/dataInfo/pageselect 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;
}
posted @ 2025-12-12 16:06  御坂10027  阅读(12)  评论(0)    收藏  举报  来源