目录
一、插件介绍
开源框架都注重自身的扩展性,为用户提供了扩展机制,这样具有很高的灵活性,同时也能根据自身的业务定制实现一些额外的功能。
MyBatis 作为优秀的 ORM 框架,自然也考虑到了扩展性,这样使其拥有强大的灵活性。Mybatis 中虽然提供的叫插件,但是实际上是通过拦截器实现的,通过拦截某些方法的调用,植入拦截器的逻辑,实现插件功能。
MyBatis 在执行SQL时涉及到了四个组件,分别为 Executor、StatementHandler、ParameterHandler、ResultSetHandler,其功能如下:
- Executor:SQL 执行器,MyBatis 中对数据库的所有增删改查都是通过 Executor 实现的。
- StatementHandler:封装了对 JDBC Statement 对象的操作,如参数设置、编译 SQL、处理结果集等,根据不同的 SQL 类型,使用不同的子类实现。
- ParameterHandler:当使用预编译时,负责设置参数值到 SQL 语句的占位符上,负责处理动态 SQL 中的参数替换。
- ResultSetHandler:负责将从数据库查询结果转换为 Java 对象,包括单个对象、集合、自定义复杂结果类型等。根据 ResultMap 配置来映射结果集中的列到 Java 对象的属性上。
MyBatis 支持用插件对这四大组件进行拦截,用来增强核心组件的功能,底层是通过动态代理技术实现的。
MyBatis 插件的应用非常广泛,主要用于增强或定制化 MyBatis 的功能,而不必直接修改源码,下面是一些常用的场景。
- 日志记录与性能监控:插件可以拦截执行器(Executor)、语句处理器(StatementHandler)等组件的方法,记录 SQL 语句、执行时间、参数信息等,用于调试、性能分析或审计目的。
- 动态 SQL 优化:插件可以分析和优化动态生成的 SQL 语句,比如去除无效的条件、合并重复的子查询等,提高查询效率。
- 自动分页:通过插件来实现分页查询的功能。
- 数据脱敏:对敏感数据(如身份证号、手机号登)进行脱敏处理,确保在查询结果中返回的事加密、脱敏后的数据
- 数据校验:在执行 INSERT、UPDATE 操作前,插件可以对即将写入数据库的参数进行合法性校验,如格式检查、唯一性验证、业务规则检查等,保证数据完整性。
下面就来实现一个记录 SQL 执行时间的插件,以实现对慢SQL的监控。
二、自定义插件实现
这里假设你的项目中已经集成了 MyBatis,直接编写插件相关类实现
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlExecutionTimeInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 获取 StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 获取 StatementHandler 的包装类
MetaObject metaObjectHandler = SystemMetaObject.forObject(statementHandler);
// 获取查询接口映射的相关信息
MappedStatement mappedStatement = (MappedStatement) metaObjectHandler.getValue("delegate.mappedStatement");
// 获取请求时的参数
Object parameterObject = statementHandler.getParameterHandler().getParameterObject();
// 获取SQL
BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
// showSql 方法中将参数值拼接在SQL语句中,具体实现省略
String sql = showSql(mappedStatement.getConfiguration(), boundSql);
// 执行
Object result = invocation.proceed();
stopWatch.stop();
System.out.println("当前执行SQL为:" + sql + ", 执行时间为:" + stopWatch.getTotalTimeMillis() + "ms");
return result;
}
/**
* 获取对象代理
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
return (target instanceof StatementHandler) ? Plugin.wrap(target, this) : target;
}
@Override
public void setProperties(Properties properties) {
}
private String showSql(Configuration configuration, BoundSql boundSql) {
// 忽略
}
}
在Mybatis的配置类中引入自定义插件
@MapperScan(basePackages = "com.test.mybatis.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
@Configuration
public class MybatisConfig {
@Autowired
private DataSource dataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**.xml"));
bean.setVfs(SpringBootVFS.class);
// 添加拦截器插件
bean.setPlugins(new Interceptor[]{myBatisSqlInterceptor()});
// 实体类位置
bean.setTypeAliasesPackage("com.test.mybatis.model.User");
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource);
}
// 设置自定义插件
private Interceptor myBatisSqlInterceptor() {
SqlExecutionTimeInterceptor sqlExecutionTimeInterceptor = new SqlExecutionTimeInterceptor();
sqlExecutionTimeInterceptor.setProperties(new Properties());
return sqlExecutionTimeInterceptor;
}
}
执行SQL语句时,就能将SQL的执行时间打印出来,如果超过了慢查询设置的值,还可以发出来预警。打印的具体内容如下:
当前执行SQL为:select id, city, `name`, age, addr from user where id = 1, 执行时间为:3ms
三、实现原理
MyBatis 插件的核心是 Interceptor 接口,它只有一个方法 Object intercept(Invocation invocation)
。插件需要实现此接口以实现对目标方法的拦截。Invocation
对象封装了被拦截方法的信息,包括方法对象(Method)、目标对象(Target)、参数列表(Arguments)以及代理对象(Proxy)。通过这些信息,插件可以在拦截时获取到需要的数据并执行相应的操作。
在插件类上使用 @Intercepts
注解,指定要拦截的方法签名。具体如代码实现。
Mybatis 使用动态代理技术,在运行时为各个核心接口(如Executor、StatementHandler等)生成代理对象。当调用代理对象的方法时,实际会调用InvocationHandler
的 invoke()
方法。
在方法运行时会调用 plugin() 方法,生成代理对象,代码如下:
通过这种设计使得开发者能够灵活地对 MyBatis 的核心行为进行扩展和定制,而不必修改 MyBatis 源码,极大地提高了框架的灵活性和可扩展性。
往期经典推荐
走进 Mybatis 内核世界:理解原理,释放更多生产力-CSDN博客
深入浅出 TiDB MVCC:揭秘分布式数据库中的多版本并发控制-CSDN博客
直击Redis集群痛点:数据倾斜优化实战,打造高效分布式缓存架构_redis集群实现的集群如何解决数据倾斜-CSDN博客