Mybatis 源码阅读之四 一次完整的请求过程

268 阅读7分钟

注册/加载过程

  1. 首先读取配置文件
        String resource    ="mybatis-config.xml";
        InputStream inputStream =null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        SqlSessionFactory sqlSessionFactory =null;
        sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
  1. 解析xml 在org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties)方法注册xml并解析。把配置文件解析成几个重要的对象:
Configuration(Mybatis配置)
Environment(可以设置为多个环境,用于隔离)
DataSource(数据源)
JdbcTransactionFactory(事务工厂)
MapperRegistry(就是存储Mapper接口的类)

执行流程:

  • sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);这一行开始解析xml。进入build方法,返回对象是DefaultSqlSessionFactory。这个工厂很简单,返回一个sqlSession对象,生成JdbcTransactionFactory,创建数据源PooledDataSourceFactory,此过程涉及到触发了
static {
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }

静态代码,从而mysql的驱动被加载。

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    }
    ...
    }
  • 看parser.parse()方法,返回对象是Configuration,调用parseConfiguration方法。
private void parseConfiguration(XNode root) {
    try {
      //1.properties
      propertiesElement(root.evalNode("properties"));
      //2.类型别名
      typeAliasesElement(root.evalNode("typeAliases"));
      //3.插件
      pluginElement(root.evalNode("plugins"));
      //4.对象工厂
      objectFactoryElement(root.evalNode("objectFactory"));
      //5.对象包装工厂
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //6.设置
      settingsElement(root.evalNode("settings"));
      // read it after objectFactory and objectWrapperFactory issue #631
      //7.环境
      environmentsElement(root.evalNode("environments"));
      //8.databaseIdProvider
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //9.类型处理器
      typeHandlerElement(root.evalNode("typeHandlers"));
      //10.映射器 todo 关闭是不是为null
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

重点看第7,10.第7的environmentsElement()里面实现比较简单,就是new 事务管理器TransactionFactory和数据源DataSourceFactory。 接下来是mapperElement()方法,看一下都干了什么。

 		private void mapperElement(XNode parent) throws Exception {
   		 if (parent != null) {
      		for (XNode child : parent.getChildren()) {
        		if ("package".equals(child.getName())) {
          		//10.4自动扫描包下所有映射器
          		String mapperPackage = child.getStringAttribute("name");
          		// 这里是生成mapper的实现类的包
          		configuration.addMappers(mapperPackage);
                ...

进入configuration.addMappers(mapperPackage)方法,最终是调用MapperRegistry.addMappers()方法

public void addMappers(String packageName, Class<?> superType) {
    //查找包下所有是superType的类
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }
public <T> void addMapper(Class<T> type) {
    //mapper必须是接口!才会添加
    if (type.isInterface()) {
      if (hasMapper(type)) {
        //如果重复添加了,报错
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        //如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

主要做了把Mapper.class加到knownMappers中,并value是MapperProxyFactory代理工厂,其实说白了就是JDK代理,客户端使用代理类的时候,调用Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);生成一个代理类,内部类Proxy#0。某个Mapper的实现类为Proxy#0。接下来把Mapper.xml解析了。

解析xml过程

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {// 判断resource是否被解析过了
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }
  1. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); 新建并把config,type关联
  • 注册一些sql的注解,比如Select,Insert,放在mapper接口的方法上的
  1. parser.parse();
  • 查找xml文件并加载
  • 解析xml成MappedStatement,过程比较复杂,简而言之,把接口的方法和sql语句对应起来,并存储在org.apache.ibatis.session.Configuration#mappedStatements中。我们看一下MappedStatement的结构:
  • MappedStatement的结构
  • 主要有id,sqlSource,parameterMap,resultMaps,sqlCommandType

小结

注册/解析过程,完成了数据库字段和JavaBean的关系映射,简称ORM。把数据库驱动和数据源配置好,MapperRegistry(已注册的接口)和MappedStatement(sql语句)配置在Configuration中。以便执行的时候使用。

sql执行过程

        1.	sqlSession=sqlSessionFactory.openSession();
        2.    RoleMapper roleMapper=sqlSession.getMapper(RoleMapper.class);
        3.    Role role=roleMapper.getRole(1L);

第一步:

创建事务,执行器,sqlSession。把事务放在执行器里面,sqlSession持有执行器的引用 第二步: 从sqlSession得到Configuration,getMapper()是在Configuration得到MapperProxyFactory,MapperProxyFactory.newInstance一个MapperProxy,这个MapperProxy持有sqlSession的引用。是一对一的关系。 第三步: 调用MapperProxy代理
查找MapperMethod,具体的方法执行。MapperMethod和MapperProxy的关系也是一对一。不同的是MapperMethod是共享的,而且是单例的(一个sql语句对应一个MapperMethod而言,而不是说整个Jvm只有一个MapperMethod)。 进入关键部分:
convertArgsToSqlCommandParam方法:
sqlSession.selectOne()方法:
sqlSession.selectList()方法:
先得到sql语句,即MappedStatement,再利用sqlSession得到执行器,执行器是初始化的时候新建的。 再进入方法:
我们都知道Mybatis 有两个缓存,一个sqlSession级别的缓存,另一个是Mapper级别的缓存。创建CacheKey,待从数据库返回数据之后,设置value。二级缓存开启方法:(默认情况下是没有开启缓存的(二级缓存).要开启二级缓存,你需要在你的 SQL 映射文件中添加一行: <cache/>)关系差不多是这样子,图画地有点丑...
接下来看一下查询数据库的逻辑:
接下来:进入具体执行器实现类SimpleExecutor:

  1. stmt = prepareStatement(handler, ms.getStatementLog());执行进入prepareStatement()方法
  2. 得到一个Connection,执行器有Transaction,Transaction可以得到数据库的Connection。
  3. 由于我们设置了连接池,所以在池中获取一个,如果没有则新建一个,使用完毕返回池中。
  4. 得到连接之后,会prepare,跟jdbc那个一样,先prepare。
  5. 然后填充参数。
  6. 开始真正执行进入handler.query(stmt, resultHandler),下图
    excute方法,由于PreparedStatement对象被代理了,所以走到PreparedStatementLogger类
    由于使用了反射,Idea无法进入具体相关实现类,statement对象是com.mysql.cj.jdbc.ClientPreparedStatement,method是java.sql.PreparedStatement#execute也就是statement对象的execute方法。这里得到数据库的数据。 7.获取到数据之后,包装成返回类型的对象。
 @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
    
    final List<Object> multipleResults = new ArrayList<Object>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    //一般resultMaps里只有一个元素
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResulSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

整个过程已完成。

  1. 回到开始的地方
 try {
            sqlSession=sqlSessionFactory.openSession();
            RoleMapper roleMapper=sqlSession.getMapper(RoleMapper.class);
            Role role=roleMapper.getRole(1L);
            log.debug("sql 已经被执行");
            System.out.println(role.getId()+":"+role.getRoleName()+":"+role.getNote());
            sqlSession.commit();
        } catch (Exception e) {
            sqlSession.rollback();
            e.printStackTrace();
        }finally {
            sqlSession.close();
        }

提交事务,由于这次是查询,最后是没提交的。如果是update的话,那肯定会提交。若有异常则回滚。关于事务的提交方式,可以参考下面链接。

MYSQL 事务处理主要有两种方法:
1、用 BEGIN, ROLLBACK, COMMIT来实现
BEGIN 开始一个事务
ROLLBACK 事务回滚
COMMIT 事务确认
2、直接用 SET 来改变 MySQL 的自动提交模式:
SET AUTOCOMMIT=0 禁止自动提交
SET AUTOCOMMIT=1 开启自动提交
对于一个MYSQL数据库(InnoDB),事务的开启与提交模式无非下面这两种情况:
1>若参数autocommit=0,事务则在用户本次对数据进行操作时自动开启,在用户执行commit命令时提交,用户本次对数据库开始进行操作到用户执行commit命令之间的一系列操作为一个完整的事务周期。若不执行commit命令,系统则默认事务回滚。总而言之,当前情况下事务的状态是自动开启手动提交。
2>若参数autocommit=1(系统默认值),事务的开启与提交又分为两种状态:
①手动开启手动提交:当用户执行start transaction命令时(事务初始化),一个事务开启,当用户执行commit命令时当前事务提交。从用户执行start transaction命令到用户执行commit命令之间的一系列操作为一个完整的事务周期。若不执行commit命令,系统则默认事务回滚。
②自动开启自动提交:如果用户在当前情况下(参数autocommit=1)未执行start transaction命令而对数据库进行了操作,系统则默认用户对数据库的每一个操作为一个孤立的事务,也就是说用户每进行一次操作系都会即时提交或者即时回滚。这种情况下用户的每一个操作都是一个完整的事务周期。

参考来源:

事务提交的方式

总结

这次学习mybatis源码,总的来说比较简单,我们可以对比出来,使用JDBC 和 Mybatis的区别。也学了很多,特别是反射,工厂,代理,装饰者。这些都是设计模式啊,日常都会涉及到,但是叫你使用起来也是有难度的。个人看法,对于Mybatis这种框架,只需要把核心流程理解了就可以了,如果要你开发Mybatis框架的话,那再进行深入探究。


接下来的话,有空看看mybatis-spring的集成。