Mybatis源码解析-SqlSessionFactoryBuilder解析配置文件过程

394 阅读17分钟

SqlSessionFactoryBuilderTest

@Test
public void readerTest() throws IOException {
    //得到一个Reader处理类(字符流)
    try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/mytest/mybatis-config.xml")) {
        // environment:数据源ID 在mybatis-config.xml <environments> 标签里面配置的 id 为development的数据源信息(非必传)
        // properties :配置文件信息。一般是数据库配置信息。可以在这里进行配置 也可以在mybatis-config.xml 通过<properties> 标签进行引入properties配置文件
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,"development", Resources.getResourceAsProperties("org/apache/ibatis/mytest/db.properties"));
    }
    SqlSession sqlSession = sqlSessionFactory.openSession();
    List<StudentDto> rs = sqlSession.selectList("org.apache.ibatis.mytest.StudentMapper.getStudentsByAlias", null);
    System.out.println(rs.toString());
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    rs = mapper.getStudentsByAlias();
    System.out.println(rs.toString());
}

SqlSessionFactoryBuilder

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

1.创建一个xml配置构建器,里面包含一个Xpath解析器

2.主要看下面这句parser.parse(),里面是所有解析的步骤,返回一个Configuration对象

接下来看parser.parse()

XMLConfigBuilder

public Configuration parse() {
    // 1
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 2 
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

1.如果解析过,就抛异常,保证只解析一次

2.解析mybatis-config.xml配置文件,从根节点/configuration开始解析,解析封装成Configuration对象,并返回这个configuration对象

接下来看parseConfiguration(parser.evalNode("/configuration"));

private void parseConfiguration(XNode root) {
    try {
        // issue #117 read properties first
        // 1
        propertiesElement(root.evalNode("properties"));
        // 2
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        // 3
        loadCustomVfs(settings);
        // 4
        loadCustomLogImpl(settings);
        // 5
        typeAliasesElement(root.evalNode("typeAliases"));
        // 6
        pluginElement(root.evalNode("plugins"));
        // 7
        objectFactoryElement(root.evalNode("objectFactory"));
        // 8
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        // 9
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        // 10
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        // 11
        environmentsElement(root.evalNode("environments"));
        // 12
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        // 13
        typeHandlerElement(root.evalNode("typeHandlers"));
        // 14
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

1.properties节点,用来读取properties配置文件

/**
 * 解析properties标签
 *
 * @param context
 *
 * @throws Exception
 */
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // 1
        Properties defaults = context.getChildrenAsProperties();
        // 2
        String resource = context.getStringAttribute("resource");
        // 3
        String url = context.getStringAttribute("url");
        if (resource != null && url != null) {
            // 4
            throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
        }
        if (resource != null) {
            // 5
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            // 6
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        // 7
        Properties vars = configuration.getVariables();
        if (vars != null) {
            // 如果variables不为空,则填充到Properties对象中
            defaults.putAll(vars);
        }
        // 8
        parser.setVariables(defaults);
        // 9
        configuration.setVariables(defaults);
        // 从这里可以看出,优先级从低到高是: 子标签 -> resource/url -> configuration.variables【这里是new XMLConfigBuilder的时候可以传入的】 ,且最终结果是把Properties塞到了XPathParser以及Configuration中
    }
}

1.先解析properties标签是否有property子标签,读取里面的name和value属性并放入到Properties对象中 2.properties标签下的resource属性
3.properties标签下的url属性
4.resource和url不可以同时配置不然不知道哪个优先
5.resource,则解析资源路径的配置到Properties中(此时是先解析的xml中的property子标签 所以会存在properties文件覆盖掉xml中的配置。properties中的配置优先级高)
6.url,则去网络请求,然后读取配置到Properties中
7.首先获取configuration中现有的variables(也是Properties类型) (优先级SqlSessionFactoryBuilder.build(properties)>xml引入的properties>property子标签定义的属性)
8.然后把Properties配置信息塞入到XPathParser中
9.再把Properties配置信息塞入到Configuration的variables中

1.

image.png

2.

image.png

2.解析settingss标签下所有settings子标签 这里只是读取 真正去解析在下面 所有setting配置以及取值

配置项作用配置选项默认值
cacheEnabled该配置影响所有映射器中配置缓存的全局开关true/falsetrue
lazyLoadingEnabled延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。在特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态 true/falsefalse
aggressiveLazyLoading当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载true/false版本3.4.1 (不包含) 之前 true,之后 false
multipleResultSetsEnabled 是否允许单一语句返回多结果集(需要兼容驱动)true/falsetrue
useColumnLabel使用列标签代替列名。不同的驱动会有不同的表现,具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果true/falsetrue
useGeneratedKeys允许JDBC 支持自动生成主键,需要驱动兼容。如果设置为 true,则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)true/falsefalse
autoMappingBehavior指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射。 PARTIAL 表示只会自动映射,没有定义嵌套结果集和映射结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)NONE、PARTIAL、FULLPARTIAL
autoMappingUnkno wnColumnBehavior指定自动映射当中未知列(或未知属性类型)时的行为。 默认是不处理,只有当日志级别达到 WARN 级别或者以下,才会显示相关日志,如果处理失败会抛出 SqlSessionException 异常NONE、WARNING、FAILINGNONE
defaultExecutorType配置默认的执行器。SIMPLE 是普通的执行器;REUSE 会重用预处理语句(prepared statements);BATCH 执行器将重用语句并执行批量更新 SIMPLE、REUSE、BATCHSIMPLE
defaultStatementTimeout设置超时时间,它决定驱动等待数据库响应的秒数任何正整数Not Set (null)
defaultFetchSize设置数据库驱动程序默认返回的条数限制,此参数可以重新设置任何正整数  Not Set (null)
safeRowBoundsEnabled允许在嵌套语句中使用分页(RowBounds)。如果允许,设置 falsetrue/falsefalse
safeResultHandlerEnabled允许在嵌套语句中使用分页(ResultHandler)。如果允许,设置falsetrue/falsetrue
mapUnderscoreToCamelCase是否开启自动驼峰命名规则映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射true/falsefalse
localCacheScopeMyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速联复嵌套査询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlScssion 的不同调用将不会共享数据  SESSIONSTATEMENTSESSION
jdbcTypeForNull当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHERNULL、VARCHAR、OTHER  OTHER
lazyLoadTriggerMethods指定哪个对象的方法触发一次延迟加载equals、clone、hashCode、toString
defaultScriptingLanguage指定动态 SQL 生成的默认语言org.apache.ibatis .script.ing.xmltags .XMLDynamicLanguageDriver
callSettersOnNulls指定当结果集中值为 null 时,是否调用映射对象的 setter(map 对象时为 put)方法,这对于 Map.kcySet() 依赖或 null 值初始化时是有用的。注意,基本类型(int、boolean 等)不能设置成 nulltrue/falsefalse
logPrefix指定 MyBatis 增加到日志名称的前缀 任何字符串Not set
loglmpl指定 MyBatis 所用日志的具体实现,未指定时将自动査找SLF4J/LOG4J/LOG4J2/JDK_LOGGING/COMMONS_LOGGING /ST DOUT_LOGGING/NO_LOGGINGNot set
proxyFactory指定 MyBatis 创建具有延迟加栽能力的对象所用到的代理工具CGLIBJAVASSISTJAVASSIST (MyBatis 版本为 3.3 及以上的)
vfsImpl指定 VFS 的实现类  提供 VFS 类的全限定名,如果存在多个,可以使用逗号分隔 Not set
useActualParamName允许用方法参数中声明的实际名称引用参数。要使用此功能,项目必须被编译为 Java 8 参数的选择。(从版本 3.4.1 开始可以使用)true/falsetrue

真正在配置中不会用到这么多的配置 一些比较常用的配置

<!-- settings是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 -->
	<settings>
		<!-- 该配置影响的所有映射器中配置的缓存的全局开关。默认值true -->
	  <setting name="cacheEnabled" value="true"/>
	  <!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。默认值false  -->
	  <setting name="lazyLoadingEnabled" value="true"/>
	  	<!-- 是否允许单一语句返回多结果集(需要兼容驱动)。 默认值true -->
	  <setting name="multipleResultSetsEnabled" value="true"/>
	  <!-- 使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。默认值true -->
	  <setting name="useColumnLabel" value="true"/>
	  <!-- 允许 JDBC 支持自动生成主键,需要驱动兼容。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 默认值false  -->
	  <setting name="useGeneratedKeys" value="false"/>
	 <!--  指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。 --> 
	 <!-- 默认值PARTIAL -->
	  <setting name="autoMappingBehavior" value="PARTIAL"/>
	  
	  <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
	 <!--  配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。默认SIMPLE  -->
	  <setting name="defaultExecutorType" value="SIMPLE"/>
	  <!-- 设置超时时间,它决定驱动等待数据库响应的秒数。 -->
	  <setting name="defaultStatementTimeout" value="25"/>
	  
	  <setting name="defaultFetchSize" value="100"/>
	  <!-- 允许在嵌套语句中使用分页(RowBounds)默认值False -->
	  <setting name="safeRowBoundsEnabled" value="false"/>
	  <!-- 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。  默认false -->
	  <setting name="mapUnderscoreToCamelCase" value="false"/>
	  <!-- MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。
	  		 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。
	   		若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。  -->
	  <setting name="localCacheScope" value="SESSION"/>
	  <!-- 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。  -->
	  <setting name="jdbcTypeForNull" value="OTHER"/>
	<!--   指定哪个对象的方法触发一次延迟加载。  -->
	  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
	</settings>

3.加载settings节点中的vfs相关配置信息

VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。 Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序。

4.加载settings节点中的logImpl相关配置信息【日志配置】

configuration的日志配置

private void loadCustomLogImpl(Properties props) {
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    configuration.setLogImpl(logImpl);
}

5.typeAliases别名配置

读取typeAliases标签下的typeAlias的子标签 通过设置别名在Mapper.xml中可以直接使用别名
例如:

<typeAliases>
       <typeAlias  alias="student" type="org.apache.ibatis.mytest.StudentDto"/>
</typeAliases>
<select id="getStudentsByAlias" resultType="student" useCache="false" >
    select id,name,age from student
</select>

可以直接进行查询操作

6.plugins插件配置

Mybatis进行插件配置。读取插件信息并注册到拦截器链中

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        // 遍历插件 其实是拦截器 可以拦截Executor ParameterHandler ResultSetHandler StatementHandler的部分方法
        // Executor:执行sql语句的地方
        // ParameterHandler: 参数处理
        // ResultSetHandler: 结果集处理
        for (XNode child : parent.getChildren()) {
            // 获取<plugin>标签的interceptor属性
            // 如果需要自定义一个插件,要继承Interceptor接口
            String interceptor = child.getStringAttribute("interceptor");
            // 获取拦截器的属性信息,转为Properties对象
            Properties properties = child.getChildrenAsProperties();
            // 创建拦截器实例 这里可以通过别名来进行配置
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            // 设置拦截器实例属性信息
            interceptorInstance.setProperties(properties);
            // 将拦截器实例添加到拦截器链中
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

实现一个分页插件

package org.apache.ibatis.mytest;




import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.testcontainers.shaded.com.google.common.base.Joiner;

import java.util.Properties;

/**
 * @author yangqiang
 * @date 2021-07-26 20:57
 */
@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PageInterceptor implements Interceptor {
    //对应上面args的序号
    private static final Integer MAPPED_STATEMENT_INDEX = 0 ;
    private static final Integer PARAMTER_INDEX = 1;
    private static final Integer ROWBOUNDS_INDEX = 2;
    private static final Integer RESULT_HANDLER_INDEX = 3;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //从invocation 获取参数
        Object[] queryArgs = invocation.getArgs();
        MappedStatement ms =  (MappedStatement)queryArgs[MAPPED_STATEMENT_INDEX];
        Object parameter = queryArgs[PARAMTER_INDEX];
        Page page = PageUtil.getPaingParam();
        if(page != null){
            //重新构造新的sql select * from ** where  limit * *
            BoundSql boundSql = ms.getBoundSql(parameter);

            String pagingSql = getPagingSql(boundSql.getSql(), page.getOffset(), page.getLimit());
            BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), pagingSql, boundSql.getParameterMappings(), boundSql.getParameterObject());

            queryArgs[MAPPED_STATEMENT_INDEX] = newMappedStatement(ms,newBoundSql);

            //重置RowBounds
            queryArgs[ROWBOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);

        }
        log.info("自定义分页插件执行");
        Object result = invocation.proceed();
        PageUtil.removePagingParam();
        return result;
    }
    public String getPagingSql(String sql, int offset, int limit) {
        StringBuilder result = new StringBuilder(sql.length() + 100);
        result.append(sql).append(" limit ");

        if (offset > 0) {
            result.append(offset).append(",").append(limit);
        }else{
            result.append(limit);
        }
        return result.toString();
    }
    /**
     * 重新构建 MappedStatement
     * 把修改后的 BoundSql覆盖原有的 BoundSql
     * @param ms 原来的 MappedStatement
     * @param newBoundSql 新的 BoundSql
     * @return 新的MappedStatement
     */
    private MappedStatement newMappedStatement(MappedStatement ms, BoundSql newBoundSql) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(),
                new BoundSqlSource(newBoundSql), ms.getSqlCommandType());
        builder.keyColumn(delimitedArrayToString(ms.getKeyColumns()));
        builder.keyGenerator(ms.getKeyGenerator());
        builder.keyProperty(delimitedArrayToString(ms.getKeyProperties()));
        builder.lang(ms.getLang());
        builder.resource(ms.getResource());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultOrdered(ms.isResultOrdered());
        builder.resultSets(delimitedArrayToString(ms.getResultSets()));
        builder.resultSetType(ms.getResultSetType());
        builder.timeout(ms.getTimeout());
        builder.statementType(ms.getStatementType());
        builder.useCache(ms.isUseCache());
        builder.cache(ms.getCache());
        builder.databaseId(ms.getDatabaseId());
        builder.fetchSize(ms.getFetchSize());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        return builder.build();
    }
    public String delimitedArrayToString(String[] array) {

        if (array == null || array.length == 0) {
            return "";
        }
        Joiner joiner = Joiner.on(",");
        return joiner.join(array);
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}
package org.apache.ibatis.mytest;

/**
 * @author yangqiang
 * @date 2021-07-26 20:59
 */
public class PageUtil {
    private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

    public static void setPagingParam(int offset, int limit) {
        Page page = new Page(offset, limit);
        LOCAL_PAGE.set(page);
    }

    public static void removePagingParam() {
        LOCAL_PAGE.remove();
    }

    public static Page getPaingParam() {
        return LOCAL_PAGE.get();
    }

}
package org.apache.ibatis.mytest;

import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.SqlSource;

/**
 * @author yangqiang
 * @date 2021-07-26 21:00
 */

public class BoundSqlSource implements SqlSource {
    public BoundSql boundSql;

    public BoundSqlSource(BoundSql boundSql) {
        this.boundSql = boundSql;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        return boundSql;
    }
}
package org.apache.ibatis.mytest;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * @author yangqiang
 * @date 2021-07-26 20:59
 */

@Data
@AllArgsConstructor
public class Page {
    /**
     * 从第几条数据开始查询
     * */
    public Integer offset;
    /**
     * 查询多少条数据
     * */
    public Integer limit;
}

测试

@Test
public void readerTest() throws IOException {
    PageUtil.setPagingParam(1,5);
    //得到一个Reader处理类(字符流)
    try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/mytest/mybatis-config.xml")) {
        // environment:数据源ID 在mybatis-config.xml <environments> 标签里面配置的 id 为development的数据源信息(非必传)
        // properties :配置文件信息。一般是数据库配置信息。可以在这里进行配置 也可以在mybatis-config.xml 通过<properties> 标签进行引入properties配置文件
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,"development", Resources.getResourceAsProperties("org/apache/ibatis/mytest/db.properties"));
    }
    SqlSession sqlSession = sqlSessionFactory.openSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    List<StudentDto> rs = mapper.getStudentsByAlias();
    System.out.println(rs.toString());
}

结果

[StudentDto(id=2, name=mybatis, age=10), StudentDto(id=3, name=mybatis, age=10), StudentDto(id=4, name=mybatis, age=10), StudentDto(id=5, name=mybatis, age=10), StudentDto(id=6, name=mybatis, age=10)]

可以看到分页功能已经成功。

7.对象工厂,可以自定义,已有默认实现

MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。 如果想覆盖对象工厂的默认行为,则可以通过创建自己的对象工厂来实现。

这一步读取objectFactory标签的自定义的对象工厂 并设置为configuration的对象工厂

自定义一个对象工厂。在创建结果对象的时候做一个输出

xml配置 提前配置了别名
 <typeAliases>
        <package name="org.apache.ibatis.mytest"/>
 </typeAliases>
<objectFactory type="MyObjectFactory"></objectFactory>
package org.apache.ibatis.mytest;

import org.apache.ibatis.reflection.factory.DefaultObjectFactory;

import java.util.*;

/**
 * @author yangqiang
 * @date 2021-07-27 17:41
 */
public class MyObjectFactory extends DefaultObjectFactory {
    private static final long serialVersionUID = -8855120656740914948L;

    @Override
    public <T> T create(Class<T> type) {
        //会调用下一个create方法进行创建
        return super.create(type);
    }

    @Override
    public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        System.out.println("create bean "+ type.getSimpleName());
        return super.create(type, constructorArgTypes, constructorArgs);
    }

    /**判断集合类型参数*/
    @Override
    public <T> boolean isCollection(Class<T> type) {
        boolean retBool = false;
        System.out.println(type.getSimpleName()+"  is collection?"+(retBool=super.isCollection(type)));
        return retBool;
    }

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

测试

@Test
public void readerTest() throws IOException {
    PageUtil.setPagingParam(1,5);
    //得到一个Reader处理类(字符流)
    try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/mytest/mybatis-config.xml")) {
        // environment:数据源ID 在mybatis-config.xml <environments> 标签里面配置的 id 为development的数据源信息(非必传)
        // properties :配置文件信息。一般是数据库配置信息。可以在这里进行配置 也可以在mybatis-config.xml 通过<properties> 标签进行引入properties配置文件
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,"development", Resources.getResourceAsProperties("org/apache/ibatis/mytest/db.properties"));
    }
    SqlSession sqlSession = sqlSessionFactory.openSession();

    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    List<StudentDto> rs = mapper.getStudentsByAlias();
    System.out.println(rs.toString());
}

结果

List  is collection?true
create bean List
create bean StudentDto
create bean StudentDto
create bean StudentDto
create bean StudentDto
create bean StudentDto
[StudentDto(id=2, name=mybatis, age=10), StudentDto(id=3, name=mybatis, age=10), StudentDto(id=4, name=mybatis, age=10), StudentDto(id=5, name=mybatis, age=10), StudentDto(id=6, name=mybatis, age=10)]

因为整个集合的长度是5.所以该StudentDto对象被创建了5次。也可以自己去扩展其他功能。

8.对象包装工厂

这个以后再研究

9.反射工厂

因为加载配置文件中的各种插件类等等,为了提供更好的灵活性,mybatis支持用户自定义反射工厂,不过总体来说,用的不多,要实现反射工厂,只要实现ReflectorFactory接口即可。默认的反射工厂是DefaultReflectorFactory。一般来说,使用默认的反射工厂就可以了。

10.拿settings解析的属性,给configuration对象赋值

解析setting标签下的配置到configuration中

private void settingsElement(Properties props) {
    configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    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"), false));
    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.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
    configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
    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.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
    configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
    configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
    configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
    configuration.setLogPrefix(props.getProperty("logPrefix"));
    configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
    configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
    configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
}

11.environments环境配置读取

加载指定的数据源或者默认的数据源信息以及事务工厂

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        // 默认环境就是default,如果不指定的话
        if (environment == null) {
            environment = context.getStringAttribute("default");
        }
        // 遍历每个env环境
        for (XNode child : context.getChildren()) {
            // 获取id
            String id = child.getStringAttribute("id");
            // 如果id匹配上了上面的environment 则加载这个环境的配置
            if (isSpecifiedEnvironment(id)) {
                // 获取事务工厂 MyBatis有两种,JDBC和MANAGED
                // JDBC:使用JDBC的事务
                // MANAGED:事务委托给容器
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // 数据源配置
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                DataSource dataSource = dsFactory.getDataSource();
                // 构建Environment对象 需要事务以及数据源
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                        .transactionFactory(txFactory)
                        .dataSource(dataSource);
                // 把Environment对象塞进Configuration中
                // 1、configuration --> Environment ---> DataSource ---> 。。。。。
                // 2、配置文件和实体类的定义层次结构是一致的,
                // todo 1 / 2 ===》 Configuration类对应的就是我们的 mybatis-config.xml 配置文件,
                //             我们配置的信息经过解析后就会存储到Configuration的成员变量中。
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

12.databaseIdProvider数据库配置

private void databaseIdProviderElement(XNode context) throws Exception {
  DatabaseIdProvider databaseIdProvider = null;
  if (context != null) {
    String type = context.getStringAttribute("type");
    // awful patch to keep backward compatibility
    if ("VENDOR".equals(type)) {
      type = "DB_VENDOR";
    }
    Properties properties = context.getChildrenAsProperties();
    databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
    databaseIdProvider.setProperties(properties);
  }
  Environment environment = configuration.getEnvironment();
  if (environment != null && databaseIdProvider != null) {
    String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
    configuration.setDatabaseId(databaseId);
  }
}

MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。

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

<databaseIdProvider type="DB_VENDOR">
    <property name="MySQL" value="mysql"/>
    <property name="Oracle" value="oracle"/>
</databaseIdProvider>
<select id="getStudentsByAlias" resultType="StudentDto" useCache="false" databaseId="mysql">
    select id,name,age from student
</select>
<select id="getStudentsByAlias" resultType="StudentDto" useCache="false">
    select id,name,age from student limit 1
</select>

idea对于这种标签ID相同的sql会标红但是可以正常执行。这样是不是说明sql标签的ID可以重复? 后面看到sqlMapper的解析

13.typeHandlers类型处理器处理,在这里,mybatis已经把基本的都封装好了,我们可以往里面替换

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 {
                String javaTypeName = child.getStringAttribute("javaType");
                String jdbcTypeName = child.getStringAttribute("jdbcType");
                String handlerTypeName = child.getStringAttribute("handler");
                Class<?> javaTypeClass = resolveClass(javaTypeName);
                JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
                Class<?> typeHandlerClass = resolveClass(handlerTypeName);
                if (javaTypeClass != null) {
                    if (jdbcType == null) {
                        typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                    } else {
                        typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                    }
                } else {
                    typeHandlerRegistry.register(typeHandlerClass);
                }
            }
        }
    }
}

接下来自己定义一个TypeHandler

实现将java集合类型以字符的形式存储到mysql数据库。如[1,2,3,4]->'1,2,3,4'

1.自定义一个TypeHandler
package org.apache.ibatis.mytest;

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

import java.util.*;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.stream.Collectors;

/**
 * @author yangqiang
 * @date 2021-07-29 14:27
 */
public class MyTypeHandler implements TypeHandler<List<Integer>> {

    @Override
    public void setParameter(PreparedStatement ps, int i, List<Integer> parameter, JdbcType jdbcType) throws SQLException {
        StringBuffer sb = new StringBuffer();
        for (Integer s : parameter) {
            sb.append(s).append(",");
        }
        ps.setString(i, sb.substring(0,sb.length()-1).toString());
    }

    @Override
    public List<Integer> getResult(ResultSet rs, String columnName) throws SQLException {
        String string = rs.getString(columnName);
        if(string!=null){
            return Arrays.stream(string.split(",")).map(Integer::valueOf).collect(Collectors.toList());
        }
        return null;
    }

    @Override
    public List<Integer> getResult(ResultSet rs, int columnIndex) throws SQLException {
        String string = rs.getString(columnIndex);
        if(string!=null){
            return Arrays.stream(string.split(",")).map(Integer::valueOf).collect(Collectors.toList());
        }
        return null;
    }

    @Override
    public List<Integer> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String string = cs.getString(columnIndex);
        if(string!=null){
            return Arrays.stream(string.split(",")).map(Integer::valueOf).collect(Collectors.toList());
        }
        return null;
    }
}
2.在mybatis-config.xml中声明该类型转换
<typeAliases>
    <package name="org.apache.ibatis.mytest"/>
</typeAliases>
<typeHandlers>
    <typeHandler handler="MyTypeHandler" javaType="java.util.List" jdbcType="VARCHAR"/>
</typeHandlers>
3.定义新增和查询接口
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "./mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.mytest.StudentMapper">


    <resultMap id="studentDto" type="StudentDto">
        <result column="id" jdbcType="INTEGER" property="id"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
        <result column="age" jdbcType="INTEGER" property="age"/>
        <result column="hobby" jdbcType="VARCHAR" property="hobby" />
    </resultMap>
    <select id="getStudentsByAlias" resultType="studentDto" useCache="false" databaseId="mysql">
        select id,name,age,hobby from student
    </select>
    <select id="getStudentsByAlias" resultType="studentDto" useCache="false">
        select id,name,age,hobby from student limit 1
    </select>
    <insert id="insertStudent" parameterType="StudentDto" databaseId="mysql">
        insert into student (id,name,age,hobby) values(
          #{id},
          #{name},
          #{age},
          #{hobby}
        )
    </insert>
</mapper>

这里我在编译源码的时候。发现不在xml字段上加上TypeHandler属性也是可以翻译过来的! 这个在后面解析mapper的时候在看。结论是:
1.对于自定义的类型转换配置来说 例如只配置了TypeHandler类型转换的话。对于所有实体类中的类型为List《Integer》的都会走这个类型转换。和数据库进行操作时都会转换为String类型存储。查询时会将字符转换为List 2.对于针对同一个JdbcType配置了多个TypeHandler的类型来说。必须显示的指定对应字段jdbcType或者是TypeHandler以便找到对应的TypeHandler 在14分析这块

4.测试
@Test
public void readerTest() throws IOException {
    PageUtil.setPagingParam(1,5);
    //得到一个Reader处理类(字符流)
    try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/mytest/mybatis-config.xml")) {
        // environment:数据源ID 在mybatis-config.xml <environments> 标签里面配置的 id 为development的数据源信息(非必传)
        // properties :配置文件信息。一般是数据库配置信息。可以在这里进行配置 也可以在mybatis-config.xml 通过<properties> 标签进行引入properties配置文件
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,"development", Resources.getResourceAsProperties("org/apache/ibatis/mytest/db.properties"));
    }
    SqlSession sqlSession = sqlSessionFactory.openSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    List<StudentDto> rs = mapper.getStudentsByAlias();
    System.out.println(rs.toString());
    int count = mapper.insertStudent(new StudentDto(11, "smallwhite", 24, Arrays.asList(1, 2, 3, 4, 5)));
    sqlSession.commit();
    System.out.println(count);
}
5.结果
[StudentDto(id=2, name=mybatis, age=10, hobby=[1, 2, 3, 4]), StudentDto(id=3, name=mybatis, age=10, hobby=[1, 2, 3, 4]), StudentDto(id=4, name=mybatis, age=10, hobby=[1, 2, 3, 4]), StudentDto(id=5, name=mybatis, age=10, hobby=[1, 2, 3, 4]), StudentDto(id=6, name=mybatis, age=10, hobby=[1, 2, 3, 4]), StudentDto(id=7, name=mybatis, age=10, hobby=[1, 2, 3, 4]), StudentDto(id=8, name=mybatis, age=10, hobby=[1, 2, 3, 4]), StudentDto(id=9, name=mybatis, age=10, hobby=[1, 2, 3, 4]), StudentDto(id=10, name=mybatis, age=10, hobby=[1, 2, 3, 4]), StudentDto(id=11, name=yangqiang, age=24, hobby=[1, 2, 3, 4, 5])]

14.重点,这里是解析mapper映射的,所有的sql之类的都会在这里进行解析

配Mappers标签有几种方式

1.根据包名映射<package name="org.apache.ibatis.mytest"/>

这种方式需要保证xml和mapper文件在同一个目录下。否则会加载不到xml文件。 首先解析的时候会去找到这个包下面的所有接口类。把接口类进行解析。解析类似@Select这种的sql标签信息。 再去找同目录下的xml文件进行解析,如果没有xml文件的话,解析结束。也不能在mappers标签里面再加一个mapper标签进行xml的单独解析。因为mapper文件解析完了之后会存储在一个hashMap中。再次解析会报错。

2.根据xml文件映射<mapper resource="org/apache/ibatis/parsing/StudentMapper.xml"/>

这种不要求xml和mapper在同一文件夹下。

15.获得sqlSessionFactory工厂

public SqlSessionFactory build(Configuration config) {
    // 返回默认的SqlSessionFactory对象,需要传入配置信息
    //获得sqlSessionFactory工厂,再从工厂中获得sqlSession
    return new DefaultSqlSessionFactory(config);
}