mybatis学习笔记--2、Mybatis启动流程

Mapper是如何执行Sql方法的

blogMapper.selectBlog(int blogId)是如何执行的

通常我们使用原生的Mybatis的步骤为:

  • 构建 SqlSessionFactoryBuilder
  • 根据SqlSessionFactoryBuilder生成SqlSessionFactory
  • 从 SqlSessionFactory 中获取 SqlSession
  • 从SqlSession 中获取 Mapper
    调用 Mapper 的方法 ,例如:blogMapper.selectBlog(int blogId)
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
TAccountDao accountDao = sqlSession.getMapper(TAccountDao.class);
TAccount account = accountDao.queryById(1);
sqlSession.commit();
sqlSession.close();
System.out.println(account.getAccountName());

读取配置文件

InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");

这里面定义了mybatis启动需要加载的数据源,加载哪些XML格式的mapper文件,日志打印级别,需要加载哪些插件等。

<configuration>
    <settings>
        <setting name="logImpl" value="NO_LOGGING"/>
    </settings>
    <environments default="DEV">
        <environment id="">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://192.168.101.130:13306?serverTimeZone=GMT+8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456789"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/TAccountDao.xml"/>
    </mappers>
</configuration>

创建工厂构建器SqlSessionFactoryBuilder

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  • 主要有2种构建方式。一种是使用InputStream构建,一种是指定好Reader。当然使用已经存在的Configuration也是可以的。
  • 同时我们还可以根据Environment配置多个环境的配置文件。方便我们进行切换,或者用于配置多数据源也可以。
    其内部使用XMLConfigBuilder,解析xml格式的配置文件。把XML中定义的每种标签内容解析出来,放到Configuration中
  • 最后呀,实际上生成的还是DefaultSqlSessionFactory。如果我们要实现按照自己的方式打开session,重写SqlSessionFactoryBuilder的build方法
//以下3个方法都是调用下面第4种方法
public SqlSessionFactory build(Reader reader);
public SqlSessionFactory build(Reader reader, String environment);
public SqlSessionFactory build(Reader reader, Properties properties);
public SqlSessionFactory build(Reader reader, String environment, Properties properties);
//以下3个方法都是调用下面第8种方法
public SqlSessionFactory build(InputStream inputStream);
public SqlSessionFactory build(InputStream inputStream, String environment);
public SqlSessionFactory build(InputStream inputStream, Properties properties)
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);
//最后一个build方法使用了一个Configuration作为参数,并返回DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config);
//第8种方法和第4种方法差不多,Reader换成了InputStream
public SqlSessionFactory build(InputStream inputStream, 
    String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = 
            new XMLConfigBuilder(inputStream, environment, properties);
        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.
        }
    }
}

//最后一个build方法使用了一个Configuration作为参数,并返回DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

我们简单看一下XMLConfigBuilder.parse()干了些什么事情。如下所示,可以看见它解析XML文件中的每个节点。然后把值存储在Configuration中。

    //解析配置
    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
            //1.properties
            propertiesElement(root.evalNode("properties"));
            //2.类型别名
            typeAliasesElement(root.evalNode("typeAliases"));
            //3.插件
            pluginElement(root.evalNode("plugins"));
            //4.对象工厂
            objectFactoryElement(root.evalNode("objectFactory"));
            //5.对象包装工厂
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            //6.设置
            settingsElement(root.evalNode("settings"));
            // read it after objectFactory and objectWrapperFactory issue #631
            //7.环境
            environmentsElement(root.evalNode("environments"));
            //8.databaseIdProvider
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            //9.类型处理器
            typeHandlerElement(root.evalNode("typeHandlers"));
            //10.映射器解析
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }

创建Session–打开Session

SqlSession sqlSession = sqlSessionFactory.openSession();

接下来就是打开和数据库的连接,从而能将sql语句提交到数据库执行。我们来看一下它是打开Session的方式。

    //8个方法可以用来创建SqlSession实例
    SqlSession openSession();

    //自动提交
    SqlSession openSession(boolean autoCommit);

    //连接
    SqlSession openSession(Connection connection);

    //事务隔离级别
    SqlSession openSession(TransactionIsolationLevel level);

    //执行器的类型
    SqlSession openSession(ExecutorType execType);

    SqlSession openSession(ExecutorType execType, boolean autoCommit);

    SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

    SqlSession openSession(ExecutorType execType, Connection connection);

    Configuration getConfiguration();

  • 首先是是否可以自动提交。如果是自动提交,盲猜在关闭session前会commit
  • 复用已经打开的连接:Connection
  • 因为mybatis有3种处理模式:SIMPLE,BATCH,REUSE。所以还可以在打开时指定模式,默认是SIMPLE。
  • 除此外,还支持指定事务级别来打开连接。这个一般都不会这样做吧,都是使用数据库的事务级别。

其内部的具体实现如下:分别是复用Connectioin建立session以及通过定义的数据源来建立session。他们的主要区别也在于事务创建的方式。具体步骤为:

  • 产生事务
  • 生成执行器
  • 构建DefaultSqlSession
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
            final Environment environment = configuration.getEnvironment();
            final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
            //通过事务工厂来产生一个事务
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            //生成一个执行器(事务包含在执行器里)
            final Executor executor = configuration.newExecutor(tx, execType);
            //然后产生一个DefaultSqlSession
            return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
            //如果打开事务出错,则关闭它
            closeTransaction(tx); // may have fetched a connection so lets call close()
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
            //最后清空错误上下文
            ErrorContext.instance().reset();
        }
    }

    private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
        try {
            boolean autoCommit;
            try {
                autoCommit = connection.getAutoCommit();
            } catch (SQLException e) {
                // Failover to true, as most poor drivers
                // or databases won't support transactions
                autoCommit = true;
            }
            final Environment environment = configuration.getEnvironment();
            final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
            final Transaction tx = transactionFactory.newTransaction(connection);
            final Executor executor = configuration.newExecutor(tx, execType);
            return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

获取Mapper代理类

TAccountDao accountDao = sqlSession.getMapper(TAccountDao.class);

UML图如下:
创建Mapper代理类的UML图

DefaultSqlSession
    @Override
    public <T> T getMapper(Class<T> type) {
        //最后会去调用MapperRegistry.getMapper
        return configuration.<T>getMapper(type, this);
    }

SqlSession中获取Mapper,实际上是通过Configuration来获取。每次开启Session会单独生成一个SqlSession,但是Configuration只会由配置生成一个。这就有效利用缓存。
Configuration的生成时机是在解析配置(XMLConfigBuilder.parse())后生成。还记得吧。

Configuration
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }

使用Mapper注册机对象进行生成。

MapperRegistry
    //返回代理类
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }

注册机根据类获取它的代理类工厂。这里的knownMappers缓存,是通过addMapper方法逐个加入的。那么什么时候addMapper。还记得上面的parse吗?

XMLConfigBuilder.mapperElement

    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            for (XNode child : parent.getChildren()) {
                if ("package".equals(child.getName())) {
                    //10.4自动扫描包下所有映射器
                    String mapperPackage = child.getStringAttribute("name");
                    configuration.addMappers(mapperPackage);
                } else {
                    String resource = child.getStringAttribute("resource");
                    String url = child.getStringAttribute("url");
                    String mapperClass = child.getStringAttribute("class");
                    if (resource != null && url == null && mapperClass == null) {
                        //10.1使用类路径
                        ErrorContext.instance().resource(resource);
                        InputStream inputStream = Resources.getResourceAsStream(resource);
                        //映射器比较复杂,调用XMLMapperBuilder
                        //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                        mapperParser.parse();
                    } else if (resource == null && url != null && mapperClass == null) {
                        //10.2使用绝对url路径
                        ErrorContext.instance().resource(url);
                        InputStream inputStream = Resources.getUrlAsStream(url);
                        //映射器比较复杂,调用XMLMapperBuilder
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                        mapperParser.parse();
                    } else if (resource == null && url == null && mapperClass != null) {
                        //10.3使用java类名
                        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.");
                    }
                }
            }
        }
    }

除此外,在addMapper的时候,还会解析Mapper类上的注解。这就可以使用@Select等方式来写Sql语句

    //看一下如何添加一个映射
    public <T> void addMapper(Class<T> type) {
        //mapper必须是接口!才会添加
        if (type.isInterface()) {
            if (hasMapper(type)) {
                //如果重复添加了,报错
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                knownMappers.put(type, new MapperProxyFactory<T>(type));
                // 解析注解。因此写在方法上的@Select等语句就可以得到应用
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                //如果加载过程中出现异常需要再将这个mapper从mybatis中删除。避免正常的Mapper加载被影响。
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
MapperProxyFactory
    protected T newInstance(MapperProxy<T> mapperProxy) {
        //用JDK自带的动态代理生成映射器
        // 代理类生成: newProxyInstance 代理classLoader,并实现interfaces方法
        // MapperProxy的命名不太好,有歧义。更改为MapperInvocationHandler,可能更易于理解
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }

最终通过MapperProxyFactory生成代理类,MapperProxy做为代理类的执行方法。

执行Mapper的方法

TAccount account = accountDao.queryById(1);

由于accout是JDK代理类,因此执行代理类的方法,就是执行它的InvocationHandler.invoke方法。MapperProxy作为实际执行类,我们看下它的执行过程。

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
        //并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
        if (Object.class.equals(method.getDeclaringClass())) {
            // method.getDeclaringClass() 获取方法来源类
            try {
                return method.invoke(this, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
        //这里优化了,去缓存中找MapperMethod
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        //执行
        return mapperMethod.execute(sqlSession, args);
    }

MapperProxy执行invoke时,从缓存中,根据Mapper定义的方法,找到Mabatis定义的对应的Sql方法(MapperMethod),然后去执行MapperMethod。而MapperMethod也可以视为我们Spring中Mapper方法的实际执行入口。

 //执行  基本可以看作方法执行的入口了
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        //可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
        if (SqlCommandType.INSERT == command.getType()) {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
        } else if (SqlCommandType.UPDATE == command.getType()) {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
        } else if (SqlCommandType.DELETE == command.getType()) {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
        } else if (SqlCommandType.SELECT == command.getType()) {
            if (method.returnsVoid() && method.hasResultHandler()) {
                //如果有结果处理器
                executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (method.returnsMany()) {
                //如果结果有多条记录
                result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
                //如果结果是map
                result = executeForMap(sqlSession, args);
            } else {
                //否则就是一条记录
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
            }
        } else {
            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;
    }

提交事务

sqlSession.commit();

事务是一个单元。全部成功,则这个事务内的操作更新到数据库。一个操作失败,事务内的操作全部回滚。

关闭资源

sqlSession.close();

疑问记录

  • InputStream与Reader的区别

    答:InputStream 字节流,Reader 字符流
  • 为什么要有SqlSessionFactoryBuilder

    答:首先因为每次操作数据库都需要新建session。因此我们会频繁操作和使用SqlSessionFactory。那么SqlSessionFactory依赖于Configuration。而Configuration的生成是复杂的。为了省去我们去了解SqlSessionFactory的生成过程,使用SqlSessionFactoryBuilder来屏蔽。
  • Spring是如何基于JavaAPI创建Mybatis的SqlSessionBuilder的。
  • Spring是如何使用Properties来创建Configuration的。
  • SqlSession的autoCommit是如何执行的,什么时候执行?
  • SqlSession的事务管理流程
  • mybatis的3种处理模式的区别
  • 什么场景会指定事务级别?
  • 写在方法上的注解是怎么被应用的

    答:在将Mapper注册到MapperRegistry的时候,会解析方法上的注解。因此注解上的Sql语句可以得到应用。
  • sqlstatement的命名规则

    答:接口+方法名称。所以在使用时,尽量不要定义相同名称的接口
mapperInterface.getName() + "." + methodName
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值