前提条件

连接池使用的是阿里的druid,其他连接池后续有空再考虑

继承FilterEventAdapter并重写statement_closeresultSet_next

statement_close 用于格式化delete,insert(没有主键id返回),update

resultSet_next 用户格式化select,insert(有主键id返回)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
package cn.allbs.mybatis.filter;

import cn.allbs.common.constant.DateConstant;
import cn.allbs.mybatis.properties.MybatisProperties;
import cn.allbs.mybatis.utils.TableConsoleUtil;
import com.alibaba.druid.DbType;
import com.alibaba.druid.filter.FilterChain;
import com.alibaba.druid.filter.FilterEventAdapter;
import com.alibaba.druid.proxy.jdbc.JdbcParameter;
import com.alibaba.druid.proxy.jdbc.ResultSetProxy;
import com.alibaba.druid.proxy.jdbc.StatementProxy;
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.util.StringUtils;
import lombok.extern.slf4j.Slf4j;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.*;

/**
* 类 DruidSqlLogFilter
* </p>
*
* @author ChenQi
* @since 2023/3/23 9:55
*/
@Slf4j
public class DruidSqlLogFilter extends FilterEventAdapter {

private static final SQLUtils.FormatOption FORMAT_OPTION = new SQLUtils.FormatOption(false, false);

private final MybatisProperties mybatisProperties;

private boolean first = true;

private static final Set<Integer> BLOB_TYPES = new HashSet<>();

private ResultSet rs;
private final Set<Integer> blobColumns = new HashSet<>();

private int rows;

private List<String> rowList = new LinkedList<>();

private boolean STATEMENT_CLOSE_RUN = true;

static {
BLOB_TYPES.add(Types.BINARY);
BLOB_TYPES.add(Types.BLOB);
BLOB_TYPES.add(Types.CLOB);
BLOB_TYPES.add(Types.LONGNVARCHAR);
BLOB_TYPES.add(Types.LONGVARBINARY);
BLOB_TYPES.add(Types.LONGVARCHAR);
BLOB_TYPES.add(Types.NCLOB);
BLOB_TYPES.add(Types.VARBINARY);
}

public DruidSqlLogFilter(MybatisProperties mybatisProperties) {
this.mybatisProperties = mybatisProperties;
}

@Override
protected void statementExecuteBefore(StatementProxy statement, String sql) {
statement.setLastExecuteStartNano();
}

@Override
protected void statementExecuteBatchBefore(StatementProxy statement) {
statement.setLastExecuteStartNano();
}

@Override
protected void statementExecuteUpdateBefore(StatementProxy statement, String sql) {
statement.setLastExecuteStartNano();
}

@Override
protected void statementExecuteQueryBefore(StatementProxy statement, String sql) {
statement.setLastExecuteStartNano();
}

@Override
protected void statementExecuteAfter(StatementProxy statement, String sql, boolean firstResult) {
statement.setLastExecuteTimeNano();
}

@Override
protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
statement.setLastExecuteTimeNano();
}

@Override
protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) {
statement.setLastExecuteTimeNano();
}

@Override
protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) {
statement.setLastExecuteTimeNano();
}

@Override
public void statement_close(FilterChain chain, StatementProxy statement) throws SQLException {
super.statement_close(chain, statement);
// 支持动态开启
if (!mybatisProperties.isShowSql()) {
return;
}

// 是否开启调试
if (!log.isInfoEnabled()) {
return;
}
// 如果当前执行sql有结果则不执行 或者是新增语句,因为有id返回也不执行
// Token token = new SQLStatementParser(statement.getLastExecuteSql()).getExprParser().getLexer().token();
// if (statement.isFirstResultSet() || token.equals(Token.INSERT)) {
// return;
// }
// 防止有结果时打印两次sql语句
if (!STATEMENT_CLOSE_RUN) {
STATEMENT_CLOSE_RUN = true;
return;
}
// 打印可执行的 sql
String sql = statement.getBatchSql();
// sql 为空直接返回
if (StringUtils.isEmpty(sql)) {
return;
}
String executeSql = statement(statement);
log.info(executeSql);
}

public String statement(StatementProxy statement) {
// 打印可执行的 sql
String sql = statement.getBatchSql();
// sql 为空直接返回
if (StringUtils.isEmpty(sql)) {
return "";
}
int parametersSize = statement.getParametersSize();
List<Object> parameters = new ArrayList<>(parametersSize);
for (int i = 0; i < parametersSize; ++i) {
// 转换参数,处理 java8 时间
parameters.add(getJdbcParameter(statement.getParameter(i)));
}
String dbType = statement.getConnectionProxy().getDirectDataSource().getDbType();
String formattedSql = SQLUtils.format(sql, DbType.of(dbType), parameters, FORMAT_OPTION);
return printSql(formattedSql, statement);
}

private static Object getJdbcParameter(JdbcParameter jdbcParam) {
if (jdbcParam == null) {
return null;
}
Object value = jdbcParam.getValue();
// 处理 java8 时间
if (value instanceof TemporalAccessor) {
return value.toString();
}
return value;
}

private static String printSql(String sql, StatementProxy statement) {
// 打印 sql
String sqlLogger = "\n--------------------------------[ %s Sql Log ]---------------------------------" + "\n%s" + "\n--------------------------------[ Sql Execute Time: %s ]---------------------------------\n";
return String.format(sqlLogger, LocalDateTime.now().format(DateTimeFormatter.ofPattern(DateConstant.NORM_DATETIME_PATTERN)), sql.trim(), format(statement.getLastExecuteTimeNano()));
}

/**
* 格式化执行时间,单位为 ms 和 s,保留三位小数
*
* @param nanos 纳秒
* @return 格式化后的时间
*/
private static String format(long nanos) {
if (nanos < 1) {
return "0ms";
}
double millis = (double) nanos / (1000 * 1000);
// 不够 1 ms,最小单位为 ms
if (millis > 1000) {
return String.format("%.3fs", millis / 1000);
} else {
return String.format("%.3fms", millis);
}
}

@Override
public boolean resultSet_next(FilterChain chain, ResultSetProxy resultSet) throws SQLException {
boolean next = super.resultSet_next(chain, resultSet);
// 支持动态开启
if (!mybatisProperties.isShowSql()) {
return next;
}

// 是否开启调试
if (!log.isInfoEnabled()) {
return next;
}
if (!next) {
String querySql = statement(resultSet.getStatementProxy());
String sqlLogger = querySql + "{}" + "--------------------------------[ Results Total {} ]---------------------------------\n";
String results = "";
if (rowList.size() > 0) {
results = TableConsoleUtil.printResult(rowList);
}
log.info(sqlLogger, results, rows);
rows = 0;
rowList = new LinkedList<>();
first = true;
STATEMENT_CLOSE_RUN = false;
return false;
}
STATEMENT_CLOSE_RUN = true;
rows++;
rs = resultSet.getResultSetRaw();
ResultSetMetaData rsmd = resultSet.getMetaData();
final int columnCount = rsmd.getColumnCount();
if (first) {
first = false;
printColumnHeaders(rsmd, columnCount);
}
printColumnValues(columnCount);
return true;
}

private void printColumnHeaders(ResultSetMetaData rsmd, int columnCount) throws SQLException {
StringJoiner row = new StringJoiner(",");
for (int i = 1; i <= columnCount; i++) {
if (BLOB_TYPES.contains(rsmd.getColumnType(i))) {
blobColumns.add(i);
}
row.add(rsmd.getColumnLabel(i));
}
rowList.add(row.toString());
}

private void printColumnValues(int columnCount) {
StringJoiner row = new StringJoiner(",");
for (int i = 1; i <= columnCount; i++) {
try {
if (blobColumns.contains(i)) {
row.add("<<BLOB>>");
} else {
row.add(rs.getString(i));
}
} catch (SQLException e) {
// generally can't call getString() on a BLOB column
row.add("<<Cannot Display>>");
}
}
rowList.add(row.toString());
}
}

将结果格式化为表格形式的工具代码

目前因为查询出的结果包含中文汉字,为了尽可能整齐,只是默认取了一个汉字为1.5倍的其他字符。有兴趣的自己修改成根据字体、字号获取实际的宽度并生成表格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package cn.allbs.mybatis.utils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;

/**
* 类 TableConsoleUtil
* </p>
*
* @author ChenQi
* @since 2023/3/24 13:45
*/
public class TableConsoleUtil {

public static String printResult(List<String> rows) {
StringBuilder sb = new StringBuilder();
String[] tempA = rows.get(0).split(",");
int maxLen = tempA.length;
for (int i = 1; i < rows.size(); i++) {
tempA = rows.get(i).split(",");
if (maxLen < tempA.length) maxLen = tempA.length;
}
String[][] row = new String[rows.size()][maxLen];
for (int i = 0; i < row.length; i++)
for (int j = 0; j < row[0].length; j++)
row[i][j] = "";
for (int i = 0; i < rows.size(); i++) {
tempA = rows.get(i).split(",");
System.arraycopy(tempA, 0, row[i], 0, tempA.length);
}
int[] maxJ = new int[maxLen];
for (int j = 0; j < maxLen; j++) {
for (int i = 0; i < rows.size(); i++) {
int vLen = (getWordCount(row[i][j]) - 1 >> 3) * 8 + 8;
if (vLen > maxJ[j]) {
maxJ[j] = vLen;
}
}
}
StringBuilder opera = new StringBuilder("+");
for (int value : maxJ) {
for (int k = 0; k < value; k++) {
opera.append('-');

}
opera.append('+');
}
boolean first = true;
for (String[] strings : row) {
if (first) {
sb.append(opera);
sb.append("\n");
}
sb.append("|");
for (int j = 0; j < row[0].length; j++) {
int len = maxJ[j] - getWordCount(strings[j]);
String format;
if (len == 0) {
format = "" + "%s";
} else {
format = "%" + len + "s";
}
sb.append(strings[j]);
sb.append(String.format(format, ""));
sb.append("|");
}
sb.append("\n");
if (first) {
sb.append(opera);
sb.append("\n");
first = false;
}
}
sb.append(opera);
sb.append("\n");
return sb.toString();
}

public static int getWordCount(String s) {
int length = 0;
int chineseNum = 0;
for (int i = 0; i < s.length(); i++) {
int ascii = Character.codePointAt(s, i);
if (ascii >= 0 && ascii <= 255) length++;
else chineseNum++;

}
return BigDecimal.valueOf(length + chineseNum * 1.5).setScale(0, RoundingMode.HALF_UP).intValue();
}
}

注册过滤器

1
2
3
4
@Bean
public DruidSqlLogFilter sqlLogFilter(MybatisProperties properties) {
return new DruidSqlLogFilter(properties);
}

添加了配置开关当前日志打印效果

1
2
3
4
5
6
7
8
9
10
@Data
@Component
@ConfigurationProperties("mybatis-plus")
public class MybatisProperties {

/**
* 是否打印可执行 sql
*/
private boolean showSql = true;
}

使用说明

需要将mybatis实现的日志打印注释,防止和本文打印的日志重复。同时将show-sql设置为true。如果生产环境不需要日志打印将其设置为false即可。

image-20230324171355944

实际使用示例

查询

image-20230324171742293

新增

返回值为插入数据的主键

image-20230324173942066

更新

image-20230324174140031

删除

因为使用逻辑删除所以是更新语句

image-20230324174229284

移出逻辑删除后

image-20230324174316869

xml中的自定义sql

image-20230327110013088