MyBatis拦截器是MyBatis框架提供的扩展机制,它可以在执行SQL语句的过程中拦截和干预,用于对SQL语句进行增强或修改。
MyBatis拦截器可以做以下几件事情:
具体例子: 我们常用的分页插件
Pagehelper
其实就是一个拦截器实现
MyBatis的主要的核心部件有以下几个:
在Mybatis中,Executor、StatementHandler、ParameterHandler和ResultSetHandler是核心组件,它们分别负责不同的任务。
Executor:统筹全局
StatementHandler:执行SQL
ParameterHandler:参数封装
ResultSetHandler:返回结果映射
这四个组件在Mybatis中协同工作,完成了从数据库操作到Java对象映射的整个过程。Executor负责整体的控制和协调,StatementHandler负责处理SQL语句的操作,ParameterHandler负责处理参数,ResultSetHandler负责处理结果集。它们各自分工明确,相互配合,共同完成数据库操作和对象映射的任务。
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class MyInterceptor implements Interceptor {
MyBatis
拦截器默认可以拦截的类型只有四种,即四种接口类型Executor
、StatementHandler
、ParameterHandler
和ResultSetHandler
。对于我们的自定义拦截器必须使用MyBatis
提供的@Intercepts
注解来指明我们要拦截的是四种类型中的哪一种接口。
注解 | 描述 |
---|---|
@Intercepts |
标志该类是一个拦截器 |
@Signature |
指明该拦截器需要拦截哪一个接口的哪一个方法 |
@Signature
注解的参数:
参数 | 描述 |
---|---|
type |
四种类型接口中的某一个接口,如Executor.class 。 |
method |
对应接口中的某一个方法名,比如Executor 的query 方法。 |
args |
对应接口中的某一个方法的参数,比如Executor 中query 方法因为重载原因,有多个,args 就是指明参数类型,从而确定是具体哪一个方法。 |
MyBatis
拦截器默认会按顺序拦截以下的四个接口中的所有方法:
org.apache.ibatis.executor.Executor //拦截执行器方法
org.apache.ibatis.executor.statement.StatementHandler //拦截SQL语法构建处理
org.apache.ibatis.executor.parameter.ParameterHandler //拦截参数处理
org.apache.ibatis.executor.resultset.ResultSetHandler //拦截结果集处理
具体是拦截这四个接口对应的实现类:
org.apache.ibatis.executor.CachingExecutor
org.apache.ibatis.executor.statement.RoutingStatementHandler
org.apache.ibatis.scripting.defaults.DefaultParameterHandler
org.apache.ibatis.executor.resultset.DefaultResultSetHandler
进行拦截的时候要执行的方法。该方法参数Invocation
类中有三个字段:
private final Object target;
private final Method method;
private final Object[] args;
可通过这三个字段分别获取下面的信息:
Object target = invocation.getTarget();//被代理对象
Method method = invocation.getMethod();//代理方法
Object[] args = invocation.getArgs();//方法参数
插件用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理,可以决定是否要进行拦截进而决定要返回一个什么样的目标对象,官方提供了示例:return Plugin.wrap(target, this);
可以在这个方法中提前进行拦截对象类型判断,提高性能:
@Override
public Object plugin(Object target) {
//只对要拦截的对象生成代理
if(target instanceof StatementHandler){
//调用插件
return Plugin.wrap(target, this);
}
return target;
}
MyBatis拦截器用到责任链模式+动态代理+反射机制;
所有可能被拦截的处理类都会生成一个代理类,如果有N个拦截器,就会有N个代理,层层生成动态代理是比较耗性能的。而且虽然能指定插件拦截的位置,但这个是在执行方法时利用反射动态判断的,初始化的时候就是简单的把拦截器插入到了所有可以拦截的地方。所以尽量不要编写不必要的拦截器。另外我们可以在调用插件的地方添加判断,只要是当前拦截器拦截的对象才进行调用,否则直接返回目标对象本身,这样可以减少反射判断的次数,提高性能。
如果我们拦截器需要用到一些变量参数,而且这个参数是支持可配置的,类似Spring
中的@Value("${}")
从application.properties
文件获取自定义变量属性,这个时候我们就可以使用这个方法。
private String property1;
private int property2;
@Override
public void setProperties(Properties properties) {
// 从配置文件中获取属性值
this.property1 = properties.getProperty("property1");
this.property2 = Integer.parseInt(properties.getProperty("property2"));
}
在setProperties
方法中,我们可以通过传入的Properties
对象获取配置文件中的属性值,并进行相应的处理。
然后,在MyBatis的配置文件中注册自定义的拦截器:
@Slf4j
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.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 = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class SqlPrintInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
// 获取xml中的一个select/update/insert/delete节点,是一条SQL语句
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = null;
// 获取参数,if语句成立,表示sql语句有参数,参数格式是map形式
if (invocation.getArgs().length > 1) {
parameter = invocation.getArgs()[1];
log.info("SQL打印拦截器参数 = " + parameter);
}
// 获取到节点的id, 即sql语句的id
String sqlId = mappedStatement.getId();
//log.info("sqlId = " + sqlId);
// BoundSql就是封装myBatis最终产生的sql类
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
// 获取节点的配置
Configuration configuration = mappedStatement.getConfiguration();
// 获取到最终的sql语句
String sql = getSql(configuration, boundSql, sqlId);
log.info("SQL打印拦截器完整SQL = " + sql);
} catch (Exception e) {
e.printStackTrace();
}
// 执行完上面的任务后,不改变原有的sql执行过程
return invocation.proceed();
}
// 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句
private static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
String sql = showSql(configuration, boundSql);
StringBuilder str = new StringBuilder(100);
str.append(sqlId);
str.append(":");
str.append(sql);
return str.toString();
}
// 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号; 对参数是null和不是null的情况作了处理
private static String getParameterValue(Object obj) {
String value = null;
if (obj instanceof String) {
value = "'" + obj.toString() + "'";
} else if (obj instanceof Date) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,
DateFormat.DEFAULT, Locale.CHINA);
value = "'" + formatter.format(new Date()) + "'";
} else {
if (obj != null) {
value = obj.toString();
} else {
value = "";
}
}
return value;
}
// 进行?的替换
private static String showSql(Configuration configuration, BoundSql boundSql) {
// 获取参数
Object parameterObject = boundSql.getParameterObject();
List parameterMappings = boundSql.getParameterMappings();
// sql语句中多个空格都用一个空格代替
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (CollectionUtils.isNotEmpty(parameterMappings) && parameterObject != null) {
// 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 如果根据parameterObject.getClass()可以找到对应的类型,则替换
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(getParameterValue(parameterObject)));
} else {
// MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,
// 主要支持对JavaBean、Collection、Map三种类型对象的操作
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(getParameterValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
// 该分支是动态sql
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(getParameterValue(obj)));
} else {
// 打印出缺失,提醒该参数缺失并防止错位
sql = sql.replaceFirst("\\?", "缺失");
}
}
}
}
return sql;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
主要是这一句bean.setPlugins(new Interceptor[]{ new SqlPrintInterceptor()});
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
//设置分页的拦截器
PageInterceptor pageInterceptor = new PageInterceptor();
//创建插件需要的参数集合
Properties properties = new Properties();
//配置数据库方言 为oracle
properties.setProperty("helperDialect", "mysql");
//配置分页的合理化数据
properties.setProperty("reasonable", "true");
pageInterceptor.setProperties(properties);
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setPlugins(new Interceptor[]{ new SqlPrintInterceptor()});
bean.setDataSource(dataSource);
bean.setMapperLocations(resolveMapperLocations());//重点2 指定包扫描,当前这个数据源对应的mapper.xml文件在哪个resource下的包里,这里路径就指定哪里
return bean.getObject();
}
完整注册配置类
@Slf4j
@Configuration
@MapperScan(basePackages = {"com.*.dao"}, sqlSessionTemplateRef = "sqlSessionTemplate")
//重点1
// 指定包扫描,当前这个数据源对应的mapper.java文件放在哪个包下,这里路径就指定哪里
public class MySQLDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.mysql")//重点3 这里对应yml的当前数据源的前缀
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
//设置分页的拦截器
PageInterceptor pageInterceptor = new PageInterceptor();
//创建插件需要的参数集合
Properties properties = new Properties();
//配置数据库方言 为oracle
properties.setProperty("helperDialect", "mysql");
//配置分页的合理化数据
properties.setProperty("reasonable", "true");
pageInterceptor.setProperties(properties);
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
//设置拦截器!!!!
bean.setPlugins(new Interceptor[]{ new SqlPrintInterceptor()});
bean.setDataSource(dataSource);
bean.setMapperLocations(resolveMapperLocations());//重点2 指定包扫描,当前这个数据源对应的mapper.xml文件在哪个resource下的包里,这里路径就指定哪里
return bean.getObject();
}
/**
* 获取多个路径下的mapper
*
* @return
*/
public Resource[] resolveMapperLocations() {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List mapperLocations = new ArrayList<>();
mapperLocations.add("classpath:com/mapper/**/*.xml");
List resources = new ArrayList();
if (!CollectionUtils.isEmpty(mapperLocations)) {
for (String mapperLocation : mapperLocations) {
try {
Resource[] mappers = resourceResolver.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) {
//log.error("Get myBatis resources happened exception", e);
}
}
}
return resources.toArray(new Resource[0]);
}
@Bean
@Primary
public DataSourceTransactionManager mysqlTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
@Primary
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
参考文章:
https://blog.csdn.net/wb1046329430/article/details/111501755