MyBatis拦截器的原理与使用

目录

  • 一、拦截对象和接口实现示例
  • 二、拦截器注册的三种方式
    • 1.XML注册
    • 2.配置类注册
    • 3.注解方式
  • 三、ParameterHandler参数改写-修改时间和修改人统一插入
    • 四、通过StatementHandler改写SQL

      一、拦截对象和接口实现示例
      MyBatis拦截器的作用是在于Dao到DB中间进行额外的处理。大部分情况下通过mybatis的xml配置sql都可以达到想要的DB操作效果,然而存在一些类似或者相同的查询条件或者查询要求,这些可以通过拦截器的实现可以提升开发效率,比如:分页、插入和更新时间/人、数据权限、SQL监控日志等。
      • Mybatis支持四种对象拦截Executor、StatementHandler、PameterHandler和ResultSetHandler
      1. Executor:拦截执行器的方法。
      2. StatementHandler:拦截Sql语法构建的处理。
      3. ParameterHandler:拦截参数的处理。
      4. ResultHandler:拦截结果集的处理。
      public interface Executor {ResultHandler NO_RESULT_HANDLER = null; int update(MappedStatement var1, Object var2) throws SQLException; List query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException; List query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException; Cursor queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException; List flushStatements() throws SQLException; void commit(boolean var1) throws SQLException; void rollback(boolean var1) throws SQLException; CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4); boolean isCached(MappedStatement var1, CacheKey var2); void clearLocalCache(); void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class var5); Transaction getTransaction(); void close(boolean var1); boolean isClosed(); void setExecutorWrapper(Executor var1); }public interface StatementHandler {Statement prepare(Connection var1, Integer var2) throws SQLException; void parameterize(Statement var1) throws SQLException; void batch(Statement var1) throws SQLException; int update(Statement var1) throws SQLException; List query(Statement var1, ResultHandler var2) throws SQLException; Cursor queryCursor(Statement var1) throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }public interface ParameterHandler {Object getParameterObject(); void setParameters(PreparedStatement var1) throws SQLException; }public interface ResultHandler {void handleResult(ResultContext var1); }

      拦截的执行顺序是Executor->StatementHandler->ParameterHandler->ResultHandler
      • MyBatis提供的拦截器接口:
      public interface Interceptor {Object intercept(Invocation var1) throws Throwable; default Object plugin(Object target) {return Plugin.wrap(target, this); }default void setProperties(Properties properties) {}}

      Object intercept方法用于拦截器的实现;
      Object plugin方法用于判断执行拦截器的类型;
      void setProperties方法用于获取配置项的属性。
      • 拦截对象和拦截器接口的结合,自定义的拦截器类需要实现拦截器接口,并通过注解@Intercepts和参数@Signature来声明要拦截的对象。
      @Signature参数type是拦截对象,method是拦截的方法,即上面的四个类对应的方法,args是拦截方法对应的参数(方法存在重载因此需要指明参数个数和类型)
      @Intercepts可以有多个@Signature,即一个拦截器实现类可以同时拦截多个对象及方法,示例如下:
      1. Executor->intercept
      2. StatementHandler->intercept
      3. ParameterHandler->intercept
      4. ResultHandler->intercept
      @Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})public class SelectPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {if (invocation.getTarget() instanceof Executor) {System.out.println("SelectPlugin"); }return invocation.proceed(); }@Overridepublic Object plugin(Object target) {if (target instanceof Executor) {return Plugin.wrap(target, this); }return target; }@Overridepublic void setProperties(Properties properties) {}}@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})public class StatementPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {if (invocation.getTarget() instanceof StatementHandler) {System.out.println("StatementPlugin"); }return invocation.proceed(); }@Overridepublic Object plugin(Object target) {if (target instanceof StatementHandler) {return Plugin.wrap(target, this); }return target; }@Overridepublic void setProperties(Properties properties) {}}@Intercepts({@Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class})})public class ParameterPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {if (invocation.getTarget() instanceof ParameterHandler) {System.out.println("ParameterPlugin"); }return invocation.proceed(); }@Overridepublic Object plugin(Object target) {if (target instanceof ParameterHandler) {return Plugin.wrap(target, this); }return target; }@Overridepublic void setProperties(Properties properties) {}}@Intercepts({@Signature(type = ResultHandler.class,method = "handleResult",args = {ResultContext.class})})public class ResultPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {if (invocation.getTarget() instanceof ResultHandler) {System.out.println("ResultPlugin"); }return invocation.proceed(); }@Overridepublic Object plugin(Object target) {if (target instanceof ResultHandler) {return Plugin.wrap(target, this); }return target; }@Overridepublic void setProperties(Properties properties) {}}


      二、拦截器注册的三种方式 前面介绍了Mybatis的拦截对象及其接口的实现方式,那么在项目中如何注册拦截器呢?本文中给出三种注册方式。
      【MyBatis拦截器的原理与使用】
      1.XML注册
      xml注册是最基本的方式,是通过在Mybatis配置文件中plugins元素来进行注册的。一个plugin对应着一个拦截器,在plugin元素可以指定property子元素,在注册定义拦截器时把对应拦截器的所有property通过Interceptor的setProperties方法注入给拦截器。因此拦截器注册xml方式如下:


      2.配置类注册
      配置类注册是指通过Mybatis的配置类中声明注册拦截器,配置类注册也可以通过Properties类给Interceptor的setProperties方法注入参数。具体参考如下:
      @Configurationpublic class MyBatisConfig {@Beanpublic String MyBatisInterceptor(SqlSessionFactory sqlSessionFactory) {UpdatePlugin executorInterceptor = new UpdatePlugin(); Properties properties = new Properties(); properties.setProperty("prop1", "value1"); // 给拦截器添加自定义参数executorInterceptor.setProperties(properties); sqlSessionFactory.getConfiguration().addInterceptor(executorInterceptor); sqlSessionFactory.getConfiguration().addInterceptor(new StatementPlugin()); sqlSessionFactory.getConfiguration().addInterceptor(new ResultPlugin()); sqlSessionFactory.getConfiguration().addInterceptor(new ParameterPlugin()); // sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin()); return "interceptor"; }// 与sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin()); 效果一致@Beanpublic SelectPlugin SelectInterceptor() {SelectPlugin interceptor = new SelectPlugin(); Properties properties = new Properties(); // 调用properties.setProperty方法给拦截器设置自定义参数interceptor.setProperties(properties); return interceptor; }}


      3.注解方式
      通过@Component注解方式是最简单的方式,在不需要转递自定义参数时可以使用,方便快捷。
      @Component@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})public class SelectPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {if (invocation.getTarget() instanceof Executor) {System.out.println("SelectPlugin"); }return invocation.proceed(); }@Overridepublic Object plugin(Object target) {if (target instanceof Executor) {return Plugin.wrap(target, this); }return target; }@Overridepublic void setProperties(Properties properties) {}}


      三、ParameterHandler参数改写-修改时间和修改人统一插入 针对具体的拦截器实现进行描述。日常编码需求中会碰到修改时需要插入修改的时间和人员,如果要用xml的方式去写非常麻烦,而通过拦截器的方式可以快速实现全局的插入修改时间和人员。先看代码:
      @Component@Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),})public class MyBatisInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 参数代理if (invocation.getTarget() instanceof ParameterHandler) {System.out.println("ParameterHandler"); // 自动添加操作员信息autoAddOperatorInfo(invocation); }return invocation.proceed(); }@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this); }@Overridepublic void setProperties(Properties properties) {}/*** 自动添加操作员信息** @param invocation 代理对象* @throws Throwable 异常*/private void autoAddOperatorInfo(Invocation invocation) throws Throwable {System.out.println("autoInsertCreatorInfo"); // 获取代理的参数对象ParameterHandlerParameterHandler ph = (ParameterHandler) invocation.getTarget(); // 通过MetaObject获取ParameterHandler的反射内容MetaObject metaObject = MetaObject.forObject(ph,SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory()); // 通过MetaObject反射的内容获取MappedStatementMappedStatement mappedStatement = (MappedStatement) metaObject.getValue("mappedStatement"); // 当sql类型为INSERT或UPDATE时,自动插入操作员信息if (mappedStatement.getSqlCommandType() == SqlCommandType.INSERT ||mappedStatement.getSqlCommandType() == SqlCommandType.UPDATE) {// 获取参数对象Object obj = ph.getParameterObject(); if (null != obj) {// 通过反射获取参数对象的属性Field[] fields = obj.getClass().getDeclaredFields(); // 遍历参数对象的属性for (Field f : fields) {// 如果sql是INSERT,且存在createdAt属性if ("createdAt".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {// 设置允许访问反射属性f.setAccessible(true); // 如果没有设置createdAt属性,则自动为createdAt属性添加当前的时间if (null == f.get(obj)) {// 设置createdAt属性为当前时间f.set(obj, LocalDateTime.now()); }}// 如果sql是INSERT,且存在createdBy属性if ("createdBy".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {// 设置允许访问反射属性f.setAccessible(true); // 如果没有设置createdBy属性,则自动为createdBy属性添加当前登录的人员if (null == f.get(obj)) {// 设置createdBy属性为当前登录的人员f.set(obj, 0); }}// sql为INSERT或UPDATE时均需要设置updatedAt属性if ("updatedAt".equals(f.getName())) {f.setAccessible(true); if (null == f.get(obj)) {f.set(obj, LocalDateTime.now()); }}// sql为INSERT或UPDATE时均需要设置updatedBy属性if ("updatedBy".equals(f.getName())) {f.setAccessible(true); if (null == f.get(obj)) {f.set(obj, 0); }}}// 通过反射获取ParameterHandler的parameterObject属性Field parameterObject = ph.getClass().getDeclaredField("parameterObject"); // 设置允许访问parameterObject属性parameterObject.setAccessible(true); // 将上面设置的新参数对象设置到ParameterHandler的parameterObject属性parameterObject.set(ph, obj); }}}}

      拦截器的接口实现参考前文,这里着重介绍autoAddOperatorInfo方法里的相关类。
      1.ParameterHandler
      接口源码:
      public interface ParameterHandler {Object getParameterObject(); void setParameters(PreparedStatement var1) throws SQLException; }

      提供两个方法:
      getParameterObject是获取参数对象,可能存在null,需要注意null指针。
      setParameters是控制如何设置SQL参数,即sql语句中配置的java对象和jdbc类型对应的关系,例如#{id,jdbcType=INTEGER},id默认类型是javaType=class java.lang.Integer。
      该接口有一个默认的实现类,源码如下:
      public class DefaultParameterHandler implements ParameterHandler {private final TypeHandlerRegistry typeHandlerRegistry; private final MappedStatement mappedStatement; private final Object parameterObject; private final BoundSql boundSql; private final Configuration configuration; public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {this.mappedStatement = mappedStatement; this.configuration = mappedStatement.getConfiguration(); this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry(); this.parameterObject = parameterObject; this.boundSql = boundSql; }public Object getParameterObject() {return this.parameterObject; }public void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId()); List parameterMappings = this.boundSql.getParameterMappings(); if (parameterMappings != null) {for(int i = 0; i < parameterMappings.size(); ++i) {ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) {String propertyName = parameterMapping.getProperty(); Object value; if (this.boundSql.hasAdditionalParameter(propertyName)) {value = https://www.it610.com/article/this.boundSql.getAdditionalParameter(propertyName); } else if (this.parameterObject == null) {value = null; } else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {value = this.parameterObject; } else {MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject); value = metaObject.getValue(propertyName); }TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) {jdbcType = this.configuration.getJdbcTypeForNull(); }try {typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (SQLException | TypeException var10) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10); }}}}}}

      通过DefaultParameterHandler实现类我们知道通过ParameterHandler可以获取到哪些属性和方法,其中包括我们下面一个重要的类MappedStatement。
      2.MappedStatement
      MyBatis的mapper文件中的每个select/update/insert/delete标签会被解析器解析成一个对应的MappedStatement对象,也就是一个MappedStatement对象描述一条SQL语句。MappedStatement对象属性如下:
      // mapper配置文件名private String resource; // mybatis的全局信息,如jdbcprivate Configuration configuration; // 节点的id属性加命名空间,如:com.example.mybatis.dao.UserMapper.selectByExampleprivate String id; private Integer fetchSize; private Integer timeout; private StatementType statementType; private ResultSetType resultSetType; private SqlSource sqlSource; private Cache cache; private ParameterMap parameterMap; private List resultMaps; private boolean flushCacheRequired; private boolean useCache; private boolean resultOrdered; // sql语句的类型:select、update、delete、insertprivate SqlCommandType sqlCommandType; private KeyGenerator keyGenerator; private String[] keyProperties; private String[] keyColumns; private boolean hasNestedResultMaps; private String databaseId; private Log statementLog; private LanguageDriver lang; private String[] resultSets;

      在本例中通过MappedStatement对象的sqlCommandType来判断当前的sql类型是insert、update来进行下一步的操作。

      四、通过StatementHandler改写SQL StatementHandler是用于封装JDBC Statement操作,负责对JDBC Statement的操作,如设置参数,并将Statement结果集转换成List集合。
      实现代码如下:
      删除注解标记
      @Target({ElementType.METHOD})//表示注解的使用范围@Retention(RetentionPolicy.RUNTIME) //注解的保存时间@Documented//文档显示public @interface DeletedAt {boolean has() default true; }

      Dao层添加删除注解,为false时不添加删除标志
      @Mapper public interface AdminProjectDao {@DeletedAt(has = false)List selectProjects(AdminProjectPo po); }

      拦截器通过删除注解标记判断是否添加删除标志
      @Component@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),})public class MyBatisInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {if (invocation.getTarget() instanceof StatementHandler) {System.out.println("StatementHandler"); checkHasDeletedAtField(invocation); }return invocation.proceed(); }@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this); }@Overridepublic void setProperties(Properties properties) {}/*** 检查查询是否需要添加删除标志字段** @param invocation 代理对象* @throws Throwable 异常*/private void checkHasDeletedAtField(Invocation invocation) throws Throwable {System.out.println("checkHasDeletedAtField"); StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); // 通过MetaObject访问对象的属性MetaObject metaObject = MetaObject.forObject(statementHandler,SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory()); // 获取成员变量mappedStatementMappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); // 如果sql类型是查询if (mappedStatement.getSqlCommandType() == SqlCommandType.SELECT) {// 获取删除注解标志DeletedAt annotation = null; String id = mappedStatement.getId(); String className = id.substring(0, id.lastIndexOf(".")); String methodName = id.substring(id.lastIndexOf(".") + 1); Class aClass = Class.forName(className); Method[] declaredMethods = aClass.getDeclaredMethods(); for (Method declaredMethod : declaredMethods) {declaredMethod.setAccessible(true); //方法名相同,并且注解是DeletedAtif (methodName.equals(declaredMethod.getName()) && declaredMethod.isAnnotationPresent(DeletedAt.class)) {annotation = declaredMethod.getAnnotation(DeletedAt.class); }}// 如果注解不存在或者注解为true(默认为true) 则为mysql语句增加删除标志if (annotation == null || annotation.has()) {BoundSql boundSql = statementHandler.getBoundSql(); //获取到原始sql语句String sql = boundSql.getSql(); //通过反射修改sql语句Field field = boundSql.getClass().getDeclaredField("sql"); field.setAccessible(true); String newSql = sql.replaceAll("9=9", "9=9 and deleted_at is null "); field.set(boundSql, newSql); }}}}

      在SQL语句替换上需要能识别到要被替换的内容,因此在xml的sql语句中加入特殊标志"9=9",该标志不影响原来SQL的执行结果,不同的过滤条件可以设置不同的标志,是一个比较巧妙的替换方式。
      以上就是MyBatis拦截器的原理与使用的详细内容,更多关于MyBatis拦截器的资料请关注脚本之家其它相关文章!

        推荐阅读