最近在写报表功能,当出现数据不准确时,就需要查看当前接口运行的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
上代码:
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();
}
因为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方法执行完就返回了 可以打个断点看一下
所以拦截器行不通,最后我才用了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();
}
}
}