自定义mybatis插件如何实现sql日志打印
作者:不会打字314 发布时间:2023-11-29 11:22:37
mysql插件实现原理
官网的关键信息
参考文档
https://mybatis.org/mybatis-3/zh/configuration.html#plugins
官方文档
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。
默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
插件运行源码流程
mybatis 在 配置文件里面设置 plug , 所以 configuration 可以添加插件
Configuration.java
Executor
ParameterHandler
ResultSetHandler
StatementHandler
通过查看源码 , 知道插件增强的是 mybatis 以上的几个对象
interceptorChain
interceptorChain.pluginAll(…)方法
mybatis 中的 InterceptorChain 类似于 springmvc 中的 * 链
Interceptor
实现 InvocationHandler , 我们知道 Plugin 类使用了 JDK *
最后执行插件方法 , 调用的是该类的 invoke 方法
inovke方法
参考pagehelper
实现sql日志打印插件
前置环境
springboot + mybatis + mysql
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.3</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
实现 Interceptor 接口
package com.example.plug;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Properties;
/**
* 1.可以用来分析SQL执行效率
* 2.可以用来获取实际执行的SQL
*/
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}
)
@Slf4j
public class SqlInterceptor implements Interceptor {
// private static final Log log = LogFactory.getLog(SqlInterceptor.class);
/**
* 最小打印时间 sql时间超过这个值才打印日志 毫秒
**/
private int MIN_SIZE = 0;
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = null;
if (invocation.getArgs().length > 1) {
parameter = invocation.getArgs()[1];
}
String sqlId = mappedStatement.getId();
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
Configuration configuration = mappedStatement.getConfiguration();
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = invocation.proceed();
} finally {
try {
long sqlCostTime = System.currentTimeMillis() - startTime;
String sql = getSql(configuration, boundSql);
formatSqlLog(mappedStatement.getSqlCommandType(), sqlId, sql, sqlCostTime, result);
} catch (Exception ignored) {
log.error("SQL插件执行失败 Mapper:{} 参数对象:{}", sqlId, JSON.toJSONString(boundSql.getParameterObject()), ignored);
}
}
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
if (properties == null) {
return;
}
if (properties.containsKey("minLogSize")) {
MIN_SIZE = Integer.valueOf(properties.getProperty("minLogSize"));
}
}
private String getSql(Configuration configuration, BoundSql boundSql) {
// 输入sql字符串空判断
String sql = boundSql.getSql();
if (StrUtil.isBlank(sql)) {
return "";
}
//去掉换行符
sql = sql.replaceAll("[\\s\n ]+", " ");
//填充占位符, 目前基本不用mybatis存储过程调用,故此处不做考虑
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (!parameterMappings.isEmpty() && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = this.replacePlaceholder(sql, parameterObject);
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = replacePlaceholder(sql, obj);
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = replacePlaceholder(sql, obj);
}
}
}
}
return sql;
}
private String replacePlaceholder(String sql, Object parameterObject) {
String result;
if (parameterObject == null) {
result = "NULL";
} else if (parameterObject instanceof String) {
result = String.format("'%s'", parameterObject.toString());
} else if (parameterObject instanceof Date) {
result = String.format("'%s'", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(parameterObject));
} else {
result = parameterObject.toString();
}
return sql.replaceFirst("\\?", result);
}
private void formatSqlLog(SqlCommandType sqlCommandType, String sqlId, String sql, long costTime, Object obj) {
if (costTime > MIN_SIZE) {
if (sqlCommandType == SqlCommandType.UPDATE || sqlCommandType == SqlCommandType.INSERT || sqlCommandType == SqlCommandType.DELETE) {
log.info("[{}ms] [{}] {}; 影响行数:{}", costTime, sqlId, sql, obj);
}
if (sqlCommandType == SqlCommandType.SELECT) {
log.info("[{}ms] [{}] {}; 结果行数:{}", costTime, sqlId, sql, ((Collection<?>) obj).size());
}
}
}
}
mybatis 添加该插件
SpringBoot 中 SqlSessionFactory 是单例的
通过 SqlSessionFactory
得到 Configuration
configuration 的 addInterceptor 方法添加插件
@Configuration
public class MybatisPlugConfig implements SmartInitializingSingleton {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Override
public void afterSingletonsInstantiated() {
sqlSessionFactory.getConfiguration().addInterceptor(new SqlInterceptor());
}
}
测试
测试方便 , 省略service
controller
@RestController
@RequestMapping("/test")
public class HelloController {
@Autowired
private PersonDao personDao;
@GetMapping("/ssm")
public List<PersonEntity> ssm() {
return personDao.list();
}
@GetMapping("/insert")
public String insert(){
PersonEntity personEntity = new PersonEntity();
personEntity.setPid(null);
personEntity.setPname(UUID.randomUUID().toString().substring(8).toString());
personDao.insert(personEntity);
return "插入成功";
}
@GetMapping("/select")
public List<PersonEntity> select(Integer pid , String pname){
return personDao.selectByParam(pid , pname);
}
}
dao
@Mapper
public interface PersonDao {
@Select("select * from person")
List<PersonEntity> list();
int insert(PersonEntity personEntity);
List<PersonEntity> selectByParam(@Param("pid") Integer pid, @Param("pname") String pname);
}
控制台打印
2022-10-28 22:07:18.913 INFO 26316 — [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet ‘dispatcherServlet’
2022-10-28 22:07:18.913 INFO 26316 — [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Initializing Servlet ‘dispatcherServlet’
2022-10-28 22:07:18.917 INFO 26316 — [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
2022-10-28 22:07:27.247 INFO 26316 — [nio-8080-exec-6] com.example.plug.SqlInterceptor : [48ms] [com.example.dao.PersonDao.list] select * from person; 结果行数:5
2022-10-28 22:07:30.759 INFO 26316 — [nio-8080-exec-8] com.example.plug.SqlInterceptor : [34ms] [com.example.dao.PersonDao.selectByParam] select * from person WHERE pname like concat(‘%’,concat(‘1’,‘%’)) and pid = 1; 结果行数:0
2022-10-28 22:07:32.936 INFO 26316 — [nio-8080-exec-9] com.example.plug.SqlInterceptor : [31ms] [com.example.dao.PersonDao.selectByParam] select * from person WHERE pname like concat(‘%’,concat(‘1’,‘%’)); 结果行数:2
2022-10-28 22:07:36.464 INFO 26316 — [io-8080-exec-10] com.example.plug.SqlInterceptor : [34ms] [com.example.dao.PersonDao.selectByParam] select * from person WHERE pid = 1; 结果行数:1
2022-10-28 22:07:38.605 INFO 26316 — [nio-8080-exec-1] com.example.plug.SqlInterceptor : [33ms] [com.example.dao.PersonDao.selectByParam] select * from person; 结果行数:5
oncat(‘1’,‘%’)); 结果行数:2
2022-10-28 22:07:36.464 INFO 26316 — [io-8080-exec-10] com.example.plug.SqlInterceptor : [34ms] [com.example.dao.PersonDao.selectByParam] select * from person WHERE pid = 1; 结果行数:1
2022-10-28 22:07:38.605 INFO 26316 — [nio-8080-exec-1] com.example.plug.SqlInterceptor : [33ms] [com.example.dao.PersonDao.selectByParam] select * from person; 结果行数:5
2022-10-28 22:07:45.440 INFO 26316 — [nio-8080-exec-2] com.example.plug.SqlInterceptor : [53ms] [com.example.dao.PersonDao.insert] INSERT INTO person ( pid, pname ) VALUES ( 6, ‘-9bac-472f-a95c-9d97b11ab0e1’ ); 影响行数:1
来源:https://blog.csdn.net/shaoming314/article/details/127580490
猜你喜欢
- 1. 为什么要进行参数校验在后端进行工作时,需要接收前端传来的数据去数据库查询,但是如果有些数据过于离谱,我们就可以直接把它pass掉,不让
- 此处网上最多的做法是需要修改tomcat的参数配置大致如下:<Connector port="8080" prot
- 本文实例为大家分享了java实现简单的猜数字的具体代码,供大家参考,具体内容如下题目描述:猜数字(又称 Bulls and Cows )是一
- Rmb.javapublic class Rmb { /** *人民币的基本信息和操作 *@auth
- 前言回老家,实在太无聊,于是乎给自己整了一套台式机配置,总价 1W+,本以为机器到位后可以打打游戏,学学技术打发无聊的时光。但是我早已不是从
- 本文以实例阐述了C++中形参与实参的区别,有助于读者加深对于C++形参与实参的认识。形参出现在函数定义中,在整个函数体内都可以使用, 离开该
- 本文实例讲述了C#获取网页源代码的方法。分享给大家供大家参考。具体如下:public string GetPageHTML(string u
- 正在尝试分配更低的访问权限?在进行Java编程时会给我们报出如下提示怎么办?这里我们将给大家介绍详细的解决方法。首先,查看,控制台给出的提示
- 本文实例讲述了java实现的简单猜数字游戏代码。分享给大家供大家参考。具体代码如下:import java.util.InputMismat
- 关于UIToolbarToolBar工具栏是视图View的属性,可以在工具栏上添加工具栏按钮Bar Button Item(可以是自定义的C
- 这篇文章主要介绍了java property配置文件管理工具框架过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考
- springboot配置文件中属性变量引用@@这种属性应用方式是field_name=@field_value@。两个@符号是springb
- 配置操作第一步操作如图:选择右侧的database页签,一般在idea的右边会有Database界面,点击它即可。有时候我们会发现这个Dat
- 在Java中,可以通过Runtime类或ProcessBuilder类来实现调用外部程序。Runtime类与ProcessBuilder类使
- Java调用接口获取json数据保存到数据库今天给大家带来一个调用接口,来获取数据解析后再保存到数据库中的业务,业务中的Mapper和实体类
- 一、打印直角三角形这个循环控制打印十行空格for (int x = 1; x <= 10; x++) {//因为要打印一个十行的直角三
- 引言:编写高效简洁的C语言代码,是许多软件工程师追求的目标。本文就工作中的一些体会和经验做相关的阐述,不对的地方请各位指教。第1招:以空间换
- springboot对kafka的client很好的实现了集成,使用非常方便,本文也实现了一个在springboot中实现操作kafka的d
- 第一部分 问题描述1.1 具体任务本次作业任务是轨迹压缩,给定一个GPS数据记录文件,每条记录包含经度和维度两个坐标字段,所有记录的经纬度坐
- 初学C++的朋友经常在类中看到public,protected,private以及它们在继承中表示的一些访问范围,很容易搞糊涂。今天本文就来