MyBatis解析
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis解析相关的知识,希望对你有一定的参考价值。
参考技术A 从命名上可以看出,这个是一个 Builder 模式的,用于创建 SqlSessionFactory 的类。SqlSessionFactoryBuilder 根据配置来构造 SqlSessionFactory。其中配置方式有两种:mybatis-config.xml 就是我们的配置文件:
Java Config 相比较 XML 文件的方式而言,会有一些限制。比如修改了配置文件需要重新编译,注解方式没有 XML 配置项多等。所以,业界大多数情况下是选择 XML 文件的方式。但到底选择哪种方式,这个要取决与自己团队的需要。比如,项目的 SQL 语句不复杂,也不需要一些高级的 SQL 特性,那么 Java Config 则会更加简洁一点;反之,则可以选择 XML 文件的方式。
创建配置文件解析器XMLConfigBuilder
解析mybatis-config.xml里的配置为Configuration对象,Mybatis的全局配置对象。
XMLConfigBuilder#parseConfiguration解析mapper下的xml
XMLMapperBuilder#bindMapperForNamespace,根据xml里的 namespace 反射出 mapper接口 的 class,如果有mapper接口,则把该mapper接口的class添加到Configuration的mapperRegistry里。
如果该接口已经注册,则抛出已经绑定的异常。
为该接口注册MapperProxyFactory,但这里只是注册其创建MapperProxy的工厂,并不是创建MapperProxy。
如果Mapper对应的xml资源未加载,触发xml的绑定操作,将xml中的sql语句与Mapper建立关系。
addMapper方法,只是为**Mapper创建对应对应的MapperProxyFactory。
根据Mapper接口与SqlSession创建MapperProxy对象。
根据接口类获取MapperProxyFactory。
调用MapperProxyFactory的newInstance创建MapperProxy对象。
SqlSessionFactory 顾名思义,是用于生产 SqlSession 的工厂。 通过如下的方式来获取 SqlSession 实例:
SqlSession 包含了执行 SQL 的所有的方法。以下是示例:
当然,下面的方式可以做到类型安全:
MapperProxy是MapperProxyFactory使用SqlSession创建出来的。所以MapperProxy中包含SqlSession。
可以看到MapperProxy调用invoke方法,进而调用MapperMethod的execute(),这些MapperMethod就是和你要执行的命令相关,比如执行select语句,则会通过SqlSession的select()方法,最终调用到Executor的query方法。Executor会再协调另外三个核心组件。
MapperProxy:
MapperMethod:
插件的构建:
谈原理首先要知道StatementHandler,ParameterHandler,Result Handler都是代理,他们是Configuration创建,在创建过程中会调用interceptorChain.pluginAll()方法,为四大组件组装插件(再底层是通过Plugin.wrap(target,XX, new Plugin( interceptor))来来创建的)。
插件链是何时构建的:
在执行SqlSession的query或者update方法时,SqlSession会通过Configuration创建Executor代理,在创建过程中就调用interceptor的pluginAll方法组装插件。然后executor在调用doQuery()方法的时候,也会调用Configuration的newStatementHandler方法创建StatemenHandler(和上面描述的一样,这个handler就是个代理,也是通过interceptorChain的pluginAll方法构建插件)
插件如何执行:
以statementhandler的prepare方法的插件为例,正如前面所说,statementhandler是一个proxy,执行他的prepare方法,将调用invokeHandler的invoke方法,而invokeHandler就是Plugin.wrap(target, xxx, new Plugin(interceptor))中的第三个参数,所以很自然invokeHanlder的invoke的方法最终就会调用interceptor对象的intercept方法。
Mybatis的插件配置在configuration内部,初始化时,会读取这些插件,保存于Configuration对象的InterceptorChain中。
org.apache.ibatis.plugin.InterceptorChain.java源码。
上面的for循环代表了只要是插件,都会以责任链的方式逐一执行,所谓插件,其实就类似于拦截器。
插件的编写
插件必须实现org.apache.ibatis.plugin.Interceptor接口。
-intercept()方法:执行拦截内容的地方,拦截目标对象的目标方法的执行
-plugin()方法:决定是否触发intercept()方法。 作用:包装目标对象,包装就是为目标对象创建一个代理对象
-setProperties()方法:给自定义的拦截器传递xml配置的属性参数。将插件注册时的property属性设置进来
下面自定义一个拦截器:
为什么要写Annotation注解?注解都是什么含义?
Mybatis规定插件必须编写Annotation注解,是必须,而不是可选。@Intercepts注解:装载一个@Signature列表,一个@Signature其实就是一个需要拦截的方法封装。那么,一个拦截器要拦截多个方法,自然就是一个@Signature列表。
type = Executor.class, method = "query", args = MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
解释:要拦截Executor接口内的query()方法,参数类型为args列表。
Plugin.wrap(target, this)是干什么的?
使用JDK的动态代理,给target对象创建一个delegate代理对象,以此来实现方法拦截和增强功能,它会回调intercept()方法。
Mybatis可以拦截哪些接口对象?
Mybatis只能拦截ParameterHandler、ResultSetHandler、StatementHandler、Executor共4个接口对象内的方法。
重新审视interceptorChain.pluginAll()方法:该方法在创建上述4个接口对象时调用,其含义为给这些接口对象注册拦截器功能,注意是注册,而不是执行拦截。
拦截器执行时机:plugin()方法注册拦截器后,那么,在执行上述4个接口对象内的具体方法时,就会自动触发拦截器的执行,也就是插件的执行。
Invocation
可以通过invocation来获取拦截的目标方法,以及执行目标方法。
分页插件原理
由于Mybatis采用的是逻辑分页,而非物理分页,那么,市场上就出现了可以实现物理分页的Mybatis的分页插件。 要实现物理分页,就需要对String sql进行拦截并增强,Mybatis通过BoundSql对象存储String sql,而BoundSql则由StatementHandler对象获取。
因此,就需要编写一个针对StatementHandler的query方法拦截器,然后获取到sql,对sql进行重写增强。
Mybatis源码解析MyBatis解析全局配置文件
MyBatis介绍
MyBatis是一个持久层的ORM框架,使用简单,学习成本较低。可以执行自己手写的SQL语句,比较灵活。但是MyBatis的自动化程度不高,移植性也不高,有时从一个数据库迁移到另外一个数据库的时候需要自己修改配置,所以称只为半自动ORM框架
传统JDBC和Mybatis相比的弊病
传统JDBC
@Test
public void test() throws SQLException
Connection conn=null;
PreparedStatement pstmt=null;
try
// 1.加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.创建连接
conn= DriverManager.
getConnection("jdbc:mysql://localhost:3306/mybatis_example", "root", "123456");
// SQL语句
String sql="select id,user_name,create_time from t_user where id=?";
// 获得sql执行者
pstmt=conn.prepareStatement(sql);
pstmt.setInt(1,1);
// 执行查询
//ResultSet rs= pstmt.executeQuery();
pstmt.execute();
ResultSet rs= pstmt.getResultSet();
rs.next();
User user =new User();
user.setId(rs.getLong("id"));
user.setUserName(rs.getString("user_name"));
user.setCreateTime(rs.getDate("create_time"));
System.out.println(user.toString());
catch (Exception e)
e.printStackTrace();
finally
// 关闭资源
try
if(conn!=null)
conn.close();
if(pstmt!=null)
pstmt.close();
catch (SQLException e)
e.printStackTrace();
传统JDBC的问题如下:
1.数据库连接创建,释放频繁造成西戎资源的浪费,从而影响系统性能,使用数据库连接池可以解决问题。
2.sql语句在代码中硬编码,造成代码的不已维护,实际应用中sql的变化可能较大,sql代码和java代码没有分离开来维护不方便。
3.使用preparedStatement向有占位符传递参数存在硬编码问题因为sql中的where子句的条件不确定,同样是修改不方便/
4.对结果集中解析存在硬编码问题,sql的变化导致解析代码的变化,系统维护不方便。
mybatis对传统的JDBC的解决方案
1、数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
解决:在SqlMapConfig.xml中配置数据连接池,使用连接池管理数据库链接。
2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
解决:Mybatis自动将java对象映射至sql语句,通过statement中的parameterType定义输入参数的类型。
4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
解决:Mybatis自动将sql执行结果映射至java对象,通过statement中的resultType定义输出结果的类型。
Mybaits整体体系图
一个Mybatis最简单的使用例子如下:
public class App
public static void main(String[] args)
String resource = "mybatis-config.xml";
Reader reader;
try
//将XML配置文件构建为Configuration配置类
reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory DefaultSqlSessionFactory
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
// 数据源 执行器 DefaultSqlSession
SqlSession session = sqlMapper.openSession();
try
// 执行查询 底层执行jdbc
//User user = (User)session.selectOne("com.tuling.mapper.selectById", 1);
UserMapper mapper = session.getMapper(UserMapper.class);
System.out.println(mapper.getClass());
User user = mapper.selectById(1L);
System.out.println(user.getUserName());
catch (Exception e)
e.printStackTrace();
finally
session.close();
catch (IOException e)
e.printStackTrace();
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--properties 扫描属性文件.properties -->
<properties resource="db.properties"></properties>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<plugins>
<plugin interceptor="com.tuling.plugins.ExamplePlugin" ></plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<!--// mybatis内置了JNDI、POOLED、UNPOOLED三种类型的数据源,其中POOLED对应的实现为org.apache.ibatis.datasource.pooled.PooledDataSource,它是mybatis自带实现的一个同步、线程安全的数据库连接池 一般在生产中,我们会使用c3p0或者druid连接池-->
<dataSource type="POOLED">
<property name="driver" value="$mysql.driverClass"/>
<property name="url" value="$mysql.jdbcUrl"/>
<property name="username" value="$mysql.user"/>
<property name="password" value="$mysql.password"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--1.必须保证接口名(例如IUserDao)和xml名(IUserDao.xml)相同,还必须在同一个包中-->
<package name="com.tuling.mapper"/>
<!--2.不用保证同接口同包同名
<mapper resource="com/mybatis/mappers/EmployeeMapper.xml"/>
3.保证接口名(例如IUserDao)和xml名(IUserDao.xml)相同,还必须在同一个包中
<mapper class="com.mybatis.dao.EmployeeMapper"/>
4.不推荐:引用网路路径或者磁盘路径下的sql映射文件 file:///var/mappers/AuthorMapper.xml
<mapper url="file:E:/Study/myeclipse/_03_Test/src/cn/sdut/pojo/PersonMapper.xml"/>-->
</mappers>
</configuration>
总结下就是分为下面四个步骤:
- 从配置文件(通常是XML文件)得到SessionFactory;
- 从SessionFactory得到SqlSession;
- 通过SqlSession进行CRUD和事务的操作;
- 执行完相关操作之后关闭Session。
MyBatis 源码编译
MyBatis的源码编译比较简单, 随便在网上找一篇博客即可,在这里不多说
https://www.cnblogs.com/mokingone/p/9108999.html
如果希望在spring源码中引入你自己的这份源码,可以做如下操作
1.修改你mybatis源码的pom的 这样可以和官方的区分开来
<version>3.5.3-xsls</version>
2.这样你在spring源码中就可以引入这份mybatis源码了.
- 如果引入mybatis-spring 同样需要做1、3步骤
compile("org.mybatis:mybatis-spring:2.0.3-xsls")
compile("org.mybatis:mybatis:3.5.3-xsls")
3.当然,如果你想在spring这边看到你mybatis源码相关的注释,还得在mybatis源码的pom里面加入plugin,使它生成 jar 的同时 生成 sources 包
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<attach>true</attach>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
Mybatis启动流程分析
String resource = "mybatis-config.xml";
//将XML配置文件构建为Configuration配置类
reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory DefaultSqlSessionFactory
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
通过上面代码发现,创建SqlSessionFactory的代码在SqlSessionFactoryBuilder中,进去一探究竟:
//整个过程就是将配置文件解析成Configration对象,然后创建SqlSessionFactory的过程
//Configuration是SqlSessionFactory的一个内部属性
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties)
try
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
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.
public SqlSessionFactory build(Configuration config)
return new DefaultSqlSessionFactory(config);
下面我们看下解析配置文件过程中的一些细节。
先给出一个配置文件的例子:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--SqlSessionFactoryBuilder中配置的配置文件的优先级最高;config.properties配置文件的优先级次之;properties标签中的配置优先级最低 -->
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
<!--一些重要的全局配置-->
<settings>
<setting name="cacheEnabled" value="true"/>
<!--<setting name="lazyLoadingEnabled" value="true"/>-->
<!--<setting name="multipleResultSetsEnabled" value="true"/>-->
<!--<setting name="useColumnLabel" value="true"/>-->
<!--<setting name="useGeneratedKeys" value="false"/>-->
<!--<setting name="autoMappingBehavior" value="PARTIAL"/>-->
<!--<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>-->
<!--<setting name="defaultExecutorType" value="SIMPLE"/>-->
<!--<setting name="defaultStatementTimeout" value="25"/>-->
<!--<setting name="defaultFetchSize" value="100"/>-->
<!--<setting name="safeRowBoundsEnabled" value="false"/>-->
<!--<setting name="mapUnderscoreToCamelCase" value="false"/>-->
<!--<setting name="localCacheScope" value="STATEMENT"/>-->
<!--<setting name="jdbcTypeForNull" value="OTHER"/>-->
<!--<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>-->
<!--<setting name="logImpl" value="STDOUT_LOGGING" />-->
</settings>
<typeAliases>
</typeAliases>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!--默认值为 false,当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果-->
<!--如果某些查询数据量非常大,不应该允许查出所有数据-->
<property name="pageSizeZero" value="true"/>
</plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://10.59.97.10:3308/windty"/>
<property name="username" value="windty_opr"/>
<property name="password" value="windty!234"/>
</dataSource>
</environment>
</environments>
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql" />
<property name="Oracle" value="oracle" />
</databaseIdProvider>
<mappers>
<!--这边可以使用package和resource两种方式加载mapper-->
<!--<package name="包名"/>-->
<!--<mapper resource="./mappers/SysUserMapper.xml"/>-->
<mapper resource="./mappers/CbondissuerMapper.xml"/>
</mappers>
</configuration>
下面是解析配置文件的核心方法:
private void parseConfiguration(XNode root)
try
//issue #117 read properties first
//解析properties标签,并set到Configration对象中
//在properties配置属性后,在Mybatis的配置文件中就可以使用$key的形式使用了。
propertiesElement(root.evalNode("properties"));
//解析setting标签的配置
Properties settings = settingsAsProperties(root.evalNode("settings"));
//添加vfs的自定义实现,这个功能不怎么用
loadCustomVfs(settings);
//配置类的别名,配置后就可以用别名来替代全限定名
//mybatis默认设置了很多别名,参考附录部分
typeAliasesElement(root.evalNode("typeAliases"));
//解析拦截器和拦截器的属性,set到Configration的interceptorChain中
//MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
//Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
//ParameterHandler (getParameterObject, setParameters)
//ResultSetHandler (handleResultSets, handleOutputParameters)
//StatementHandler (prepare, parameterize, batch, update, query)
pluginElement(root.evalNode("plugins"));
//Mybatis创建对象是会使用objectFactory来创建对象,一般情况下不会自己配置这个objectFactory,使用系统默认的objectFactory就好了
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//设置在setting标签中配置的配置
settingsElement(settings);
//解析环境信息,包括事物管理器和数据源,SqlSessionFactoryBuilder在解析时需要指定环境id,如果不指定的话,会选择默认的环境;
//最后将这些信息set到Configration的Environment属性里面
environmentsElement(root.evalNode("environments"));
//
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。解析typeHandler。
typeHandlerElement(root.evalNode("typeHandlers"));
//解析Mapper
mapperElement(root.evalNode("mappers"));
catch (Exception e)
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
上面解析流程结束后会生成一个Configration对象,包含所有配置信息,然后会创建一个SqlSessionFactory对象,这个对象包含了Configration对象。
简单总结
对于MyBatis启动的流程(获取SqlSession的过程)这边简单总结下:
- SqlSessionFactoryBuilder解析配置文件,包括属性配置、别名配置、拦截器配置、环境(数据源和事务管理器)、Mapper配置等;解析完这些配置后会生成一个Configration对象,这个对象中包含了MyBatis需要的所有配置,然后会用这个Configration对象创建一个SqlSessionFactory对象,这个对象中包含了Configration对象;
这里解析的东西比较多,大致概况:会把所有的信息都解析到Configration对象中,比较简单不多介绍。
以上是关于MyBatis解析的主要内容,如果未能解决你的问题,请参考以下文章