0%

mybatis控制部分字段不打印

mybatis控制部分字段不打印

背景

​ 最近遇到生产环境的运维反馈,每天的生成的日志信息占用较大的磁盘空间,希望研发能减少部分无效日志信息,把日志文件拉回来看了一下,主要是在MySQL的一些执行打印了text等大字段的数据,当这些表频繁进行插入、修改动作时,导致一直打印这些大字段数据,从而导致占用较大的磁盘空间,但是我们又不能直接关闭mybatis的执行打印日志,在生产环境出现异常时,我们还是需要根据这写执行日志来定位问题,所以最后想是否能将打印的控制逻辑细化到字段上,也就是说对于类型为text等类型的大字段不输出,简单使用一个占位符替代,从而达到既不影响异常问题的定位,也减少了一些无用日志信息。

原理

​ 具体的mybatis的几个核心组件执行流程这里不过多介绍,这里仅简单说明一条SQL的执行,是要先经过参数解析器,将具体的参数和数据类型解析处理,而后通过执行器,通过Statement或者是PrepareStatement等进行执行之后,再由结果集处理器进行结果映射相关逻辑。

​ 在跟踪了一下mybatis的执行代码之后发现,日志的打印逻辑是在执行器Executor中,而数据类型相关的处理是在参数解析器中,也就是说如果想做到部分字段不打印,就需要在执行器中修改打印的逻辑。

我这边测试的是通过PreparedStatement方式进行插入数据,处理打印的类是PreparedStatementLoggerinvoke方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
if (EXECUTE_METHODS.contains(method.getName())) {
if (isDebugEnabled()) {
// 打印数据在这里
debug("Parameters: " + getParameterValueString(), true);
}
// ... 省略很多无效代码
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}

然后调用父类BaseJdbcLoggergetParameterValueString方法获取要打印的数据。可以看到这里就是直接将columnValues列表中的值进行拼接。

1
2
3
4
5
6
7
8
9
10
11
12
protected String getParameterValueString() {
List<Object> typeList = new ArrayList<>(columnValues.size());
for (Object value : columnValues) {
if (value == null) {
typeList.add("null");
} else {
typeList.add(objectValueString(value) + "(" + value.getClass().getSimpleName() + ")");
}
}
final String parameters = typeList.toString();
return parameters.substring(1, parameters.length() - 1);
}

综上,由于这个地方并没有提供扩展点,所以如果我想要打印的时候忽略某些类型的字段,我需要重写这个类,再有我需要在这里能获取到对应的数据类型,尝试之后发现,这个对象中主要是有一个Map对象,键是序号(填充最后执行SQL的序号),值就是对应要填充SQL的值。所以单纯的重写这个类型,获取不到对应的类型,也就无法进行类型的判断,所以需要外层这个类的地方,将执行SQL的类型信息传进来。

类型相关信息是解析到了BoundSql对象中。所以需要想方设法的将BoundSql对象传进来,这个PreparedStatementLogger的创建和被调用并不是在同一个时间节点,也就是说将类型相关参数传进来有两种手段,一种是在PreparedStatementLogger创建的时候,跟着原本构造函数相关的参数带进来,另一种是在invoke方法在调用之前传进来。当然了根据尝试,否定了第一种方式,原因是创建这个对象的地方也没有类型相关信息。

后来发现在PreparedStatementLogger#invoke方法被调用之前是可以获取到BoundSql对象,调用的地方是PreparedStatementHandlerupdate方法。

1
2
3
4
5
6
7
8
9
10
11
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 这里是动态代理,通过这里进入到PreparedStatementLogger#invoke中
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}

所以只需要在这里做文章,将BoundSql对象传到PreparedStatementLogger中基本上就可以达到目的。

上手

1.将PreparedStatementLogger拷贝到应用的类路径下,并创建同名类路径org.apache.ibatis.logging.jdbc.PreparedStatementLogger类,然后添加一个属性字段BoundSql

1
2
3
4
5
6
7
8
9
public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {

private final PreparedStatement statement;

public BoundSql boundSql;

// ... 省略很多代码

}

2.同样,将PreparedStatementHandler拷贝到应用类路径下,并创建同名类路径org.apache.ibatis.executor.statement.PreparedStatementHandler,并在执行前设置boundSql的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 这里获取到的是PreparedStatementLogger的动态代理对象,所以需要获取到被代理对象target
// 然后判断被代理对象是否为PreparedStatementLogger,是的话就赋值boundSql
if (Proxy.isProxyClass(statement.getClass())) {
MetaObject metaObject = SystemMetaObject.forObject(statement);
Object target = metaObject.getValue("h");
if(target instanceof PreparedStatementLogger){
PreparedStatementLogger preparedStatementLogger = (PreparedStatementLogger) target;
preparedStatementLogger.boundSql = boundSql;
}
}

ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}

3.最后在需要忽略日志打印的实体字段上标记能识别到jdbcType的注解,我这里用的是tk.mybatis,所以标记注解是tk.mybatis.mapper.annotation.ColumnType。如果用的是mybatis-plus应该是com.baomidou.mybatisplus.annotation.TableField注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Team {

@Column(name = "ID")
private String id;

@Column(name = "GROUP_ID")
private String groupId;

@Column(name = "PROJECT_ID")
@TableField(jdbcType = JdbcType.BLOB)
@ColumnType(jdbcType = JdbcType.BLOB)
private String extraInfo;

}

具体的打印逻辑我这里就不去写了,附一张到这里的断点图,到这里已经能获取到字段的类型和字段的值。

mybaits控制字段打印.png

覆盖

​ 这里通过自己定义了与mybatis中相关的包路径和同名类来达到替换第三方jar包中的同名对象。原理实际上就是jdk的类加载机制,采用的是双亲委派模型,如果一个类加载器收到了需要加载类的请求,它不会马上进行解析,而是把这个请求委派给父类去加载,每一个层级的类加载器都是入参,当最上层加载器无法解析之后,它才会一层一层往下委派。

​ 比如java.lang.Object存放在rt.jar中,如果编写另一个java.lang.Object并放到ClassPath中,编译上是没有问题的,但是由于双亲委派模型,当获取java.lang.Object时会委派到最上级的启动类加载器,而类路径ClassPath是应用程序类加载器。

​ 但是对于上面重写的两个类,使用到的都是应用程序类加载器,但是如果在在应用ClassPath下创建同名路径,那么编译之后,代码是在classes文件夹中,而第三方包是在lib文件夹中,这里加载类进行查找对应.class文件时也有一个优先级的关系,会优先获取classes中的.class文件。

-------------本文结束感谢您的阅读-------------