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);
    
  • 相关阅读:
    20155213 2016-2017-2 《Java程序设计》第五周学习总结
    20155213 2016-2017-2《Java程序设计》第四周学习总结
    20155213 2016-2017-2《Java程序设计》第三周学习总结
    20155213 2016-2017-2 《Java程序设计》第二周学习总结
    20145109《Java程序设计》第一周学习总结
    《暗时间》读书笔记(三)
    调查问卷
    《Python学习手册》(四)
    《Python学习手册》(三)
    20165322 第五周学习总结
  • 原文地址:https://www.cnblogs.com/ginb/p/14575444.html
Copyright © 2011-2022 走看看