SpringBoot+Mybatis打印完整SQL并展示到前端

概况

最近在写报表功能,当出现数据不准确时,就需要查看当前接口运行的SQL,对比SQL是否有问题,那么就需要在业务侧拿到此次接口运行的SQL语句。实现这个功能主要从两个方面去考虑。
1.Mybatis框架的插件
Mybatis通过设置插件形式,拦截执行的SQL并设置进ThreadLocal里,就能实现整个请求线程中拿到完整SQL,但是目前其他博客都是SQL占位符,并没有能完整打印SQL。
2.Mybatis Log pluging
Mybatis Log pluging看了源码主要是从控制台去组装SQL,对于业务代码是零侵入的,不适合此次需求。
此次主要用了Mybatis框架的插件去实现的功能,废话不多说,
先看效果:
后端控制台效果:

2021-06-26 15:10:09,040 INFO  [http-nio-31058-exec-6] com.xxxx.xx.xx.config.SqlStatementInterceptor - 执行SQL: [select operate_area as primaryKey, count(*) as openDays from ( select operate_area from app.holo_dws_pay_date_shop_business where order_amt > 0 and pay_date between '20210501' and '20210531' and operate_area is not null and order_type = 'ecom' group by pay_date, shop_id, operate_area ) t1 group by operate_area]花费769ms

前端Base64加密效果:
SpringBoot+Mybatis打印完整SQL并展示到前端_第1张图片

上代码:

实现步骤

创建ThreadLocal

public class Constant {

    /**
     * 当前线程
     */
    public final static ThreadLocal<String> THREAD_SQL = new ThreadLocal<>();
}

创建拦截器

@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})})
public class SqlStatementInterceptor implements Interceptor {

    private final Logger logger = LoggerFactory.getLogger(SqlStatementInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long endTime = System.currentTimeMillis();
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            BoundSql boundSql = statementHandler.getBoundSql();
            String sql = boundSql.getSql();
            //args里面包含完整sql
            if (invocation.getArgs().length > 0) {
                try {
                    //getTarget可以获取,如果报错尝试下 invocation.getArgs()[0].toString() 不同的SQL连接不同噢
                    sql = getTarget(invocation.getArgs()[0]).toString();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            //sql整理
            sql = sql.replace("\n", "").replace("\t", "").replaceAll("\\s+", " ");
            //收集此次访问的SQL
            String threadSql = Constant.THREAD_SQL.get();
            threadSql = threadSql == null ? "" : threadSql;
            threadSql = threadSql + "|" + sql;
            Constant.THREAD_SQL.set(threadSql);

            logger.info("执行SQL: [{}]花费{}ms", sql, (endTime - startTime));
        }
    }

    /**
     * 从代理对象中获取被代理的对象
     *
     * @param proxy
     * @return
     * @throws Exception
     */
    public Object getTarget(Object proxy) throws Exception {
        Field[] fields = proxy.getClass().getSuperclass().getFields();
        for (Field field : fields) {
            System.out.println(field.getName());
        }
        Field field = proxy.getClass().getSuperclass().getDeclaredField("h");
        field.setAccessible(true);
        //获取指定对象中此字段的值
        //获取Proxy对象中的此字段的值
        PreparedStatementLogger personProxy = (PreparedStatementLogger) field.get(proxy);
        Field statement = personProxy.getClass().getDeclaredField("statement");
        statement.setAccessible(true);
        return statement.get(personProxy);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

添加插件:

    @Bean(name = "SqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setPlugins(new Interceptor[]{new SqlStatementInterceptor()});
        bean.setDataSource(dynamicDataSource);
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/**/*.xml"));
        return bean.getObject();
    }

设置response header

因为SQL语句是非业务层面的东西,所以规范来说是不好放在响应结果体里面的。
刚开始我采用拦截器去实现,但是踩了个坑 拦截器的postHandler方法设置请求头不生效问题,因为请求头在执行完controller就返回给前端了,再去执行postHandler。下面是springMvc的部分源码

// DispatcherServlet.java类
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	....省略代码
	// #1 进行前置处理,进入controller方法之前的处理
	if (!mappedHandler.applyPreHandle(processedRequest, response)) {
		return;
	}

	// #2 执行controller方法
	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

	if (asyncManager.isConcurrentHandlingStarted()) {
		return;
	}

	applyDefaultViewName(processedRequest, mv);
	// #3 执行后置处理,也就是controller方法执行完成后进行的处理
	mappedHandler.applyPostHandle(processedRequest, response, mv);
	....省略代码
}

在#2方法执行完就返回了 可以打个断点看一下
SpringBoot+Mybatis打印完整SQL并展示到前端_第2张图片
SpringBoot+Mybatis打印完整SQL并展示到前端_第3张图片
所以拦截器行不通,最后我才用了AOP的方式实现的,代码如下:

@Aspect
@Order(1)
@Component
public class SQLAspect implements HandlerInterceptor {

    /**
     * 选择切入点
     */
    @Pointcut("execution(* com.xxxx.xx.xxx.web.controller..*Controller.*(..))")
    public void dsPointCut() {
    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletResponse response = null;
        if (sra != null) {
            response = sra.getResponse();
        }

        try {
            return point.proceed();
        } finally {
            String sqlList = Constant.THREAD_SQL.get();
            if (!StringUtils.isEmpty(sqlList)) {
                if (response != null) {
                    response.addHeader("Query-Sql", sqlList);
                }
            }
            // 销毁数据源 在执行方法之后
            Constant.THREAD_SQL.remove();
        }
    }
}

至此,整个业务需求就已经实现了,还是要说明一下,SQL这样打印在前端会产生暴露数据库的结构的风险,所以正式环境不要用。此次代码仅在测试分支上开发,并不会提交到主分支。

补充:由于SQL存在部分特殊字符以及中文的情况,存放header要解决乱码解决字符情况。我这里做了个编码上的调整:

@Aspect
@Order(1)
@Component
public class SQLAspect implements HandlerInterceptor {

    /**
     * 选择切入点
     */
    @Pointcut("execution(* com.zhuizhi.data.center.web.controller..*Controller.*(..))")
    public void dsPointCut() {
    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletResponse response = null;
        if (sra != null) {
            response = sra.getResponse();
        }

        try {
            return point.proceed();
        } finally {
            String sqlList = Constant.THREAD_SQL.get();
            if (!StringUtils.isEmpty(sqlList)) {
                if (response != null) {
                    //原文直接传会出各种符号,编码问题  采用Base64 加密传输 到时候前端解密
                    byte[] bytes = sqlList.getBytes();
                    String encoded = Base64.getEncoder().encodeToString(bytes);
                    response.addHeader("Query-Sql", encoded);
                }
            }
            //  在执行方法之后
            Constant.THREAD_SQL.remove();
        }
    }

}

前端那边用Base64解密就能拿到原文
SpringBoot+Mybatis打印完整SQL并展示到前端_第4张图片

关注我的公众号,获取更多有用信息噢~
SpringBoot+Mybatis打印完整SQL并展示到前端_第5张图片

你可能感兴趣的:(Mybatis,mybatis,aop,spring,java)