MyBatis的整体架构分为三层,分别是基础支持层、核心处理层和接口层
中文注释源码Git地址
解析器模块
,主要负责解析配置文件提示:该模块中涉及XML文件的解析方式,感兴趣的可以自行了解DOM(Document Object Model)解析方式和SAX(Simple API for XML)解析方式,以及从JDK 6.0版本开始,JDK开始支持的StAX(Streaming API for XML)解析方式。
其中☕️XPathParser
类提供解析mybatis-config.xml配置文件的方法,XPathParser
中提供了一系列的eval*()方法用于解析boolean、short、long、int、String、Node等类型的信息,但是在处理String类型信息时会调用☕️PropertyParser
类的 parse()方法
public String evalString(Object root, String expression) {
String result = (String) evaluate(expression, root, XPathConstants.STRING);
result = PropertyParser.parse(result, variables);
return result;
}
PropertyParser.parse()方法中会创建GenericTokenParser
解析器,并将默认值的处理委托给GenericTokenParser.parse()方法
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
//将默认值的处理委托给GenericTokenParser.parse()
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
GenericTokenParser是一个通用的占位符解析器,可以解析配置文件中的变量。底层使用TokenHandler
类完成对占位符的解析。
reflection包中对常见的反射操作做了进一步封装,提供了更加简洁方便的反射API。其中Reflector
类是MyBatis中反射模块的基础,每个Reflector对象都对应一个类,在Reflector中缓存了反射操作需要使用的类的元信息。
Reflector类中有两个比较核心的方法即获取目标类的getter和setter方法,以获取getter方法为例,
其中 getClassMethods() 方法会获取当前类以及其父类中定义的所有方法的唯一签名以及相应的Method对象,从该方法返回的Method数组中查找该类中定义的getter方法,然后判断是否是以get获取is开头(之前的版本好像没有is,所以会导致一些Boolean的属性为null,未证实),最后经过多层处理会记录到Reflector类的getMethods集合中以供后期使用。
Reflector.addFields()方法会处理类中定义的所有字段,并且将处理后的字段信息添加到setMethods集合、setTypes集合
ReflectorFactory接口主要实现了对Reflector对象的创建和缓存
/**
* 为指定的Class创建Reflector对象,并将其加入reflectorMap中
* @param type 指定Class
* @return Reflector对象
*/
@Override
public Reflector findForClass(Class<?> type) {
if (classCacheEnabled) { //检测是否开启缓存
// synchronized (type) removed see issue #461
Reflector cached = reflectorMap.get(type);
if (cached == null) {
cached = new Reflector(type); //创建Reflector对象
reflectorMap.put(type, cached); //放入reflectorMap
}
return cached;
} else {
return new Reflector(type); //未开启缓存,直接返回Reflector对象
}
}
reflection包中的TypeParameterResolver
类可以解析存在复杂的继承关系以及泛型定义时的类的字段、方法参数或方法返回值的类型,感兴趣可以自行查看源码,此类也是Reflector类的基础。
MyBatis源代码中提供了TypeParameterResolverTest这个测试类,其中从更多角度测试了TypeParameterResolver的功能,感兴趣可以参考该测试类的实现,可以更全面地了解TypeParameterResolver的功能。
MyBatis中有很多模块会使用到ObjectFactory接口,该接口提供了多个create()方法的重载,通过这些create()方法可以创建指定类型的对象。
DefaultObjectFactory类是MyBatis提供的ObjectFactory接口的唯一实现,他是一个反射工厂,其实现ObjectFactory的create()方法是通过调用自身私有的instantiateClass()方法实现的
用户也可以自定义ObjectFactory实现类,在配置文件中指定即可
private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
try {
Constructor<T> constructor;
//通过无参构造函数创建对象
if (constructorArgTypes == null || constructorArgs == null) {
constructor = type.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
return constructor.newInstance();
}
//根据指定的参数列表查找构造函数,并实例化对象
constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[constructorArgTypes.size()]));
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
return constructor.newInstance(constructorArgs.toArray(new Object[constructorArgs.size()]));
} catch (Exception e) {
StringBuilder argTypes = new StringBuilder();
if (constructorArgTypes != null && !constructorArgTypes.isEmpty()) {
for (Class<?> argType : constructorArgTypes) {
argTypes.append(argType.getSimpleName());
argTypes.append(",");
}
argTypes.deleteCharAt(argTypes.length() - 1); // remove trailing ,
}
StringBuilder argValues = new StringBuilder();
if (constructorArgs != null && !constructorArgs.isEmpty()) {
for (Object argValue : constructorArgs) {
argValues.append(String.valueOf(argValue));
argValues.append(",");
}
argValues.deleteCharAt(argValues.length() - 1); // remove trailing ,
}
throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + e, e);
}
}
在使用MyBatis的过程中,我们经常会碰到一些属性表达式,例如,在查询某用户(User)的订单(Order)的结果集如下:
对应的对象模型如下:
假设现在需要将结果集中的item1列与用户第一个订单(Order)的第一条目(Item)的名称映射,item2列与用户第一个订单(Order)的第二条目(Item)的名称映射(这里仅仅是一个示例,在实际生产中很少这样设计),我们可以得到下面的映射规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iSichaek-1675960016481)(E:\Users\yuel\Documents\MyBatis源码解析.assets\image-20230207180307970.png)]
在上例中,“orders[0].items[0].name”这种由“.”和“[]”组成的表达式是由PropertyTokenizer进行解析的
提供了下列静态方法帮助完成方法名到属性名的转换,以及多种检测操作。
具体如下:
//将方法名转为属性名
public static String methodToProperty(String name) {
if (name.startsWith("is")) {
name = name.substring(2);
} else if (name.startsWith("get") || name.startsWith("set")) {
name = name.substring(3);
} else {
throw new ReflectionException("Error parsing property name '" + name + "'. Didn't start with 'is', 'get' or 'set'.");
}
if (name.length() == 1 || (name.length() > 1 && !Character.isUpperCase(name.charAt(1)))) {
name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
}
return name;
}
//检测方法名是否对应属性名
public static boolean isProperty(String name) {
return name.startsWith("get") || name.startsWith("set") || name.startsWith("is");
}
//检测方法是否为getter方法
public static boolean isGetter(String name) {
return name.startsWith("get") || name.startsWith("is");
}
//检测方法是否为setter方法
public static boolean isSetter(String name) {
return name.startsWith("set");
}
PropertyCopier是一个属性拷贝的工具类,其核心方法是copyBeanProperties()方法,主要实现相同类型的两个对象之间的属性值拷贝。具体如下:
//主要实现相同类型的两个对象之间的属性值拷贝
public static void copyBeanProperties(Class<?> type, Object sourceBean, Object destinationBean) {
Class<?> parent = type;
while (parent != null) {
final Field[] fields = parent.getDeclaredFields();//获取所有属性
for (Field field : fields) {
try {
field.setAccessible(true);
//将sourceBean对象的属性值设置到destinationBean
field.set(destinationBean, field.get(sourceBean));
} catch (Exception e) {
// Nothing useful to do, will only fail on final fields, which will be ignored.
}
}
parent = parent.getSuperclass();//继续拷贝父类中的字段
}
}
MetaClass通过Reflector和PropertyTokenizer组合使用,实现了对复杂的属性表达式的解析,并实现了获取指定属性描述信息的功能。
MetaClass中比较重要的是findProperty()方法,它是通过调用MetaClass.buildProperty()方法实现的,而buildProperty()方法会通过PropertyTokenizer类(该类在上文中的属性工具集中)
解析复杂的属性表达式。
MetaClass.findProperty()方法只查找“.”导航的属性,并没有检测下标
以解析User类中的tele.num这个属性表达式为例解释上述过程:首先使用PropertyTokenizer解析tele.num表达式得到其children字段为num,name字段为tele;然后将tele追加到builder中保存,并调用metaClassForProperty()方法为Tele类创建对应的MetaClass对象,调用其buildProperty()方法处理子表达式num,逻辑同上,此时已经没有待处理的子表达式,最终得到builder中记录的字符串为tele.num
MetaClass中有hasGetter()和hasSetter()两个方法负责判断属性表达式所表示的属性是否有对应的属性,本质上是底层调用了Reflector中的方法集合判断(补充:按照JavaBean规范,有getter和setter方法的字段成为“属性”,否则称为“字段”,特殊情况:存在getA()和setA(String)两个方法,无论类中是否有a字段,我们都应该认为其有a属性)
public boolean hasSetter(String name) {
PropertyTokenizer prop = new PropertyTokenizer(name);//解析表达式
if (prop.hasNext()) {//存在处理的子表达式
//指定属性有setter方法才能处理子表达式
if (reflector.hasSetter(prop.getName())) {
MetaClass metaProp = metaClassForProperty(prop.getName());
return metaProp.hasSetter(prop.getChildren());
} else {
return false;
}
} else {
return reflector.hasSetter(prop.getName());
}
}
这里依然通过一个示例分析MetaClass.hasGetter()方法的执行流程。假设现在通过orders[0].id这个属性表达式,检测User类中orders字段中的第一个元素(Order对象)的id字段是否有getter方法,大致步骤如下:
(1)我们调用MetaClass.forClass()方法创建User对应的MetaClass对象并调用其hasGetter()方法开始解析,经过PropertyTokenizer对属性表达式的解析后,PropertyTokenizer对象的name值为orders,indexName为orders[0],index为0,children为name。
(2)进入到MetaClass.getGetterType()方法,此时(1)处条件成立,调用getGenericGetterType()方法解析orders字段的类型,得到returnType为List<Order>对应的ParameterizedType对象,此时条件(2)成立,更新returnType为Order对应的Class对象。
(3)继续检测Order中的id字段是否有getter方法,具体逻辑同上。
另外,MetaClass中有一个public修饰的getGetterType(String)重载,其逻辑与hasGetter()类似,也是先对表达式进行解析,然后调用metaClassForProperty()方法或getGetterType (PropertyTokenizer)方法进行下一步处理
ObjectWrapper接口是对对象的包装,抽象了对象的属性信息,它定义了一系列查询对象属性信息的方法,以及更新属性的方法
ObjectWrapperFactory负责创建ObjectWrapper对象
BaseWrapper.resolveCollection()方法会调用MetaObject.getValue()方法,它会解析属性表达式并获取指定的属性
BaseWrapper.getCollectionValue()方法和setCollectionValue()方法会解析属性表达式的索引信息,然后获取/设置对应项
BeanWrapper类继承了BaseWrapper抽象类,其中封装了一个JavaBean对象以及该JavaBean类相应的MetaClass对象,当然,还有从BaseWrapper继承下来的、该JavaBean对象相应的MetaObject对象
BeanWrapper.get()方法和set()方法会根据指定的属性表达式,获取/设置相应的属性值
ObjectWrapper提供了获取/设置对象中指定的属性值、检测getter/setter等常用功能,但是ObjectWrapper只是这些功能的最后一站,我们省略了对属性表达式解析过程的介绍,而该解析过程是在MetaObject中实现的
MetaObject和ObjectWrapper中关于类级别的方法,例如hasGetter()、hasSetter()、findProperty()等方法,都是直接调用MetaClass的对应方法实现的
其他方法都是关于对象级别的方法,这些方法都是与ObjectWrapper配合实现
类型转换
模块type包
主要负责Java类型与JDBC类型转换,JDBC数据类型与Java语言中的数据类型并不是完全对应的,所以在PreparedStatement为SQL语句绑定参数时,需要从Java类型转换成JDBC类型,而从结果集中获取数据时,则需要从JDBC类型转换成Java类型
其中有三个比较重要的类,分别是:
MyBatis中所有的类型转换器都继承了TypeHandler接口,在TypeHandler接口中定义了如下四个方法,这四个方法分为两类:setParameter()方法负责将数据由JdbcType类型转换成Java类型;getResult()方法及其重载负责将数据由Java类型转换成JdbcType类型,最后的实现其实就是调用了PreparedStatement和ResultSet对应类型的get()和set()方法。
TypeHandlerRegistry类负责管理众多TypeHandler接口的实现,以及确定何时哪个类型使用哪个TypeHandler
在编写SQL语句时,使用别名可以方便理解以及维护,例如表名或列名很长时,我们一般会为其设计易懂易维护的别名。MyBatis将SQL语句中别名的概念进行了延伸和扩展,MyBatis可以为一个类添加一个别名,之后就可以通过别名引用该类。
MyBatis通过TypeAliasRegistry类完成别名注册和管理的功能,TypeAliasRegistry的结构比较简单,它通过TYPE_ALIASES字段(Map<String, Class<?>>类型)管理别名与Java类型之间的对应关系,通过TypeAliasRegistry.registerAlias()方法完成注册别名。
日志模块
在Java开发中常用的日志框架有Log4j、Log4j2、Apache Commons Log、java.util.logging、slf4j等,这些工具对外的接口不尽相同。为了统一这些工具的接口,MyBatis定义了一套统一的日志接口供上层使用,并为上述常用的日志框架提供了相应的适配器
在LogFactory类加载时会执行其静态代码块,其逻辑是按序加载并实例化对应日志组件的适配器,然后使用LogFactory.logConstructor这个静态字段,记录当前使用的第三方日志组件的适配器
资源加载
模块在IO包中提供的ClassLoaderWrapper是一个ClassLoader的包装器,其中包含了多个ClassLoader对象。通过调整多个类加载器的使用顺序,ClassLoaderWrapper可以确保返回给系统使用的是正确的类加载器。使用ClassLoaderWrapper就如同使用一个ClassLoader对象,ClassLoaderWrapper会按照指定的顺序依次检测其中封装的ClassLoader对象,并从中选取第一个可用的ClassLoader完成相关功能
ClassLoaderWrapper的主要功能可以分为三类,分别是getResourceAsURL()方法、classForName()方法、getResourceAsStream()方法,这三个方法都有多个重载,这三类方法最终都会调用参数为String和ClassLoader[]的重载
Resources是一个提供了多个静态方法的工具类,其中封装了一个ClassLoaderWrapper类型的静态字段,Resources提供的这些静态工具都是通过调用该ClassLoaderWrapper对象的相应方法实现的
ResolverUtil可以根据指定的条件查找指定包下的类,其中使用的条件由Test接口表示。ResolverUtil中使用classLoader字段(ClassLoader类型)记录了当前使用的类加载器,默认情况下,使用的是当前线程上下文绑定的ClassLoader,我们可以通过setClassLoader()方法修改使用类加载器
MyBatis提供了两个常用的Test接口实现,分别是IsA和AnnotatedWith。IsA用于检测类是否继承了指定的类或接口,AnnotatedWith用于检测类是否添加了指定的注解。该接口也可以自定义实现扩展
VFS表示虚拟文件系统(Virtual File System),它用来查找指定路径下的资源。VFS是一个抽象类,MyBatis中提供了JBoss6VFS 和 DefaultVFS两个VFS的实现。也可以提供自定义的VFS实现类。初始化时会使用到该类及子类。
数据源
模块在数据持久层中,数据源是一个非常重要的组件,其性能直接关系到整个数据持久层的性能。在实践中比较常见的第三方数据源组件有Apache Common DBCP、C3P0、Proxool等,MyBatis不仅可以集成第三方数据源组件,还提供了自己的数据源实现。
常见的数据源组件都实现了javax.sql.DataSource接口,MyBatis自身实现的数据源实现也不例外。MyBatis提供了两个javax.sql.DataSource接口实现,分别是PooledDataSource和UnpooledDataSource。Mybatis使用不同的DataSourceFactory接口实现创建不同类型的DataSource,这里使用了工厂方法模式。
DataSourceFactory接口扮演工厂接口的角色。UnpooledDataSourceFactory 和PooledDataSourceFactory则扮演着具体工厂类的角色,DataSourceFactory如下:
public interface DataSourceFactory {
//设置DataSource的相关属性,一般紧跟在初始化完成之后
void setProperties(Properties props);
//获取DataSource对象
DataSource getDataSource();
}
在UnpooledDataSourceFactory的构造函数中会直接创建UnpooledDataSource对象,并初始化UnpooledDataSourceFactory.dataSource字段。UnpooledDataSourceFactory.setProperties()方法会完成对UnpooledDataSource对象的配置
UnpooledDataSourceFactory.getDataSource()方法实现比较简单,它直接返回dataSource字段记录的UnpooledDataSource对象。
PooledDataSourceFactory继承了UnpooledDataSourceFactory,但并没有覆盖setProperties()方法和getDataSource()方法。两者唯一的区别是PooledDataSourceFactory的构造函数会将其dataSource字段初始化为PooledDataSource对象
JndiDataSourceFactory是依赖JNDI服务从容器中获取用户配置的DataSource
UnpooledDataSource实现了javax.sql.DataSource接口中定义的getConnection()方法及其重载方法,用于获取数据库连接。每次通过UnpooledDataSource.getConnection()方法获取数据库连接时都会创建一个新连接
数据库连接的创建过程是非常耗时的,数据库能够建立的连接数也非常有限,所以在绝大多数系统中,数据库连接是非常珍贵的资源,使用数据库连接池就显得尤为必要。使用数据库连接池会带来很多好处,例如,可以实现数据库连接的重用、提高响应速度、防止数据库连接过多造成数据库假死、避免数据库连接泄露等。
数据库连接池在初始化时,一般会创建一定数量的数据库连接并添加到连接池中备用。当程序需要使用数据库连接时,从池中请求连接;当程序不再使用该连接时,会将其返回到池中缓存,等待下次使用,而不是直接关闭。当然,数据库连接池会控制连接总数的上限以及空闲连接数的上限,如果连接池创建的总连接数已达到上限,且都已被占用,则后续请求连接的线程会进入阻塞队列等待,直到有线程释放出可用的连接。如果连接池中空闲连接数较多,达到其上限,则后续返回的空闲连接不会放入池中,而是直接关闭,这样可以减少系统维护多余数据库连接的开销。
如果将总连接数的上限设置得过大,可能因连接数过多而导致数据库僵死,系统整体性能下降;如果总连接数上限过小,则无法完全发挥数据库的性能,浪费数据库资源。如果将空闲连接的上限设置得过大,则会浪费系统资源来维护这些空闲连接;如果空闲连接上限过小,当出现瞬间的峰值请求时,系统的快速响应能力就比较弱。所以在设置数据库连接池的这两个值时,需要进行性能测试、权衡以及一些经验。
PooledDataSource实现了简易数据库连接池的功能,其中需要注意的是,PooledDataSource创建新数据库连接的功能是依赖其中封装的UnpooledDataSource对象实现的。它依赖的组件如下图所示
PooledDataSource并不会直接管理java.sql.Connection对象,而是管理PooledConnection对象。在PooledConnection中封装了真正的数据库连接对象(java.sql.Connection)以及其代理对象,这里的代理对象是通过JDK动态代理产生的。PooledConnection继承了InvocationHandler接口(补充:InvocationHandler接口是jdk动态代理的核心接口)
这里重点关注PooledConnection.invoke()方法的实现,该方法是proxyConnection这个连接代理对象的真正代理逻辑,它会对close()方法的调用进行代理,并且在调用真正数据库连接的方法之前进行检测,代码如下:
PoolState是用于管理PooledConnection对象状态的组件,它通过两个ArrayList <PooledConnection>集合分别管理空闲状态的连接和活跃状态的连接,定义如下:
//空闲的PooledConnection集合
protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
//活跃的PooledConnection集合
protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
PooledDataSource中管理的真正的数据库连接对象是由PooledDataSource中封装的UnpooledDataSource对象创建的,并由PoolState管理所有连接的状态。
PooledDataSource.getConnection()方法首先会调用PooledDataSource.popConnection()方法获取PooledConnection对象,然后通过PooledConnection.getProxyConnection()方法获取数据库连接的代理对象。popConnection()方法是PooledDataSource的核心逻辑之一,其具体逻辑如下图:
当调用连接的代理对象的close()方法时,并未关闭真正的数据连接,而是调用PooledDataSource.pushConnection()方法将PooledConnection对象归还给连接池,供之后重用。PooledDataSource.pushConnection()方法也是PooledDataSource的核心逻辑之一,其逻辑如下图所示
PooledDataSource.forceCloseAll()方法,当修改PooledDataSource的字段时,例如数据库URL、用户名、密码、autoCommit配置等,都会调用forceCloseAll()方法将所有数据库连接关闭,同时也会将所有相应的PooledConnection对象都设置为无效,清空activeConnections集合和idleConnections集合。应用系统之后通过PooledDataSource. getConnection()获取连接时,会按照新的配置重新创建新的数据库连接以及相应的PooledConnection对象,如下,在修改属性都会调用该方法。
事务管理
模块MyBatis使用Transaction接口对数据库事务进行了抽象,Transaction接口的定义如下:
public interface Transaction {
/**
* 获取对应的数据库连接对象
*/
Connection getConnection() throws SQLException;
/**
* 提交事务
*/
void commit() throws SQLException;
/**
* 回滚事务
*/
void rollback() throws SQLException;
/**
* 关闭数据库连接
*/
void close() throws SQLException;
/**
* 获取事务超时时间
*/
Integer getTimeout() throws SQLException;
}
Transaction接口有JdbcTransaction、ManagedTransaction两个实现,其对象分别由JdbcTransactionFactory和ManagedTransactionFactory负责创建。这里也使用了工厂方法模式。
JdbcTransaction依赖于JDBC Connection控制事务的提交和回滚。JdbcTransaction中字段的含义如下:
protected Connection connection;//事务对应的数据库连接
protected DataSource dataSource;//数据库连接对应的DataSource
protected TransactionIsolationLevel level;//事务隔离级别
// MEMO: We are aware of the typo. See #941
protected boolean autoCommmit;//是否自动提交
在JdbcTransaction的构造函数中会初始化除connection字段之外的其他三个字段,而connection字段会延迟初始化,它会在调用getConnection()方法时通过dataSource.getConnection()方法初始化,并且同时设置autoCommit和事务隔离级别。JdbcTransaction的commit()方法和rollback()方法都会调用Connection对应方法实现的。
ManagedTransaction的实现更加简单,它同样依赖其中的dataSource字段获取连接,但其commit()、rollback()方法都是空实现,事务的提交和回滚都是依靠容器管理的。ManagedTransaction中通过closeConnection字段的值控制数据库连接的关闭行为。相当于使用ManagedTransaction的commit和rollback功能不会对事务有任何的影响(实际上也是因为他并未实现功能),它什么都不会做,它将事务管理的权利移交给了容器来实现。
在实践中,MyBatis通常会与Spring集成使用,数据库的事务是交给Spring进行管理的,从而使用Transaction接口的另一个实现类SpringManagedTransaction。
Binding
模块背景:在iBatis(MyBatis的前身)中,查询一个Blog对象时需要调用SqlSession.queryForObject (“selectBlog”, blogId)方法。其中,SqlSession.queryForObject()方法会执行指定的SQL语句进行查询并返回一个结果对象,第一个参数“selectBlog”指明了具体执行的SQL语句的id,该SQL语句定义在相应的映射配置文件中。如果我们错将“selectBlog”写成了“selectBlog1”,在初始化过程中,MyBatis是无法提示该错误的,而在实际调用queryForObject(“selectBlog1”, blogId)方法时才会抛出异常,MyBatis提供了binding模块用于解决上述问题
我们可以定义一个接口(为方便描述,后面统一称为“Mapper接口”),该示例中为BlogMapper接口,具体代码如下所示。注意,这里的BlogMapper接口并不需要继承任何其他接口,而且开发人员不需要提供该接口的实现
该Mapper接口中定义了SQL语句对应的方法,这些方法在MyBatis初始化过程中会与映射配置文件中定义的SQL语句相关联。如果存在无法关联的SQL语句,在MyBatis的初始化节点就会抛出异常。我们可以调用Mapper接口中的方法执行相应的SQL语句,这样编译器就可以帮助我们提早发现上述问题。查询Blog对象就变成了如下代码:
//首先,获取BlogMapper对应的代理对象
BlogMapper mapper = session.getMapper(BlogMapper.class);
//调用Mapper接口中定义的方法执行对应的SQL语句
Blog blog = mapper.selectBlog(1);
MapperRegistry是Mapper接口及其对应的代理对象工厂的注册中心。提供给Configuration对象使用,(Configuration是MyBatis全局性的配置对象,在MyBatis初始化的过程中,所有配置信息会被解析成相应的对象并记录到Configuration对象中)
private final Configuration config;//MyBatis全局唯一的配置对象,其中包含了所有配置信息
//记录Mapper接口与对应MapperProxyFactory之间的关系
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
在MyBatis初始化过程中会读取映射配置文件以及Mapper接口中的注解信息,并调用MapperRegistry.addMapper()方法填充MapperRegistry.knownMappers集合,该集合的key是Mapper接口对应的Class对象,value为MapperProxyFactory工厂对象,可以为Mapper接口创建代理对象。
MapperRegistry.addMapper()方法如下:
在需要执行某SQL语句时,会先调用MapperRegistry.getMapper()方法获取实现了Mapper接口的代理对象
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//查找指定type对应的MapperProxyFactory对象
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//创建实现了type的接口代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
(上面所提到的session.getMapper(BlogMapper.class)方法得到的实际上是MyBatis通过JDK动态代理为BlogMapper接口生成的代理对象)
MapperProxyFactory主要负责创建代理对象,其中核心字段的含义和功能如下:
//当前MapperProxyFactory对象可以创建实现了mapperInterface接口的代理对象
private final Class<T> mapperInterface;
//缓存 ,key是mapperInterface接口中某方法对应的Method对象,value是对应的MapperMethod对象
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
MapperProxy实现了InvocationHandler接口,是代理对象的核心逻辑,MapperProxy中核心字段的含义和功能如下:
private final SqlSession sqlSession;//记录关联的SqlSession对象
private final Class<T> mapperInterface;//Mapper接口对应的Class对象
// 用于缓存MapperMethod对象,其中key是Mapper接口中的方法对应的Method对象,value是对应的MapperMethod对象,MapperMethod对象会完成参数的转换以及SQL语句的执行功能需要注意的是,MapperMethod中并不记录如何状态相关的信息,所以可能在多个代理对象之间共享
private final Map<Method, MapperMethod> methodCache;
MapperProxy.invoke()方法是代理对象执行的主要逻辑,实现如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
//如果目标方法继承自Object,则直接调用
return method.invoke(this, args);
} else if (isDefaultMethod(method)) { //针对jdk7
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//从缓存中获取MapperMethod对象,如果缓存中没有,则创建新的MapperMethod对象并添加到缓存中
final MapperMethod mapperMethod = cachedMapperMethod(method);
//调用MapperMethod.execute()方法执行sql语句
return mapperMethod.execute(sqlSession, args);
}
MapperProxy.cachedMapperMethod()方法主要负责维护methodCache这个缓存集合,实现如下:
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method); //在缓存中查找MapperMethod
if (mapperMethod == null) {
//创建MapperMethod对象,并添加到methodCached中
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
MapperMethod中封装了Mapper接口中对应方法的信息,以及对应SQL语句的信息。读者可以将MapperMethod看作连接Mapper接口以及映射配置文件中定义的SQL语句的桥梁。MapperMethod中各个字段的信息如下:
private final SqlCommand command;//记录了SQL语句的名称和类型
private final MethodSignature method;//Mapper接口中对应方法的相关信息
SqlCommand是MapperMethod中定义的内部类,它使用name字段记录了SQL语句的名称,使用type字段(SqlCommandType类型)记录了SQL语句的类型。SqlCommandType是枚举类型,有效取值为UNKNOWN、INSERT、UPDATE、DELETE、SELECT、FLUSH。
SqlCommand构造函数中会通过接口名类全限定名与对应的方法名相加组成的statementId
在Configuration中查找指定的MappedStatement对象来赋值SqlCommand的name和type字段,若是没有查到MappedStatement对象且当前方法不是标识@Flush注解的方法,则会爆出Invalid bound statement (not found)
异常。
MethodSignature也是MapperMethod中定义的内部类,其中封装了Mapper接口中定义的方法的相关信息,MethodSignature中核心字段的含义如下:
private final boolean returnsMany;//返回值类型是否为Collection类型或是数组类型
private final boolean returnsMap;//返回值是否为Map类型
private final boolean returnsVoid;//返回值是否为void
private final boolean returnsCursor;//返回值是否为Cursor类型
private final Class<?> returnType;//返回值类型
private final String mapKey;//返回值类型是map,则该字段记录了作为key的列名
private final Integer resultHandlerIndex;//用来标记该方法参数列表中resultHandler位置
private final Integer rowBoundsIndex;//用来标记该方法参数列表中 rowBounds位置
private final ParamNameResolver paramNameResolver;//该方法对应的ParamNameResolver对象
其中ParamNameResolver用来处理Mapper接口中定义的方法的参数列表,ParamNameResolver使用name字段(SortedMap<Integer, String>类型)记录了参数在参数列表中的位置索引与参数名称之间的对应关系,其中key表示参数在参数列表中的索引位置,value表示参数名称(),参数名称可以通过@Param注解指定,如果没有指定@Param注解,则使用参数索引作为其名称。
ParamNameResolver的hasParamAnnotation字段(boolean类型)记录对应方法的参数列表中是否使用了@Param注解。
在ParamNameResolver的构造方法中,会通过反射的方式读取Mapper接口中对应方法的信息,并初始化上述两个字段
在MethodSignature的构造函数中会解析相应的Method对象,并初始化上述字段。
回到MapperMethod继续分析,MapperMethod中最核心的方法是execute()方法,它会根据SQL语句的类型调用SqlSession对应的方法完成数据库操作
当执行INSERT、UPDATE、DELETE类型的SQL语句时,其执行结果都需要经过MapperMethod.rowCountResult()方法处理。SqlSession中的insert()等方法返回的是int值,rowCountResult()方法会将该int值转换成Mapper接口中对应方法的返回值
如果Mapper接口中定义的方法准备使用ResultHandler处理查询结果集,则通过MapperMethod.executeWithResultHandler()方法处理
如果Mapper接口中对应方法的返回值为数组或是Collection接口实现类,则通过MapperMethod.executeForMany ()方法处理
如果Mapper接口中对应方法的返回值为Map类型,则通过MapperMethod.executeForMap ()方法处理
executeForCursor()方法与executeForMap ()方法类似,唯一区别就是调用了SqlSession的selectCursor()方法
缓存
模块MyBatis中的缓存是两层结构的,分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是Cache接口的实现,此处只对源码进行大概解释。使用功能后续与执行器整合介绍,在缓存模块中涉及了装饰器模式的相关知识。所以在此补充一下装饰器模式。
**Component(组件):**组件接口定义了全部组件实现类以及所有装饰器实现的行为
ConcreteComponent(具体组件实现类):具体组件实现类实现了Component接口。通常情况下,具体组件实现类就是被装饰器装饰的原始对象,该类提供了Component接口中定义的最基本的功能,其他高级功能或后续添加的新功能,都是通过装饰器的方式添加到该类的对象之上的
Decorator(装饰器):所有装饰器的父类,它是一个实现了Component接口的抽象类,并在其中封装了一个Component对象,也就是被装饰的对象。而这个被装饰的对象只要是Component类型即可,这就实现了装饰器的组合和复用。如图2-41所示,装饰器C(ConcreteDecorator1类型)修饰了装饰器B(ConcreteDecorator2类型)并为其添加功能W,而装饰器B(ConcreteDecorator2类型)又修饰了组件A(ConcreteComponent类型)并为其添加功能V。其中,组件对象A提供的是最基本的功能,装饰器B和装饰器C会为组件对象A添加新的功能。
**ConcreteDecorator:**具体的装饰器实现类,该实现类要向被装饰对象添加某些功能。如图2-41所示,装饰器B、C就是该角色,被装饰的对象只要是Component类型即可
在MyBatis的缓存模块中,使用了装饰器模式的变体,其中将Decorator接口和Component接口合并为一个Component接口,类图如下:
MyBatis中缓存模块相关的代码位于cache包下,其中Cache接口是缓存模块中最核心的接口,它定义了所有缓存的基本行为,Cache接口的定义如下:
public interface Cache {
/**
* 该缓存对象的id
*/
String getId();
/**
* 向缓存中添加数据,一般情况下,key是CacheKey
*/
void putObject(Object key, Object value);
/**
* 根据指定的key,在缓存中查找对应的缓存结果对象
*/
Object getObject(Object key);
/**
* 删除key对应的项
*/
Object removeObject(Object key);
/**
* 清空缓存
*/
void clear();
/**
* 缓存的个数,该方法不会被MyBatis核心代码使用,所以可以提供空现实
*/
int getSize();
/**
* 获取读写锁,该方法不会被MyBatis核心代码使用,所以可以提供空现实
* @return A ReadWriteLock
*/
ReadWriteLock getReadWriteLock();
}
Cache接口的实现类有多个,但大部分都是装饰器,只有PerpetualCache提供了Cache接口的基本实现。
PerpetualCache在缓存模块中扮演着ConcreteComponent的角色,其实现比较简单,底层使用HashMap记录缓存项,也是通过该HashMap对象的方法实现的Cache接口中定义的相应方法
下面来介绍cache.decorators包下提供的装饰器,它们都直接实现了Cache接口,扮演着ConcreteDecorator的角色。这些装饰器会在PerpetualCache的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求,在二级缓存中,会见到这些装饰器是如何完成动态组合的。
BlockingCache是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定key对应的数据假设线程A在BlockingCache中未查找到keyA对应的缓存项时,线程A会获取keyA对应的锁,这样后续线程在查找keyA时会发生阻塞,如图所示:
在开始介绍SoftCache和WeakCache实现之前,先简单介绍一下Java提供的4种引用类型,它们分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和幽灵引用(Phantom Reference)
- 强引用是Java编程中最普遍的引用,例如Object obj = new Object()中,新建的Object对象就是被强引用的。如果一个对象被强引用,即使是Java虚拟机内存空间不足时,GC(垃圾收集器)也绝不会回收该对象。当Java虚拟机内存不足时,就可能会导致内存溢出,我们常见的就是OutOfMemoryError异常。
- 软引用是引用强度仅弱于强引用的一种引用,它使用类SoftReference 来表示。当Java虚拟机内存不足时,GC会回收那些只被软引用指向的对象,从而避免内存溢出。在GC释放了那些只被软引用指向的对象之后,虚拟机内存依然不足,才会抛出OutOfMemoryError异常。软引用适合引用那些可以通过其他方式恢复的对象,例如,数据库缓存中的对象就可以从数据库中恢复,所以软引用可以用来实现缓存,下面将要介绍的SoftCache就是通过软引用实现的
- 弱引用的强度次于软引用。弱引用使用WeakReference来表示,它可以引用一个对象,但并不阻止被引用的对象被GC回收。在JVM虚拟机进行垃圾回收时,如果指向一个对象的所有引用都是弱引用,那么该对象会被回收。
- 幽灵引用,又叫“虚引用”,它是最弱的一种引用类型,由类PhantomReference表示。在引用的对象未被GC回收时,调用前面介绍的SoftReference以及WeakReference的get()方法,得到的是其引用的对象;当引用的对象已经被GC回收时,则得到null。但是PhantomReference.get()方法始终返回null。
SoftCache中各个字段的含义如下:
//在SoftCache中,最近使用的一部分缓存不会被GC回收,这就是通过将其value添加到hardLinksToAvoidGarbageCollection集合中实现的(即有强引用指向其value),hardLinksToAvoidGarbageCollection集合是LinkedList类型
private final Deque<Object> hardLinksToAvoidGarbageCollection;
//引用队列,用于记录被GC回收的的缓存所对应的SoftEntry对象
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private final Cache delegate;// 被装饰的底层Cache对象
private int numberOfHardLinks;//强连接的个数 默认值是256
private static class SoftEntry extends SoftReference<Object> {
private final Object key;
SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
super(value, garbageCollectionQueue);//指向value的引用是软引用
this.key = key;//强引用
}
}
@Override
public void putObject(Object key, Object value) {
removeGarbageCollectedItems();//清除已经被gc回收的缓存
//添加缓存项
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
private void removeGarbageCollectedItems() {
SoftEntry sv;
//遍历queueOfGarbageCollectedEntries集合
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);//将已经被gc回收的value对象对应的缓存项清除
}
}
public Object getObject(Object key) {
Object result = null;
//从缓存中查找对应的缓存项
@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
if (softReference != null) {//检测缓存中是否有对应的缓存项
result = softReference.get();//获取softReference引用中的value
if (result == null) {//已经被gc回收
delegate.removeObject(key);//从缓存中清除对应缓存项
} else {
// See #586 (and #335) modifications need more than a read lock
synchronized (hardLinksToAvoidGarbageCollection) {
//缓存项的value添加到hardLinksToAvoidGarbageCollection集合中保存
hardLinksToAvoidGarbageCollection.addFirst(result);
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
//超过numberOfHardLinks,则将最老的缓存项清除,此处类似先进先出
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
SoftCache.removeObject()方法在清除缓存项之前,也会调用removeGarbageCollectedItems()方法清理被GC回收的缓存项
SoftCache.clear()方法首先清理hardLinksToAvoidGarbageCollection集合,然后清理被GC回收的缓存项,最后清理底层delegate缓存中的缓存项
WeakCache的实现与SoftCache基本类似,唯一的区别在于其中使用WeakEntry(继承自WeakReference)封装真正的value对象,其他实现完全一样
ScheduledCache是周期性清理缓存的装饰器,它的clearInterval字段记录了两次缓存清理之间的时间间隔,默认是一小时,lastClear字段记录了最近一次清理的时间戳。ScheduledCache 的getObject()、putObject()、removeObject()等核心方法在执行时,都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。
LoggingCache在Cache的基础上提供了日志功能,它通过hit字段和request字段记录了Cache的命中次数和访问次数。在LoggingCache.getObject()方法中会统计命中次数和访问次数这两个指标,并按照指定的日志输出方式输出命中率。LoggingCache代码比较简单,请读者参考代码学习。
-SynchronizedCache通过在每个方法上添加synchronized关键字,为Cache添加了同步功能,有点类似于JDK中Collections中的SynchronizedCollection内部类的实现。SynchronizedCache代码比较简单,请读者参考代码学习。
SerializedCache提供了将value对象序列化的功能。SerializedCache在添加缓存项时,会将value对应的Java对象进行序列化,并将序列化后的byte[]数组作为value存入缓存。SerializedCache在获取缓存项时,会将缓存项中的byte[]数组反序列化成Java对象。使用前面介绍的Cache装饰器实现进行装饰之后,每次从缓存中获取同一key对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程以及缓存中的对象;而SerializedCache每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象
在Cache中唯一确定一个缓存项需要使用缓存项的key,MyBatis中因为涉及动态SQL等多方面因素,其缓存项的key不能仅仅通过一个String表示,所以MyBatis提供了CacheKey类来表示缓存项的key,在一个CacheKey对象中可以封装多个影响缓存项的因素。
CacheKey中可以添加多个对象,由这些对象共同确定两个CacheKey对象是否相同。CacheKey中核心字段的含义和功能如下:
private int multiplier;//参与计算hashcode,默认值是37
private int hashcode;//CacheKey对象的hashcode,初始值是17
private long checksum;//校验和
private List<Object> updateList;//由该集合中的所有对象共同决定两个CacheKey是否相同
private int count;//updateList集合的个数