信息发布→ 登录 注册 退出

MyBatis拦截器的原理与使用

发布时间:2026-01-11

点击量:
目录
  • 一、拦截对象和接口实现示例
  • 二、拦截器注册的三种方式
    •         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;
          <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;
          <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
          <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;
          List<BatchResult> 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;
          <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;
          <E> Cursor<E> queryCursor(Statement var1) throws SQLException;
          BoundSql getBoundSql();
          ParameterHandler getParameterHandler();
      }
      public interface ParameterHandler {
          Object getParameterObject();
          void setParameters(PreparedStatement var1) throws SQLException;
      }
      public interface ResultHandler<T> {
          void handleResult(ResultContext<? extends T> 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 {
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              if (invocation.getTarget() instanceof Executor) {
                  System.out.println("SelectPlugin");
              }
              return invocation.proceed();
          }
          @Override
          public Object plugin(Object target) {
              if (target instanceof Executor) {
                  return Plugin.wrap(target, this);
              }
              return target;
          }
          @Override
          public void setProperties(Properties properties) {}
      }
      @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
      public class StatementPlugin implements Interceptor {
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              if (invocation.getTarget() instanceof StatementHandler) {
                  System.out.println("StatementPlugin");
              }
              return invocation.proceed();
          }
          @Override
          public Object plugin(Object target) {
              if (target instanceof StatementHandler) {
                  return Plugin.wrap(target, this);
              }
              return target;
          }
          @Override
          public void setProperties(Properties properties) {}
      }
      @Intercepts({@Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class})})
      public class ParameterPlugin implements Interceptor {
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              if (invocation.getTarget() instanceof ParameterHandler) {
                  System.out.println("ParameterPlugin");
              }
              return invocation.proceed();
          }
          @Override
          public Object plugin(Object target) {
              if (target instanceof ParameterHandler) {
                  return Plugin.wrap(target, this);
              }
              return target;
          }
          @Override
          public void setProperties(Properties properties) {}
      }
      @Intercepts({@Signature(type = ResultHandler.class,method = "handleResult",args = {ResultContext.class})})
      public class ResultPlugin implements Interceptor {
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              if (invocation.getTarget() instanceof ResultHandler) {
                  System.out.println("ResultPlugin");
              }
              return invocation.proceed();
          }
          @Override
          public Object plugin(Object target) {
              if (target instanceof ResultHandler) {
                  return Plugin.wrap(target, this);
              }
              return target;
          }
          @Override
          public void setProperties(Properties properties) {}
      }

      二、拦截器注册的三种方式

              前面介绍了Mybatis的拦截对象及其接口的实现方式,那么在项目中如何注册拦截器呢?本文中给出三种注册方式。

              1.XML注册

              xml注册是最基本的方式,是通过在Mybatis配置文件中plugins元素来进行注册的。一个plugin对应着一个拦截器,在plugin元素可以指定property子元素,在注册定义拦截器时把对应拦截器的所有property通过Interceptor的setProperties方法注入给拦截器。因此拦截器注册xml方式如下:

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>
          <!-- ...... -->
          <plugins>
             <plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor">
                 <property name="prop1" value="prop1"/>
                 <property name="prop2" value="prop2"/>
             </plugin>
          </plugins>
          <!-- ...... -->
      </configuration>

              2.配置类注册

             配置类注册是指通过Mybatis的配置类中声明注册拦截器,配置类注册也可以通过Properties类给Interceptor的setProperties方法注入参数。具体参考如下:

      @Configuration
      public class MyBatisConfig {
          @Bean
          public 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());效果一致
          @Bean
          public 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 {
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              if (invocation.getTarget() instanceof Executor) {
                  System.out.println("SelectPlugin");
              }
              return invocation.proceed();
          }
      
          @Override
          public Object plugin(Object target) {
              if (target instanceof Executor) {
                  return Plugin.wrap(target, this);
              }
              return target;
          }
      
          @Override
          public void setProperties(Properties properties) {
      
          }
      }

      三、ParameterHandler参数改写-修改时间和修改人统一插入

              针对具体的拦截器实现进行描述。日常编码需求中会碰到修改时需要插入修改的时间和人员,如果要用xml的方式去写非常麻烦,而通过拦截器的方式可以快速实现全局的插入修改时间和人员。先看代码:

      @Component
      @Intercepts({
              @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
      })
      public class MyBatisInterceptor implements Interceptor {
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              // 参数代理
              if (invocation.getTarget() instanceof ParameterHandler) {
                  System.out.println("ParameterHandler");
                  // 自动添加操作员信息
                  autoAddOperatorInfo(invocation);
              }
              return invocation.proceed();
          }
      
          @Override
          public Object plugin(Object target) {
              return Plugin.wrap(target, this);
          }
      
          @Override
          public void setProperties(Properties properties) {
      
          }
      
          /**
           * 自动添加操作员信息
           *
           * @param invocation 代理对象
           * @throws Throwable 异常
           */
          private void autoAddOperatorInfo(Invocation invocation) throws Throwable {
              System.out.println("autoInsertCreatorInfo");
              // 获取代理的参数对象ParameterHandler
              ParameterHandler ph = (ParameterHandler) invocation.getTarget();
              // 通过MetaObject获取ParameterHandler的反射内容
              MetaObject metaObject = MetaObject.forObject(ph,
                      SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                      SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                      new DefaultReflectorFactory());
              // 通过MetaObject反射的内容获取MappedStatement
              MappedStatement 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<ParameterMapping> 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 = 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的全局信息,如jdbc
          private Configuration configuration;
          // 节点的id属性加命名空间,如:com.example.mybatis.dao.UserMapper.selectByExample
          private 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<ResultMap> resultMaps;
          private boolean flushCacheRequired;
          private boolean useCache;
          private boolean resultOrdered;
          // sql语句的类型:select、update、delete、insert
          private 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<AdminProjectPo> selectProjects(AdminProjectPo po);
       }

              拦截器通过删除注解标记判断是否添加删除标志

      @Component
      @Intercepts({
              @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
      })
      public class MyBatisInterceptor implements Interceptor {
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              if (invocation.getTarget() instanceof StatementHandler) {
                  System.out.println("StatementHandler");
                  checkHasDeletedAtField(invocation);
              }
              return invocation.proceed();
          }
      
          @Override
          public Object plugin(Object target) {
              return Plugin.wrap(target, this);
          }
      
          @Override
          public 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());
              // 获取成员变量mappedStatement
              MappedStatement 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);
                      //方法名相同,并且注解是DeletedAt
                      if (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拦截器的资料请关注其它相关文章!

      在线客服
      服务热线

      服务热线

      4008888355

      微信咨询
      二维码
      返回顶部
      ×二维码

      截屏,微信识别二维码

      打开微信

      微信号已复制,请打开微信添加咨询详情!