快速搭建MyBatis开发环境 在文章开始前我们先搭一个最简单的MyBatis环境,只要3个步骤,这里省去建立表和类等七七八八。
1.XML 配置文件 XML 配置文件(configuration XML)中包含了对 MyBatis 系统的核心设置,包含获取数据库连接实例的数据源(DataSource)和决定事务范围和控制方式的事务管理器(TransactionManager)。XML 配置文件的详细内容后面再探讨,这里先给出一个简单的示例:
2. SQL 语句映射文件 而 SQL 语句映射是个相当大的话题,可能会占去文档的大部分篇幅。这里会给出几个例子。
select * from Blog where id = #{id}
3.代码调用
//1.获取配置文件
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//2.构建SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//3.获取SqlSession
SqlSession session = sqlSessionFactory.openSession();
try {
//4..取得映射器
BlogMapper mapper = session.getMapper(BlogMapper.class);
//5.调用对应sql
Blog blog = mapper.selectBlog(101);
} finally {
session.close();
}
源码解析 接下来根据代码调用的几个步骤进行逐一分析,看看MyBatis的底层是如何实现这些功能的。
1.获取配置文件 MyBatis使用Resourcs类来完成配置文件的获取,Resourcs类的方法如下:
文章图片
文章图片
从上图可以看出Resourcs类可以处理资源文件以及URL,并且可以转化为File、Reader、InputStream、Properties几种类,除了File外都可以用于SqlSessionFactory 的构建。
Resourcs类的解析方式并不复杂
public static InputStream getResourceAsStream(String resource) throws IOException {
return getResourceAsStream(null, resource);
}public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) {
throw new IOException("Could not find resource " + resource);
}
return in;
}
具体的解析是在ClassLoaderWrapper类中进行, ClassLoaderWrapper类获取各种ClassLoader对象,通过遍历的方式使用不同的ClassLoader来获取资源文件,确保各种环境下都能找到资源文件。
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
//用户传入的ClassLoader
classLoader,
//默认的ClassLoader,在Resources类中调用setDefaultClassLoader方法设置
defaultClassLoader
//当前线程的ClassLoader
Thread.currentThread().getContextClassLoader(),
//当前类的ClassLoader
getClass().getClassLoader(),
//ClassLoader.getSystemClassLoader()
systemClassLoader};
}
public InputStream getResourceAsStream(String resource) {
return getResourceAsStream(resource, getClassLoaders(null));
}
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
for (ClassLoader cl : classLoader) {
if (null != cl) {
// 尝试获取InputStream
InputStream returnValue = https://www.it610.com/article/cl.getResourceAsStream(resource);
// 某些ClassLoader是以"/"开头,若找不到资源文件则再一次尝试
if (null == returnValue) {
returnValue = https://www.it610.com/article/cl.getResourceAsStream("/" + resource);
}
//找到资源文件则返回
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
2.构建SqlSessionFactory 获取的资源对象作为SqlSessionFactoryBuilder类的参数,SqlSessionFactoryBuilder支持多种方式构建SqlSessionFactory
文章图片
文章图片
正常情况下SqlSessionFactoryBuilder只需要1个参数便可以进行解析,那为什么要重载多个参数的方法呢?在实际情况下,我们可能会有多种配置方式,例如XML,注解,Properties等,这些配置会出现一些属性的冲突,MyBatis允许同时传入多个不同类型的配置,有着固定的优先顺序。
以下内容摘自官方文档:
如果属性在不只一个地方进行了配置,那么 MyBatis 将按照下面的顺序来加载:
- 在 properties 元素体内指定的属性首先被读取。
- 然后根据 properties 元素中的 resource 属性读取类路径下属性文件或根据 url 属性指定的路径读取属性文件,并覆盖已读取的同名属性。
- 最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。
接下来看看SqlSessionFactoryBuilder类是如何解析资源
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//这里由XMLConfigBuilder类完成解析
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//parse方法返回Configuration类对象,Configuration类保存了各种配置
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接口的实现类DefaultSqlSessionFactory类
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
XMLConfigBuilder类的工作是把XML中配置的内容转化相应的对象,这些对象全部存在Configuration类中,实际的XML解析是由XPathParser类来完成,XPathParser类采用了XPath方式来解析XML文件,这里就省略不提了。
private XPathParser parser;
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
//实际由XPathParser来解析,结果存在parser对象中
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
public Configuration parse() {
//这里先判断是否解析过,避免重复解析
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//configuration是配置文件的根节点
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//对不同的配置进行处理,基本看名字就知道对应哪部分了,这里暂不详细展开
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectionFactoryElement(root.evalNode("reflectionFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
这里来看看propertiesElement方法,该方法体现了文档中描述的属性加载顺序
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//先是获取配置文件中的Properties属性
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.Please specify one or the other.");
}
//然后是从resource或url属性中的资源文件获取,两者不能同时配置
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
//该对象就是build中的prop参数
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
//最后将属性进行保存,加载顺序和文档中一致
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}(root.evalNode("properties"));
这里只是对解析配置的流程做个简单介绍,针对每个配置的具体解析将在讲解到后再具体阐述。 3.获取SqlSession
SqlSessionFactory 有六个方法可以用来创建 SqlSession 实例。通常来说,如何决定是你 选择下面这些方法时:
- Transaction (事务): 你想为 session 使用事务或者使用自动提交(通常意味着很多 数据库和/或 JDBC 驱动没有事务)?
- Connection (连接): 你想 MyBatis 获得来自配置的数据源的连接还是提供你自己
- Execution (执行): 你想 MyBatis 复用预处理语句和/或批量更新语句(包括插入和 删除)?
文章图片
默认的 openSession()方法没有参数,它会创建有如下特性的 SqlSession:
- 会开启一个事务(也就是不自动提交)
- 连接对象会从由活动环境配置的数据源实例中得到。
- 事务隔离级别将会使用驱动或数据源的默认设置。
- 预处理语句不会被复用,也不会批量处理更新。
public SqlSession openSession() {
//默认的设置
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx);
// may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session.Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
4.获取Mapper 获得SqlSession后,我们可以从中取得对应的Mapper,这些对象在SqlSession初始化时通过configuration传入。Mapper的生成是在构建SqlSessionFactory阶段进行的,位置如下:
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectionFactoryElement(root.evalNode("reflectionFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//这里解析Mapper
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我们知道Mapper的配置是MyBatis最强大也是最复杂的一部分,这篇文章作为一个入门,只对Mapper的原理进行分析,暂时不对各种高级配置进行讲解。
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");
//这里也是讲究一个顺序,避免配置冲突的问题
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//增对Mapper配置文件由专门的XMLMapperBuilder类来解析
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) {
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.");
}
}
}
}
}//XMLMapperBuilder的parse方法
public void parse() {
//先判断是否解析过了,只解析一次
if (!configuration.isResourceLoaded(resource)) {
//解析各种配置
configurationElement(parser.evalNode("/mapper"));
//放入已解析资源列表,避免重复解析
configuration.addLoadedResource(resource);
//将解析的结果放入configuration中
bindMapperForNamespace();
}//在上面的解析过程中有些类具有继承关系,可能在解析时该类的父类还没解析,
//这种情况下会先暂停解析,通过调用下面的代码来完成。
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
//在这里将Mapper的类存入configuration中
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
以上的过程中最终存储的是Mapper的类,该类只是用户定义的一个接口,当然还需要实现其中的方法。
在SqlSession调用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 {
//由MapperProxyFactory类来实例化
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy mapperProxy) {
//返回的是JDK代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
到这里整个原理已经基本明了了,实际上我们是调用了代理对象的方法,具体的定义在MapperProxy类中
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//从method缓存中获取需要的方法
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
//这里对method进行缓存
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
最终框架内部为会每个方法实现新建MapperMethod对象,并进行缓存,随后调用MapperMethod的execute方法返回用户想要的结果。
这里没有分析MapperMethod的代码实现,如果有机会写到Mapper配置文件的解析时会再讲解。
其他 附上官方文档的地址,对于MyBatis的学习十分有帮助,本文红字部分均摘自官方文档。
http://www.mybatis.org/mybatis-3/zh/index.html
【MyBatis|MyBatis官方教程及源码解析——入门】
推荐阅读
- 源码系列|Mybatis源码初探——优雅精良的骨架
- 数据库|学习 MyBatis 的一点小总结 —— 底层源码初步分析
- 90天Java---mybatis与mybatis plus-1
- 聊聊Mybatis的数据源之工厂模式
- 框架|TUP第19期综述(从12306看海量并发网站架构)
- IDEA+Maven实现MyBatis逆向工程
- 看Feign的源码一定要看这个类
- 框架|Spring Boot面试必问(自动配置原理)
- spring|Spring Boot 面试杀手锏(自动配置原理)