JPA拦截器——打印调用堆栈+点击日志跳转对应方法

之前写过一篇,JPA 打印的原生 SQL太恶心了,怎么办

使用JPA拦截器和正则,来打印稍微好看一点的SQL

打印JPA的原生SQL

#原生打印sql
#spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=debug
#spring.jpa.properties.hibernate.format_sql=true
#打印参数
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace
复制代码

spring.jpa.show-sqllogging.level.org.hibernate.SQL效果一样。打印SQL如下:

select user0_.id as id1_1_0_, user0_.age as age2_1_0_, user0_.name 
as name3_1_0_, user0_.version as version4_1_0_ from user user0_ where user0_.id=?
复制代码

spring.jpa.properties.hibernate.format_sql这个是格式化SQL,效果如下:

select
    user0_.id as id1_1_0_,
    user0_.age as age2_1_0_,
    user0_.name as name3_1_0_,
    user0_.version as version4_1_0_ 
from
    user user0_ 
where
    user0_.id=?
复制代码

格式个毛线,并没有变很漂亮...

使用自定义拦截器
spring.jpa.properties.hibernate.session_factory.statement_inspector=com.ler.demo.interceptor.JpaInterceptor自定义拦截器后,效果如下:

select us.id , us.age , us.name , us.version  from user us where us.id=?
复制代码

本来关于JPA也就告一段落了,对于JPA,实在喜欢不起来。

一个神奇的BUG,让我只能硬着头皮来再一次优化这个拦截器。

JPA里,saveupdate是合二为一的,而这个项目的代码也比较奇葩,各种继承,各种嵌套,各种绑定,然后没有this,没有super,一个空荡荡的save(),变着法的写出难以阅读的代码,不知道什么仇什么怨...

而且JPA的特性还比较恶心,嵌套、绑定更新和保存是同步的,而且数据库里还有乐观锁,本来莫名其妙的好好的,可是有一个方法,莫名其妙的乐观锁冲突了。看着打印的原生的SQL,一股恶心之感升起。

如果能打印出SQL的调用堆栈就好了。

####具体方法:

private static void defaultTag(String startTag) {
    List<String> logList = new ArrayList<>();
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    for (StackTraceElement e : stackTrace) {
        if (!e.getClassName().startsWith(startTag) || e.getClassName().endsWith("JpaInterceptor")) {
            continue;
        }
        //获取使用该工具类的代码所在的方法,java文件名,以及行号,
        //并拼装成 MethodName(FileName.java:LineNumber) 这样的格式,
        //前边的MethodName是辅助信息,后边的是用于让开发工具识别能够跳转的信息。
        logList.add(String.format("JpaInterceptor_class: %s.%s (%s:%s)", e.getClassName(), e.getMethodName(), e.getFileName(), e.getLineNumber()));
    }
    if (logList.size() > 5) {
        logList = logList.subList(0, 5);
    }
    Collections.reverse(logList);
    logList.forEach(log::warn);
}
复制代码

只要在inspect方法入口那里调用这个方法就可以

@Override
public String inspect(String sql) {
    defaultTag("com.ler.demo");
    ...
}
复制代码

参数是顶级包名,过滤掉其他的堆栈信息。

而且使用MethodName(FileName.java:LineNumber)这样的格式,在控制台中,可以直接跳转到代码中的位置。

括号里是蓝色的,点击可以直接跳转到对应的代码。

这里因为是测试代码,比较简单,调用链也少,在那个项目里调用链基本都是七八层。如果很长的话,可以设置深度,我这里设置为5,一般就可以看到从接收到请求,到执行SQL的全过程了。

还有这里使用了reverse方法,因为是逆序查找的堆栈,所以要再逆序一下。

当然这个不光可以在JPA拦截器里使用,如果你想看一个方法的调用链,也可以在方法入口处加上这个方法。

对啦,全局异常拦截器虽然很好用,可是一般只打一个异常信息,具体哪里出现的异常还得自己拿着异常信息定位,其实可以加上下面的代码,打印堆栈信息,可以直接定位到抛异常的地方:

List<StackTraceElement> stackTraceElements = Arrays.asList(ex.getStackTrace());
if (stackTraceElements.size() > MAX_DEPTH) {
    stackTraceElements = stackTraceElements.subList(0, MAX_DEPTH);
}
ex.setStackTrace(stackTraceElements.toArray(new StackTraceElement[stackTraceElements.size()]));
LOGGER.error("handleException, ex caught, uri={}, httpResult={}", request.getRequestURI(), JSON.toJSONString(httpResult), ex);
复制代码

效果如下:

好啦,今天就这样啦

最后欢迎大家关注我的公众号,南诏Blog 共同学习,一起进步。加油🤣