• mybatis一文全解


    mybatis核心组件

    Configuration

    Configuration是mybatis的全局配置类,保存了环境对象Enviroment(Environment表示数据源相关环境),各种配置信息,以及作为各种资源解析后的注册表。
    例如,MapperRegister表示Mapper的注册表,TypeHandlerRegistryTypeHandler的注册表,TypeAliasRegistryTypeAlias的注册表,另外还以Map的形式保存了MappedStatement, ResultMap,ParameterMaps等的映射关系,其中key均是namespace + id的形式。

    SqlSessionFactory

    SqlSessionFactory是负责创建SqlSession的工厂。

    public interface SqlSessionFactory {
    
      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();
    
    }
    

    主要是通过openSession()创建SqlSession。此外,还有一个返回全局配置对象的方法getConfiguration()。可以猜测其实现类应该会直接或间接的保持对Congfiguration的引用。
    观察openSession()的参数,猜测创建SqlSession的方式有两种,一种直接基于传入的数据库连接Connection。另一种通过全局配置对象Configuration在获取数据源环境Enviroment,在获取环境。
    SqlSessionFactory的默认实现是DefualtSqlSessionFactory

    SqlSession

    SqlSession表示某次数据库操作会话,因此SqlSession接口定义的主要是CRUD和事务操作的相关接口。
    另外还有个重要的方法getMapper,可以返回对应Mapper的对象。

    注意:SqlSession CRUD相关方法的参数第一个参数均为statement的字符串,这个字符串并非SQL语句,而是MappedStatement的ID。

    SqlSession的默认实现是DefualtSqlSession
    DefaultSqlSession不是线程安全的,使用者需要自己确保线程安全问题,或者是使用SqlSessionManager,它提供了SqlSession的线程安全管理。

    Executor

    执行器,负责真正执行数据库操作,并且提供了缓存的能力。
    每个DefualtSqlSession内部都有一个Executor,在创建DefaultSqlSession实例时,同时创建了Executor对象,因此ExcecutorSqlSession是一对一绑定的。
    Executor可以分为两类:
    第一类是BaseExecutor以及子类,这类的Executor有操作数据库的能力,并且提供了mybatis的一级缓存。
    第二类是CachingExecutor,它对第一个的执行器进行了包装,提供了二级缓存,并在二级缓存未命中时,委托给内部的第一类执行器处理。

    MappedStatement

    MappedStatement表示的是mapper.xml中定义的一个SQL节点。当创建Configuration对象在创建xml时,就会将一个个节点解析成对应的MappedStatement对象。
    MappedStatement中大部分属性都可以在xml的定义中找到相关的配置。

    四种处理器 TypeHandler,ParameterHandler,StatementHandler,ResultSetHandler

    • TypeHandler 类型处理器,提供了Java对象和JDBC TYPE的转换。
      public interface TypeHandler<T> {
        //将某个Parameter Java类型 转成 JDBC 类型 用于执行
        void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
        //将结果集中中的某列 转成 Java类型
        T getResult(ResultSet rs, String columnName) throws SQLException;
        T getResult(ResultSet rs, int columnIndex) throws SQLException;
        
        T getResult(CallableStatement cs, int columnIndex) throws SQLException;
      
      }
      
      

    通常我们可以拓展这个接口实现自定义枚举类型与JDBC的转换类型。

    • ParameterHandler 参数处理器,负责将PreparedStatement中的占位符替换成对应的参数。

      public interface ParameterHandler {
      
        Object getParameterObject();
      
        void setParameters(PreparedStatement ps)
            throws SQLException;
      
      }
      
    • StatementHandler 核心组件,与数据库交互,从数据库连接中获取Statement对象,执行SQL,并映射结果集等功能。

      public interface StatementHandler {
      
        Statement prepare(Connection connection, Integer transactionTimeout)
            throws SQLException;
      
        void parameterize(Statement statement)
            throws SQLException;
      
        void batch(Statement statement)
            throws SQLException;
      
        int update(Statement statement)
            throws SQLException;
      
        <E> List<E> query(Statement statement, ResultHandler resultHandler)
            throws SQLException;
      
        <E> Cursor<E> queryCursor(Statement statement)
            throws SQLException;
      
        BoundSql getBoundSql();
      
        ParameterHandler getParameterHandler();
      
      }
      
    • ResultSetHandler 结果集处理器,StatementHandler获取到结果集后ResultSet,会提交给ResultSetHandler处理,以转换成Java对象集合。

      public interface ResultSetHandler {
      
        <E> List<E> handleResultSets(Statement stmt) throws SQLException;
      
        <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
      
        void handleOutputParameters(CallableStatement cs) throws SQLException;
      
      }
      

    SqlSource和BoundSql

    一个SqlSource表示MappedStatement定义的Sql片段,一个SqlSource可能由多个SqlNode组成。
    BoundSqlSqlSource应用了上下文环境(指用户输入参数)后得到的对象,对SqlSource中条件和参数做了筛选,形成的实际SQL(仍可能有'?'占位符)。

    执行过程

    以一个简单的程序作为入口来看看mybatis一次查询执行的主要流程。

    class MybatisTest{
        public static void main(){
            //STEP 1
            String resource = "org/mybatis/example/mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            
            //STEP 2
            try (SqlSession session = sqlSessionFactory.openSession()) {
            //STEP 3
            BlogMapper mapper = session.getMapper(BlogMapper.class);  
            //STEP 4
            Blog blog = mapper.selectBlog(101);
            }
        }
    }
    
    

    形成全局配置对象,构建SqlSessionFactory

    第一步是读取全局的配置文件,解析文件形成我们的全局配置Configuration。 解析的过程主要是针对xml文件各节点的解析,本文目的为把握主体流程,这里不深入分析。
    得到配置对象后,SqlSessionFacotryBuilder会将Configuration对象传入创建SqlSessionFactory对象。

    打开SqlSession

    得到SqlSessionFactory工厂对象后,可以通过openSession()方式获得SqlSession对象(默认是DefaultSqlSession)。

    DefaultSqlSession的构造函数依赖三个参数,分别是ConfigurationExecutorautocommitConfiguration是全局的,而Executor却是跟DefaultSqlSession一一绑定的(也就是说在创建DefaultSqlSession的时候, 会创建一个新的Executor,并且这个Executor不会暴露给其他SqlSession使用),理解这一点对搞清一级缓存很有用。

    获得代理对象

    当调用SqlSession.getMapper()时,首先会从Congifuration的注册表中查找对应类型是否已经注册,没有则抛出异常。
    如果存在,则通过MapperProxyFactory创建代理对象。
    MapperProxyFactory主要是通过JDK动态代理创建代理对象的,这一过程分为两步:

    • 先创建JDK动态代理中的重要组件InvocationHandler,该接口在这里对应的实现是MapperProxy对象,而且MapperProxy保存了对SqlSession的引用。
    • 再通过Proxy.newProxyInstance() 获得动态代理的对象。

    注意:就算是相同的SqlSession,每次getMapper得到的代理对象也并非同一个,只不过对于相同SqlSession创建的Mapper而言,MapperProxy引用的SqlSession相同。

    代理对象通过反射调用执行方法

    既然代理对象是JDK动态代理创建的,那么其方法的执行最终会落到InvocationHandler,也就这里的MapperProxy的invoke中。
    MapperProxy.invoke()又调用了MapperMethod.execute()
    MapperMethod.execute()在SQL的执行前后做了两件事,处理参数,以及对执行结果进行计数,而核心的SQL执行还是交回给了SqlSession对象。

    Executor执行器执行

    SqlSession在执行CRUD时,会从Configuration查找对应的MappedStatement对象,然后将MappedStatement传递给Executor对象执行。
    此时,如果开启了二级缓存,CachingExecutor会先从MappedStatement的Cache中查找,如果缓存未命中,CachingExecutor则会将查找任务委托给内部的BaseExecutor。而BaseExecutor则会先从内部的LocalCache中查找,如果缓存未命中,则将SQL的执行交给StatementHandler

    StatementHandler执行SQL

    StatementHandler的执行过程分为两个阶段:

    • 准备阶段:这一阶段的主要目的是得到Statement对象
    • 执行阶段:通过Statement执行SQL

    当得到Sql的执行结果后,还会应用ResultSetHandler,将结果集转换成Java容器类。

    用一副粗糙的图概括上述业务流程图:
    image

    一级缓存与二级缓存

    什么是一级缓存

    一级缓存是Executor内部的缓存机制。主要原理是BaseExecutor有一个叫localCache的字段用来存放这个会话的执行结果。因此,一级缓存是SqlSession内部的缓存(因为Executor和SqlSession是一一绑定的)。
    一级缓存的有效期是某一次会话过程,一旦会话关闭,一级缓存也就失效。另外,如果会话中发生了增删改的写操作,一级缓存的会话同样会失效。

    什么是二级缓存

    二级缓存是MappedStatement的缓存,MappedStatement有一个Cache字段用来存放二级缓存。因此,我们常说二级缓存是跨SqlSession的。二级缓存默认是关闭的,如果希望开启二级缓存需要同时确保mybatis设置中的Cache打开,以及对应的MappedStatement开启了缓存。
    那么二级缓存的实现原理是怎么样的?
    我们知道二级缓存的使用者是CachingExecutor,在CachingExecutor执行查询前会先查看MappedStatement中是否存放对应的缓存。
    如果缓存未命中,CachingExecutor会由内部的BaseExecutor执行数据库查询操作,得到查询结果后,CachingExecutor交给内部的TransactionCacheManager保存。只有当事务提交完成后,TransactionCacheManager保存的缓存才会写入MappedStatement的Cache中。
    读者可以自己思考下这么做的用意。

    二级缓存的脏读

    因为二级缓存是与MappedStatement绑定的,换句话说就是和命名空间绑定的,假设存在这个一个情况,MappedStatement A 缓存了User的数据,但是MappedStatment B 可能也对User表进行了修改,但是 A中的缓存无法感知这一变化,缓存一直生效。这就产生了二级缓存的脏读问题。
    为了避免上述问题,首先我们在开发的时候需要确保相应的规范,让相同表的操作尽量在相同的命名空间下。如果实在需要在不同的命名空间下操作相同的表,就需要CacheRef设置让二者使用相同的缓存。

    自定义拦截器

    mybatis通过Interceptor接口向用户提供了拓展的机制。
    其底层实现原理依旧是利用了JDK的动态代理。当我们通过Configuraion.newExecutor()时会将创建得到Executor在经过动态代理包装一层,以达到实现拦截方法执行的目的。
    此处InvocationHandler的实现是Proxy对象,可以看其invoke()方法的实现。

     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          //是否匹配拦截器的Signature
          Set<Method> methods = signatureMap.get(method.getDeclaringClass());
          if (methods != null && methods.contains(method)) {
            //将连接点的信息(方法,参数,目标对象)封装成 Invocation对象,传入由Interceptor执行
            return interceptor.intercept(new Invocation(target, method, args));
          }
          return method.invoke(target, args);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }
    

    mybatis与Spring的整合

    在mybatis与Spring集成的过程中,以下几个组件承担了重要角色:

    • ClassPathMapperScanner:负责扫描相关Mapper对象,并作为BeanDefinition注册到容器中。
    • MapperFactoryBean:注册的BeanDefinition都是FactoryBean,当实例化Mapper时,会调用其getObject()方法,主要流程依旧是通过JDK动态代理创建Mapper实例,只不过这里关联的SqlSession是SqlSessionTemplate。
    • SqlSessionTemplate(核心): SqlSessionTemplate虽然实现了SqlSession接口,但其方法实现均是委托给一个SqlSession的动态代理,其InvocationHandler的实现是SqlSessionInterceptor。它会在执行前先去获取真正的SqlSession,从而保证SqlSession在Spring环境中的线程安全。
      image
  • 相关阅读:
    对象的创建
    Java运行时数据区域
    Java内存模型
    LinkedList小练习及相关算法
    面试题之矩阵与转置矩阵相乘
    快速排序
    垃圾收集器
    java垃圾收集相关问题
    Win7下安装Centos7双系统出错:No valid bootloader target device found.
    Scanner类的方法
  • 原文地址:https://www.cnblogs.com/insaneXs/p/12997368.html
Copyright © 2020-2023  润新知