Mybatis 拦截器

Mybatis 相关文章:

Mybatis 拦截器

拦截器的作用,我们就不做过多解释了;

这篇文章梳理完,后面还会梳理一篇 Mybatis的设计模式,还有一个 执行链

该篇文章主要讲述下面几个内容:

  • 1、Mybatis拦截器的实现
  • 2、Mybatis拦截器的应用场景(主要将应用场景)

一、Mybatis拦截器的实现

1.1 配置拦截器节点

首先,拦截器是添加到 Mybatis配置文件的 节点中:

xml 复制代码
<configuration>
    <!--拦截器-->
    <plugins>
         <!--CustomDefineInterceptor 自定义拦截器类-->
        <plugin interceptor="com.yogurt.example.interceptor.CustomDefineInterceptor">
            <!--设置拦截器的属性-->
            <property name="preInfo" value="本次查询记录的数目"/>
        </plugin>
    </plugins>
</configuration>

1.2 解析plugin节点信息

其实这个在源码执行流程文章中已经讲解过这一步,解析plugin节点只是解析整个mybatis的configuration配置文件的一个小小步骤。

有不懂源码执行流程的,可以看一下本人的:『Mybatis 源码执行流程』这篇文章。

我们再来大致跟一下:这里我们就直接从解析过程开始了,不在说注册bean信息这些了。

直接来到 解析plugin吧,简洁一点,这篇文章主要讲Mybatis拦截器的应用。

来到源码这个方法里面:org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration

java 复制代码
public class XMLConfigBuilder extends BaseBuilder {
    /**
     * 这里是解析配置文件的起始方法,解析的所有信息都放入到了configuration中
     * @param root
     */
    private void parseConfiguration(XNode root) {
        try {
            //issue #117 read properties first
            propertiesElement(root.evalNode("properties"));
            Properties settings = settingsAsProperties(root.evalNode("settings"));
            loadCustomVfs(settings);
            loadCustomLogImpl(settings);
            typeAliasesElement(root.evalNode("typeAliases"));
​
            // 这里解析 plugins 节点
            pluginElement(root.evalNode("plugins"));
​
            objectFactoryElement(root.evalNode("objectFactory"));
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            reflectorFactoryElement(root.evalNode("reflectorFactory"));
            settingsElement(settings);  //所有的 settings 节点中的属性都放在这这里解析了
            // read it after objectFactory and objectWrapperFactory issue #631
            environmentsElement(root.evalNode("environments"));
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            typeHandlerElement(root.evalNode("typeHandlers"));
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
​
    /**
     * 解析 <plugins> 节点
     * @param parent <plugins> 节点
     * @throws Exception
     */
    private void pluginElement(XNode parent) throws Exception {
        // <plugins> 节点 存在
        if (parent != null) {
            // 依次取出<plugins>节点下的 每一个<plugin>节点
            for (XNode child : parent.getChildren()) {
                // 读取拦截器的类名
                String interceptor = child.getStringAttribute("interceptor");
                // 读取拦截器的属性
                Properties properties = child.getChildrenAsProperties();
                // 实例化拦截器类
                Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
                // 设置拦截器的属性
                interceptorInstance.setProperties(properties);
                // 将当前拦截器加入拦截器链中!!!
                configuration.addInterceptor(interceptorInstance);
            }
        }
    }
}

可以看到,interceptor 就是我们上面在配置文件中,配置的拦截器节点。

1.3 注册拦截器到拦截器链上

configuration.addInterceptor(interceptorInstance); 会将该拦截器注册到 configuration中的 拦截器链 interceptorChain中;

java 复制代码
public class Configuration {
    // 拦截器链(用来支持插件的插入)
    protected final InterceptorChain interceptorChain = new InterceptorChain();
​
    // 注册
    public void addInterceptor(Interceptor interceptor) {
        interceptorChain.addInterceptor(interceptor);
    }
}

interceptorChain类型就是 InterceptorChain;该类中 有一个List: interceptors + 利用for循环,实现责任链模式;

其实不是说实现责任链模式,而是利用责任链模式解决了 目标方法在被多个拦截器拦截的时候,能够顺序的利用责任链上的拦截器拦截目标方法,达到我们使用拦截器的效果。

java 复制代码
public class InterceptorChain {
​
  // 这里就是为了形成 拦截器链
  private final List<Interceptor> interceptors = new ArrayList<>();
​
  // target是支持拦截的几个类的实例。
  // 该方法依次向所有拦截器插入这几个类的实例,如果某个插件真的需要发挥作用,则返回一个代理对象即可;
  // 如果不需要发挥作用,则返回原对象即可。
  /**
   * 向所有的拦截器链提供目标对象,由拦截器链给出替换目标对象的对象
   * @param target 目标对象,是MyBatis中支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
   * @return 用来替换目标对象的对象
   */
  public Object pluginAll(Object target) {
    // 依次交给每个拦截器完成目标对象的替换工作。
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
​
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
​
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }
​
}

二、应用场景

2.1 应用场景一

拦截器目的:在获取到查询结果之后,打印结果的条数。

详细的demo不再写了,只要先正常将Mybatis配置好,可以利用Mapper接口方法访问数据库就行;之后配置该拦截器,注意在配置文件中加上 节点,添加上这个拦截器引用就行。

主要看下面 注释对拦截器方法的介绍和注解参数介绍。

java 复制代码
/**
 * 用户可以自定的拦截器;
 *     这个拦截器的目的是为了,在获取到查询结果之后,打印结果的条数;
 *     这里设置了拦截器,还需要在配置文件中添加plugin节点才可以生效。
 *     <!--拦截器-->
 *     <plugins>
 *         <plugin interceptor="com.yogurt.example.interceptor.CustomDefineInterceptor">
 *             <property name="preInfo" value="本次查询记录的数目"/>
 *         </plugin>
 *     </plugins>
 *
 * 拦截器写在这个位置其实是有问题的,应该写在项目中,如:mybatis-learning-demo中,而不是写在源码中!!
 * 如果有需要就可以写在自己的项目中即可!
 *
 * 1、注解介绍:
 *      拦截器类上有注解 Intercepts,Intercepts的参数是 Signature注解数组。每个Signature注解都声明了当前拦截器类要拦截的方法。
 *
 *      Signature注解中参数的含义如下:
 *        ① type:拦截器要拦截的类型。CustomDefineInterceptor拦截器要拦截的类型是ResultSetHandler类型。
 *        ② method:拦截器要拦截的 type类型中的方法。
 *                  CustomDefineInterceptor 拦截器要拦截的是ResultSetHandler类型中的 handleResultSets方法。
 *        ③ args:拦截器要拦截的type类型中method方法的参数类型列表。
 *                在CustomDefineInterceptor拦截器中,ResultSetHandler类型中的 handleResultSets方法只有一个 Statement类型的参数。
 *
 *      当要拦截多个方法时,只需在 Intercepts数组中放入多个 Signature注解即可。
 *
 * 2、从 Interceptor继承过来的方法:
 *      ① intercept:
 *            拦截器类必须实现该方法。
 *            拦截器拦截到目标方法时,会将操作转接到该 intercept 方法上,其中的参数 Invocation 为拦截到的目标方法。
 *      ② plugin:
 *            拦截器类可以选择实现该方法。该方法中可以输出一个对象来替换输入参数传入的目标对象。
 *      ③ setProperties:
 *            拦截器类可以选择实现该方法。该方法用来为拦截器设置属性。
 *            在CustomDefineInterceptor拦截器中,我们使用该方法为拦截器设置 info属性的值。
 */
@Intercepts(
  @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
)
public class CustomDefineInterceptor implements Interceptor {
​
  private String info;
​
  /**
   * 拦截器这个方法是必须要实现的。
   * @param invocation  该参数 Invocation 为拦截到的目标方法
   * @return
   * @throws Throwable
   */
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    // 执行原有方法
    Object result = invocation.proceed();
    // 打印原方法输出结果的数目
    System.out.println(info + " : " + ((List)result).size());
    // 返回原有结果
    return result;
  }
​
  /**
   * 这个就暂时不用了。
   * 拦截器类可以选择实现该方法。该方法中可以输出一个对象来替换输入参数传入的目标对象
   * @param target
   * @return
   */
  @Override
  public Object plugin(Object target) {
    return Interceptor.super.plugin(target);
  }
​
  /**
   * 为拦截器设置属性
   * @param properties
   */
  @Override
  public void setProperties(Properties properties) {
    // Interceptor.super.setProperties(properties);
​
    // 为拦截器设置属性
    info = properties.get("preInfo").toString();
  }
}

demo测试结果,可以看到 在执行玩方法执行,有打印条数信息:

2.2 应用场景二

该场景引用文章:guochenglai.com/article/14
拦截器目的:一个结果构建成一个HashMap返回

有时候,我们需要将一个结果构建成一个HashMap返回,便于我们后面处理,但是Mybatis对接结果集的返回,没有Map类型的;所以我们可以利用拦截器,对结果集进行特殊处理。

其实利用拦截器,只是花里胡哨点,因为返回的结果集有这种类型 : List<String,HashMap<String,Stirng>>,拿着这种类型去处理结果也行。

这里目的就是为了学习一下 Mybatis拦截器。

一、拦截器

java 复制代码
package com.yogurt.example.interceptor.map;
​
import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
​
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.*;
​
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
public class MapInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        // 这里一定要注意 target 类型是 DefaultResultSetHandler;
        // 要想 target 类型是 DefaultResultSetHandler!!!
        //    必须将该 MapInterceptor 放在拦截器链的第一个,因为拦截器链是一个List;
        //    然后顺序执行的,如果不在第一个,先被别的拦截器拦截,拦截器会返回一个被该拦截器处理过的代理对象Proxy,进而继续执行下面的拦截器;
        //    这时候,如果来到该方法,那么 invocation.getTarget()就是一个代理对象类型,而不是 DefaultResultSetHandler类型,
        //    没有办法将代理对象转为目标对象,就会报错,所以,这里要注意一下。
        //
        //    或者这里,我们可以获取代理对象,然后将代理对象转成一个目标对象也行;
        //    怎么转,我还不知道怎么转;
        //    还真不能转,为啥?
        //    因为:JDK动态代理,只能代理接口,这里我们要将target转成 DefaultResultSetHandler类型,并获取该类的mappedStatement参数
        //    而,DefaultResultSetHandler类型是 ResultSetHandler接口的实现类,所以不能转。
        //
        //    目前是,拦截器链里面只有这一 MapInterceptor拦截器,可以实现 将结果集处理成 HashMap类型,
        //    主要是靠下面的方法: com.yogurt.example.interceptor.map.MapInterceptor.result2Map()
        DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) target;
        // 这里通过反射,根据类名称来获取内部类,如果出现变更,则需要修改,不过Mybatis的插件目前都是这样一种情况
        MappedStatement mappedStatement = 
            (MappedStatement) ReflectUtil.getFieldValue(defaultResultSetHandler, "mappedStatement");
​
        // mappedStatement.getId() 是mapper接口里面的方法的 全称限定名: com.yogurt.example.dao.TestMapperDao.interceptorBuildMap
        // 那么:
        //   className: com.yogurt.example.dao.TestMapperDao
        //   methodName : interceptorBuildMap
        String className = substringBeforeLast(mappedStatement.getId(), ".");
        String methodName = substringAfterLast(mappedStatement.getId(), ".");
        Method[] methods = Class.forName(className).getDeclaredMethods();
        if (methods == null) {
            return invocation.proceed();
        }
​
        // 找到需要执行的method 注意这里是根据方法名称来查找,如果出现方法重载,需要认真考虑
        for (Method method : methods) {
            if (methodName.equalsIgnoreCase(method.getName())) {
                //如果添加了注解标识,就将结果转换成map
                MapResult map = method.getAnnotation(MapResult.class);
                if (map == null) {
                    return invocation.proceed();
                }
                //进行map的转换
                Statement statement = (Statement) invocation.getArgs()[0];
                return result2Map(statement);
            }
        }
        return invocation.proceed();
    }
​
    @Override
    public Object plugin(Object target) {
        return Interceptor.super.plugin(target);
    }
​
    @Override
    public void setProperties(Properties properties) {
    }
​
    /**
     * 将SQL返回的结果集,构建成一个HashMap返回
     * @param statement
     * @return
     * @throws Throwable
     */
    private Object result2Map(Statement statement) throws Throwable{
        ResultSet resultSet = statement.getResultSet();
        if (resultSet == null) {
            return null;
        }
        List<Object> resultList = new ArrayList<Object>();
        Map<Object, Object> map = new HashMap<Object, Object>();
        while (resultSet.next()) {
            map.put(resultSet.getObject(1), resultSet.getObject(2));
        }
        resultList.add(map);
        return resultList;
    }
​
​
    private static String substringBeforeLast(String str, String separator){
        final int pos = str.lastIndexOf(separator);
        return str.substring(0, pos);
    }
​
    public static String substringAfterLast(final String str, final String separator) {
        final int pos = str.lastIndexOf(separator);
        return str.substring(pos + separator.length());
    }
}

二、反射工具类,为了获取属性 mappedStatement 属性

java 复制代码
package com.yogurt.example.interceptor.map;
​
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
​
import java.lang.reflect.Field;
​
public class ReflectUtil {
    //private static final Logger LOGGER = LoggerFactory.getLogger(ReflectUtil.class);
    /**
     * 根据反射或者一个对象的方法
     * @param obj
     * @param fieldName
     * @param <T>
     * @return
     */
    public static <T> T getFieldValue(Object obj, String fieldName) {
        Object result = null;
        Field field = ReflectUtil.getField(obj, fieldName);
        if (field != null) {
            field.setAccessible(true);
            try {
                result = field.get(obj);
            } catch (IllegalArgumentException e) {
                //LOGGER.info("ReflectUtil getFieldValue cause IllegalArgumentException",e);
                System.out.println("ReflectUtil getFieldValue cause IllegalArgumentException : "+e);
            } catch (IllegalAccessException e2) {
                //LOGGER.info("ReflectUtil getFieldValue cause IllegalAccessException",e2);
                System.out.println("ReflectUtil getFieldValue cause IllegalAccessException : "+e2);
            }
        }
        return (T)result;
    }
    private static Field getField(Object obj, String fieldName) {
        Field field = null;
        for (Class<?> clazz=obj.getClass(); clazz != Object.class; clazz=clazz.getSuperclass()) {
            try {
                field = clazz.getDeclaredField(fieldName);
                break;
            } catch (NoSuchFieldException e) {
                //LOGGER.info("ReflectUtil getField cause NoSuchFieldException",e);
​
                System.out.println("ReflectUtil getField cause NoSuchFieldException : "+e);
            }
        }
        return field;
    }
}

三、处理HashMap结果集映射的 注解标志

java 复制代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Inherited
public @interface MapResult {
    String value();
}
​
​
public interface TestMapperDao {
    // 注解打在 Mapper映射方法上,目的是为了让拦截器找到之后,对其特殊处理
    @MapResult("interceptorBuildMap")
    Map<Integer,Integer> interceptorBuildMap(Integer id);
}

四、加入到 plugin节点中

xml 复制代码
<!--拦截器-->
<plugins>
    <!--将结果集构建成map返回-->
    <plugin interceptor="com.yogurt.example.interceptor.map.MapInterceptor">
    </plugin>
</plugins>

五、执行结果:

OK,结束!!!


现在我们来说一下,在 map.MapInterceptor#intercept方法,在转类型是时候出现的问题;

MapInterceptor 在 配置文件中配置 里面放在第一个,或者是只有一个 MapInterceptor 拦截器的话,

ini 复制代码
Object target = invocation.getTarget();
DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) target;

这里可以正常转换,因为执行器链里面只有一个拦截器,不会有其他拦截器干扰。

我们可以看到,下面的 target类型就是 DefaultResultSetHandler类型的。


**但是,如果 `MapInterceptor`不在拦截器第一个位置的话,就会出现问题。**

这里加入我们场景一的拦截器,并将MapInterceptor拦截器放在第二位置:

xml 复制代码
<!--拦截器-->
<plugins>
    <plugin interceptor="com.yogurt.example.interceptor.CustomDefineInterceptor">
        <property name="preInfo" value="本次查询记录的数目"/>
    </plugin>

    <!--将结果集构建成map返回-->
    <plugin interceptor="com.yogurt.example.interceptor.map.MapInterceptor">
    </plugin>
</plugins>

因为拦截器链是一个List;然后顺序执行的,如果不在第一个,先被别的拦截器拦截,拦截器会返回一个被该拦截器处理过的代理对象Proxy,进而继续执行下面的拦截器; 这时候,如果来到该方法,那么 invocation.getTarget() 就是一个代理对象类型,而不是 DefaultResultSetHandler类型,没有办法将代理对象转为目标对象,就会报错,所以,这里要注意一下。


可以看到 target是个代理类型, 因为:JDK动态代理,只能代理接口,这里我们要将target转成 DefaultResultSetHandler类型,并获取该类的mappedStatement参数;

而,DefaultResultSetHandler类型是 ResultSetHandler接口的实现类,所以不能转。

这里就会报错:

OK,彻底完结,撒花。

相关推荐
P.H. Infinity几秒前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天4 分钟前
java的threadlocal为何内存泄漏
java
caridle16 分钟前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^21 分钟前
数据库连接池的创建
java·开发语言·数据库
苹果醋324 分钟前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花29 分钟前
【JAVA基础】Java集合基础
java·开发语言·windows
小松学前端32 分钟前
第六章 7.0 LinkList
java·开发语言·网络
Wx-bishekaifayuan39 分钟前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer0843 分钟前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
全栈开发圈1 小时前
新书速览|Java网络爬虫精解与实践
java·开发语言·爬虫