mybatis控制部分字段不打印
背景
最近遇到生产环境的运维反馈,每天的生成的日志信息占用较大的磁盘空间,希望研发能减少部分无效日志信息,把日志文件拉回来看了一下,主要是在MySQL
的一些执行打印了text
等大字段的数据,当这些表频繁进行插入、修改动作时,导致一直打印这些大字段数据,从而导致占用较大的磁盘空间,但是我们又不能直接关闭mybatis
的执行打印日志,在生产环境出现异常时,我们还是需要根据这写执行日志来定位问题,所以最后想是否能将打印的控制逻辑细化到字段上,也就是说对于类型为text
等类型的大字段不输出,简单使用一个占位符替代,从而达到既不影响异常问题的定位,也减少了一些无用日志信息。
原理
具体的mybatis
的几个核心组件执行流程这里不过多介绍,这里仅简单说明一条SQL
的执行,是要先经过参数解析器,将具体的参数和数据类型解析处理,而后通过执行器,通过Statement
或者是PrepareStatement
等进行执行之后,再由结果集处理器进行结果映射相关逻辑。
在跟踪了一下mybatis
的执行代码之后发现,日志的打印逻辑是在执行器Executor
中,而数据类型相关的处理是在参数解析器中,也就是说如果想做到部分字段不打印,就需要在执行器中修改打印的逻辑。
我这边测试的是通过PreparedStatement
方式进行插入数据,处理打印的类是PreparedStatementLogger
的invoke
方法中。
1 |
|
然后调用父类BaseJdbcLogger
的getParameterValueString
方法获取要打印的数据。可以看到这里就是直接将columnValues
列表中的值进行拼接。
1 | protected String getParameterValueString() { |
综上,由于这个地方并没有提供扩展点,所以如果我想要打印的时候忽略某些类型的字段,我需要重写这个类,再有我需要在这里能获取到对应的数据类型,尝试之后发现,这个对象中主要是有一个Map
对象,键是序号(填充最后执行SQL
的序号),值就是对应要填充SQL
的值。所以单纯的重写这个类型,获取不到对应的类型,也就无法进行类型的判断,所以需要外层这个类的地方,将执行SQL
的类型信息传进来。
类型相关信息是解析到了BoundSql
对象中。所以需要想方设法的将BoundSql
对象传进来,这个PreparedStatementLogger
的创建和被调用并不是在同一个时间节点,也就是说将类型相关参数传进来有两种手段,一种是在PreparedStatementLogger
创建的时候,跟着原本构造函数相关的参数带进来,另一种是在invoke
方法在调用之前传进来。当然了根据尝试,否定了第一种方式,原因是创建这个对象的地方也没有类型相关信息。
后来发现在PreparedStatementLogger#invoke
方法被调用之前是可以获取到BoundSql
对象,调用的地方是PreparedStatementHandler
的update
方法。
1 |
|
所以只需要在这里做文章,将BoundSql
对象传到PreparedStatementLogger
中基本上就可以达到目的。
上手
1.将PreparedStatementLogger
拷贝到应用的类路径下,并创建同名类路径org.apache.ibatis.logging.jdbc.PreparedStatementLogger
类,然后添加一个属性字段BoundSql
。
1 | public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler { |
2.同样,将PreparedStatementHandler
拷贝到应用类路径下,并创建同名类路径org.apache.ibatis.executor.statement.PreparedStatementHandler
,并在执行前设置boundSql
的值。
1 |
|
3.最后在需要忽略日志打印的实体字段上标记能识别到jdbcType
的注解,我这里用的是tk.mybatis
,所以标记注解是tk.mybatis.mapper.annotation.ColumnType
。如果用的是mybatis-plus
应该是com.baomidou.mybatisplus.annotation.TableField
注解。
1 | public class Team { |
具体的打印逻辑我这里就不去写了,附一张到这里的断点图,到这里已经能获取到字段的类型和字段的值。
覆盖
这里通过自己定义了与mybatis
中相关的包路径和同名类来达到替换第三方jar
包中的同名对象。原理实际上就是jdk
的类加载机制,采用的是双亲委派模型,如果一个类加载器收到了需要加载类的请求,它不会马上进行解析,而是把这个请求委派给父类去加载,每一个层级的类加载器都是入参,当最上层加载器无法解析之后,它才会一层一层往下委派。
比如java.lang.Object
存放在rt.jar
中,如果编写另一个java.lang.Object
并放到ClassPath
中,编译上是没有问题的,但是由于双亲委派模型,当获取java.lang.Object
时会委派到最上级的启动类加载器,而类路径ClassPath
是应用程序类加载器。
但是对于上面重写的两个类,使用到的都是应用程序类加载器,但是如果在在应用ClassPath
下创建同名路径,那么编译之后,代码是在classes
文件夹中,而第三方包是在lib
文件夹中,这里加载类进行查找对应.class
文件时也有一个优先级的关系,会优先获取classes
中的.class
文件。