从零开始的 MyBatis 拦截器之旅:实战经验分享

文章目录

    • MyBatis拦截器可以做什么?
    • Mybatis核心对象介绍
    • 四大核心对象
    • 如何实现?接口讲解
      • Interceptor接口
      • intercept方法
      • plugin方法
      • setProperties
    • 完整SQL打印拦截器实战
      • 拦截器实现
      • 拦截器注册

MyBatis拦截器可以做什么?

MyBatis拦截器是MyBatis框架提供的扩展机制,它可以在执行SQL语句的过程中拦截和干预,用于对SQL语句进行增强或修改。

MyBatis拦截器可以做以下几件事情:

  1. 拦截SQL语句的执行:拦截器可以在SQL语句执行前后进行拦截,可以在SQL语句执行之前对参数进行处理,也可以在SQL语句执行之后对结果进行处理。
  2. 修改SQL语句:拦截器可以对原始的SQL语句进行修改,可以增加、删除或修改SQL语句的部分内容,以满足一些特定需求。比如可以在SQL语句前后添加额外的条件或修改排序方式。
  3. 记录日志:拦截器可以用于记录SQL语句的执行日志,包括SQL语句的执行时间、执行结果等。这对于系统的性能监控和调优非常有帮助。
  4. 实现分页功能:拦截器可以在执行原始SQL语句之前,根据传入的参数进行分页处理,将查询结果限制在指定的页数和每页的记录数范围内。
  5. 实现缓存功能:拦截器可以在执行SQL语句之前,先检查缓存中是否存在对应的结果,如果存在则直接返回缓存结果,避免不必要的数据库查询操作。
  6. 在很多时候,对表中的数据都需要记录插入时间,修改时间,插入人和修改人,若每次都在插入或修改代码中去设置这些信息,就显得有些冗余。那么此时可以通过Mybatis提供的拦截器加上我们自定义的拦截器实现对在需要记录的操作人信息sql执行前,自动补充这些信息,也就是所谓的对Mybatis的核心对象进行增强。这里只拦截Executor对象,给更新的sql语句动态的增加参数。(可参考https://www.cnblogs.com/zys2019/p/16966866.html )

具体例子: 我们常用的分页插件Pagehelper其实就是一个拦截器实现

Mybatis核心对象介绍

MyBatis的主要的核心部件有以下几个:

  • Configuration:初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中。
  • SqlSessionFactory:SqlSession工厂。
  • SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
  • Executor:MyBatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射,另外,它还处理二级缓存的操作。
  • StatementHandler:MyBatis直接在数据库执行SQL脚本的对象。另外它也实现了MyBatis的一级缓存。
  • ParameterHandler:负责将用户传递的参数转换成JDBC Statement所需要的参数。是MyBatis实现SQL入参设置的对象。
  • ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。是MyBatis把ResultSet集合映射成POJO的接口对象。
  • TypeHandler:负责Java数据类型和JDBC数据类型之间的映射和转换。
  • MappedStatement:MappedStatement维护了一条节点的封装。
  • SqlSource :负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
  • BoundSql:表示动态生成的SQL语句以及相应的参数信息。

从零开始的 MyBatis 拦截器之旅:实战经验分享_第1张图片

从零开始的 MyBatis 拦截器之旅:实战经验分享_第2张图片

四大核心对象

在Mybatis中,Executor、StatementHandler、ParameterHandler和ResultSetHandler是核心组件,它们分别负责不同的任务。

  1. Executor:统筹全局
    Executor是Mybatis中的执行器,它负责处理数据库的操作。它的职责是接收并执行SQL语句,管理事务的提交和回滚,以及处理缓存。 在Mybatis中,有三种类型的Executor:SimpleExecutor、ReuseExecutor和BatchExecutor,它们分别提供了不同的执行策略。
  2. StatementHandler:执行SQL
    StatementHandler负责处理SQL语句的操作,它是Executor的一个重要组成部分。它的主要职责是创建PreparedStatement对象,设置参数,并执行SQL语句。 StatementHandler可以根据不同的数据库厂商提供的驱动,生成不同的Statement对象,如PreparedStatement、CallableStatement等。
  3. ParameterHandler:参数封装
    ParameterHandler负责处理SQL语句中的参数。它的主要职责是将Java对象中的属性值映射到SQL语句中的参数位置。ParameterHandler可以根据参数的类型,将Java对象的属性值转换为数据库可以接受的类型,并设置到PreparedStatement对象中。
  4. ResultSetHandler:返回结果映射
    ResultSetHandler负责处理SQL语句的结果集。它的主要职责是将查询结果集中的数据映射到Java对象中。ResultSetHandler会根据映射规则,将数据库中的每一行数据转换为Java对象,并将这些对象放入一个集合中返回给调用者。

从零开始的 MyBatis 拦截器之旅:实战经验分享_第3张图片

这四个组件在Mybatis中协同工作,完成了从数据库操作到Java对象映射的整个过程。Executor负责整体的控制和协调,StatementHandler负责处理SQL语句的操作,ParameterHandler负责处理参数,ResultSetHandler负责处理结果集。它们各自分工明确,相互配合,共同完成数据库操作和对象映射的任务。

如何实现?接口讲解

  • 写一个实现org.apache.ibatis.plugin.Interceptor接口的拦截器类,并实现其中的方法。
  • 添加@Intercepts注解,写上需要拦截的对象和方法,以及方法参数。
  • Spring项目注意添加@Component注解即可,使其成为Spring管理的一个Bean。

Interceptor接口

@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拦截器默认可以拦截的类型只有四种,即四种接口类型ExecutorStatementHandlerParameterHandlerResultSetHandler。对于我们的自定义拦截器必须使用MyBatis提供的@Intercepts注解来指明我们要拦截的是四种类型中的哪一种接口。

注解 描述
@Intercepts 标志该类是一个拦截器
@Signature 指明该拦截器需要拦截哪一个接口的哪一个方法

@Signature注解的参数:

参数 描述
type 四种类型接口中的某一个接口,如Executor.class
method 对应接口中的某一个方法名,比如Executorquery方法。
args 对应接口中的某一个方法的参数,比如Executorquery方法因为重载原因,有多个,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

img

intercept方法

进行拦截的时候要执行的方法。该方法参数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();//方法参数

plugin方法

插件用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理,可以决定是否要进行拦截进而决定要返回一个什么样的目标对象,官方提供了示例: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个代理,层层生成动态代理是比较耗性能的。而且虽然能指定插件拦截的位置,但这个是在执行方法时利用反射动态判断的,初始化的时候就是简单的把拦截器插入到了所有可以拦截的地方。所以尽量不要编写不必要的拦截器。另外我们可以在调用插件的地方添加判断,只要是当前拦截器拦截的对象才进行调用,否则直接返回目标对象本身,这样可以减少反射判断的次数,提高性能。

setProperties

如果我们拦截器需要用到一些变量参数,而且这个参数是支持可配置的,类似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的配置文件中注册自定义的拦截器:


    
    
    
        
            
            
        
    

完整SQL打印拦截器实战

拦截器实现

@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

你可能感兴趣的:(SSH/SSM,mybatis,tomcat,java)