Mybatis源码解析 第一章 初始化 SqlSessionFactory

92 阅读6分钟

第一章 初始化 SqlSessionFactory 类

前言

1,创建一个Spring项目
2,引入Mybatis并且配置好Mysql地址
3,创建一个MyBatisTest类

1.0 创建测试类

@Slf4j
@SpringBootTest
public class MyBatisTest {

    @Autowired
    private MyBatisService myBatisService;

    @Test
    void init() {
        MyBatis myBatis = myBatisService.getOne(1L);
        log.info("MyBatis::=>{}", JSON.toJSON(myBatis));
    }

    public SqlSessionFactory getSqlSessionFactory() throws IOException {
        //注意此处路径不要写错
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        return new SqlSessionFactoryBuilder().build(inputStream);
    }

    /**
     * 1、根据xml配置文件(全局配置文件)创建一个SqlSessionFactory对象
     * 2、sql映射文件;配置了每一个sql,以及sql的封装规则等。
     * 3、将sql映射文件注册在全局配置文件中
     * 4、写代码:
     * 1)、根据全局配置文件得到SqlSessionFactory;
     * 2)、使用sqlSession工厂,获取到sqlSession对象使用他来执行增删改查
     * 一个sqlSession就是代表和数据库的一次会话,用完关闭
     * 3)、使用sql的唯一标志来告诉MyBatis执行哪个sql。sql都是保存在sql映射文件中的。
     * @throws IOException
     */
    @Test
    void test() throws IOException {
        //1、获取SqlSessionFactory实例
        // XMLConfigBuilder装配mybatis-config.xml到Configuration中的environment对象里去
        // XMLMapperBuilder装配xxMapper.xml文件的select|insert|update|delete到Configuration中的mappedStatements对象里去
        // XMLMapperBuilder装配xxMapper.xml文件的resultMap到Configuration中的resultMaps对象里去
        // SqlSessionFactory 取默认的实现类 DefaultSqlSessionFactory
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        //2、打开一个会话
        // try (SqlSession openSession = sqlSessionFactory.openSession()) 会自动关闭调openSession
        // 这里SqlSession取的也是默认的:DefaultSqlSession
        try (SqlSession openSession = sqlSessionFactory.openSession()) {
            // 3、获取接口的实现类对象,会为接口自动的创建一个代理对象,代理对象去执行增删改查方法
            // 获取到的代理对象中好像并没有对方法做什么操作,只是原味的方法
            MyBatisMapper mapper = openSession.getMapper(MyBatisMapper.class);
            //org.apache.ibatis.binding.MapperProxy@29ebbdf4 获取@Mapper的代理对象,使用代理对象做查询
            // 疑问,代理对象调用方法时,又谁代理的呢?
            MyBatis myBatis = mapper.getOne(1L);
            log.info("MyBatis::=>{}", JSON.toJSON(myBatis));
        }
        //4、使用完毕后关闭会话
    }
}

SqlSessionFactory 的主要作用是创建SqlSession,同时初始化mybatis所需要的各种基础配置

1.1 加载配置类并初始化 SqlSessionFactory

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

build 方法做的事

/**
 * environment = null
 * properties = null
 */
public SqlSessionFactory build(InputStream inputStream,String environment,Properties properties){

    try{
    // 这里加载配置类
    XMLConfigBuilder parser=new XMLConfigBuilder(inputStream,environment,properties);
    // parser 作用是个解析器
    return build(parser.parse());
    }
 }

new XMLConfigBuilder(inputStream, environment, properties) 做的事

private XMLConfigBuilder(XPathParser parser,String environment,Properties props){
    // 初始化配置项
    super(new Configuration());
    // mybatis放在threadlocal中的异常处理类
    ErrorContext.instance().resource("SQL Mapper Configuration");
    // 此时props还是null
    this.configuration.setVariables(props);
    // 标记是否解析过
    this.parsed=false;
    // 设置环境 此时也为null
    this.environment=environment;
    // 设置解析器 (到此为止已经初始化成功了一个XMLConfigBuilder)
    this.parser=parser;
}

super(new Configuration()); 做的事

/**
 * 这里的 new Configuration() 是实例化了一个mybatis的配置类,里面包含了很多mybatis需要使用的基础信息
 * 比如下面的:
 * configuration.getTypeAliasRegistry() 注册类型别名
 * 就是将 Configuration中的TypeAliasRegistry属性中的map赋值,map里包含了各种各样的java类型
 * configuration.getTypeHandlerRegistry() 注册处理器
 * 处理器是mybatis针对上述每种类型都设置了对应的处理机制,每个类型都有其处理器
 */
public BaseBuilder(Configuration configuration){
        this.configuration=configuration;
        this.typeAliasRegistry=this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry=this.configuration.getTypeHandlerRegistry();
        }

build(parser.parse()) 做的事

// 会去解析配置,返回一个Configuration对象
parser.parse();

public Configuration parse(){
        // 上面初始化方法已经设置了为 parsed = false
        // 如果已经解析过则抛出重复解析异常
        if(parsed){
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed=true;
        /**
         * parser.evalNode("/configuration")
         * 这里解析出来的是 mybatis-config.xml中的所有配置的标签和值:
         * <configuration>
         <environments default="mysql">
         <environment id="mysql">
         <transactionManager type="JDBC"/>
         <dataSource type="POOLED">
         <property name="driver" value="com.mysql.jdbc.Driver"/>
         <property name="url" value="jdbc:mysql://localhost:3306/path2java?useUnicode=true"/>
         <property name="username" value="root"/>
         <property name="password" value="root"/>
         </dataSource>
         </environment>
         </environments>
         <mappers>
         <mapper resource="mapper/AdvertiseSiteMapper.xml"/>
         </mappers>
         </configuration>
         进入evalNode()方法中后,会有一个document对象,是在创建XMLConfigBuilder时初始化的
         */
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
        }

private void parseConfiguration(XNode root){
        try{
        // 读取 mybatis-config.xml中的environments标签,并将其配置放入环境信息 environment 这个变量里
        environmentsElement(root.evalNode("environments"));
        // 读取 mappers 标签的内容,将xml文件加入配置
        mapperElement(root.evalNode("mappers"));
        }catch(Exception e){
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: "+e,e);
        }
        }

xxMapper.xml 对应的解析类是 XMLMapperBuilder

mapperElement 解析各种mapper.xml配置 XMLMapperBuilder 类是专门解析 **mapper.xml文件的

if(resource!=null&&url==null&&mapperClass==null){
        ErrorContext.instance().resource(resource);
        InputStream inputStream=Resources.getResourceAsStream(resource);
        XMLMapperBuilder mapperParser=new XMLMapperBuilder(inputStream,configuration,resource,configuration.getSqlFragments());
        mapperParser.parse();
        }

mapperParser.parse();

public void parse(){
        // 判断此xml是否被加载过
        if(!configuration.isResourceLoaded(resource)){
        // 加载mapper文件
        configurationElement(parser.evalNode("/mapper"));
        // 放入已加载列表
        configuration.addLoadedResource(resource);
        // 绑定带有@Mapper的接口 Configuration类中有个方法,叫hasMapper,是用来校验@mapper有没有被解析过的
        // hasMapper 会调用Configuration类中mapperRegistry属性的hasMapper方法,而这个方法内部,是去请求
        // MapperRegistry 对象中的knownMappers属性,knownMappers属性中存放的是当前所有被映射过的mapper
        // MapperRegistry::hasMapper() -> 
        // public <T> boolean hasMapper(Class<T> type) {
        //    return knownMappers.containsKey(type);
        // }
        // 以加载的类名,也会放到Configuration类中loadedResources属性里,这里是一个Set<String>
        // 值是@mapper的全路径名:cn.kgm.mapper.MyBatisMapper
        // 这些都做完了,会将@mapper对象放入Configuration->MapperRegistry->knowsMappers属性里并且会包装一层,放入MapperProxyFactory对象里
        // 最后根据@mapper.class获取实体类的时候也是从MapperProxyFactory中实例化的
        // knownMappers.put(type, new MapperProxyFactory<T>(type));
        //    class MapperProxyFactory{
        //        private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
        //    }
        bindMapperForNamespace();
        }
        }

configurationElement(parser.evalNode("/mapper"));

 private void configurationElement(XNode context){
        try{
        // 获取@mapper的地址
        String namespace=context.getStringAttribute("namespace");
        if(namespace==null||namespace.equals("")){
        throw new BuilderException("Mapper's namespace cannot be empty");
        }
        // 构建器助手设置@mapper的路径
        builderAssistant.setCurrentNamespace(namespace);
        // 获取缓存
        cacheRefElement(context.evalNode("cache-ref"));
        // 获取缓存配置
        cacheElement(context.evalNode("cache"));
        // 获取 <paramentMap>
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        // 获取资源映射关系: 
        // <resultMap id="BaseResultMap" type="cn.kgm.entity.MyBatis">
        //    <id column="id" property="id"/>
        //    <id column="content" property="content"/>
        //    <result column="create_time" property="createTime"/>
        // </resultMap>
        // 经过resultMapElements()方法解析过的resultMap配置,会先放入XMLMapperBuilder的助手类
        // MapperBuilderAssistant中,然后再由MapperBuilderAssistant中::addResultMap()方法,将
        // 解析出来的List<ResultMapping>对象放入Configuration类中的resultMaps属性中
        //protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
        // StrictMap MyBatis的严格map,获取和插入时不允许为null值
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 获取预置文件:
        //    <sql id="BaseColumnList">
        //       id,content,create_time
        //    </sql>
        sqlElement(context.evalNodes("/mapper/sql"));
        // 获取四种标签并解析,解析完成后形成MappedStatement对象,放到Configuration对象中的MappedStatement对象中
        // protected final Map<String, MappedStatement> mappedStatements =
        // new StrictMap<MappedStatement>("Mapped Statements collection");
        // public void addMappedStatement(MappedStatement ms) {
        //    mappedStatements.put(ms.getId(), ms);
        // }
        // key是@mapper接口中方法的全路径名加方法名:cn.kgm.mapper.MyBatisMapper.getOne,
        // value 是MappedStatement对象
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        }catch(Exception e){
        throw new BuilderException("Error parsing Mapper XML. Cause: "+e,e);
        }
        }

openSession.getMapper(MyBatisMapper.class);

    //此方法旨在根据@mapper.class获取其代理对象,而最后返回的也是一个被MapperProxy包裹的代理对象
//MapperProxy继承了InvocationHandler,在调用Proxy.newInstance时传入MapperProxy包裹的对象,可以通过invoke方法对@mapper中的方法做代理
    MyBatisMapper mapper=openSession.getMapper(MyBatisMapper.class);

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

MyBatis myBatis = mapper.getOne(1L);

    // 前面已经看到了,mapper对象是被MapperProxy类包裹过的,所以请求mapper对象的任何方法时,都会走一遍MapperProxy.invoke方法做代理
@Override
public Object invoke(Object proxy,Method method,Object[]args)throws Throwable{
        ...
// 检查是否已经缓存过此方法,如果没有缓存过,则methodCache.put(method, mapperMethod);
// methodCache 是MapperProxy类中的一个属性
final MapperMethod mapperMethod=cachedMapperMethod(method);
        // 执行SQL
        return mapperMethod.execute(sqlSession,args);
        }

执行SQL mapperMethod.execute(sqlSession, args)

public Object execute(SqlSession sqlSession,Object[]args){
        Object result;
        switch(command.getType()){
        case INSERT:{
        ...
        }
        case UPDATE:{
        ...
        }
        case DELETE:{
        ...
        }
        case SELECT:
        // 没有返回值或者设置了返回值处理器
        if(method.returnsVoid()&&method.hasResultHandler()){
        executeWithResultHandler(sqlSession,args);
        result=null;
        }
        else if(method.returnsMany()){
        // 返回的是list
        result=executeForMany(sqlSession,args);
        }else if(method.returnsMap()){
        // 返回的map?
        result=executeForMap(sqlSession,args);
        }else if(method.returnsCursor()){
        // 返回的指针?
        result=executeForCursor(sqlSession,args);
        }else{
        // 自定义返回值
        // 解析参数,并设置一个以param+数字的变量,例如只传id属性时,param中就有两个属性
        // {
        //     "id":1,
        //     "param1":1
        // }
        Object param=method.convertArgsToSqlCommandParam(args);
        // 这里的selectOne到后面还是selectList,只不过get(0)了
        result=sqlSession.selectOne(command.getName(),param);
        }
        break;
        case FLUSH:
        ...
default:
        throw new BindingException("Unknown execution method for: "+command.getName());
        }
        if(result==null&&method.getReturnType().isPrimitive()&&!method.returnsVoid()){
        throw new BindingException("Mapper method '"+command.getName()
        +" attempted to return null from a method with a primitive return type ("+method.getReturnType()+").");
        }
        return result;
        }

问题和解答

  • MyBatis是如何判断一个xxMapper.xml文件有没有加载的?

Configuration 中有个 Set 类型属性
protected final Set loadedResources = new HashSet();
已加载的xxMapper.xml文件的全路径名会被当做一个Key放进去,每次解析xxMapper.xml时都会判断这个Set中是否已经存在此路径
具体代码在 XMLMapperBuilder::parse()方法中
if (!configuration.isResourceLoaded(resource)){...}
xxMapper.xml解析完成后下面有段代码会将xxMapper.xml的全路径名放入Set中
configuration.addLoadedResource(resource);