MyBatis-Spring:Mapper 接口是怎么定位到 Mapper.xml 的

本贴最后更新于 2037 天前,其中的信息可能已经天翻地覆

mybatis0001.jpg
图片来源 https://terasolunaorg.github.io,非常推荐此文

  • org.mybatis-3.2.8
  • mybatis-spring-1.2.3

mybatis-spring-1.2.xsd

mybatis0002.jpg

通过上面图片中描述可知,Spring 会扫描 basePackage 下的所有 Mapper 接口并注册为 MapperFactoryBean

MyBatis-Spring 官网描述
mybatis0006.jpg

MapperFactoryBean

mybatis0003.jpg

  • 上图中可看出,MapperFactoryBean 实现了 Spring 的 FactoryBean 接口,可见 MapperFactoryBean 是通过 FactoryBean 接口中定义的 getObject 方法来获取对应的 Mapper 对象(JDK 代理对象)

mybatis0004.jpg

  • MapperFactoryBean 还继承了 SqlSessionDaoSupport,需要注入生产 SqlSessionsqlSessionFactory 对象
    • MapperScannerConfigurer 配置中的 sqlSessionFactoryBeanName 属性 ,在做多数据源的时候,需要指定不同的 SqlSessionFactoryBean,这样 Spring 在注册 MapperFactoryBean 的时候会使用不同的 SqlSessionFactory 来生成 SqlSession
    • Spring 整合 MyBatis 时,使用的 SqlSession 类型是 SqlSessionTemplate
      mybatis0007.jpg

@Autowired Mapper

# MapperFactoryBean中getObject方法
public T getObject() throws Exception {
   return this.getSqlSession().getMapper(this.mapperInterface);
}

MapperFactoryBean 会从它的 getObject 方法中获取对应的 Mapper 接口,而 getObject 内部还是通过我们注入的属性调用 SqlSession 接口的 getMapper(Mapper接口) 方法来返回对应的 Mapper 接口的代理对象。这样就通过把 SqlSessionFactory 和相应的 Mapper 接口交给 Spring 管理实现了 MybatisSpring 的整合。

追踪一下 getMapper 方法

  1. SqlSessionTemplate

    ......省略
    public <T> T getMapper(Class<T> type) {
        return this.getConfiguration().getMapper(type, this);
    }
    
  2. Configuration

    ......省略
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return this.mapperRegistry.getMapper(type, sqlSession);
    }
    
  3. MapperRegistry

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

可以看出是从 knownMappers 这个 Map 中取出了 MapperProxyFactory 对象,然后去构建动态代理类,下面先通过 knownMappers 来分析下 MyBatis 的初始化流程。

SqlSessionFactoryBean

SqlSessionFactoryBean 实现了 InitializingBean 接口,在初始化时会执行 afterPropertiesSet 方法。此方法中会 buildSqlSessionFactory

public void afterPropertiesSet() throws Exception {
   Assert.notNull(this.dataSource, "Property 'dataSource' is required");
   Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
   this.sqlSessionFactory = this.buildSqlSessionFactory();
}

摘取几段 buildSqlSessionFactory 中的逻辑

......
`xmlConfigBuilder.parse();`
......
`xmlMapperBuilder.parse();`
......
`return this.sqlSessionFactoryBuilder.build(configuration);`

XMLConfigBuilder

private void parseConfiguration(XNode root) {
   try {
	[properties(属性)](http://www.mybatis.org/mybatis-3/zh/configuration.html#properties)
	this.propertiesElement(root.evalNode("properties"));
	[typeAliases(类型别名)](http://www.mybatis.org/mybatis-3/zh/configuration.html#typeAliases)
	this.typeAliasesElement(root.evalNode("typeAliases"));
	[plugins(插件)](http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins)
	this.pluginElement(root.evalNode("plugins"));
	[objectFactory(对象工厂)](http://www.mybatis.org/mybatis-3/zh/configuration.html#objectFactory)
	this.objectFactoryElement(root.evalNode("objectFactory"));
	this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
	[settings(设置)](http://www.mybatis.org/mybatis-3/zh/configuration.html#settings)
	this.settingsElement(root.evalNode("settings"));
	[environments(环境配置)](http://www.mybatis.org/mybatis-3/zh/configuration.html#environments)
	this.environmentsElement(root.evalNode("environments"));
	[databaseIdProvider(数据库厂商标识)](http://www.mybatis.org/mybatis-3/zh/configuration.html#databaseIdProvider)
	this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
	[typeHandlers(类型处理器)](http://www.mybatis.org/mybatis-3/zh/configuration.html#typeHandlers)
	this.typeHandlerElement(root.evalNode("typeHandlers"));
	[mappers(映射器)](http://www.mybatis.org/mybatis-3/zh/configuration.html#mappers)
	this.mapperElement(root.evalNode("mappers"));
   } catch (Exception var3) {
	throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
   }
}

Mapper 加载的四种方式

  • 参看 MyBatis 官网 http://www.mybatis.org/mybatis-3/zh/configuration.html#mappers
    mybatis0005.jpg

  • 下面分析 this.mapperElement(root.evalNode("mappers")) 方法,对应上图中的几种加载方式

  • 说明:与 Spring 结合,用的是包自动扫描的方式,MyBatis 的全局配置文件中 mappers 节点是空的,也就是 this.mapperElement(root.evalNode("mappers")) 其实是跳过的,因为 root.evalNode("mappers") == NULL,这个时候用的是 buildSqlSessionFactory 方法中 xmlMapperBuilder.parse() 来加载

    resource = child.getStringAttribute("resource");
    String url = child.getStringAttribute("url");
    String mapperClass = child.getStringAttribute("class");
    XMLMapperBuilder mapperParser;
    InputStream inputStream;
    
    if (resource != null && url == null && mapperClass == null) {
       `<!-- 使用相对于类路径的资源引用 -->`
       ErrorContext.instance().resource(resource);
       inputStream = Resources.getResourceAsStream(resource);
       mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
       mapperParser.parse();
    } else if (resource == null && url != null && mapperClass == null) {
       `<!-- 使用完全限定资源定位符(URL) -->`
       ErrorContext.instance().resource(url);
       inputStream = Resources.getUrlAsStream(url);
       mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
       mapperParser.parse();
    } else {
       if (resource != null || url != null || mapperClass == null) {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
       }
       `<!-- 使用映射器接口实现类的完全限定类名 -->`
       Class<?> mapperInterface = Resources.classForName(mapperClass);
       this.configuration.addMapper(mapperInterface);
    }
    

XMLMapperBuilder

前两种 Mapper 的加载方式,都会调用 mapperParser.parse(); 方法,第三种方式是直接加入 Configuration 的配置中 configuration.addMapper(mapperInterface);

public void parse() {
   if (!this.configuration.isResourceLoaded(this.resource)) {
	this.configurationElement(this.parser.evalNode("/mapper"));
	# Set<String> loadedResources 集合,标识已经加载
	this.configuration.addLoadedResource(this.resource);
        # 绑定映射器到 namespace
	this.bindMapperForNamespace();
   }
   this.parsePendingResultMaps();
   this.parsePendingChacheRefs();
   this.parsePendingStatements();
}

private void bindMapperForNamespace() {
   String namespace = this.builderAssistant.getCurrentNamespace();
	`可以看出最终还是以第三种方式加载 <!-- 使用映射器接口实现类的完全限定类名 -->`
        try {
	   boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException var4) {
        }
	if (boundType != null && !this.configuration.hasMapper(boundType)) {
		this.configuration.addLoadedResource("namespace:" + namespace);
		this.configuration.addMapper(boundType);
	}
   }
}

Configuration.addMapper

Configuration.addMapper 调用的是 mapperRegistry.addMapper

public <T> void addMapper(Class<T> type) {
   this.mapperRegistry.addMapper(type);
}

MapperRegistry.addMapper

注册接口,到这里正好和上面 MapperFactoryBeangetObject 方法 对应起来了

public T getObject() throws Exception { 
   return  this.getSqlSession().getMapper(this.mapperInterface); 
}

看下在 MapperRegistry addMapper() 方法,里面维护了 knownMappers,key 为 Class<T> type,值为 MapperProxyFactory (创建 Mapper 代理对象的工厂),并且用 MapperAnnotationBuilder 来解析构建 MappedStatement

public <T> void addMapper(Class<T> type) {
   if (type.isInterface()) {
	if (this.hasMapper(type)) {
		throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
	}
	boolean loadCompleted = false;
	try {
		this.knownMappers.put(type, new MapperProxyFactory(type));
		MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
		parser.parse();
		loadCompleted = true;
	} finally {
		if (!loadCompleted) {
			this.knownMappers.remove(type);
		}
	}
   }
}

MapperAnnotationBuilder

看下 addMapper 中的 parser.parse();

MapperBuilderAssistant

构建 MappedStatement 对象并加入全局配置 configuration.addMappedStatement(statement); 相对应的 Configuration 中提供了 getMappedStatement(String id) 方法

public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
   if (this.unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
   } else {
      id = this.applyCurrentNamespace(id, false);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      org.apache.ibatis.mapping.MappedStatement.Builder statementBuilder = new 
      org.apache.ibatis.mapping.MappedStatement.Builder(this.configuration, id, sqlSource, sqlCommandType);
      statementBuilder.resource(this.resource);
      statementBuilder.fetchSize(fetchSize);
      statementBuilder.statementType(statementType);
      statementBuilder.keyGenerator(keyGenerator);
      statementBuilder.keyProperty(keyProperty);
      statementBuilder.keyColumn(keyColumn);
      statementBuilder.databaseId(databaseId);
      statementBuilder.lang(lang);
      statementBuilder.resultOrdered(resultOrdered);
      statementBuilder.resulSets(resultSets);
      this.setStatementTimeout(timeout, statementBuilder);
      this.setStatementParameterMap(parameterMap, parameterType, statementBuilder);
      this.setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
      this.setStatementCache(isSelect, flushCache, useCache, this.currentCache, statementBuilder);
      MappedStatement statement = statementBuilder.build();
      this.configuration.addMappedStatement(statement);
      return statement;
   }
}

MapperProxyFactory

@Autowired 注入的 Mapper 最终会调用 MapperRegistrygetMapper(Class<T> type, SqlSession sqlSession) 方法, getMapper 中会通过 knownMappers.get(type) 获取到的 MapperProxyFactory 来构建 JDK 代理对象。

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

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

MapperProxy

看下代理类的 invoke 方法,是交给了 MapperMethod 来执行。

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

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

🐾 参考

  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    170 引用 • 414 回帖 • 387 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • someone

    感谢兄弟 帮了大忙了,写的很好👍

    该回帖因偏离主题而被折叠
    1 操作
    14032 在 2019-05-21 16:56:30 折叠了该回帖
  • Eddie

    你的标题说 mybatis,内容怎么聊到 spring 去了。。。。

  • someone
    作者

    改成 Mybatis-Spring 了。V

  • someone
    作者

    粉的味道太重😰

    该回帖因偏离主题而被折叠
    1 操作
    14032 在 2019-05-21 16:55:38 折叠了该回帖