zoukankan      html  css  js  c++  java
  • MyBatis 中 Mapper 接口的使用原理

    MyBatis 中 Mapper 接口的使用原理

    MyBatis 3 推荐使用 Mapper 接口的方式来执行 xml 配置中的 SQL,用起来很方便,也很灵活。在方便之余,想了解一下这是如何实现的,之前也大致知道是通过 JDK 的动态代理做到的,但这次想知道细节。

    东西越多就越复杂,所以就以一个简单的仅依赖 MyBatis 3.4.0 的 CRUD 来逐步了解 Mapper 接口的调用。

    通常是通过 xml 配置文件来创建SqlSessionFactory对象,然后再获取SqlSession对象,接着获取自定义的 Mapper 接口的代理对象,最后调用接口方法,示例如下:

    /**
     *
     * @author xi
     * @date 2018/10/01 14:12
     */
    public class Demo {
        public static void main(String[] args) throws IOException {
            String resource = "mybatis-config.xml";
            InputStream is = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory =
                    new SqlSessionFactoryBuilder().build(is);
            SqlSession sqlSession = sqlSessionFactory.openSession();
            RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
            Role role = roleMapper.getRole(1L);
            System.out.println(role);
        }
    }
    

    如何解析配置文件,创建工厂,获取会话皆不是本次关注的重点,直接看下面这行即可:

    RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
    

    获取自定义 Mapper 代理对象的方法位于:org.apache.ibatis.session.SqlSession#getMapper,还是个范型方法

      /**
       * Retrieves a mapper.
       * @param <T> the mapper type
       * @param type Mapper interface class
       * @return a mapper bound to this SqlSession
       */
      <T> T getMapper(Class<T> type);
    

    实现该方法的子类有:DefaultSqlSessionSqlSessionManager,这里关注默认实现即可:org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper

      @Override
      public <T> T getMapper(Class<T> type) {
        return configuration.<T>getMapper(type, this);
      }
    

    这里面出现了 Configuration 对象,简单来说就是包含了 xml 配置解析内容的对象,同样它也不是现在关注的重点,继续往下跟进:org.apache.ibatis.session.Configuration#getMapper

      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
      }
    

    这里出现了MapperRegistry对象,它是解析 Mapper.xml 中的内容(mapper标签中的namespace就包含了 Mapper 接口的全限定名称)得来的,含有一个 HashMap 类型的成员变量org.apache.ibatis.binding.MapperRegistry#knownMappers,key 是 Mapper 接口的Class对象,value 是org.apache.ibatis.binding.MapperProxyFactory,从名称就可以看出是用来创建 Mapper 接口的代理对象的工厂,后面会用到。

    具体这个knownMappers是怎么填充的,详见org.apache.ibatis.binding.MapperRegistry#addMapper方法,暂时不管,先往下走:org.apache.ibatis.binding.MapperRegistry#getMapper

      @SuppressWarnings("unchecked")
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
          throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
          return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
          throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
      }
    

    根据 Mapper 接口的类型,从knownMappers中拿到对应的工厂,然后创建代理对象,继续跟进:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)

      public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }
    

    这里又出现了一个MapperProxy对象,理解起来是一个代理对象,打开一看它实现了java.lang.reflect.InvocationHandler接口,这是挂羊头卖狗肉哇。
    先不看狗肉了,继续跟进:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.binding.MapperProxy<T>)

      @SuppressWarnings("unchecked")
      protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
      }
    

    在这,看到了熟悉的java.lang.reflect.Proxy,这里的mapperInterface是创建工厂时传入的 Mapper 接口。真正的 Mapper 接口的代理对象此时才产生,是真羊头。

    这不是既熟悉又陌生的 JDK 动态代理么,说它熟悉,因为前面的狗肉mapperProxy是一个InvocationHandler对象,它拦截了所有对代理对象接口方法的调用。说它陌生是因为之前使用 JDK 动态代理时会有实现了该接口的被代理类和被代理对象,而 MyBatis 中只有接口和代理对象(代理类)。因为 MyBatis 只需要拦截接口方法,找到方法对应的 SQL 去执行就可以了,没必要多此一举加上被代理类,这也正是 MyBatis 方便的地方。接下来看看org.apache.ibatis.binding.MapperProxy

    /**
     * @author Clinton Begin
     * @author Eduardo Macarron
     */
    public class MapperProxy<T> implements InvocationHandler, Serializable {
    
      private static final long serialVersionUID = -6424540398559729838L;
      private final SqlSession sqlSession;
      private final Class<T> mapperInterface;
      private final Map<Method, MapperMethod> methodCache;
    
      public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
      }
    
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          // 判断 method 是不是 Object 类的方法,如 hashCode()、toString()
        if (Object.class.equals(method.getDeclaringClass())) {
          try {// 如果是,则调用当前 MapperProxy 对象的这些方法
          // 跟 Mapper 接口的代理对象没关系
            return method.invoke(this, args);
          } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
          }
        }
        // 到这了,说明调用的是接口中的方法,具体的执行就不是本次关注的重点了
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
      }
    
    // 对 MapperMethod 做了缓存,这个 methodCache 是个 ConcurrentHashMap,在 MapperProxyFactory 中创建的
      private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
          mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
          methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
      }
    
    }
    

    具体说明都在代码注释里面,没啥好说的了。

    总结

    1. 通过 JDK 动态代理模式,创建 Mapper 接口的代理对象,拦截对接口方法的调用;
    2. Mapper 接口中不能使用重载,具体原因参见org.apache.ibatis.binding.MapperMethod.SqlCommand#SqlCommand,MyBatis 是通过mapperInterface.getName() + "." + method.getName()去获取 xml 中解析出来的 SQL 的,具体可能还要看一下org.apache.ibatis.session.Configuration#mappedStatements

    最后的最后,古话说的好:遇事不决,先开大龙(看源码)(逃

  • 相关阅读:
    Weblogic(CVE-2018-2894)
    WebLogic (CVE-2018-2628)
    WebLogic(CVE-2017-10271)
    DC-5靶机渗透
    DC-4靶机渗透
    DC-3靶机渗透
    DC-2靶机渗透
    时间戳倒计时
    服务器的一些Linux命令
    CCF试题清单
  • 原文地址:https://www.cnblogs.com/magexi/p/11756515.html
Copyright © 2011-2022 走看看