使用 p6spy,拦截到持久层执行的sql及参数

Posted Peter-OK

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 p6spy,拦截到持久层执行的sql及参数相关的知识,希望对你有一定的参考价值。

声明:文章内容是 自己使用后整理,大部分工具代码出自大牛,但因无法确认出处,故仅在此处由衷的对无私分享源代码的作者表示感谢与致敬!

本人在拦截到sql的基础上加了分析功能和异常告警功能

1、导入p6spy的jar包,如果是maven项目引入pom

<dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.1</version>
        </dependency>

2、修改 datasource数据源 的 driverClassName驱动和 url地址 为 com.p6spy.engine.spy.P6SpyDriver

spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.url=jdbc:p6spy:mysql://$DB_HOST:3306/$DB_DATABASE?autoReconnect=true&useUnicode=true&characterEncoding=UTF8&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=CTT&allowMultiQueries=true

3、添加配置文件(springboot项目是放在resource目录下)

spy.properties

module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印 自定义P6SpyLogger类的地址
logMessageFormat=com.xxx.xxx.xxx.config.P6SpyLoggerFormatStrategy
# 使用日志系统记录sql (default is com.p6spy.engine.spy.appender.FileLogger)
#appender=com.p6spy.engine.spy.appender.StdoutLogger
#appender=com.p6spy.engine.spy.appender.FileLogger
appender=com.p6spy.engine.spy.appender.Slf4JLogger
## 配置记录Log例外
excludecategories=info,debug,result,batc,resultset
# 设置使用p6spy driver来做代理
deregisterdrivers=true
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动
driverlist=com.mysql.cj.jdbc.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 秒
outagedetectioninterval=2

①、修改driverlist为我们的实际驱动

②、指定日志输出样式logMessageFormat 默认为com.p6spy.engine.spy.appender.SingleLineFormat , 单行输出 不格式化语句。如果不满足,可以仿照其源码,实现MessageFormattingStrategy,自定义日志打印类(如下P6SpyLoggerFormatStrategy.java )

③、可以使用默认的日志系统类:Slf4JLogger、StdoutLogger、FileLogger,如果均不满足,可以仿照其源码,继承 FormattedLogger,自定义类

4、然后自定义一个类实现 MessageFormattingStrategy 接口,自定义日志格式化方式

P6SpyLoggerFormatStrategy .java

public class P6SpyLoggerFormatStrategy implements MessageFormattingStrategy 

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

    /**
     * 日志格式化方式(打印SQL日志会进入此方法,耗时操作,生产环境不建议使用)
     *
     * @param connectionId: 连接ID
     * @param now:          当前时间
     * @param elapsed:      花费时间
     * @param category:     类别
     * @param prepared:     预编译SQL
     * @param sql:          最终执行的SQL
     * @param url:          数据库连接地址
     * @return 格式化日志结果
     **/
    @Override
    public String formatMessage(int connectionId, String now, long elapsed,
                                String category, String prepared, String sql,
                                String url) 

        if (Func.isEmpty(sql)) 
            return "";
        
        // 格式化sql
        sql = Func.clearExtraSpaces(sql);
        // sql的DML
        String dml = SqlUtils.getSqlDML(sql);
        // 分析sql
        String analyzeResult = SqlUtils.analyzeSqlWhere(sql);
        // 拼装显示
        StringBuilder sb = new StringBuilder().append(formatter.format(LocalDateTime.now())).append(" |");
        if (Func.isNotEmpty(analyzeResult)) 
            sb.append(analyzeResult).append("|");
        
        sb.append(dml).append(" |took ").append(elapsed).append("ms |")
                .append(category).append("|").append(connectionId).append("|")
                .append(sql).append(";");
        // 风险sql会发送告警(邮件、短信、钉钉。。。),目前采用钉钉群告警,异步发送
        if (Func.isNotEmpty(analyzeResult)) 
            ThreadPoolUtils.execute(new SendNotice(sb.toString(), dml, analyzeResult, MDC.get("traceId")));
        
        return sb.toString();
    

    /** 内部线程类 */
    class SendNotice extends Thread 

        private String msg;
        private String dml;
        private String analyzeResult;
        private String traceId;

        public SendNotice(String msg, String dml, String analyzeResult,String traceId) 
            this.msg = msg;
            this.dml = dml;
            this.analyzeResult = analyzeResult;
            this.traceId = traceId;
        

        @Override
        public void run() 
//            HttpServletRequest request = SpringContextHolder.getBean(HttpServletRequest.class);
//            String uri = request.getRequestURI();
//            if (!StringUtils.isEmpty(request.getQueryString())) 
//                uri = uri + "?" + request.getQueryString();
//            

            HashMap<String, Object> parms = new HashMap<>(MapUtils.getSize(7));

            parms.put("title", dml);
            parms.put("traceId", traceId);
            parms.put("message", analyzeResult);
            parms.put("body", msg);

            EventSendService eventSendService = SpringContextUtil.getBean(EventSendService.class);
            eventSendService.sendAlertMsg(parms, "sql");
        
    

这样,p6spy就可以记录执行sql并输出了。后续是扩展部分:分析sql和异步发送告警

扩展部分一:分析sql的工具类  SqlUtils.java

@Slf4j
public class SqlUtils 

    /**
     * 获取aop中的SQL语句
     *
     * @param pjp
     * @param sqlSessionFactory
     * @return
     * @throws IllegalAccessException
     */
    public static String getMybatisSql(ProceedingJoinPoint pjp, SqlSessionFactory sqlSessionFactory) throws IllegalAccessException 
        Map<String, Object> map = new HashMap<>(16);
        //1.获取namespace+methdoName
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        String namespace = method.getDeclaringClass().getName();
        String methodName = method.getName();
        //2.根据namespace+methdoName获取相对应的MappedStatement
        Configuration configuration = sqlSessionFactory.getConfiguration();
        MappedStatement mappedStatement = configuration.getMappedStatement(namespace + "." + methodName);
        //3.获取方法参数列表名
        Parameter[] parameters = method.getParameters();
        //4.形参和实参的映射,获取实参
        Object[] objects = pjp.getArgs();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) 
            Object object = objects[i];
            //说明该参数没有注解,此时该参数可能是实体类,也可能是Map,也可能只是单参数
            if (parameterAnnotations[i].length == 0) 
                if (object.getClass().getClassLoader() == null && object instanceof Map) 
                    map.putAll((Map<? extends String, ?>) object);
                    log.info("该对象为Map");
                 else //形参为自定义实体类
                    map.putAll(objectToMap(object));
                    log.info("该对象为用户自定义的对象");
                
             else //说明该参数有注解,且必须为@Param
                for (Annotation annotation : parameterAnnotations[i]) 
                    if (annotation instanceof Param) 
                        map.put(((Param) annotation).value(), object);
                    
                
            
        
        //5.获取boundSql
        BoundSql boundSql = mappedStatement.getBoundSql(map);
//        BoundSql boundSql = mappedStatement.getBoundSql();
        return showSql(configuration, boundSql);
    

    /**
     * 解析BoundSql,生成不含占位符的SQL语句
     *
     * @param configuration
     * @param boundSql
     * @return
     */
    private static String showSql(Configuration configuration, BoundSql boundSql) 
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) 
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) 
                sql = sql.replaceFirst("\\\\?", getParameterValue(parameterObject));
             else 
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) 
                    String propertyName = parameterMapping.getProperty();
                    String[] s = metaObject.getObjectWrapper().getGetterNames();
                    s.toString();
                    if (metaObject.hasGetter(propertyName)) 
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\\\?", getParameterValue(obj));
                     else if (boundSql.hasAdditionalParameter(propertyName)) 
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\\\?", getParameterValue(obj));
                    
                
            
        
        return sql;
    

    /**
     * 若为字符串或者日期类型,则在参数两边添加''
     *
     * @param obj
     * @return
     */
    private static String getParameterValue(Object obj) 
        String value = null;
        if (obj instanceof String) 
            value = "'" + obj.toString() + "'";
         else if (obj instanceof Date) 
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
         else 
            if (obj != null) 
                value = obj.toString();
             else 
                value = "";
            
        
        return value;
    

    /**
     * 获取利用反射获取类里面的值和名称
     *
     * @param obj
     * @return
     * @throws IllegalAccessException
     */
    private static Map<String, Object> objectToMap(Object obj) throws IllegalAccessException 
        Map<String, Object> map = new HashMap<>(16);
        Class<?> clazz = obj.getClass();
        log.info("Class<?>=",clazz);
        // 获取本类及其父类的属性,↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
        List<Field> fieldList = new ArrayList<>();
        while (clazz != null) 
            fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
            clazz = clazz.getSuperclass();
        
        // 获取本类及其父类的属性,↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
        for (Field field : fieldList) 
            field.setAccessible(true);
            String fieldName = field.getName();
            Object value = field.get(obj);
            map.put(fieldName, value);
        
        return map;
    

    /**
     * 获取 DML 的 方式(insert、delete、update、select)
     * @param sql
     * @return
     */
    public static String getSqlDML(String sql) 

        if(Func.isEmpty(sql))
            return null;
        
        try 
            sql=sql.trim();
            int endIndex = sql.indexOf(" ")!=-1?sql.indexOf(" "):sql.length()>=6?6:sql.length();
            return sql.substring(0, endIndex).toUpperCase();
         catch (Exception e) 
            log.error("SqlUtils- 获取sql的DML 异常",e);
            return null;
        
    

    /**
     * 获取表名
     * @param sql
     * @return
     */
    public static String getTableName(String sql) 

        String dml = getSqlDML(sql);

        if(Func.isEmpty(dml))
            return null;
        
        // LogSqlEnum.DmlEnum.UPDATE.equals(dml) || LogSqlEnum.DmlEnum.DELETE.equals(dml) || LogSqlEnum.DmlEnum.SELECT.equals(dml)
        try 
            sql = sql.substring(6).trim().toUpperCase();
            if((LogSqlConstant.DmlEnum.DELETE.equals(dml) || LogSqlConstant.DmlEnum.SELECT.equals(dml))
                    && sql.contains("FROM"))
                sql=sql.substring(sql.indexOf("FROM")+4).trim();
                String[] s = sql.split(" ");
                if(s.length>1)
                    return s[0].trim();
                
            
            if(LogSqlConstant.DmlEnum.UPDATE.equals(dml) && sql.contains("SET"))
                return sql.substring(6,sql.indexOf("SET")).trim();
            
            return null;
         catch (Exception e) 
            log.error("SqlUtils- 获取表名 异常",e);
            return null;
        
    

    /**
     * 分析sql的where条件
     * @param sql
     * @return
     */
    public static String analyzeSqlWhere(String sql) 
        try 
            String dml = getSqlDML(sql);
            // 忽略枚举表
            if(LogSqlConstant.DmlEnum.SELECT.equals(dml)
                    && LogSqlConstant.getIgnoreTable().contains(getTableName(sql)))
                return null;
            

            if(LogSqlConstant.DmlEnum.UPDATE.equals(dml) || LogSqlConstant.DmlEnum.DELETE.equals(dml) || LogSqlConstant.DmlEnum.SELECT.equals(dml))
                String sqlUpperCase = sql.toUpperCase();

                if(!sqlUpperCase.contains("WHERE"))
                    return "【高风险sql】"+dml+"无WHERE条件";
                else 
                    String whereStr = sqlUpperCase.substring(sqlUpperCase.indexOf("WHERE")+5);
                    if(whereStr.contains("GROUP BY"))
                        whereStr = whereStr.substring(0,whereStr.indexOf("GROUP BY"));
                    else if(whereStr.contains("ORDER BY"))
                        whereStr = whereStr.substring(0,whereStr.indexOf("ORDER BY"));
                    
                    whereStr = whereStr.trim();
                    List<String> blList = Lists.newArrayList();
                    if(whereStr.contains("("))
                        String[] bl = whereStr.split("\\\\(");
                        for (String b : bl) 
                            if(Func.isNotEmpty(b))
                                blList.add(b);
                            
                        
                    
                    List<String> brList = Lists.newArrayList();
                    for (String bl : blList) 
                        String[] br = bl.split("\\\\)");
                        for (String b : br) 
                            if(Func.isNotEmpty(b))
                                brList.add(b);
                            
                        
                    
                    List<String> andList = Lists.newArrayList();
                    for (String br : brList) 

                        String[] ands = br.split("AND");
                        for (String s : ands) 
                            if(Func.isNotEmpty(s))
                                andList.add(s);
                            
                        
                    

                    List<String> orList = Lists.newArrayList();
                    for (String an : andList) 
                        String[] ors = an.split("OR");
                        for (String s : ors) 
                            if(Func.isNotEmpty(s))
                                orList.add(s);
                            
                        
                    
                    StringBuilder nullSB = new StringBuilder();
                    for (String or : orList) 
                        String[] eqs = or.split("=");
                        if(eqs.length==2
                                && (Func.isEmpty(eqs[1]) || "null".equalsIgnoreCase(eqs[1].trim()) ))
                            nullSB.append(or).append(";");
                        
                    

                    return nullSB.length()>0?nullSB.insert(0,"【中风险sql】条件值为空:").toString():null;
                
            
            return null;
         catch (Exception e) 
            log.error("SqlUtils-分析sql的where条件异常",e);
            return "SqlUtils-分析sql的where条件异常";
        
    

扩展部分二:异步发送告警

配置异步线程池 AsyncConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 线程池的配置
 */
@Configuration
public class AsyncConfig 

    /** 核心线程数 */
    private static final int CORE_POOL_SIZE =10;
    /** 最大线程数 */
    private static final int MAX_POOL_SIZE = 50;
    /** 队列最大长度 >=mainExecutor.maxSize  */
    private static final int QUEUE_CAPACITY =10;
    /** 线程池维护线程所允许的空闲时间 */
    private static final int KEEP_ALIVE_SECONDS =20;
    /** 线程池对拒绝任务(无线程可用)的处理策略 */
    private RejectedExecutionHandler rejectedExecutionHandler =new ThreadPoolExecutor.AbortPolicy();


    @Bean("asyncTaskExecutor")
    public AsyncTaskExecutor asyncTaskExecutor() 
        ThreadPoolTaskExecutor asyncTaskExecutor = new ThreadPoolTaskExecutor();
        asyncTaskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
        asyncTaskExecutor.setCorePoolSize(CORE_POOL_SIZE);
        asyncTaskExecutor.setQueueCapacity(QUEUE_CAPACITY);
        asyncTaskExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
        asyncTaskExecutor.setRejectedExecutionHandler(rejectedExecutionHandler);
        asyncTaskExecutor.setThreadNamePrefix("async-task-thread-pool-");
        asyncTaskExecutor.initialize();
        return asyncTaskExecutor;
    

线程工具类 ThreadPoolUtils.java

import java.util.concurrent.*;

/**
 * 线程 工具类
 */
public class ThreadPoolUtils 

    private static ExecutorService executor = Executors.newCachedThreadPool();

    /**
     * 直接在公共线程池中执行线程
     *
     * @param runnable 可运行对象
     */
    public static void execute(Runnable runnable) 
        try 
            executor.execute(runnable);
     catch (Exception e) 
            throw new RuntimeException("Exception when running task!", e);
        
    

    /**
     * 重启公共线程池
     */
    public static void restart() 
        executor.shutdownNow();
        executor = Executors.newCachedThreadPool();
    

    /**
     * 新建一个线程池
     *
     * @param threadSize 同时执行的线程数大小
     * @return ExecutorService
     */
    public static ExecutorService newExecutor(int threadSize) 
        return Executors.newFixedThreadPool(threadSize);
    

    /**
     * 获得一个新的线程池
     *
     * @return ExecutorService
     */
    public static ExecutorService newExecutor() 
        return Executors.newCachedThreadPool();
    

    /**
     * 获得一个新的线程池,只有单个线程
     *
     * @return ExecutorService
     */
    public static ExecutorService newSingleExecutor() 
        return Executors.newSingleThreadExecutor();
    

    /**
     * 执行异步方法
     *
     * @param runnable 需要执行的方法体
     * @return 执行的方法体
     */
    public static Runnable excAsync(final Runnable runnable, boolean isDeamon) 
        Thread thread = new Thread() 
            @Override
            public void run() 
                runnable.run();
            
        ;
        thread.setDaemon(isDeamon);
        thread.start();

        return runnable;
    

    /**
     * 执行有返回值的异步方法<br/>
     * Future代表一个异步执行的操作,通过get()方法可以获得操作的结果,如果异步操作还没有完成,则,get()会使当前线程阻塞
     *
     * @return Future
     */
    public static <T> Future<T> execAsync(Callable<T> task) 
        return executor.submit(task);
    

    /**
     * 新建一个CompletionService,调用其submit方法可以异步执行多个任务,最后调用take方法按照完成的顺序获得其结果。,若未完成,则会阻塞
     *
     * @return CompletionService
     */
    public static <T> CompletionService<T> newCompletionService() 
        return new ExecutorCompletionService<T>(executor);
    

    /**
     * 新建一个CompletionService,调用其submit方法可以异步执行多个任务,最后调用take方法按照完成的顺序获得其结果。,若未完成,则会阻塞
     *
     * @return CompletionService
     */
    public static <T> CompletionService<T> newCompletionService(ExecutorService executor) 
        return new ExecutorCompletionService<T>(executor);
    

    /**
     * 新建一个CountDownLatch
     *
     * @param threadCount 线程数量
     * @return CountDownLatch
     */
    public static CountDownLatch newCountDownLatch(int threadCount) 
        return new CountDownLatch(threadCount);
    

    /**
     * 挂起当前线程
     *
     * @param millis 挂起的毫秒数
     * @return 被中断返回false,否则true
     */
    public static boolean sleep(Number millis) 
        if (millis == null) 
            return true;
        

        try 
            Thread.sleep(millis.longValue());
         catch (InterruptedException e) 
            return false;
        
        return true;
    

    /**
     * @return 获得堆栈列表
     */
    public static StackTraceElement[] getStackTrace() 
        return Thread.currentThread().getStackTrace();
    

    /**
     * 获得堆栈项
     *
     * @param i 第几个堆栈项
     * @return 堆栈项
     */
    public static StackTraceElement getStackTraceElement(int i) 
        StackTraceElement[] stackTrace = getStackTrace();
        if (i < 0) 
            i += stackTrace.length;
        
        return stackTrace[i];
    

    /**
     * 创建本地线程对象
     *
     * @return 本地线程
     */
    public static <T> ThreadLocal<T> createThreadLocal(boolean isInheritable) 
        if (isInheritable) 
            return new InheritableThreadLocal<>();
         else 
            return new ThreadLocal<>();
        
    

以上是关于使用 p6spy,拦截到持久层执行的sql及参数的主要内容,如果未能解决你的问题,请参考以下文章

使用 p6spy,拦截到持久层执行的sql及参数

使用 p6spy,拦截到持久层执行的sql及参数

通过spring的AOP的切面,拦截到持久层执行的sql及参数

通过spring的AOP的切面,拦截到持久层执行的sql及参数

通过spring的AOP的切面,拦截到持久层执行的sql及参数

通过spring的AOP的切面,拦截到持久层执行的sql及参数