Mybatis源码之Configuration

1,230 阅读26分钟

公众号搜索“码路印记”,点关注不迷路!

Configuration存储着mybatis运行时所需要的全部配置信息,那么它是如何从mybatis-config.xml转换过来的呢?实际运行中它又起到什么作用呢?今天我通过一个小例子,结合源码一步一步探索一下Configuration的解析流程,以便更加深入的了解其运行机制。

从Demo开始

下面是一个小小的示例。指定了配置文件mybatis-config.xml,通过SqlSessionFactoryBuilder创建SqlSessionFactory,打开SqlSession获取Mapper,执行Mapper方法。

  public static void main(String[] args) throws IOException {
      // mybatis配置文件
      String path = "mybatis-config.xml";
      InputStream inputStream = Resources.getResourceAsStream(path);
      // 获取 SqlSessionFactory
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
      // 创建 SqlSession
      try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
          CompanyDao dao = sqlSession.getMapper(CompanyDao.class);
          CompanyDO companyDO = dao.selectById(1, "test");
          System.out.println(companyDO);
      }
  }

从main方法中我们并没有看到Configuration的影子,是因为在实际运行中,操作数据库是使用SqlSession,而SqlSession是由SqlSessionFactory按需创建的,Configuration对象保存在SqlSessionFactory中,当创建SqlSession时会把Configuration传递到SqlSession。

配置解析流程

示例代码3-6行就完成了Configuration的解析工作,这个过程发生在方法SqlSessionFactoryBuilder#build()中,进入方法内部会发现实际执行xml解析的是类XMLConfigBuilder。

  public SqlSessionFactory build(InputStream inputStream) {
    // 调用重载方法
    return build(inputStream, null, null);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 创建XMLConfigBuilder对象,这个是Configuration解析类
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 首先把xml解析为Configuration对象,然后创建SqlSessionFactory对象。
      // 我们下来主要关心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.
      }
    }
  }

在mybatis中XMLConfigBuilder负责解析mybatis-config.xml,实现配置信息从文件到内存对象的转换与映射。首先看下构造方法。

  // 接收配置文件流对象、environment以及通过代码传入的属性配置。
  public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
  }

  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }

XMLConfigBuilder构造方法创建了xml的解析器parser,parser负责处理复杂的xml读取工作。另外,构造方法还接收了props对象,它会覆盖从配置文件中解析的同名属性信息。

parse()方法及parseConfiguration()是解析流程的主干流程,其中,parseConfiguration中逐个完成了xml中各部分的解析,每种配置封装为了独立的方法,理解起来比较容易。每个XMLConfigBuilder只能解析一次,重复解析会导致异常。

  public Configuration parse() {
    //只能解析一次,否则异常
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    // 标记为已解析
    parsed = true;
    //解析configuration节点下的内容
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      //解析属性
      propertiesElement(root.evalNode("properties"));
      //解析配置
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      //加载vfs
      loadCustomVfs(settings);
      //加载自定义日志
      loadCustomLogImpl(settings);
      //加载类型别名
      typeAliasesElement(root.evalNode("typeAliases"));
      //加载插件
      pluginElement(root.evalNode("plugins"));
      //加载对象工厂
      objectFactoryElement(root.evalNode("objectFactory"));
      //加载对象包装器工厂
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //加载反射器工厂
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      //设置配置信息
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      //加载环境配置
      environmentsElement(root.evalNode("environments"));
      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);
    }
  }

以上每部分配置的加载方法,其内部都会为configuration赋值,最终解析完成所有配置,再由parse方法返回给调用方。下面逐个了解各部分mybatis配置的解析流程。

属性(properties)

用于定义mybatis运行所需的属性信息,如数据库连接相关配置。属性的定义有三种方式:在mybatis-config.xml#properties中定义属性、通过外部properties文件定义,然后使用resource方式引入mybatis-config.xml、通过 SqlSessionFactoryBuilder.build() 方法中传入属性值。

  // 入参为root.evalNode("properties"),即properties节点下所有子节点
  private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      //读取所有子节点中配置的属性信息,所有信息存储defaults
      Properties defaults = context.getChildrenAsProperties();
      //若以resource或url方式导入外部配置,则加载resource或url中配置信息
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      if (resource != null && url != null) {
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      //外部配置写入defaults,这里需要注意:外部配置会覆盖上面从properties中读取的配置信息
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      //从configuration读取以代码方式传入的配置信息,如果有也写入defaults,
      //此时也会覆盖前两步加载的同名配置
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      //属性解析完成,存储。
      parser.setVariables(defaults);
      configuration.setVariables(defaults);
    }
  }

从这个方法的执行流程可知,如果同一个属性在不同的位置进行重复配置,那么MyBatis 将按照下面的顺序来加载:

  • 首先读取在 properties 元素体内指定的属性。
  • 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。
  • 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。

设置(settings)

设置项

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。通过以下代码+注释解释各个setting及其用途。其中,大部分setting都拥有默认值,实际可按需更改。

public class Configuration {

    //环境配置
    protected Environment environment;

    //是否允许在嵌套语句中使用分页(RowBounds),默认false
    protected boolean safeRowBoundsEnabled;

    //是否允许在嵌套语句中使用结果处理器(ResultHandler),默认true
    protected boolean safeResultHandlerEnabled = true;

    //是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。
    protected boolean mapUnderscoreToCamelCase;

    //开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载。
    protected boolean aggressiveLazyLoading;

    //是否允许单个语句返回多结果集(需要数据库驱动支持),默认值true
    protected boolean multipleResultSetsEnabled = true;

    //允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。
    // 尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。
    protected boolean useGeneratedKeys;

    //使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。
    protected boolean useColumnLabel = true;

    //全局性地开启或关闭所有映射器配置文件中已配置的任何缓存,默认true
    protected boolean cacheEnabled = true;

    //指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。
    // 注意基本类型(int、boolean 等)是不能设置成 null 的。
    // 默认false
    protected boolean callSettersOnNulls;

    //允许使用方法签名中的名称作为语句参数名称。
    //为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。
    //默认值true
    protected boolean useActualParamName = true;

    //当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。
    // 请注意,它也适用于嵌套的结果集(如集合或关联),默认false
    protected boolean returnInstanceForEmptyRow;

    //指定 MyBatis 增加到日志名称的前缀。默认无
    protected String logPrefix;

    //指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
    //SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
    //默认无
    protected Class <? extends Log> logImpl;

    //指定 VFS 的实现
    protected Class <? extends VFS> vfsImpl;

    //MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。
    // 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。
    // 可选值:SESSION | STATEMENT
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

    //当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。
    //某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。
    //默认值:OTHER
    protected JdbcType jdbcTypeForNull = JdbcType.OTHER;

    //指定对象的哪些方法触发一次延迟加载。用逗号分隔的方法列表。
    //例如:equals,clone,hashCode,toString
    protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));

    //设置超时时间,它决定数据库驱动等待数据库响应的秒数。默认未设置。
    protected Integer defaultStatementTimeout;

    //为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。默认未设置
    protected Integer defaultFetchSize;

    //配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。
    //可选值:SIMPLE,REUSE,BATCH,默认值:SIMPLE
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;

    //指定 MyBatis 应如何自动映射列到字段或属性。
    // NONE 表示关闭自动映射;
    // PARTIAL 只会自动映射没有定义嵌套结果映射的字段。
    // FULL 会自动映射任何复杂的结果集(无论是否嵌套)。
    //默认值:PARTIAL
    protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;

    //指定发现自动映射目标未知列(或未知属性类型)的行为。
    //NONE: 不做任何反应
    //WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN)
    //FAILING: 映射失败 (抛出 SqlSessionException)
    //默认值:NONE
    protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

    // 属性列表
    protected Properties variables = new Properties();

    protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();

    //每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。
    //默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。
    //如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。
    protected ObjectFactory objectFactory = new DefaultObjectFactory();
    protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

    //延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。
    //默认值:false
    protected boolean lazyLoadingEnabled = false;
    protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL

    protected String databaseId;
    /**
     * Configuration factory class.
     * Used to create Configuration for loading deserialized unread properties.
     *
     * @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a>
     */
    protected Class<?> configurationFactory;

    //语言驱动注册中心
    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

    //存储所有Mapper中的映射声明,k-v结构
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
            .conflictMessageProducer((savedValue, targetValue) ->
                    ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
    
    //数据缓存,k-v结构
    protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
    
    //存储所有结果映射,来自Mapper
    protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
    
    //存储所有参数迎神,来自Mapper
    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
    
    
    protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");

    //存储已加载的资源文件,如mapper.xml
    protected final Set<String> loadedResources = new HashSet<>();
    
    //存储sql片段
    protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

    //存储未解析完成的声明
    protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
    
    //存储未解析完成的缓存解析
    protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
    
    //存储未解析完成的结果映射
    protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
    
    //存储未解析完成的方法
    protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();

    //……
}

加载流程

mybatis提供的配置项很多,为了方便使用,都提供了默认值。多数情况下,我们只需要按需更改即可。settingsAsProperties用于完成setting的配置解析工作:

  private Properties settingsAsProperties(XNode context) {
    //节点为空,返回空的属性对象
    if (context == null) {
      return new Properties();
    }
    //读取所有设置信息,存入props
    Properties props = context.getChildrenAsProperties();
    // Check that all settings are known to the configuration class
    // 通过反射方式检查props加载的配置信息是否为已知的配置项,如果存在未知配置项,抛出异常。
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    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).");
      }
    }
    return props;
  }

设置加载过程中采用了反射方式检查配置文件中配置项的合法性,存在未知配置项会导致异常。另外,这个方法把解析到的配置项返回,并没有直接为configuration设置,而是通过后面的settingsElement方法进行初始化。因为还要继续加载vfs、logImpl的配置信息。

类型别名

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。例如:

    <typeAliases>
        <typeAlias type="com.raysonxin.dataobject.CompanyDO" alias="CompanyDO"/>
    </typeAliases>

别名降低了使用的复杂度,它在类型查找、映射方面起到很大的作用,typeAliasesElement负责完成别名的解析加载工作:

  private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      // 逐个遍历
      for (XNode child : parent.getChildren()) {
        //是否以包名方式配置别名
        if ("package".equals(child.getName())) {
          // 获取包名
          String typeAliasPackage = child.getStringAttribute("name");
          //按照包名获取包下所有类,然后注册别名:若有注解,则使用注解;否则使用类名首字母小写作为别名。
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {//这是按照类名配置别名的方式
          //获取别名
          String alias = child.getStringAttribute("alias");
          //获取类型:全限定类名
          String type = child.getStringAttribute("type");
          try {
            //获取类型信息
            Class<?> clazz = Resources.classForName(type);
            //别名为空,默认类名首字母小写;不是空,按照alias注册
            if (alias == null) {
              typeAliasRegistry.registerAlias(clazz);
            } else {
              typeAliasRegistry.registerAlias(alias, clazz);
            }
          } catch (ClassNotFoundException e) {
            throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
          }
        }
      }
    }
  }

从代码可知,mybatis支持两种别名的注册方式:

  • 按照包名注册:按照指定的包扫描类,若存在别名注解,则使用注解指定的名称;否则使用类的首字母小写默认;
  • 按照类全限定名称注册:指定了别名则使用其作为别名,否则使用类的首字母小写默认;

另外需要注意的是,这是mybatis提供的自定义别名的方式,其实mybatis已经提供了绝大多数常用的别名,具体在Configuration的构造方法中,大家可以自行查看。

插件(plugins)

MyBatis 允许我们在映射语句执行过程中的某一点进行拦截调用,这可以很方便的让我们扩展mybatis的能力。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)


这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。先看下接口Interceptor的定义:

public interface Interceptor {

  //拦截接口,这里面实现拦截处理逻辑
  Object intercept(Invocation invocation) throws Throwable;

  //为上述目标增加拦截,增强
  Object plugin(Object target);

  //设置插件属性
  void setProperties(Properties properties);

}

自定义插件

自定义插件只需实现 Interceptor 接口,并指定想要拦截的方法签名即可,这里的接口方法和签名必须与Mybatis源码中的定义完全一致,下面通过一个例子进行说明。

@Intercepts(
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        )
)
public class CustomInterceptor implements Interceptor {

    Properties properties = new Properties();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("before CustomInterceptor");
        Object object = invocation.proceed();
        System.out.println("after CustomInterceptor");
        return object;
    }

    @Override
    public Object plugin(Object target) {
        return target instanceof Executor ? Plugin.wrap(target, this) : target;
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

CustomInterceptor对Executor#query方法进行了拦截,在query前后输出一些埋点信息(可以替换为自己的业务逻辑);plugin方法实现逻辑是:当目标组件为Executor时使用Plugin.wrap方法会为其增加插件功能。接下来在mybatis-config.xml中进行配置:

<plugins>
    <plugin interceptor="com.raysonxin.plugin.CustomInterceptor">
        <property name="name" value="abcd"/>
    </plugin>
</plugins>

看下运行效果:
image.png

插件加载与执行流程

直接上代码:org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement

  //入参为root.evalNode("plugins")节点
  private void pluginElement(XNode parent) throws Exception {
    //节点为空不处理
    if (parent != null) {
      //依次遍历子节点
      for (XNode child : parent.getChildren()) {
        //获取插件名称
        String interceptor = child.getStringAttribute("interceptor");
        //获取插件属性列表
        Properties properties = child.getChildrenAsProperties();
        //实例化插件对象
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        //设置插件属性字段
        interceptorInstance.setProperties(properties);
        //添加插件信息到configurtation
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

插件加载流程比较简单:获取名称,获取属性,实例化插件,设置插件属性,最终添加到configuration。XMLConfigBuilder完成插件加载后把插件保存在Configuration#interceptorChain,那实际运行中是如何应用的呢?

如前文所说,mybatis支持在Executor、StatementHandler、ParameterHandler、ResultSetHandler四大组件的若干方法上通过插件增强,mybatis正是在四大组件的创建过程中采用动态代理的方式应用插件的。四大组件的创建方法在Configuration中,如下所示,以newExecutor为例对插件应用过程进行梳理。

  
  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //添加插件逻辑,采用动态代理进行包装
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

组件对象创建完成后通过interceptorChain.pluginAll()方法依次应用插件,四个方法的逻辑类似。interceptor.plugin()由插件实现,它会调用Plugin#wrap方法进行增强。看下这一段的源码逻辑:

  public Object pluginAll(Object target) {
    //循环依次遍历插件
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  public static Object wrap(Object target, Interceptor interceptor) {
    //获取插件类的签名:就是确认插件为哪个方法进行增强
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    //获取插件类实现的所有接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      //采用动态代理创建代理类实现,在我们的例子中相当于为Executor对象进行了增强。
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

所以,经过Plugin#wrap包装后的Executor对象其实是一个代理对象,其中包含了Executor原有的逻辑,按照动态代理的原理,我们看下Plugin#invoke的执行逻辑:

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //从缓存中查询缓存的方法
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      //判断当前方式是否为插件增强的方法
      if (methods != null && methods.contains(method)) {
        //如果是则执行插件的intercept方法,内部会调用原有的method,入口是:Invocation#proceed
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //未被增强,直接调用原来的方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

编写插件需要注意对原有mybatis的影响,防止破坏 MyBatis 的核心模块。通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

类型处理器(typeHandler)


MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。通过TypeHandler接口定义也可以清晰的看到TypeHandler的作用:

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}

自定义类处理器

如果默认的类型处理器不能满足使用需求,可以按照要求自定义类型处理器。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。

以第二种方式举例,自定义类型处理器CustomTypeHandler,该处理器对应的java类型是String,jdbc类型是VARCHAR,includeNullJdbcType为true。

@MappedJdbcTypes(value = JdbcType.VARCHAR,includeNullJdbcType = true)
public class CustomTypeHandler extends BaseTypeHandler<String> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter);
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getString(columnName);
    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getString(columnIndex);
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getString(columnIndex);
    }
}

然后在mybatis-config.xml中添加如下配置:

<typeHandlers>
    <typeHandler handler="com.raysonxin.typehandler.CustomTypeHandler"/>
</typeHandlers>

加载流程

类型处理器的注册过程比较复杂,原因是代码中包含了许多个重载方法,看上去晕头转向,我们先来认识一下TypeHandlerRegistry是如何设计的,直接看代码。

public final class TypeHandlerRegistry {

  //这个字段存储了jdbcType到类型处理器的映射,使用EnumMap
  private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
  
  //存储javaType对应的处理器映射,需要根据jdbcType选择处理器
  private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<>();
    
  //未知类型处理器,内部会进一步解析处理器类型并路由
  private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
    
  //存储所有的类型与类型处理器的映射,jdbcType或者javaType
  private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();

  //NULL处理器
  private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();

  //默认枚举类型处理器
  private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;

  //构造方法内默认注册了常用的类型处理器
  public TypeHandlerRegistry() {
    register(Boolean.class, new BooleanTypeHandler());
    register(boolean.class, new BooleanTypeHandler());
    register(JdbcType.BOOLEAN, new BooleanTypeHandler());
    register(JdbcType.BIT, new BooleanTypeHandler());
    //....
  }
    
  //...
}

TypeHandlerRegistry内部维护了java类型、jdbc类型与类型处理器之间的关系,分别从不同的角度进行映射:

  • JdbcType对应的处理器(JDBC_TYPE_HANDLER_MAP):意思是当处理一个jdbc类型时,可以从这里获取处理器;
  • javaType对应的处理器(TYPE_HANDLER_MAP):这个字段使用了两层映射。第一层是javaType,代表了该javaType具有的处理器列表;第二层是jdbcType与处理器之间的对应关系。综合下来,确认一个处理器需要<javaType,jdbcType>进行最终确认处理器。
  • 全类型映射(ALL_TYPE_HANDLERS_MAP):维护了jdbc类型和java类型所对应的所有映射器。
  • 空类型处理器和默认枚举类型处理器。
  //入参为root.evalNode("typeHandlers")节点
  private void typeHandlerElement(XNode parent) {
    if (parent != null) {
      //遍历节点的子节点
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          //单个类型处理器注册的配置方式
          //获取javaType,这个是类型的别名,应该注册在typeAliasRegistry
          String javaTypeName = child.getStringAttribute("javaType");
          //获取jdbcType
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          //获取handler名称,这是类的全限定名称
          String handlerTypeName = child.getStringAttribute("handler");
          //通过javaType获取其对应的类型,可能为空;若指定了但是没有找到,会报异常
          Class<?> javaTypeClass = resolveClass(javaTypeName);
          //获取JdbcType,枚举类型
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          //获取处理器对应的类型
          Class<?> typeHandlerClass = resolveClass(handlerTypeName);
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              //Case-1:按照java类型、处理器类型注册
              //a、根据typeHandlerClass创建处理器对象typeHandler;
              //b、获取typeHandler注解中的jdbcTypes,可能为空
              //c、jdbcTypes!=null:注册javaType-jdbcType-handler,如果注解中includeNullJdbcType=true,注册空类型;
              //   jdbcTypes==null:仅注册javaType-handler;
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              //Case-2:按照java类型、jdbc类型、处理器类型注册
              //a、创建typeHandler对象;
              //b、注册关联javaType-jdbcType-handler
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
            //Case-3:按照处理器类型注册
            //a、获取typeHandlerClass的MappedTypes注解;
            //b、javaType不为空:按照javatype注册,内部会获取MappedJdbcTypes,关联三者。
            //c、这种情况还没看明白。。。
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
  }

  //Case-1~3,最终都会走到这里
  private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
      //从map中查询已有的javaType对应的处理器
      Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
      //不存在或者为空,执行初始化与添加操作
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
        TYPE_HANDLER_MAP.put(javaType, map);
      }
      //关联javaType-jdbcType-handler
      map.put(jdbcType, handler);
    }
    //加入总的映射
    ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
  }

通过代码可以发现,JDBC_TYPE_HANDLER_MAP仅仅是在构造方法逻辑中进行了添加,相当于是mybatis默认的处理器。我们自定义的处理器会添加到TYPE_HANDLER_MAP、ALL_TYPE_HANDLERS_MAP,虽然有不同的注册方式,但是殊途同归,都是通过上述方法实现了注册。

总结一下:

  • 通过配置文件自定义的类型处理器,会添加到TYPE_HANDLER_MAP、ALL_TYPE_HANDLERS_MAP,而获取类型处理器是优先使用javaType来查询的。如果自定义的类型处理器了重复定义了javaType或jdbcType,会对系统默认的处理器产生覆盖。
  • 如果在类型处理器的注解和xml配置中同时声明了javaType,那么xml中配置的javaType会生效。

映射器(mapper)

MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。——摘自《XML 映射器》

映射器是mybatis面向接口编程的利器,它使用动态代理技术,实现了接口与mapper中sql语句的动态绑定,免去了JDBC中手动实现执行Statement的繁杂过程。mybatis提供多种方式支持mapper资源查找:可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。

映射器元素

SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置。
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • parameterMap – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句


以上顶级元素还有较多的属性用于控制其行为,大家可以自行查阅官方文档,这里不再贴出来了。

加载流程

我们随着示例及源码来看下mybatis是如何加载解析mapper的,然后通过另外一篇文章来剖析其执行过程。示例配置信息,示例中定义了CompanyMapper.xml,以资源方式引入到mybatis-config.xml。

<!--mybatis-config.xml-->
<mappers>
    <mapper resource="mapper/CompanyMapper.xml"/>
</mappers>

<!--CompanyMapper.xml-->
<mapper namespace="com.raysonxin.dao.CompanyDao">

    <resultMap id="baseResultMap" type="com.raysonxin.dataobject.CompanyDO">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="cpy_type" property="cpyType"/>
    </resultMap>

    <sql id="BaseColumns">
        id,name,cpy_type
    </sql>

    <select id="selectById" resultMap="baseResultMap">
        select
        <include refid="BaseColumns"></include>
        from company
        where id= #{id} and name = #{name}
    </select>
</mapper>

mapper解析入口为XMLConfigBuilder#mapperElement,通过代码注释走下流程。

  //入参为root.evalNode("mappers"),即示例中的mappers节点,它可包含多个mapper子节点
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 整包方式添加mapper
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          //这里包含了三种方式:资源导入、资源全限定路径、类名,分别获取mapper节点属性值
          //三个值中只能配置一个,否则会报异常(最后的else)
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          
          // 资源方式
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //使用XMLMapperBuilder类进行mapper解析,这里是重点。
            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类进行mapper解析,同上。
            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方式引入mapper,按照这条线走下去,创建XMLMapperBuilder后调用其parse方法。这里与XMLConfigBuilder类似,同样使用XPathParser进行xml解析,另外使用MapperBuilderAssistant构建MappedStatement。

  private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
  }

接下来进入解析流程,即XMLMapperBuilder#parse方法。大体流程是:若资源未被加载过,则解析mapper文件中的元素,把mapper添加到已加载资源,绑定mapper与namespace。无论是否加载过当前资源,都会处理那些之前未完成的ResultMap、CacheRef、Statement。

  public void parse() {
    //判断资源是否已经加载
    if (!configuration.isResourceLoaded(resource)) {
      //这个是mapper解析的核心方法,入参是mapper对应的资源文件,示例中的CompanyMapper.xml
      configurationElement(parser.evalNode("/mapper"));
      //添加已加载资源到configuration
      configuration.addLoadedResource(resource);
      //这个方法是绑定mapper与namespace
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

核心解析方式是configurationElement,它依次解析mapper文件中的cache-ref、cache、parameterMap、resultMap、sql、select|insert|update|delete,本次重点分析后面三者的解析过程。

  private void configurationElement(XNode context) {
    try {
      //获取mapper中的namespace,示例中为:com.raysonxin.dao.CompanyDao
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      //设置builderAssistant的namespace
      builderAssistant.setCurrentNamespace(namespace);
      //解析cache-ref内容
      cacheRefElement(context.evalNode("cache-ref"));
      //解析cache内容
      cacheElement(context.evalNode("cache"));
      //解析parameterMap节点
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //解析resultMap节点
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //解析sql节点
      sqlElement(context.evalNodes("/mapper/sql"));
      //构建statement
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

resultMap

下可以定义多个resultMap,通常我们会把常用的返回字段定义为一个resultMap,方便在查询语句中引用。我们从resultMapElements开始看下源码:

  private void resultMapElements(List<XNode> list) throws Exception {
    for (XNode resultMapNode : list) {
      try {
        resultMapElement(resultMapNode);
      } catch (IncompleteElementException e) {
        // ignore, it will be retried
      }
    }
  }

  private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
    return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList(), null);
  }

  //真正的解析工作从这里开始
  private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    //获取resultMap的id属性,示例中baseResultMap
    String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
    //获取resultMap的type属性,后面是默认值设置顺序
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    //获取extends属性
    String extend = resultMapNode.getStringAttribute("extends");
    //获取autoMapping属性
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    //获取type对应的类型对象,首先从typeAliasRegistry获取,否则按照全路径限定名称反射创建
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
      typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<>();
    resultMappings.addAll(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    //依次解析子节点
    for (XNode resultChild : resultChildren) {
      //是否为构造方法
      if ("constructor".equals(resultChild.getName())) {
        //处理构造方法
        processConstructorElement(resultChild, typeClass, resultMappings);
      } 
      //discriminator鉴别器
      else if ("discriminator".equals(resultChild.getName())) {
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } 
      //其他节点  
      else {
        List<ResultFlag> flags = new ArrayList<>();
        // 是否为id节点
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        //构建resultMap:buildResultMappingFromContext
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }

不考虑复杂的resultMap,我们示例中的节点类型为id和result两种,我们直接进入方法buildResultMappingFromContext:

  private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
      property = context.getStringAttribute("name");
    } else {
      //获取property值,对应javaType的字段
      property = context.getStringAttribute("property");
    }
    //数据表的字段
    String column = context.getStringAttribute("column");
    //获取javaType
    String javaType = context.getStringAttribute("javaType");
    //获取jdbcType
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    String nestedResultMap = context.getStringAttribute("resultMap",
        processNestedResultMappings(context, Collections.<ResultMapping> emptyList(), resultType));
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    //类型处理器
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
    //获取javaType类型对象
    Class<?> javaTypeClass = resolveClass(javaType);
    //获取类型处理器
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    //jdbc类型枚举
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    //创建ResultMapping对象并返回
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
  }

这个解析过程包含了resultMap高级用法的逻辑,比如association、collections等,我们先从简单、常用的方式入手。buildResultMappingFromContext最终返回了resultMap中某个字段的映射信息,构建了java类型与数据库字段的映射关系。这样,通过遍历所有字段把resultMap下的所有字段一一映射。

我们再回到方法resultMapElement,遍历所有节点后得到resultMappings,创建了ResultMapResolver对象。从名称可以知道,这个类是ResultMap的解析器,它最终把resultMap配置信息转为mybaits的ResultMap对象并添加到configuration。

sql

实际开发中,我们可以使用sql标签定义那些可复用的sql片段,简化mapper文件的配置。在mybatis解析sql时,它也只是一个中间过程,它是解析select|update|insert|delete语句的基础。

  private void sqlElement(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
  }

  private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      String databaseId = context.getStringAttribute("databaseId");
      String id = context.getStringAttribute("id");
      //拼接namespace,结果为:namespace+"."+id
      id = builderAssistant.applyCurrentNamespace(id, false);
      //是否符合datbaseId要求
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        //sqlFragments保存所有sql片段
        sqlFragments.put(id, context);
      }
    }
  }

select|insert|update|delete

select|insert|update|delete对于sql语句中的同名命令,是我们使用最频繁的部分,最终mybatis会把这些标签转为MappedStatement,存储在configuration,同时与Mapper接口方法在运行时绑定。

  private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

从代码可以看到XMLStatementBuilder,它是命令标签的处理器,核心方法是parseStatementNode。到这里,我们已经见过了XMLConfigBuilder、XMLMapperBuilder。看下处理过程:

  public void parseStatementNode() {
    //获取标签中的id属性,如:selectById
    String id = context.getStringAttribute("id");
    //databaseId,为空
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    //属性fetchSize
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //属性timeout
    Integer timeout = context.getIntAttribute("timeout");
    //属性parameterMap
    String parameterMap = context.getStringAttribute("parameterMap");
    //属性parameterType
    String parameterType = context.getStringAttribute("parameterType");
    //解析参数类型信息
    Class<?> parameterTypeClass = resolveClass(parameterType);
    //获取属性resultMap
    String resultMap = context.getStringAttribute("resultMap");
    //获取属性resultType
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    //获取属性statementType,未设置时使用默认值PREPARED
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    //获取nodeName,就是标签的类型,select、insert之类
    String nodeName = context.getNode().getNodeName();
    //命令类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //判断是否为select命令
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //是否刷新缓存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否使用缓存
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    //这里解析sql语句中的include标签
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    //解析selectKey标签
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //创建SqlSource,必须在inclue和selectKey解析完成后执行,
    //内部会根据sql语句是否包含动态标签创建不同类型的sqlSource,解析sql所需的参数映射信息
    //这个地方需要一篇单独的文章来分析
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    //处理主键生成器
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    //创建MappedStatement,添加到configuration
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

源码的过程梳理一下:

  • 首先解析标签的基础属性,如id、fetchSize等,
  • 解析sql语句中inclue标签(把include的内容替换拼接进来),
  • 处理selcetKey标签(特定的databaseProvider需要),
  • 接下来创建SqlSource:这个是mybatis的核心内容,涉及到sql语句中参数提取、动态赋值等,以后在详细说明;
  • 处理KeyGenerator;
  • 使用builderAssistant创建MappedStatement,添加到configuration

总结

Configuration是mybatis的全局配置类,mybatis运行时所需的一切都缓存在这里,它伴随mybatis的整个生命周期。理论上讲,Configuration是配置文件mybatis-config.xml的代码映射,mybatis提供了充分的灵活性、可扩展性,方便开发人员通过配置文件改变其运行行为。

本文通过示例及源码把mybatis-config.xml的解析过程、Configuration配置的大部分内容进行了梳理,通过梳理确实获益匪浅。由于水平优先,文中很多内容确实没有描述清楚,随着学习的不断深入,再做完善。

公众号搜索“码路印记”,点关注不迷路!