一、前言
每个基于MyaBatis的应用都是以"一个"SqlSessionFactory实例为核心和基础的,而SqlSessionFactory是由SqlSessionFactoryBuilder创建的,而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。
二、MyBatis内部的几个类
1、SqlSessionFactoryBuilder
创建 SqlSessionFactory 实例。
2、SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,不能销毁或重新创建另一个SqlSessionFactory实例。 SqlSessionFactory 在应用运行期间不要重复创建多次。它的作用就是开启一个SqlSession,用户直接与SqlSession打交道。
3、SqlSession
SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。
try (SqlSession session = sqlSessionFactory.openSession()) {
Author author = (Author) session.selectOne("org.mybatis.example.AuthorMapper.selectAutor", 1);
}
更简洁的方式 ——使用接口的形式,可以执行更清晰和类型安全的代码。
try (SqlSession session = sqlSessionFactory.openSession()) {
AuthorMapper mapper = session.getMapper(AuthorMapper.class);
Author author = mapper.selectBlog(101);
}
确保 SqlSession 关闭的标准模式:
// 接口SqlSession 继承了Closeable接口,可以使用Java7 的 try-with-resources语法来保证session的关闭
try (SqlSession session = sqlSessionFactory.openSession()) {
// just do it
}
4、Configuration
存储了MyBatis的所有配置项和解析后的信息,极为重要的一个类。MyBatis初始化时,将解析出的信息一股脑的扔给Configuration进行存储。执行sql语句或者其它操作时,从此类里取出相关的配置信息进行操作。
两个重要属性:
- MapperRegistry mapperRegistry; 映射注册机实例。
- Map
mappedStatements,存储了 dao接口完全限定名+方法名 - MappedStatement实例的键值对。
5、XMLConfigBuilder
XML配置构建器,建造者模式。解析MyBatis的xml配置文件并以Document的形式保存下来,方便其内部解析方法解析出MyBatis的具体配置信息。
6、MapperRegistry
映射注册机,其属性 Map
存储了 Class对象 - 映射器代理工厂实例 的键值对,MyBatis初始化时存入,使用时取出
7、MapperProxyFactory
映射器代理工厂,映射器代理类的工厂方法,生产一个dao接口的代理实例提供给用户。
8、MapperProxy
映射器代理类,利用jdk动态代理返回给用户代理类实例,除Object通用方法(toString()、hashcode()等等)、接口默认方法(java8 新增默认方法)外,皆走代理方法,在代理方法内部执行数据库的crud。
9、MappedStatement
映射语句,存储了解析注解、xml后的sql语句相关信息。
10、MapperMethod
映射器方法。负责sql语句参数填充及语句的分类执行。
两个极为重要的属性:
SqlCommand一个内部类 封装了SQL标签的类型 insert delete update select
MethodSignature一个内部类 封装了方法的参数信息 返回类型信息等
一个execute方法,负责执行sql语句,并将结果处理好后返回给用户。
三、配置
可以通过XML文件方式和代码方式构建SqlSessionFactory,建议通过XML文件方式进行构建,简单、直观、明了
四、使用
// 配置文件路径
String resource = "org/apache/ibatis/builder/MapperConfig.xml";
/*
* Resources作用:通过类加载器简化对资源访问的类。
* 读取配置文件内容,返回流
*/
InputStream inputStream = Resources.getResourceAsStream(resource);
// 解析并构建出SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// SqlSession实现了Closeable接口
try(SqlSession sqlSession = sqlSessionFactory.openSession()) {
// 实际上获取到的是一个通过代理对象
AuthorMapper authorMapper = sqlSession.getMapper(AuthorMapper.class);
// 执行查询sql,并进行结果集映射
List list = author.getAuthors();
}
上述代码,就是使用MyBatis查询数据库某个表,并返回结果列表的过程,增删改一样的道理。在返回结果集后,sqlSession调用close()方法,关闭这个session。
这套配置及使用没有引入Spring和MyBatis-Spring。关于MyBatis-Spring在这里不做过多解释,有兴趣可以自己看一下MyBatis-Spring的文档及源码,基本上和我们要把MyBatis融入到Spring的思路差不太多。
五、开始解读
1、SqlSessionFactoryBuilder().build(inputStream)
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream)
这行代码的意思是通过SqlSessionFactoryBuilder创建一个SqlSessionFactory,而build具体的内部创建细节如下:
public class SqlSessionFactoryBuilder {
/*
* ① 在这里,内部调用了build(InputStream inputStream, String environment, Properties properties)方法
*/
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
/*
* ②
* 文件流、环境配置、属性,后两者都为null,有可能在MyBatis配置文件中有定义
*/
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
/*
* XML配置构建器 这样的写法棒棒哒,逻辑清晰明了,有点类似
*/
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
/*
* parser.parse() 返回MyBatis最重要的一个配置类 Configuration,作为DefaultSqlSessionFactory构造函数的参数传递进去,进行DefaultSqlSessionFactory的实例化
*/
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.
}
}
}
/*
* 创建SqlSessionFactory
*/
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
核心解析代码在XMLConfigBuilder.parse()方法里,由它解析出Configuration实例。
XMLConfigBuilder的parse代码如下:
XMLConfigBuilder:
public Configuration parse() {
// 只能解析一次 否则多次解析会打乱配置信息。而且多次解析本身就是一种错误
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 将解析标识置为true,防止二次解析
parsed = true;
// 内部私有方法,进一步进行配置解析,解析目标节点为.....
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
parseConfiguration(parser.evalNode("/configuration"));
内部私有方法,进一步进行配置解析。
XMLConfigBuilder:
// 解析配置
private void parseConfiguration(XNode root) {
try {
// #解析子节点properties
// issue #117 read properties first
// 这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来传递
propertiesElement(root.evalNode("properties"));
// mybatis中很重要的配置
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// #解析子节点typeAliases 别名
// 类型别名是为 Java 类型设置一个短的名字。 它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余。
typeAliasesElement(root.evalNode("typeAliases"));
// #解析子节点plugins 插件
pluginElement(root.evalNode("plugins"));
// #解析子节点objectFactory mybatis为结果创建对象时都会用到objectFactory
objectFactoryElement(root.evalNode("objectFactory"));
// #解析子节点objectWrapperFactory
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 3.3.0之前不需要 有默认实现
/*
Reflector 这个类的用途主要是是通过反射获取目标类的 getter 方法及其返回值类型,setter 方法及其参数值类型等元信息。并将获取到的元信息缓存到相应的集合中,供后续使用
*/
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// #解析environments 可以配置多个运行环境,但是每个SqlSessionFactory 实例只能选择一个运行环境
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
// #解析databaseIdProvider MyBatis能够执行不同的语句取决于提供的数据库供应商。许多数据库供应商的支持是基于databaseId映射
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// #解析typeHandlers 当MyBatis设置参数到PreparedStatement 或者从ResultSet 结果集中取得值时,就会使用TypeHandler 来处理数据库类型与java 类型之间转换
typeHandlerElement(root.evalNode("typeHandlers"));
// #解析mappers 主要的crud操作都是在mappers中定义的,这个是主线,继续往下走
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
在解析mapperElement方法之前,先看一下
// ① 扫描该目录包下所有mapper接口 xml文件必须在同一目录
// ② 使用相对classpath的路径
// ③ 使用完全限定资源定位符(URL)
// ④ 使用映射器接口实现类的完全限定名,xml文件必须在同一目录
mapperElement方法里,是这样解析的:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// attribute 目前只能是resource url class 其中之一
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 返回对应Dao接口 Class对象
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.");
}
}
}
}
}
为了方便,我们选择resource == null && url == null && mapperClass != null
这个条件分支。
Resouurces.classForName返回dao接口 Class对象,然后调用configuration.addMapper方法,configuration.addMapper里调用的是mapperRegistry.addMapper方法,加入到映射注册机。
MapperRegistry:
//将已经添加的映射都放入HashMap
private final Map, MapperProxyFactory>> knownMappers = new HashMap, MapperProxyFactory>>();
public void addMapper(Class 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(type));
// ②注解解析器,但同时也进行了同级目录下dao.xml的解析,如果没有xml文件,则去解析接口方法上的注解内容,如果二者皆有,报错 Mapped Statements collection already contains value for xx.xx.xx.xx
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
①创建映射器代理工厂,并加入到映射注册机的kownMappers
②在注解解析器的parse()方法里去探查同级目录下是否存在dao对应的xml文件。
加载sql信息时会有以下几种情况:
1)如果存在,则使用XMLMapperBuilder的parse方法,解析dao对应的配置文件为MappedStatement实例,将解析好的MappedStatement实例放入configuration的mappedStatements Map里,key是类的完全限定名+方法名,value就是MappedStatement实例
。
2)如果不存在dao接口对应的xml文件,则去加载注解sql。
public interface AtuhorMapper {
@Select("select * from author")
List getAuthors();
}
3)既存在注解又在xml里存在对应的sql标签,会抛出非法参数异常java.lang.IllegalArgumentException
提示: Mapped Statements collection already contains value for DAO类名.方法名()
。
4)如果二者皆不存在,启动没有任何问题,但是在项目里使用时,会通过dao接口完全限定名+方法名查找sql信息,没有找到会抛出Invalid bound statement (not found): cn.asae.e.contract.dao.ContractDao.方法
5)从这里我们知晓,dao接口里的方法不能重载,否则会抛出非法参数异常 提示: Mapped Statements collection already contains value for DAO类名.方法名()
。
ps: 再强调一遍,MappedStatement里存储着dao方法对应的sql所有信息。
以上是MyBatis启动时,需要做的工作,当然,我们仅仅只是讲了最为核心的dao接口与xml的绑定映射原理。
做完绑定映射之后,我们在对数据库进行crud时,又是怎么运行的呢?
2、sqlSession.getMapper(AuthorMapper.class)
sqlSession.getMapper返回的是AuthorMapper代理类的实例,其内部执行过程为:
1)调用configuration.getMapper
2)在configuration.getMapper里又调用了(映射注册机)mapperRegistry.getMapper,还有没有印象,在MyBatis初始化时,我们将dao的映射器代理工厂实例放入到了映射注册机里。
3)在mapperRegistry.getMapper方法里取出对应的映射器代理工厂,然后用JDK动态代理生成代理类实例返回给用户。代码如下:
MapperRegistry:
public T getMapper(Class type, SqlSession sqlSession) {
final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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);
}
}
MapperProxyFactory:
public class MapperProxyFactory {
private final Class mapperInterface;
private Map methodCache = new ConcurrentHashMap();
public MapperProxyFactory(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class getMapperInterface() {
return mapperInterface;
}
public Map getMethodCache() {
return methodCache;
}
// mapperProxy是一个实现java.lang.reflect.InvocationHandler接口的代理类
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy mapperProxy) {
//用JDK自带的动态代理生成映射器
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
将代理类实例返回给用户之后,用户进行了如下操作:
List
实际上调用的是代理类的invoke方法。
MapperProxy如下:
public class MapperProxy implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class mapperInterface;
private final Map methodCache;
public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
// mybatis dao 执行处理核心入口
// Method类,主要用于在程序运行状态中,动态地获取方法信息, args 参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
// 并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
// 方法返回表示声明由此Method对象表示的方法的类的Class对象
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
// 第一次肯定为空,之后由于缓存了,所以速度应该会有所增加
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
throws Throwable {
final Constructor constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Class> declaringClass = method.getDeclaringClass();
return constructor
.newInstance(declaringClass,
MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
.unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
}
/**
* Backport of java.lang.reflect.Method#isDefault()
*/
private boolean isDefaultMethod(Method method) {
return (method.getModifiers()
& (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
&& method.getDeclaringClass().isInterface();
}
}
invoke方法内部执行过程:
1)过滤掉接口默认方法及Object中通用的方法,真正执行的是MapperMethod.execute方法。
2)MapperMethod属性有方法签名、sql命令相关信息。
3)在实例化MaperMethod时,通过dao的完全限定名+方法名找到对应的MappedStatement实例,将MappedStatement的id、SqlCommandType赋值给SqlCommond的 name、type。
4)当执行查询时,通过SqlCommond的name 找到MappedStatement,从而找到对应的sql语句、入参类型、返回类型等等,然后填充参数,由SqlSession将填充好的sql语句提交到数据库,数据返回后,通过反射装配数据,返回给用户。
看一下MapperMethod的execute方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
// SqlCommandType是通过MappedStatement里存储的sql信息确定的
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;
}
重新梳理一下过程:
一、在MyBatis初始化时,加载并解析配置文件,将dao信息与sql配置文件进行绑定映射及其它配置信息的解析。
Configuration实例属性mapperRegistry,使用dao的Class对象作为key,存储映射器代理工厂实例。
Configuration实例属性mappedStatements使用dao接口的完全限定名+方法名作为key,存储MappedStatement实例。
二、在执行查询操作时,通过dao的Class对象作为key,取出代理工厂实例,由代理工厂生产一个dao接口的代理类实例,在代理类实例invoke方法里面,通过dao接口的完全限定名+方法名,取出MappedStatement实例,进行参数映射后发送到数据库,数据库返回结果后,根据MappedStatement存储的结果集映射策略,进行结果集的装配并返回给用户。
六、拓展
如果项目存在多数据源,SqlSessionFactory实例存在几个?dao方法是怎么做到区分的?事务又是如何配置的?
七、最后
1、知识点:
在刚刚梳理代码中,涉及到了单例模式、工厂模式、建造者模式,代理模式等设计模式,带来的好处就是代码层级和架构清晰,类职责明确。从实现来看,基本符合高内聚、低耦合。
MyBatis的核心就在于配置文件解析和sql语句映射这一块,也是精髓所在,尤其是利用jdk的代理,达到crud的操作,堪称画龙点睛之笔。
2、个人存疑:
- 有些地方的代码写的有点摸不着头脑,如下图。
- 其中的一些类,如:MapperAnnotationBuilder 存在穿插解析注解sql和xml文件sql的逻辑。
- 为什么不支持dao接口方法重载?
总得来说,MyBatis源码的功底还是很不错的,对我的启迪与收货也很大。
感谢MyBatis团队。