Mybatis的整体框架分为三层,分别是接口层、核心处理层、和基础支持层。如下图
Mybatis的核心工作流程图如下:
基础支持层位于MyBatis整体架构的最底层,支撑着MyBatis的核心处理层,是整个框架的基石。基
础支持层中封装了多个较为通用的、独立的模块。不仅仅为MyBatis提供基础支撑,也可以在合适的场
景中直接复用。
MyBatis在进行参数处理、结果集映射等操作时会使用到大量的反射操作,Java中的反射功能虽然强
大,但是代码编写起来比较复杂且容易出错,为了简化反射操作的相关代码,MyBatis提供了专门的反
射模块,该模块位于org.apache.ibatis.reflection包下,它对常见的反射操作做了进一步的封装,提供
了更加简洁方便的反射API。
Reflector类
// 对应的Class 类型
private final Class<?> type;
// 可读属性的名称集合 可读属性就是存在 getter方法的属性,初始值为null
private final String[] readablePropertyNames;
// 可写属性的名称集合 可写属性就是存在 setter方法的属性,初始值为null
private final String[] writablePropertyNames;
// 记录了属性相应的setter方法,key是属性名称,value是Invoker方法
// 他是对setter方法对应Method对象的封装
private final Map<String, Invoker> setMethods = new HashMap<>();
// 属性相应的getter方法
private final Map<String, Invoker> getMethods = new HashMap<>();
// 记录了相应setter方法的参数类型,key是属性名称 value是setter方法的参数类型
private final Map<String, Class<?>> setTypes = new HashMap<>();
// 和上面的对应
private final Map<String, Class<?>> getTypes = new HashMap<>();
// 记录了默认的构造方法
private Constructor<?> defaultConstructor;
// 记录了所有属性名称的集合
private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
然后我们可以看看Reflector中提供的公共的API方法
了解了Reflector对象的基本信息后我们需要如何来获取Reflector对象呢?在MyBatis中给我们提供了一个ReflectorFactory工厂对象。所以我们先来简单了解下ReflectorFactory对象,当然你也可以直接new 出来.
ReflectorFactory
ReflectorFactory接口主要实现了对Reflector对象的创建和缓存。
public interface ReflectorFactory {
// 检测该ReflectorFactory是否缓存了Reflector对象
boolean isClassCacheEnabled();
// 设置是否缓存Reflector对象
void setClassCacheEnabled(boolean classCacheEnabled);
// 创建指定了Class的Reflector对象
Reflector findForClass(Class<?> type);
}
ReflectorFactory的使用:
@Test
public void test02() throws Exception{
ReflectorFactory factory = new DefaultReflectorFactory();
Reflector reflector = factory.findForClass(Student.class);
System.out.println("可读属性:"+Arrays.toString(reflector.getGetablePropertyNames()));
System.out.println("可写属性:"+Arrays.toString(reflector.getSetablePropertyNames()));
System.out.println("是否具有默认的构造器:" +reflector.hasDefaultConstructor());
System.out.println("Reflector对应的Class:" + reflector.getType());
}
Invoker
针对于Class中Field和Method的调用,在MyBatis中封装了Invoker对象来统一处理
public void test03() throws Exception{
ReflectorFactory factory = new DefaultReflectorFactory();
Reflector reflector = factory.findForClass(Student.class);
// 获取构造器 生成对应的对象
Object o = reflector.getDefaultConstructor().newInstance();
MethodInvoker invoker1 = (MethodInvoker) reflector.getSetInvoker("id");
invoker1.invoke(o,new Object[]{999});
// 读取
Invoker invoker2 = reflector.getGetInvoker("id");
invoker2.invoke(o,null);
}
MetaClass
在Reflector中可以针对普通的属性操作,但是如果出现了比较复杂的属性,比如 private Person
person; 这种,我们要查找的表达式 person.userName.针对这种表达式的处理,这时就可以通过
MetaClass来处理了。我们来看看主要的属性和构造方法
@Test
public void test7(){
ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
MetaClass meta = MetaClass.forClass(RichType.class, reflectorFactory);
System.out.println(meta.hasGetter("richField"));
System.out.println(meta.hasGetter("richProperty"));
System.out.println(meta.hasGetter("richList"));
System.out.println(meta.hasGetter("richMap"));
System.out.println(meta.hasGetter("richList[0]"));
System.out.println(meta.hasGetter("richType"));
System.out.println(meta.hasGetter("richType.richField"));
System.out.println(meta.hasGetter("richType.richProperty"));
System.out.println(meta.hasGetter("richType.richList"));
System.out.println(meta.hasGetter("richType.richMap"));
System.out.println(meta.hasGetter("richType.richList[0]"));
// findProperty 只能处理 . 的表达式
System.out.println(meta.findProperty("richType.richProperty"));
System.out.println(meta.findProperty("richType.richProperty1"));
System.out.println(meta.findProperty("richList[0]"));
System.out.println(Arrays.toString(meta.getGetterNames()));
}
Mybatis中的反射模块,用在了SqlSessionFactory类中,以及执行SQL的时候在Statement获取结果集后,在做结果集映射的使用有使用到,
在PreparedStatement为SQL语句绑定参数时,需要从Java类型转换为JDBC类型,而从结果集中获取数据时,则需要从JDBC类型转换为Java类型,所以我们来看下在MyBatis中是如何实现类型的转换的。
TypeHandler
MyBatis中的所有的类型转换器都继承了TypeHandler接口,在TypeHandler中定义了类型转换器的最基本的功能。
TypeHandlerRegistry
MyBatis中是将所有的TypeHandler都保存注册在了TypeHandlerRegistry中的
TypeAliasRegistry
我们在MyBatis的应用的时候会经常用到别名,这能大大简化我们的代码,其实在MyBatis中是通过TypeAliasRegistry类管理的。首先在构造方法中会注入系统常见类型的别名
在构建SqlSessionFactory时,在Configuration对象实例化的时候在成员变量中完成了TypeHandlerRegistry和TypeAliasRegistry的实例化
在MyBatis系统启动的时候日志框架是如何选择的呢?首先我们在全局配置文件中我们可以设置对应的日志类型选择
这个"STDOUT_LOGGING"是怎么来的呢?在Configuration的构造方法中其实是设置的各个日志实现的别名的
当我们开启了 STDOUT的日志管理后,当我们执行SQL操作时我们发现在控制台中可以打印出相关的日志信息
那这些日志信息是怎么打印出来的呢?原来在MyBatis中的日志模块中包含了一个jdbc包,它并不是将日志信息通过jdbc操作保存到数据库中,而是通过JDK动态代理的方式,将JDBC操作通过指定的日志框架打印出来。下面我们就来看看它是如何实现的。
MapperRegistry
这个注册中心是用来保存MapperProxyFactory对象的
MapperProxyFactory
MapperProxyFactory是一个工厂对象,专门负责创建MapperProxy对象
MapperProxy
通过MapperProxyFactory创建的MapperProxy是Mapper接口的代理对象,实现了InvocationHandler接口
MapperMethod
MapperMethod中封装了Mapper接口中对应方法的信息,以及SQL语句的信息,我们可以把MapperMethod看成是配置文件中定义的SQL语句和Mapper接口的桥梁。
SqlCommand
SqlCommand是MapperMethod中定义的内部类,记录了SQL语句名称以及对应的类型(UNKNOWN,INSERT,UPDATE,DELETE,SELECT,FLUSH)
MethodSignature
MethodSignature也是MapperMethod的内部类,在其中封装了Mapper接口中定义的方法相关信息。
execute方法
最后我们需要来看下再MapperMethod中最核心的方法execute方法,这个方法完成了数据库操作
Mybatis中的缓存分为一级缓存和二级缓存。但本质上是一样的,都是使用Cache接口实现的
Cache接口的实现类很多,但是大部分都是装饰器,只有PerpetualCache提供了Cache接口的基本实现。
PerpetualCache
PerpetualCache 实现比较简单,底层使用HashMap记录缓存项
然后我们可以来看看cache.decorators包下提供的装饰器。他们都实现了Cache接口。这些装饰器都在PerpetualCache的基础上提供了一些额外的功能,通过多个组合实现一些特殊的需求。
BlockingCache 阻塞同步的缓存
@Mapper
@CacheNamespace(blocking = true)
通过源码我们能够发现,BlockingCache本质上就是在我们操作缓存数据的前后通过ReentrantLock对象来实现了加锁和解锁操作。其他的具体实现类,大家可以自行查阅
一级缓存
一级缓存也叫本地缓存(Local Cache),MyBatis的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis的一级缓存是默认开启的,不需要任何的配置(如果要关闭,localCacheScope设置为STATEMENT)。
二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别的,可以被多个SqlSession共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。
二级缓存的设置,首先是settings中的cacheEnabled要设置为true,当然默认的就是为true,这个步骤决定了在创建Executor对象的时候是否通过CachingExecutor来装饰。那么设置了cacheEnabled标签为true是否就意味着 二级缓存是否一定可用呢?当然不是,我们还需要在对应的映射文件中添加 cache 标签才行。
<!-- 声明这个namespace使用二级缓存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" <!—- 最多缓存对象个数,默认1024 -->
eviction="LRU" <!—- 回收策略 -->
flushInterval="120000" <!—- 自动刷新时间 ms,未配置时只有调用时刷新 -->
readOnly="false"/> <!—- 默认是false(安全),改为true可读写时,对象必须支持序列化 -->
这样的设置表示当前的映射文件中的相关查询操作都会触发二级缓存,但如果某些个别方法我们不希望走二级缓存怎么办呢?我们可以在标签中添加一个 useCache=false 来实现的设置不使用二级缓存
第三方缓存
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
然后加上Cache标签的配置
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
然后添加redis的属性文件
host=192.168.100.120
port=6379
connectionTimeout=5000
soTimeout=5000
database=0
插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或者
改变原有的功能,MyBatis中也提供的有插件,虽然叫插件,但是实际上是通过拦截器(Interceptor)实现
的,在MyBatis的插件模块中涉及到责任链模式和JDK动态代理
1.自定义插件
1.1 创建Interceptor实现类,我们创建的拦截器必须要实现Interceptor接口,Interceptor接口的定义为
public interface Interceptor {
// 执行拦截逻辑的方法
Object intercept(Invocation invocation) throws Throwable;
// 决定是否触发 intercept()方法
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 根据配置 初始化 Intercept 对象
default void setProperties(Properties properties) {
// NOP
}
}
在MyBatis中Interceptor允许拦截的内容是
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
1.2 配置拦截器,创建好自定义的拦截器后,我们需要在全局配置文件中添加自定义插件的注册
<plugins>
<plugin interceptor="com.gupaoedu.interceptor.FirstInterceptor">
<property name="testProp" value="1000"/>
</plugin>
</plugins>
2.插件实现原理
以PageHelper为例
首先会获取一个SqlUtils对象
sqlUtil.processPage(invocation);方法
通过ThreadLocal来获取分页数据信息
3.插件的应用场景分析
在实际开发中,控制数据库事务是一件非常重要的工作,MyBatis使用Transaction接口对事务进行了抽象,定义的接口为
public interface Transaction
Transaction接口的实现有两个分别是 JdbcTransaction 和 ManagedTransaction两个
JdbcTransaction 依赖于JDBC Connection来控制事务的提交和回滚,声明的相关的属性为
protected Connection connection; // 事务对应的数据库连接
protected DataSource dataSource; // 数据库连接所属的 数据源
protected TransactionIsolationLevel level; // 事务的隔离级别
protected boolean autoCommit; // 是否自动提交
在构造方法中会完成除了 Connection 属性外的另外三个属性的初始化,而Connection会延迟初始化,在我们执行getConnection方法的时候才会执行相关的操作。源码比较简单,请自行查阅
ManagedTransaction的实现更加的简单,它同样依赖 DataSource 字段来获取 Connection 对象,
但是 commit方法和rollback方法都是空的,事务的提交和回滚都是依赖容器管理的。在实际开发中
MyBatis通常会和Spring集成,数据库的事务是交给Spring进行管理的,这个我们会在MyBatis整合
Spring中给大家介绍 SpringManagedTransaction。