相关文章推荐
16

MyBatis 是 Java 开发中非常流行的 ORM 框架,其封装了 JDBC 并且解决了 Java 对象与输入参数和结果集的映射,同时又能够让用户方便地手写 SQL 语句。MyBatis 的行为类似于以下几行代码:

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, usr, password);
PraparedStatement st = conn.prepareStatement(sql);
st.setInt(0, 1);
st.execute();
ResultSet rs = st.getResultSet();
while (rs.next()) {
    String result = rs.getString(colname);

上面是 JDBC 的使用流程,MyBatis 其实就是对上面的代码进行分解包装。本文将对 MyBatis 的代码进行分析,探究其中的逻辑。

首先从 MyBatis 的基本用法开始,下面是 MyBatis 官网的入门示例:

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

其中 mabatis-config.xml 是 MyBatis 的核心配置文件,其中包括数据源、事务管理器、别名以及 SQL 对应的 Mapper 文件等,如下所示:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

有了 SqlSessionFactory 后就可以创建 SqlSession 来调用 select 以及 update 等方法请求数据了:

try {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
} finally {
  session.close();

配置文件解析

我们按照上面的代码流程开始分析源码,首先是配置文件的解析:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream) SqlSessionFactoryBuilder 显然是为了构建 SqlSessionFactory,而且是从配置文件的输入流构建,代码如下:

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 创建 XMLConfigBuilder
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // parse.parse() 进行解析
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.

首先是创建了一个 XMLConfigBuilder 对象,它是用来解析 Config 文件的。XMLConfigBuilder 继承自 BaseBuilderBaseBuilder 中有个 Configuration 类型的变量,这个类需要重点关注,Config 文件中解析出来的所有信息都保存在这个变量中。

创建了 XMLConfigBuilder 后调用了其 parse 方法:

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    parsed = true;
    // 在这个函数中解析
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;

这里主要逻辑在 parseConfiguration 中:

private void parseConfiguration(XNode root) {
    try {
      // 解析 properties
      propertiesElement(root.evalNode("properties")); //issue #117 read properties first
      // 解析 type alias
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));      
      // 解析 setting
      settingsElement(root.evalNode("settings"));
      // 解析 environment 
      environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析 mapper
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);

这里解析了 config 文件中所有的标签,包括 propertiessettings 以及 mappers 等,下面挑几个看一下。

settings

settings 是对 MyBatis 的一些配置项,包括缓存的开启以及是否使用驼峰转换(mapUnderscoreToCamelCase)等,代码如下:

private void settingsElement(XNode context) throws Exception {
   if (context != null) {
   // 将配置项保存到 Properties 中
     Properties props = context.getChildrenAsProperties();
     // Check that all settings are known to the configuration class
     MetaClass metaConfig = MetaClass.forClass(Configuration.class);
     for (Object key : props.keySet()) {
       if (!metaConfig.hasSetter(String.valueOf(key))) {
         throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
     configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
     // 默认开启缓存
     configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
     configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));      
     configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
     configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
     configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
     configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
     configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
     configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
     configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
     configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
     configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
     configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
     configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
     configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
     configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
     configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
     configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
     configuration.setLogPrefix(props.getProperty("logPrefix"));
     configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));

可以看出,settings 的子节点保存在 Properties 中,然后校验是否有不合法的子节点,最后提取出其中的属性保存到 Configuration 中,上面提到这个类专门用于保存 Config 文件解析出的信息。

从上面也可以看到 MyBatis 的一些默认属性,例如一级缓存如果没有配置,那么默认是开启的。

environments

environments 包含了数据源(dataSource) 和事务管理器(transactionManager) 的配置,代码如下:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          // 解析 transactionManager
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          // 解析 dataSource
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          // 设置 environment 到 configuration
          configuration.setEnvironment(environmentBuilder.build());
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      // 通过反射实例化
      TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    throw new BuilderException("Environment declaration requires a TransactionFactory.");
  private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      // 通过反射实例化
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");

其中主要是两部分,第一部分解析 transactionManager,第二部分解析 dataSource。从 transactionManagerElementdataSourceElement 中可以看出通过对应 Class 文件的 newInstance 实例化出对应的工厂对象。最终解析出的 transactionManagerdataSource 依然是设置到 Configuration 中。

mappers

mappers 对应了具体的 SQL Mapper 文件,也是我们要分析的重点。

mappers 标签可以多种子标签,上面的示例中是 mapper 配合 resource

 <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>

我们下面看一下此种形式在源码中的解析:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 这个分支解析 resource 形式的标签
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 创建 XMLMapperBuilder 
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 进行解析
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");

resource 标签解析的对应分支是 (resource != null && url == null && mapperClass == null),其中创建了一个 XMLMapperBuilder 对象然后调用 parse 方法进行解析:

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 解析 mapper 下面的标签,包括 namespace、cache、parameterMap、resultMap 等
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
 private void configurationElement(XNode context) {
    try {
      // namespace 对应 Mapper 对应接口的全名(包名 + 类名)
      String namespace = context.getStringAttribute("namespace");
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      // 解析生成 ParameterMap
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析生成 ResultMap
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      // 每一个 sql 语句生成一个 MappedStatement
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);

configurationElement 用于解析具体的子标签,如 namespacecacheparameterMapresultMap 以及 select|insert|update|delete 等。

namespace 对应了 Mapper 接口类的包名 + 类名,通过 namespace 可以唯一定位一个 Class 文件,解析的 namespace 保存在 builderAssistant 中,后面会用到。

parameterMapresultMap 解析会生成 ParameterMapResultMap 对象。每个 SQL 语句解析会生成 MappedStatement

在上面的 parse 方法中,解析完标签后调用了 bindMapperForNamespace,这个实现了加载 namespace 对应的 Class,并且为每个 Class 创建了代理类工厂对象(MapperProxyFactory)。

MapperProxyFactory

MapperProxyFactory 用于为 Mapper 接口类创建代理对象,代理对象指的是
BlogMapper mapper = session.getMapper(BlogMapper.class) 生成的对象。

下面从 bindMapperForNamespace 开始:

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        // 加载类
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          // 添加 mapper 和 MapperProxyFactory
          configuration.addMapper(boundType);

其中先从 builderAssistant 取出 namespace,然后加载对应的 Class(boundType = Resources.classForName(namespace))。最后调用 configuration.addMapper(boundType) 添加到 configuration 中。 configuration.addMapper(boundType) 很关键,看代码:

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      boolean loadCompleted = false;
      try {
        // 添加到  Map<Class<?>, MapperProxyFactory<?>> 中
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);

关键的一行是 knownMappers.put(type, new MapperProxyFactory<T>(type)),其中 knownMappers 的类型是 Map<Class<?>, MapperProxyFactory<?>>,即 key 是 Class,value 是 MapperProxyFactory。这里的 MapperProxyFactory 即是动态代理对象的工厂,下面是其 newInstance 方法的代码:

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

从中可以看出,这里用的是 Java 的动态代理,Proxy.newProxyInstance 方法生成指定接口的代理对象,这个方法的第三个参数是用于方法拦截的对象,这里是 MapperProxy 的实例。

由此可以知道,具体的执行 SQL 语句的操作是由这个类拦截并且执行的,看看这个类的 invoke 方法:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);

如果是 Object 类中声明的方法,则直接执行,否则调用 MapperMethodexecute,其中便是 JDBC 相关的逻辑了。限于篇幅,具体内容留到下一篇文章再看。

在分析完配置文件的解析后,再回到 XMLConfigBuilder 中:

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 构建 SqlSessionFactory
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.

parser.parse() 执行完后,生成一个 Configuration 对象,最后调用 build 构建 SqlSessionFactory,代码如下:

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
 
推荐文章