zoukankan      html  css  js  c++  java
  • MyBatis 源码解析-Mapper的执行流程

    MyBatis官网:https://mybatis.org/mybatis-3/zh/index.html

    MyBatis的测试代码如下:

            //解析mybatis-config.xml配置文件和Mapper文件,保存到Configuration对象中
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
            //通过SqlSessionFactory创建SqlSession对象
            SqlSession session = sqlSessionFactory.openSession();
            //生成Mapper接口的代理类
            BlogMapper mapper = session.getMapper(BlogMapper.class);
            //调用代理类的方法
            Blog blog = mapper.selectBlogById(2,"Bally Slog");
    

    解析配置文件

    执行第一行代码时,会去加载并解析 mybatis-config.xml 配置文件,配置文件的每一个节点在Configuration类里的都有一个属性与之对应,会把解析的值存放到对应的属性中,然后根据配置文件配置的Mapper,去解析Mapper文件。

    SqlSession

    执行第二行代码时,SqlSessionFactory当然是负责创建SqlSession了,具体就是DefaultSqlSessionFactory负责DefaultSqlSession的创建。最终也就是执行下面这段代码创建了DefaultSqlSession。

      private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          //获取Environment对象,也就是mybatis-config.xml中的<environment>节点
          final Environment environment = configuration.getEnvironment();
          //获取TransactionFactory,创建 Transaction 对象
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
          tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
          //创建执行器
          final Executor executor = configuration.newExecutor(tx, execType);
          //上面这些都是为创建DefaultSqlSession而准备的
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
          closeTransaction(tx); // may have fetched a connection so lets call close()
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    SqlSession是暴露给客户端操作数据库的接口,调用者无需了解内部复杂的执行流程,就可以轻松操作数据库执行CRUD操作。这应该就是门面模式了。

    Mapper接口是如何被调用的?

    执行第三行代码时,就会生成一个Mapper接口的代理类,为什么要生成代理类呢?当然是增强方法了。但由于Mapper接口并没有实现类,所以也谈不上增强,这里主要是为了调用Mapper接口后,接下来的流程能够继续执行。

    动态代理

    MyBatis 中大量使用了动态代理,先来看一个动态代理的例子 - 动态地给房东生成一个中介,代替房东出租房屋。

    // 1.定义接口,JDK动态代理支持接口类型,所以先得准备一个接口
    public interface IHouse {
        //房屋出租
        void chuZuHouse();
    }
    
    // 2.准备一个实现类,也就是房东
    public class HouseOwner implements IHouse{
        @Override
        public void chuZuHouse() {
            System.out.println("我有房子要出租");
        }
    }
    
      // 3.动态生成一个代理类,也就是中介
      public class TestDynamicProxy {
        public static void main(String[] args) {
    //        ClassLoader loader:类加载器,这个类加载器他是用于加载代理对象字节码的,写的是被代理对象的类加载器,也就是和被代理对象使用相同的类加载器,固定写法,代理谁用谁的
    //        Class<?>[] interfaces:它是用于让代理对象和被代理对象有相同的方法。写被代理对象实现的接口的字节码,固定写法代理谁就获取谁实现的接口
    //        InvocationHandler h:用于提供增强的代码。它是让我们写如何代理,写InvocationHandlder接口的实现类
            IHouse houseOwner = new HouseOwner();
            IHouse houseAgent = (IHouse)Proxy.newProxyInstance(HouseOwner.class.getClassLoader(), HouseOwner.class.getInterfaces(), new InvocationHandler() {
                /**
                 * @param proxy 代理对象的引用,在方法中如果想使用代理对象,就可以使用它,一般不用
                 * @param method 表示代理对象执行的被代理对象的方法
                 * @param args 表示执行当前被代理对象的方法所需要的参数
                 * @return 返回值: 和被代理对象有相同的返回值
                 * @throws Throwable
                 */
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //method.invoke(proxy, args);不能这样写,否则会出现自己调自己
                    Object invoke = method.invoke(houseOwner, args);
                    System.out.println("我是房产中介,让我来替你租吧");
                    return invoke;
                }
            });
            //调用代理类的方法,就能执行invoke方法里的代码逻辑了
            houseAgent.chuZuHouse();
        }
    }
    

    上面就是很简单的动态代理,通过中介调用chuZuHouse()方法时,就会执行增强的逻辑,动态代理的好处就是动态生成代理类,假如有大量的类的方法需要被增强,如果用静态代理,有多少个类就得写多少个代理类,而使用动态代理只需实现InvocationHandler接口,不需要自己动手写代理类。

    那如果不需要调用被代理对象的方法,那就可以不要实现类了,这就是MyBatis动态代理Mapper接口的原理。比如:

    public interface IHouse {
        //房屋出租
        void chuZuHouse();
    }
    
    public class Test2DynamicProxy {
        public static void main(String[] args) {
            //由于不需要实现类,所以只需要定义接口就可以了
            IHouse houseAgent = (IHouse)Proxy.newProxyInstance(IHouse.class.getClassLoader(), new Class[]{IHouse.class}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //此处不需要实现类(房东)
                    System.out.println("我是房产中介,让我来替你租吧");
                    return null;
                }
            });
            //调用代理类的方法,就能执行invoke方法里的代码逻辑了
            houseAgent.chuZuHouse();
        }
    }
    

    生成Mapper接口的代理类

    有了动态代理的基础,接下来看看 BlogMapper mapper = session.getMapper(BlogMapper.class);做了什么?

       //Configuration.java
        public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
            //从Configuration中根据类的全类名获取
            return mapperRegistry.getMapper(type, sqlSession);
        }
    
      //MapperRegistry.java
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // 从knownMappers中根据类的全类名获取
        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);
        }
      }
    
      //MapperProxyFactory.java
      public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }
    
     protected T newInstance(MapperProxy<T> mapperProxy) {
        //此处就是Mapper接口被动态代理的关键,返回代理类对象
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
      }
    

    从以上代码可以看到,首先会从Configuration中根据类的全类名获取,然后交给了MapperRegistry,该类中有一个名叫knownMappers的map,保存了需要的Mapper,那什么时候存进去的呢?答案是在解析Mapper文件之后,就会把当前Mapper文件对应的Mapper接口封装到MapperProxyFactory对象,存到名叫knownMappers的HashMap中。然后进入MapperProxyFactory类可以看到,最终是通过Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);生成了Mapper的代理类。

    有了代理类对象,就可以调用Mapper接口中的方法了。

    调用Mapper接口方法

    接下来看看 Blog blog = mapper.selectBlogById(2,"Bally Slog");这句代码做了什么?我们先分析一下:
    1、mapper xml文件中写的参数是#{XXX},执行SQL前,参数肯定得处理;
    2、返回了最终的执行结果,说明对数据库进行了操作,肯定会有JDBC相应的代码,比如PreparedStatement对象;
    3、在查询的时候,MyBatis是有缓存功能的,所以还会对一级二级缓存进行处理;
    4、MyBatis直接返回了我们需要的对象,所以还会对数据库的返回结果进行解析;

      //MapperProxy.java
      //调用代理类的方法,也就会执行InvocationHandler接口的实现类(MapperProxy)的invoke方法
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          //如果method.getDeclaringClass()是Object,则调用method.invoke(this, args);不知道干嘛的,不过对正常流程无影响
          //method.getDeclaringClass()正常情况下是Mapper接口的Class,这里是yyb.useful.start01.BlogMapper
          if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
          } else if (isDefaultMethod(method)) {
           
            return invokeDefaultMethod(proxy, method, args);
          }
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
      }
    
      private MapperMethod cachedMapperMethod(Method method) {
        //如果methodCache里面存在method对应的MapperMethod,则取出,否则会创建并存放到methodCache中,并返回
        return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    
    

    通过以上代码,又把接下来的流程交给了MapperMethod来处理了,接下来看看MapperMethod做了什么?

      //MapperMethod.java
    
      /**
       * 解析参数和返回值(此处的返回值已经是java类型了),具体执行交给sqlSession来做
       */
      public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {
          case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
          }
          case UPDATE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
            break;
          }
          case DELETE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
            break;
          }
          case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
              executeWithResultHandler(sqlSession, args);
              result = null;
            } else if (method.returnsMany()) {
              result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
              result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
              result = executeForCursor(sqlSession, args);
            } else {
              Object param = method.convertArgsToSqlCommandParam(args);
              result = sqlSession.selectOne(command.getName(), param);
              if (method.returnsOptional()
                  && (result == null || !method.getReturnType().equals(result.getClass()))) {
                result = Optional.ofNullable(result);
              }
            }
            break;
          case FLUSH:
            result = sqlSession.flushStatements();
            break;
          default:
            throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
          throw new BindingException("Mapper method '" + command.getName()
              + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
      }
    

    可以看到,MapperMethod的功能有三个:
    1、获取SQL的类型,用来判断接下来该调用CRUD中的哪一个,类型判断主要是借助于MappedStatement,MappedStatement里面封装了<select|update|insert|delete>标签对应的信息,其中有一个SqlCommandType枚举类型,就是用来保存SQL的类型的。
    2、解析参数,该功能由ParamNameResolver类来完成,逻辑如下:

    public class ParamNameResolver {
    
      private static final String GENERIC_NAME_PREFIX = "param";
    
      /**
       * key是参数的索引,值是参数的名称
       *
       * 如果用@Param指定了名称,名称就从@Param中获取
       * 如果未指定,使用参数的索引。注意,当方法具有特殊参数(RowBounds,ResultHandler)时,此索引可能与实际索引不同
       * 比如:
       * aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}
       * aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}
       * aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}
       */
      private final SortedMap<Integer, String> names;
    
      private boolean hasParamAnnotation;
    
      public ParamNameResolver(Configuration config, Method method) {
        final Class<?>[] paramTypes = method.getParameterTypes();
        final Annotation[][] paramAnnotations = method.getParameterAnnotations();
        final SortedMap<Integer, String> map = new TreeMap<>();
        int paramCount = paramAnnotations.length;
        // get names from @Param annotations
        for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
          //如果参数是RowBounds,ResultHandler时,跳过
          if (isSpecialParameter(paramTypes[paramIndex])) {
            // skip special parameters
            continue;
          }
          String name = null;
          for (Annotation annotation : paramAnnotations[paramIndex]) {
            if (annotation instanceof Param) {
              hasParamAnnotation = true;
              //如果用@Param指定了名称,名称就从@Param中获取
              name = ((Param) annotation).value();
              break;
            }
          }
          if (name == null) {
            // @Param was not specified.
            if (config.isUseActualParamName()) {
              //使用方法签名中的名称作为语句参数名称。 要使用该特性,项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1)
              //编译不加上 -parameters,则获取的值是arg0
              name = getActualParamName(method, paramIndex);
            }
            //如果未指定@Param,使用参数的索引
            if (name == null) {
              // use the parameter index as the name ("0", "1", ...)
              // gcode issue #71
              name = String.valueOf(map.size());
            }
          }
          map.put(paramIndex, name);
        }
        names = Collections.unmodifiableSortedMap(map);
      }
    
      /**
       * 单个参数且没加@Param注解,直接返回值
       * 多个参数除了使用命名规则(可能是argX,真实的name,或者索引)来命名外,此方法还添加了通用名称(param1, param2,...)
       */
      public Object getNamedParams(Object[] args) {
        final int paramCount = names.size();
        //没有参数,返回null
        if (args == null || paramCount == 0) {
          return null;
        } else if (!hasParamAnnotation && paramCount == 1) {
          //只有一个参数,且没加@Param注解,直接返回值
          return args[names.firstKey()];
        } else {
          //只要参数个数大于1,或者参数只有1个但加了@Param,返回hashMap
          final Map<String, Object> param = new ParamMap<>();
          int i = 0;
          for (Map.Entry<Integer, String> entry : names.entrySet()) {
            //把names中的数据添加到map中(可能是argX,真实的name,或者索引)
            param.put(entry.getValue(), args[entry.getKey()]);
            // add generic param names (param1, param2, ...)
            final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
            // ensure not to overwrite parameter named with @Param
            if (!names.containsValue(genericParamName)) {
              //添加(param1, param2, ...)到map中
              param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
          }
          return param;
        }
      }
    }
    
    

    3、解析返回值类型
    解析返回值(这里只是简单的处理,比如把int转换Integer,Long,Boolean等,实际的ResultSet解析过程有相应的类进行处理),具体执行又交给SqlSession来做,所以本质上,以下两种调用方式其实没什么区别,只不过mapper的方式多了一层转换,但对开发中更友好,避免写错类名的情况。

      Blog blog = mapper.selectBlogById(2);
      Blog blog1 = session.selectOne("yyb.useful.start01.BlogMapper.selectBlogById", 2);
    
  • 相关阅读:
    修复PLSQL Developer 与 Office 2010的集成导出Excel 功能
    Using svn in CLI with Batch
    mysql 备份数据库 mysqldump
    Red Hat 5.8 CentOS 6.5 共用 输入法
    HP 4411s Install Red Hat Enterprise Linux 5.8) Wireless Driver
    变更RHEL(Red Hat Enterprise Linux 5.8)更新源使之自动更新
    RedHat 5.6 问题简记
    Weblogic 9.2和10.3 改密码 一站完成
    ExtJS Tab里放Grid高度自适应问题,官方Perfect方案。
    文件和目录之utime函数
  • 原文地址:https://www.cnblogs.com/ginb/p/14575444.html
Copyright © 2011-2022 走看看