写点什么

【转】java 开发之 MyBatis 原理与核心组件

  • 2021 年 12 月 30 日
  • 本文字数:5133 字

    阅读完需:约 17 分钟

​Configuration

Configuration 是 mybatis 的全局配置类,保存了环境对象 Enviroment(Environment 表示数据源相关环境),各种配置信息,以及作为各种资源解析后的注册表java培训


例如,MapperRegister 表示 Mapper 的注册表,TypeHandlerRegistry 是 TypeHandler 的注册表,TypeAliasRegistry 是 TypeAlias 的注册表,另外还以 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 对象,因此 Excecutor 和 SqlSession 是一对一绑定的。


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 组成。而 BoundSql 是 SqlSource 应用了上下文环境(指用户输入参数)后得到的对象,对 SqlSource 中条件和参数做了筛选,形成的实际 SQL(仍可能有'?'占位符)。


执行过程

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


class MybatisTest{public static void main(){//STEP 1String 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 的构造函数依赖三个参数,分别是 Configuration,Executor 和 autocommit。Configuration 是全局的,而 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 容器类。


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


一级缓存与二级缓存

什么是一级缓存

一级缓存是 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 环境中的线程安全。


转载于业余草

用户头像

关注尚硅谷,轻松学IT 2021.11.23 加入

还未添加个人简介

评论

发布
暂无评论
【转】java开发之MyBatis 原理与核心组件