前几面的文章中,我们了解MyBatis的基础项目的搭建与配置,本文章中将项目的源码来讲解一下,从源码的角度上来看下MyBatis的启动源码。首先我们来看下基础的项目的代码

  1. @Test
  2. public void testResource() throws IOException, SQLException {
  3. // 1.创建配置文件
  4. String resource = "mybatis-config.xml";
  5. InputStream inputStream = Resources.getResourceAsStream(resource);
  6. // 构建 SQLSessionFactory,以创建 SqlSession
  7. sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  8. SqlSession sqlSession = sqlSessionFactory.openSession();
  9. // 2、执行查询语句
  10. BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
  11. List<UserDO> userDOS = mapper.find("A");
  12. // 关闭Session
  13. sqlSession.close();
  14. }

构建 SqlSessionFactory

通过 new SqlSessionFactoryBuilder().build(inputStream) 可以构建一个SQLSessionFactory ,SQLSessionFactory 可以用来创建 SQLSession ,所以如何通过配置文件 创建一个 SQLSessionFactory是本短重点。

  1. // 通过 build 构建一个 XMLConfigBuilder
  2. public SqlSessionFactory build(inputStream, environment, Properties) {
  3. try {
  4. XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
  5. return build(parser.parse());
  6. } catch (Exception e) {
  7. throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  8. } finally {
  9. ErrorContext.instance().reset();
  10. try {
  11. inputStream.close();
  12. } catch (IOException e) {
  13. }
  14. }
  15. }

XMLConfigBuilder 之后,通过 parse.parse() 方法进行解析配置文件

  1. public Configuration parse() {
  2. // 从XML文件中获取根目录的 configuration 节点
  3. parseConfiguration(parser.evalNode("/configuration"));
  4. return configuration;
  5. }
  6. private void parseConfiguration(XNode root) {
  7. try {
  8. // 配置变量参数
  9. propertiesElement(root.evalNode("properties"));
  10. // 配置设置参数
  11. Properties settings = settingsAsProperties(root.evalNode("settings"));
  12. loadCustomVfs(settings);
  13. loadCustomLogImpl(settings);
  14. // 配置类型别名
  15. typeAliasesElement(root.evalNode("typeAliases"));
  16. // 创建插件
  17. pluginElement(root.evalNode("plugins"));
  18. objectFactoryElement(root.evalNode("objectFactory"));
  19. objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  20. reflectorFactoryElement(root.evalNode("reflectorFactory"));
  21. settingsElement(settings);
  22. // 配置环境
  23. environmentsElement(root.evalNode("environments"));
  24. // 配置数据库提供商
  25. databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  26. // 配置类型处理器
  27. typeHandlerElement(root.evalNode("typeHandlers"));
  28. // 配置Mapper
  29. mapperElement(root.evalNode("mappers"));
  30. } catch (Exception e) {
  31. throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  32. }
  33. }

解析 配置项(Properties)

从 MyBatis 的配置文件中就可以看出 有个标签类别为 properties 的 属性,常见的情况下,通常如下配置

  1. <properties>
  2. <property name="title" value="TITLE"/>
  3. <property name="age" value="17"/>
  4. </properties>

上面的代码中通过 propertiesElement(root.evalNode("properties")); 的主要作用就是解析出 propterties 标签的内容,具体的解释看注释。

  1. private void propertiesElement(XNode context) throws Exception {
  2. // 如果配置文件中不存子 properties 字段属性,那么就不解析
  3. if (context != null) {
  4. // 获取Properties的内容,将其封装为 Properties,Properties继承了 Hashtable
  5. // 可以粗略的认为 Properties 就是一个 KV 集合
  6. Properties defaults = context.getChildrenAsProperties();
  7. // 从Properties中获取到 resource属性和URL属性,尝试从其他资源中获取配置
  8. String resource = context.getStringAttribute("resource");
  9. String url = context.getStringAttribute("url");
  10. // 如果都指定了,则报错,必须明确的指定一个
  11. if (resource != null && url != null) {
  12. throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
  13. }
  14. // 根据 resource或者URL 解析到新的Properties,Put 到 上面解析的结果中
  15. if (resource != null) {
  16. defaults.putAll(Resources.getResourceAsProperties(resource));
  17. } else if (url != null) {
  18. defaults.putAll(Resources.getUrlAsProperties(url));
  19. }
  20. // 获取其他的默认配置
  21. Properties vars = configuration.getVariables();
  22. if (vars != null) {
  23. defaults.putAll(vars);
  24. }
  25. //设置回Parse用于后续的解析使用
  26. parser.setVariables(defaults);
  27. //设置回Configuration中
  28. configuration.setVariables(defaults);
  29. }
  30. }

通过 propertiesElement() 方法就首先得解析出配置文件中配置的变量,并用于后续其他属性解析。

配置 设置项(Settings)

接下来的是解析settings,Setting是MyBatis的相关配置项,开发者可以通过此标签进行MyBatis相关控制以及相关选项的配置。比如一个简单的配置项如下

  1. <settings>
  2. <setting name="cacheEnabled" value="false"/>
  3. </settings>

其解析代码如下:

  1. private Properties settingsAsProperties(XNode context) {
  2. if (context == null) {
  3. return new Properties();
  4. }
  5. Properties props = context.getChildrenAsProperties();
  6. // 获取支持的属性值
  7. MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
  8. for (Object key : props.keySet()) {
  9. // 如果给定的属性值不属于 Configuration 内置的属性,则会抛出异常
  10. if (!metaConfig.hasSetter(String.valueOf(key))) {
  11. throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
  12. }
  13. }
  14. return props;
  15. }

可以看到,Setting的解析如果出现未知的配置项,则会抛出异常。那么通过 Configuration 可以看到允许的配置值.

  1. public class Configuration {
  2. protected Environment environment;
  3. protected boolean safeRowBoundsEnabled;
  4. protected boolean safeResultHandlerEnabled = true;
  5. protected boolean mapUnderscoreToCamelCase;
  6. protected boolean aggressiveLazyLoading;
  7. protected boolean multipleResultSetsEnabled = true;
  8. protected boolean useGeneratedKeys;
  9. protected boolean useColumnLabel = true;
  10. protected boolean cacheEnabled = true;
  11. protected boolean callSettersOnNulls;
  12. protected boolean useActualParamName = true;
  13. protected boolean returnInstanceForEmptyRow;
  14. protected boolean shrinkWhitespacesInSql;
  15. // .....
  16. }

在Settings 解析完成之后,会返回一个Properties 用于其他的配置,比如配置

  1. // org/apache/ibatis/builder/xml/XMLConfigBuilder.java:107
  2. Properties settings = settingsAsProperties(root.evalNode("settings"));
  3. // 配置虚拟文件系统
  4. loadCustomVfs(settings);
  5. // 配置自定义的日志系统
  6. loadCustomLogImpl(settings);

配置 类型别名(typeAliases)

类型别名是MyBatis中非常重要的一个概念, 它能够帮助开发者更简便的编写代码。类型别名的使用方式不再本文的讨论中,比如我们配置一个简单的别名如下

  1. <typeAliases>
  2. <!-- 使用 package 的方式,按包指定-->
  3. <package name="com.zhoutao123.dao.model"/>
  4. <!-- 或者使用下面的方式,单个指定-->
  5. <typeAlias type="org.mybatis.example.UserDO" alias="user"/>
  6. </typeAliases>

那么我们在书写Mapper的时候就可以直接这样使用

  1. <!--定义别名之前-->
  2. <select id="find" resultType="org.mybatis.example.UserDO">
  3. SELECT * FROM user WHERE id = #{id}
  4. </select>
  5. <!--定义别名之后-->
  6. <select id="find" resultType="user">
  7. SELECT * FROM user WHERE id = #{id}
  8. </select>

可以通过 typeAliasesElement 查看类型别名的源码

  1. if (parent != null) {
  2. for (XNode child : parent.getChildren()) {
  3. // 如果指定了按包级别扫描别名,则通过 name 属性获取到需要扫描的包下的类
  4. if ("package".equals(child.getName())) {
  5. String typeAliasPackage = child.getStringAttribute("name");
  6. configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
  7. } else {
  8. // 如果不是按照包扫描别名则直接通过标签 typeAlias 的alias属性和 type 属性注册
  9. String alias = child.getStringAttribute("alias");
  10. String type = child.getStringAttribute("type");
  11. try {
  12. Class<?> clazz = Resources.classForName(type);
  13. // 如果别名为NULL,则根据策略生成一个alias, 生成范式参照 下面的 registerAlias(Class<?> type)
  14. if (alias == null) {
  15. typeAliasRegistry.registerAlias(clazz);
  16. } else {
  17. typeAliasRegistry.registerAlias(alias, clazz);
  18. }
  19. } catch (ClassNotFoundException e) {
  20. throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
  21. }
  22. }
  23. }
  24. }
  25. public void registerAlias(Class<?> type) {
  26. // 获取类的简称
  27. String alias = type.getSimpleName();
  28. // 获取类上的Alias注解
  29. Alias aliasAnnotation = type.getAnnotation(Alias.class);
  30. // 如果注解存在,则使用注解的值作为别名,否则使用类的简称作为别名
  31. if (aliasAnnotation != null) {
  32. alias = aliasAnnotation.value();
  33. }
  34. registerAlias(alias, type);
  35. }

在最后,类型别名和类型的clazz 对象将被保存在 typeAlias 对象中, typeAliases 对象则是一个Map集合,用于保存 MyBatis系统中自定义的类型别名

  1. private final Map<String, Class<?>> typeAliases = new HashMap<>();
  2. public void registerAlias(String alias, Class<?> value) {
  3. if (alias == null) {
  4. throw new TypeException("The parameter alias cannot be null");
  5. }
  6. // issue #748
  7. String key = alias.toLowerCase(Locale.ENGLISH);
  8. if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
  9. throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
  10. }
  11. typeAliases.put(key, value);
  12. }

众所周知,MyBatis 也内置一些常用的类型别名,比如 string/int/boolean/map/list 等,这些默认的别名也是通过 registerAlias 实现注册的,在 TypeAlias 的构造方法中可以看到相关的源码

  1. public TypeAliasRegistry() {
  2. registerAlias("string", String.class);
  3. registerAlias("byte", Byte.class);
  4. registerAlias("long", Long.class);
  5. registerAlias("short", Short.class);
  6. // .... 省略部分
  7. registerAlias("map", Map.class);
  8. registerAlias("hashmap", HashMap.class);
  9. registerAlias("ResultSet", ResultSet.class);
  10. }

配置 插件(Plugin)

插件可以说是对于MyBatis 使用的一个重要的分水岭,很多非常好用的MyBatis的插件都是使用Plugin实现的,比如 PageHelp、MyBatais-Plus。下面看看Plugin的配置方式吧

  1. <plugins>
  2. <plugin interceptor="org.mybatis.example.plugins.CustomerExecutor">
  3. <property name="limit" value="123"/>
  4. <property name="max" value="456"/>
  5. </plugin>
  6. </plugins>

上面的一段XML代码实现一个MyBatis 插件的注册 , 而插件则必须是 org.apache.ibatis.plugin.Intercepto 的子类。

由于本文主要的重点是MyBatis流程的启动过程以及MyBatis配置的解析过程,所以MyBatis的插件开发将后再后续的文章通过案例(PageHelp)以源码解析的方式学习Plugin的开发。当然后面也会实现一个简单的案例

  1. for (XNode child : parent.getChildren()) {
  2. // 获取 <plugin> 标签的 interceptor 属性以及其属性值properties
  3. String interceptor = child.getStringAttribute("interceptor");
  4. Properties properties = child.getChildrenAsProperties();
  5. // 通过全类名解析出Class并获取其构造方式并创建一个新的实例
  6. Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
  7. // 为此实例设置properties
  8. interceptorInstance.setProperties(properties);
  9. // MyBatis 的 configuration中新增拦截器
  10. configuration.addInterceptor(interceptorInstance);
  11. }

配置 对象工厂(ObjectFactory)

MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。 如果想覆盖对象工厂的默认行为,则可以通过创建自己的对象工厂来实现。那么假设我们需要对创建的实体进行一些设定,就可以通过继承 org.apache.ibatis.reflection.factory.DefaultObjectFactory 的方式覆盖 其默认行为.

  1. public class CustomerObjectFactory extends DefaultObjectFactory {
  2. @Override
  3. public <T> T create(Class<T> type) {
  4. T t = super.create(type);
  5. // 构造的之后添加自定的逻辑
  6. if (t instanceof UserDO) {
  7. ((UserDO) t).setId(1L);
  8. }
  9. return t;
  10. }
  11. @Override
  12. public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes,
  13. List<Object> constructorArgs) {
  14. return super.create(type, constructorArgTypes, constructorArgs);
  15. }
  16. @Override
  17. public <T> boolean isCollection(Class<T> type) {
  18. return false;
  19. }
  20. }

其在MyBatis中的配置方式如下

  1. <objectFactory type="org.mybatis.example.CustomerObjectFactory">
  2. <property name="name" value="CustomerObjectFactory"/>
  3. </objectFactory>

而在其解析对象工厂的源码中也和插件的方式比较类似, 需要注意的是,在ObjectFactory创建完成后,其被保存在 configuration 中。

  1. private void objectFactoryElement(XNode context) throws Exception {
  2. if (context != null) {
  3. String type = context.getStringAttribute("type");
  4. Properties properties = context.getChildrenAsProperties();
  5. ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
  6. factory.setProperties(properties);
  7. configuration.setObjectFactory(factory);
  8. }
  9. }

这里同样的还有 objectWrapperFactory 以及 reflectorFactory 这里和ObjectFactory 都是比较类似的,读者可自行了解其解析方式。

配置 环境(Environments)

这里的环境指的MyBatis的运行环境,包括数据源以及事务管理等,环境是MyBatis非常重要的特性。日常开发中合理的使用环境将会非常的方便,我们看看在MyBatis的配置文件中如何使用环境。

  1. <!-- 创建环境,其默认环境为 id = dev 的配置-->
  2. <environments default="dev">
  3. <environment id="dev">
  4. <transactionManager type="JDBC"/>
  5. <dataSource type="POOLED">
  6. <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
  7. <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
  8. <property name="username" value="root"/>
  9. <property name="password" value="username123"/>
  10. </dataSource>
  11. </environment>
  12. <environment id="prod">
  13. <transactionManager type="JDBC"/>
  14. <dataSource type="POOLED">
  15. <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
  16. <property name="url" value="jdbc:mysql://127.0.03:3306/test"/>
  17. <property name="username" value="root"/>
  18. <property name="password" value="root"/>
  19. </dataSource>
  20. </environment>
  21. </environments>

运行环境的解析过程如下

  1. private void environmentsElement(XNode context) throws Exception {
  2. if (context != null) {
  3. if (environment == null) {
  4. // 获取默认环境
  5. environment = context.getStringAttribute("default");
  6. }
  7. for (XNode child : context.getChildren()) {
  8. String id = child.getStringAttribute("id");
  9. // 如果是默认环境则进行解析,否则不解析
  10. if (isSpecifiedEnvironment(id)) {
  11. // 事务工厂
  12. TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
  13. // 数据源
  14. DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
  15. DataSource dataSource = dsFactory.getDataSource();
  16. Environment.Builder environmentBuilder =
  17. new Environment.Builder(id)
  18. .transactionFactory(txFactory)
  19. .dataSource(dataSource);
  20. configuration.setEnvironment(environmentBuilder.build());
  21. }
  22. }
  23. }
  24. }

配置 数据库厂商标识(DatabaseIdProvider)

MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。 MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。 如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。 为支持多厂商特性,只要像下面这样在 mybatis-config.xml 文件中加入 databaseIdProvider 即可:

  1. <databaseIdProvider type="DB_VENDOR" />

databaseIdProvider 对应的 DB_VENDOR 实现会将 databaseId 设置为 DatabaseMetaData#getDatabaseProductName() 返回的字符串。 由于通常情况下这些字符串都非常长,而且相同产品的不同版本会返回不同的值,你可能想通过设置属性别名来使其变短:

  1. <databaseIdProvider type="DB_VENDOR">
  2. <property name="SQL Server" value="sqlserver"/>
  3. <property name="DB2" value="db2"/>
  4. <property name="Oracle" value="oracle" />
  5. </databaseIdProvider>

在提供了属性别名时,databaseIdProvider 的 DB_VENDOR 实现会将 databaseId 设置为数据库产品名与属性中的名称第一个相匹配的值,如果没有匹配的属性,将会设置为 “null”。 在这个例子中,如果 getDatabaseProductName() 返回“Oracle (DataDirect)”,databaseId 将被设置为“oracle”。在下面的示例中,我么可以为相同ID的 SELECT 配置不同的 databaseId.

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper
  3. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5. <mapper namespace="com.zhoutao123.source.mybatis.mapper.UserMapper">
  6. <!-- 使用 MySQL 数据库的时候-->
  7. <select id="getOneById" resultType="user" databaseId="mysql">
  8. SELECT *
  9. FROM user
  10. WHERE id = #{id}
  11. </select>
  12. <!-- 使用 Oracle 数据库的时候-->
  13. <select id="getOneById" resultType="user" databaseId="oracle">
  14. SELECT *
  15. FROM USER
  16. WHERE ID = #{id}
  17. </select>
  18. </mapper>

其解析的过程为

  1. private void databaseIdProviderElement(XNode context) throws Exception {
  2. DatabaseIdProvider databaseIdProvider = null;
  3. if (context != null) {
  4. String type = context.getStringAttribute("type");
  5. // awful patch to keep backward compatibility
  6. if ("VENDOR".equals(type)) {
  7. type = "DB_VENDOR";
  8. }
  9. Properties properties = context.getChildrenAsProperties();
  10. databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
  11. databaseIdProvider.setProperties(properties);
  12. }
  13. Environment environment = configuration.getEnvironment();
  14. if (environment != null && databaseIdProvider != null) {
  15. String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
  16. configuration.setDatabaseId(databaseId);
  17. }
  18. }

配置 映射器(Mappers)

既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。例如:

  1. <!-- 使用相对于类路径的资源引用 -->
  2. <mappers>
  3. <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  4. <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  5. <mapper resource="org/mybatis/builder/PostMapper.xml"/>
  6. </mappers>
  7. <!-- 使用完全限定资源定位符(URL) -->
  8. <mappers>
  9. <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  10. <mapper url="file:///var/mappers/BlogMapper.xml"/>
  11. <mapper url="file:///var/mappers/PostMapper.xml"/>
  12. </mappers>
  13. <!-- 使用映射器接口实现类的完全限定类名 -->
  14. <mappers>
  15. <mapper class="org.mybatis.builder.AuthorMapper"/>
  16. <mapper class="org.mybatis.builder.BlogMapper"/>
  17. <mapper class="org.mybatis.builder.PostMapper"/>
  18. </mappers>
  19. <!-- 将包内的映射器接口实现全部注册为映射器 -->
  20. <mappers>
  21. <package name="org.mybatis.builder"/>
  22. </mappers>

这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了,也就是接下来我们要重点讨论的通过动态代理的方式实现 Mapper 的访问和构造。