MyBatis的运行原理

平时在日常开发的时候,我们通常定义一个Mapper接口实现数据库操作,而不是一个包含逻辑的实现类。一般而言,一个接口是没有办法执行的,那MyBatis是怎么做到的呢?

答案就是动态代理,关于动态代理的内容,博主在之前的博文中已经详细介绍过了,此处不再赘述。

本文主要探析一下MyBatis在执行数据库操作时的运行原理。

假如我们有如下的Mapper接口,主要实现对一个用户信息的增删查改操作,

package com.maowei.learning.orm.dao;

import com.maowei.learning.orm.User;

public interface UserDao {
    User findUserById(String id);

    Integer updateUserById(User user);

    Integer deleteUserById(String id);

    Integer addUser(User user);
}

同时还有相对应的Mapper.xml文件,内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
        "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="com.maowei.learning.orm.dao.UserDao">
    <select id="findUserById" resultType="User" parameterType="string">
        select * from user where id = #{id}
    </select>

    <insert id="addUser" parameterType="User" >
        insert into user(id,name,age) values(#{id},#{name},#{age})
    </insert>

    <delete id="deleteUserById" parameterType="string">
        delete from user where id = #{id}
    </delete>

    <update id="updateUserById" parameterType="User" >
        update user set name = #{name}, age = #{age} where id = #{id}
    </update>

</mapper>

为了能在初始化的时候被加载,还需要如下配置:

    <mappers>
        <mapper resource="spring/mapper/UserMapper.xml"/>
    </mappers>

以上三步算是使用MyBatis操作数据库必须的步骤,那我们如何调用呢?

        //通过配置文件信息,创建SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("spring/SqlMapConfig.xml"));

        //通过SqlSessionFactory对象创建数据库会话
        SqlSession session1 = sqlSessionFactory.openSession(true);

        UserDao dao1 = session1.getMapper(UserDao.class);

        logger.debug("开始查询用户信息");

        logger.info(dao1.findUserById("A001").toString());

重点来看一下第三步中调用的SqlSession#getMaopper方法,一般默认情况下,MyBatis创建的SqlSession实例类对象为DefaultSqlSession类实例对象,我们进入DefaultSqlSession的getMapper方法,

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

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);
    }
  }

可以看到通过传入的Class类型的参数type获取对应的MapperProxyFactory对象,再调用newInstance方法生成具体的实例对象,进入MapperProxyFactory#newInstance方法,

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

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

看到这里,基本可以推断出大概的运行逻辑了。先通过sqlSession, mapperInterface, methodCache三个参数构造MapperProxy对象,再通过反射创建双数Mapper接口的代理对象。

那我们来看一下MapperProxy的内部构造,代码如下,

public class MapperProxy<T> implements InvocationHandler, Serializable {
  ……
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  //构造方法略

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
  ……
}

MapperProxy类实现了InvocationHandler接口,因此重写了invoke方法,以此拦截了外部对原先Mapper接口的调用操作。接下来的重点就是MapperMethod#execute方法,

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);
        }
        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;
  }

经过上述处理,MyBatis把外部对Mapper接口的调用方法,都委托给了SqlSession对象来处理。
MapperMethod内部有个名为SqlCommand的内部类,代码如下,

public static class SqlCommand {

    private final String name;
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      //通过Mapper接口名称和方法名称查找Statement对象
      String statementName = mapperInterface.getName() + "." + method.getName();
      MappedStatement ms = null;
      if (configuration.hasStatement(statementName)) {
        ms = configuration.getMappedStatement(statementName);
      } else if (!mapperInterface.equals(method.getDeclaringClass())) { // issue #35
        String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
        if (configuration.hasStatement(parentStatementName)) {
          ms = configuration.getMappedStatement(parentStatementName);
        }
      }
      ……
    }
    ……
  }

构造方法中通过Mapper接口名称和方法名称查找Statement对象,这就是为什么要求在使用MyBatis的时候需要将sql语句的namespace名称和Mapper接口的包路径保持一致,sql语句的id需要和mapper接口的方法名称保持一致。也正是因为这个原因,MyBatis的Mapper接口方法不支持重载。

猜你喜欢

转载自blog.csdn.net/tjreal/article/details/79941747
今日推荐