mybatis是怎样炼成的
前言
一些个人感受:不管分析什么源码,如果我们能摸索出作者的心路历程,跟着他的脚步一步一步往前走,这样才能接近事实的真相,也能更平滑更有趣的学习到知识。跟福尔摩斯探案一样,作者都经历了些什么,为什么他要这样去设计这样去做,留给我们的只有无声的代码和那一段孤独的日子。
阅读顺序建议是从上往下阅读,如果直接跳转到某一节,没有基于上面的分析推理的话可能会不容易理解。
一切的一切要从JDBC开始说起
先来一段JDBC代码回忆预热一下,方便我们后面进入正题
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
String sql = "SELECT id, first, last, age FROM student where id=?";
Statement stmt = conn.prepareStatement(sql);
pre.setBigDecimal(1, 10000);
ResultSet rs = stmt.executeQuery();
while(rs.next()){
int id = rs.getBigDecimal("id");
int age = rs.getInt("age");
}
rs.close();
stmt.close();
conn.close();
关于jdbc为什么要这样去抽象我们先放到一边,简单提取出几个关键对象:
Connection
Statement
ResultSet
一、mybatis抽象出来的关键对象
mybatis是怎样一步一步演变出来的,其中设计思路是怎样的,mybatis关键对象又是怎么被抽象出来的?
1.Sql语句提取到xml文件
众所周知,mybatis的一大创新和亮点,是将sql语句写到xml文件
StringBuilder sql = new StringBuilder("SELECT * FROM BLOG WHERE state = 'ACTIVE'");
if (title != null) {
sql.append("AND title like ?");
}
if (author!=null&&author.name!=null){
sql.append("AND author_name like ?");
}
Mybatis将sql语句提出来放到xml里,比上面java代码看起来可读性操作性都强很多,而且sql会统一放在一个地方一起管理,等于将sql与代码进行了分离,后面从全局去看sql、分析优化sql确实也会带来便利。当然,也可以通过注解的形式把sql语句写到java代码里,这样的目的和写到xml一样,也是为了把sql单独提取出来。
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
然后配置文件我们分为哪些呢,除了要执行的sql,即sql mapper外,我们还需要配置一些全局的设置吧,例如数据源等等
所以配置文件我们分为两类:
Sql语句的配置
BlogMapper.xml
<mapper namespace="BlogMapper">
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
</mapper>
全局的配置
config.xml
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC ">
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="123"/>
<property name="url" value="456"/>
<property name="username" value="789"/>
<property name="password" value="10"/>
</dataSource>
</environment>
</environments>
</configuration>
当然,以上通过xml文件进行配置的都可用java代码进行配置
这里environments我们不做过多分析,主要是把多环境的配置都写在一起,但是不管配置多少个environment,最后也只会用 default属性的那个,即只有一个在运行时生效
如果有多个数据源,则需要多个config.xml配置文件去配置对应的数据源
那么问题来了,上面两类xml解析后放到哪里,抽象出了哪些对象?
2.Configuration
将配置文件统一解析到Configuration对象,从xml解析的内容先放在这,后面谁想用拿去用就行了,这里还是很好理解
Configuration对象如何生成呢?
可以通过读取config.xml文件:
XMLConfigBuilder parser = new XMLConfigBuilder(reader);
Configuration configuration=parser.parse();
当然,也可以通过java代码来初始化:
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.setDatabaseId("mysql");
//基于java注解配置sql
configuration.addMapper(IBlogMapper.class); //基于mapper.xml配置sql
Resource[] mapperLocations = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml");
if (!isEmpty(mapperLocations)) {
for (Resource mapperLocation : mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
}
}
configuration对象为mybatis抽象出的第一个关键对象,configuration对象里面长什么样,我们接着往下分析
2.1 SqlNode
首先我们从java解析xml开始,直接通过org.w3c.dom 来解析如下一段xml(mybatis的xml映射语句格式已经深入人心,我们这里也先不去操心为什么mybatis设计出sql语句在xml中写成如下格式)
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
我们会得到父子关系如下的node集合(为了方便理解,我们忽略掉标签之间换行\n节点,后文同样也是省略掉):
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’ =>Node(type:TEXT_NODE)
<if test="title != null"> =>Node(type:Element)
AND title like #{title} =>ChildNode(type:TEXT_NODE)
</if>
<if test="author != null and author.name != null"> =>Node(type:Element)
AND author_name like #{author.name} =>ChildNode(type:TEXT_NODE)
</if>
</select>
我们得到父节点 <select>节点下一共有三个节点,然后两个Element节点里各有一个子节点
那么该xml node我们应该如何存到内存里呢,我们应该抽象成什么对象呢?
这里就引入了SqlNode对象,原始的org.w3c.dom 解析出来的Node对象已经满足不了我们的需求,就算能满足我们处理起来也很绕,所以我们要转变成我们个性化的Node对象,方便去做判断和sql的拼接等操作
所以在这里每个xml node都会转变成mybatis 的SqlNode,mybatis抽象出的SqlNode类型如下:
SqlNode | 说明 |
IfSqlNode | <if> 标签生成的node,其test属性需配合ognl使用 |
ChooseSqlNode | <choose> <when> <otherwise> 标签生成的node |
ForEachSqlNode | <foreach> 标签生成的node |
StaticTextSqlNode |
静态文本内容,可以包含#{}占位符
|
TextSqlNode |
也是动态的node,带有${}占位符的文本内容
|
VarDeclSqlNode | <bind> 标签生成的node |
TrimSqlNode | <trim> 标签生成的node |
SetSqlNode | 继承自TrimSqlNode,<set> 标签生成的node |
WhereSqlNode | 继承自TrimSqlNode,<where> 标签生成的node |
MixedSqlNode |
一种特殊的节点,不是由具体的sql标签产生,相当于org.w3c.dom 的getChildNodes()返回的NodeList,即存放父节点的子节点集合 |
共 10 种,严格意义上来说只有 9 种, MixedSqlNode是一种特殊的节点,其本身并没有什么逻辑,只是在父节点存放其子节点的集合用
那么上面xml转换成mybatis SqlNode后长什么样呢?如下图(为了方便理解,我们忽略掉标签之间换行\n节点,后文同样也是省略掉)
同org.w3c.dom 解析出来一样, 一共三个节点,然后两个Element节点里各有一个子节点(不管一个节点的子节点有多少个,其子节点都会以集合形式统一放在MixSqlNode节点下)
StaticTextSqlNode
IfSqlNode
--StaticTextSqlNode(由MixedSqlNode进行一层包装)
ifSqlNode
--StaticTextSqlNode(由MixedSqlNode进行一层包装)
有同学肯定会说不对啊,少了一层MixedSqlNode
是的,只要父节点包含子节点,不论子节点有多少个,那么子节点的集合统一都会放在MixedSqlNode节点下,是父子节点之间的媒介,为了方便理解我们这里先省略掉它
ognl
只在<if>和<foreach>标签的SqlNode中用到,例如if标签里常用到 test判断,我们如何判断对应的表达式呢,就是ognl的用武之地了
不清楚ognl的同学可以去搜索一下该关键字,如下下划线xml里面的条件判断都是通过ognl结合请求参数去执行出来结果
<if test="title != null">
<if test="author != null and author.name != null">
当把请求参数给到SqlNode时,通过参数和判断表达式,再结合ognl就能得到boolean结果,这样就可以去判断是否要append当前节点的子节点的sql语句了
伪代码如下:
if (Ognl.getValue("title != null", parameterObject)) {
sql.append("AND title like #{title}");
}
2.2 BoundSql
我们上面将xml里的每段CRUD标签解析成了对应的一批SqlNode
那么运行时,通过请求参数我们需要提取出来最终到数据库执行的jdbc statement,才能继续将我们的流程往下走
#{} 占位符
我们在mybatis xml中写sql语句时,可以写 #{} 和 ${} 占位符,这是原始jdbc statment不支持的,这样的书写方式解决了我们之前sql语句参数要用 “?” 问号,然后statment赋值要注意顺序的问题,参数一多眼睛就花了
mybatis将这个问题帮我们简化了,可以在sql段里面写 #{} 占位符,项目运行时 #{} 会被替换成 "?" 和对应排好序的参数集合
然后再去执行statement,伪代码如下:
Connection connection = transaction.getConnection();//从事务管理获取connection
PreparedStatement statement = connection.prepareStatement(sql);//准备statement for (int i = 0; i < parameterMappings.size(); i++) {//循环参数列表给statement赋值
Object value = requestObject.getValue(parameterMappings.get(i).getName());//通过反射拿到入参的属性值
preparedStatement.setBigDecimal(i, new BigDecimal(value));//给statement赋值
}
preparedStatement.execute();
几个关键点:
1.prepareStatement 的 sql语句,即#{} 替换成 "?"的sql
2.#{} 替换成 "?" 后,排好序的参数列表
3.给statement赋值时,我们怎么知道是 setInt 还是 setBigDecimal
这3个点,就是接下来要关注的,让我们来看看mybatis是怎么做的
Sql
如何通过SqlNode、请求参数 得到最终执行的sql?
其实上面说ognl的时候已经提到了,简单理解就是由请求参数和条件表达式结合拼接出来,然后再把 "#{}" 替换成 "?" 即可
ParameterMapping
排好序的参数列表,给statement赋值使用
xml使用示例:
#{property,javaType=int,jdbcType=NUMERIC}
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
有如下一些关键的属性:
property
即 #{xxx} 中的属性名,是字符串
javaType
通过 #{}占位符中定义,如果没有定义则找入参对象parameterType该属性的类型
优先级如下(由高到低):
1.xml配置文件中定义的类型
2.入参对象该property属性的java type
例如下面配置的 #{title},就是通过反射找 入参对象的title 属性的java type
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
如果传递进来的入参是Map,那么通过反射就找不到对应属性的java type,这种情况下该属性的 javaType 会设置成 Object.class
Map map=new HashMap();
map.put("title","123");
map.put("author",new Author(){{setName("tt");}});
session.select("com.tj.mybatis.IBlogMapper.findActiveBlogLike",map,null);
TypeHandler
#{property,javaType=int,jdbcType=NUMERIC}
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
优先级如下(由高到低):
1.xml配置文件中定义的类型
2.通过javaType去找对应的TypeHandler
该对象的作用就是解决给statement赋值时,让我们知道是用ps.setInt(value) 还是 ps.setBigDecimal(value)
分为get 和 set:
给statement赋值时 通过java类型找jdbc类型
给java 对象赋值时 拿到数据库查询结果ResultSet后,是用哪个方法给java对象赋值rs.getInt("age"); 还是 rs.getBigDecimal("age");通过jdbc类型找java类型
UnknownTypeHandler
上面java type为Object.class时,例如入参是Map 找不到对应的属性的java type,其对应的TypeHandler为UnknownTypeHandler
这种情况下,在给statement入参赋值时会再次根据获取到的入参的值的类型去找TypeHandler
例如 title 属性的值为 "123" 那么再通过值"123"去找其对应的 TypeHandler,即StringTypeHandler
${} 占位符
${} 和 #{} 这两种占位符的处理流程是不一样的:
${}占位符在执行时,会将sql替换成我们参数设置的sql段,有sql注入风险,且该sql段可能还包含#{}占位符
例如:
select * from blog ${where}
可能会被替换成如下sql
select * from blog where title like #{title}
即替换内容为 "where title like #{title}",所以替换完后会再走一遍#{}占位符的替换流程
如果xml中sql语句只包含 #{}占位符,那么通过请求参数,我们需要做的就是通过条件拼接sql(无sql注入风险),然后给statement参数赋值即可
如果xml中sql语句包含${}占位符,那么需要将${}占位符进行替换,然后再进行上面#{}的流程,因为 ${} 可能包含 带有#{}占位符的语句替换进去
所以mybatis流程上是统一先处理${}占位符,再处理#{}占位符(SqlSource.getBoundSql 方法的流程),然后一个有sql注入风险一个无sql注入风险。
所以执行过程中,sqlNode最后变成了 statement所需要的两大关键点:
1.sql(jdbc statement可直接使用的sql)
2.参数列表 ParameterMappings(排好序的,给statment赋值时直接按顺序遍历赋值),其又包含:属性名property和TypeHandler
这就是我们的BoundSql对象,该对象包含上面两个关键属性
如下是大致的流程:
2.3 SqlSource
RawSqlSource 与 DynamicSqlSource
首先我们先分析一下如下两段sql,在运行时执行时有什么异同?
第一段sql:
<select id="selectBlog" resultType="Blog">
SELECT * FROM BLOG WHERE id = #{id}
</select>
第二段sql:
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
第一段sql我们在执行时不需要根据传递进来的条件参数进行sql拼接,在项目启动时就可以直接得到BoundSql的两个关键属性:
1.sql
SELECT * FROM BLOG WHERE id=?
2.参数列表:
id
在执行时,也根本不需要再做#{}标签的替换,直接拿BoundSql和参数赋值给statment即可
而第二段sql我们在项目启动时没法提前得到BoundSql,只能在运行时通过传递进来的参数做判断才能得到BoundSql。
总结:
第一段sql,静态sql,执行时速度更快,项目加载时就能得到BoundSql
第二段sql,动态sql,执行时速度稍慢,运行时才能得到BoundSql
所以为了区分这两种类型的SqlNode集合
静态sql: RawSqlSource
当所有节点都是StaticTextSqlNode 或 MixedSqlNode ,就是RawSqlSource 静态sql源(不需要依据请求参数来做判断拼接sql,是固定的sql内容,如果有请求参数给statement赋值参数即可)
动态sql: DynamicSqlSource
只要包含除StaticTextSqlNode 和 MixedSqlNode 以外的其他8 种SqlNode类型 (sql中存在 ${}占位符的是TextSqlNode),则都是DynamicSqlSource 动态sql源(需要根据请求参数做动态sql拼接)
所以不同的SqlSource得到BoundSql的速度不一样,然后相同的是SqlSource下面都是放的SqlNode集合
有细心的同学看了肯定会说我漏了StaticSqlSource,其实StaticSqlSource是上面两种SqlSource生成BoundSql的一个过渡产物,所以不需要单独拎出来说明
2.4 LanguageDriver
mybatis除了可以通过xml写sql外,也可以通过如下java 注解来写sql,还可以通过freemarker、thymeleaf 等格式来写书写sql文件
@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);
@Select("SELECT * FROM BLOG")
List<Blog> selectBlog();
所以顾名思义,语言驱动 LanguageDriver的作用就是干这个,将不同来源的sql解析成SqlSource对象,不过mybatis java注解的sql也是统一用的XmlLanguageDriver去解析的,这里mybatis是为了方便扩展
2.5 MappedStatement
除了子节点SqlNode集合以外,<select> <update> <delete> 标签也包含很多属性,放到哪里呢,新开一个父级的SqlNode吗?而且从面向对象设计来说,这个Node跟下面的sql语句node区别还挺大的,至少跟上文那10种SqlNode差别挺大的,这里新开一个对象用于存放父级标签的属性:MappedStatement
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
sql语句的配置,每一段curd都会被解析成一个MappedStatement对象,可以通过id去与dao接口方法进行对应
这里的中间产物我们就叫他MappedStatement,为什么叫MappedStatement?
即mybatis最终生成jdbc statement的中间产物,mybatis做的事情就是 orm (object relational mapping),那么最终生成statement的中间物就是MappedStatement
如下图所示(右键新标签页打开可查看大图)
注: 虚线箭头表示此对象为通过某方法得到的返回值
例如:MappendStatement.getBoundSql(Object requestObject)得到的返回值为BoundSql对象
另外,每一段<select|insert|update|delete> 标签,对应生成一个SqlSource、MappedStatement,1对1的关系
ParameterType
用于说明请求参数的java type,非必须,xml的<select|insert|update|delete>标签中该属性可以不写,因为mybatis可以根据运行时传递进来的参数用反射判断其类型
ResultMap ResultType
如官方文档所说,两者只能用其中一个,不过不管用哪个,最终都是将信息放在ResultMap,用于后面ResultSetHandler创建返回对象时使用
例如如下xml配置:
<select id="findActiveBlogLike" resultType="xxx.Blog">
生成的MappedStatement中,上面resultType会存放在ResultMap对象的type属性里
2.6 TransactionFactory
顾名思义其主要就是用于创建不同的Transaction对象,这里涉及到mybatis的事务管理,关于事务管理下面内容我们会提到
3.StatementHandler
我们已经知道上面Configuration对象里面有哪些内容,然后结合BoundSql就能够将statement prepare 和 execute
如下伪代码示例:
Transaction transaction = configuration.getEnvironment().getTransactionFactory().newTransaction(dataSource, TransactionIsolationLevel.READ_COMMITTED, false);
Connection connection = transaction.getConnection();
MappedStatement mappedStatement = configuration.getMappedStatement("findActiveBlogLike");
BoundSql boundSql = mappedStatement.getBoundSql(blog);
PreparedStatement statement = connection.prepareStatement(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
MetaObject metaObject = configuration.newMetaObject(parameterObject);//MetaObject是mybatis提供的能很方便使用反射的工具对象
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
Object value = metaObject.getValue(parameterMapping.getProperty());
statement.setBigDecimal(i, new BigDecimal(value));
}
}
statement.execute();
ResultSet rs=statement.getResultSet();
while(rs.next()){
BigDecimal id = rs.getBigDecimal("id");
String title = rs.getString("title");
}
rs.close();
statement.close();
connection.close();
我们知道,jdbc的statement有三种,每种执行起来有些区别:
Statement
Statement stm = conn.createStatement()
return stm.execute(sql);
PreparedStatement
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setString(1, "Hello");
return pstm.execute();
CallableStatement
CallableStatement cs = conn.prepareCall("{call xxx(?,?,?)}");
cs.setInt(1, 10);
cs.setString(2, "Hello");
cs.registerOutParameter(3, Types.INTEGER);
return cs.execute();
所以这里抽象出三个不同的Handler再部分结合模板方法去处理不同的statement,也挺好理解,最后不管什么Statement都按如下模板来构建:
stmt = handler.prepare(transaction.getConnection(), transaction.getTimeout());
handler.parameterize(stmt);
区别是不同的hanlder里的prepare()和parameterize()方法有些区别而已,例如StatementHandler的parameterize()方法里代码为空,因为不支持参数设置
有了StatementHandler之后,我们的伪代码变成下面这样:
StatementHandler handler = configuration.newStatementHandler(mappedStatement, parameterObject, boundSql);
Statement stmt = handler.prepare(transaction.getConnection(), transaction.getTimeout());
handler.parameterize(stmt);
handler.update(stmt); StatementHandler handler1 = configuration.newStatementHandler(mappedStatement, parameterObject, boundSql);
Statement stmt1 = handler.prepare(transaction.getConnection(), transaction.getTimeout());
handler1.parameterize(stmt);
handler1.query(stmt1, resultHandler); transaction.getConnection().commit();
newStatementHandler() 创建的StatementHandler默认是PreparedStatementHandler,也可以在xml的<select|insert|update|delete>标签中自己声明类型
3.1 ParameterHandler
StatementHandler.parameterize()方法中的逻辑,交由ParameterHandler去执行,即循环BoundSql的ParameterMapping集合,结合TypeHandler给statement赋值
3.2 ResultSetHandler
顾名思义,StatementHandler执行完statement后,交由ResultSetHandler处理成xml中CRUD标签ResultType ResultMap所声明的对象
关于xml标签中的ResultMap和ResultType,先回顾一下我们上面MappedStatement的内容:
不管是用ResultMap还是ResultType,最终都是将信息放在ResultMap里,ResultType会存放在ResultMap对象的type属性里
关于返回结果:
如果是 <select>标签,这里统一返回List<ResultType> 集合,如果结果只有一条,则直接list.get(0)就可以了
如果是 <insert|update|delete>标签,则不会经过ResultSetHandler处理,statementHandler直接通过statement.getUpdateCount() 返回int值
1.创建返回ResultMap 、ResultType的对象 (ObjectFactory)
2.循环ResultSet每行,再循环每列,给对象属性进行赋值 (TypeHandler)
3.如果是集合添加到集合再返回 (ResultHandler)
伪代码如下:
ResultSet rs = statement.getResultSet();
List<Object> list = objectFactory.create(List.class);
while (rs.next()) {
ResultSetMetaData metaData = rs.getMetaData();
final int columnCount = metaData.getColumnCount(); Object resultObject = objectFactory.create(resultMap.getType());//使用ObjectFactory实例化对象
MetaObject metaObject = configuration.newMetaObject(resultObject);//MetaObject是mybatis提供的能很方便使用反射的工具对象 for (int i = 1; i <= columnCount; i++) {
String columnName = configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i); String property = metaObject.findProperty(columnName, configuration.isMapUnderscoreToCamelCase());
if (property != null && metaObject.hasSetter(property)) {
Class<?> propertyType = metaObject.getSetterType(property);
TypeHandler<?> typeHandler = getTypeHandler(propertyType, metaData.getColumnType(i));//通过属性类型找对应的jdbc TypeHandler Object value = typeHandler.getResult(rs, columnName);
metaObject.setValue(property, value);
}
}
list.add(resultObject);
}
ResultSetHandler配合ResultMap也支持嵌套查询、子查询,返回多结果集等,我们这里就不细化了
ObjectFactory
顾名思义,对象工厂,产出对象用的,什么对象呢,当然是查询数据库将结果映射到的java对象
用来创建ResultType(等同于ResultMap中的Type)等对象时使用,用反射创建对象(这里可以做一些加工,比如创建完对象后给属性赋值,但是这种情况不常见),
然后后面ResultSetHandler用TypeHandler去给新创建的对象属性赋值
最后再用ResultHandler添加到返回集合里
什么场景适合我们自定义实现呢?
这里的职责就是通过反射创建对象,一般情况下使用默认的DefaultObjectFactory就可以了;
如果想创建完对象给一些属性初始化值,这里可以做,但是可能会被后面数据库查到的结果值覆盖,使用下面的ResultHandler就可以实现
ResultHandler
为什么需要ResultHandler?
区别于ResultSetHandler,ResultSet是jdbc返回的结果集,Result则理解为经过mybatis加工的结果
默认ResultSetHandler都会循环ResultSet然后通过DefaultResultHandler添加到集合,最后从ResultHandler取结果返回给调用方法(调用方法无返回类型限制)
上面伪代码中,如下几句就是在DefaultResultHandler中执行:
List<Object> list = objectFactory.create(List.class);
list.add(resultObject);
只不过最后ResultSetHandler返回结果时自己调用了 defaultResultHandler.getResultList() 来进行返回。
如果想用自定义的ResultHandler:查询方法必须是void类型,且入参有ResultHandler对象,然后结果集自己通过resultHandler来获取,例如DefaultResultHandler.getResultList()
什么场景适合我们自定义实现呢?
因为这里的职责是创建返回集合List<ResultType>,并添加记录行;所以我们可以对集合里创建的对象进行一些统一的操作,例如给集合里的对象某个字段设置默认值
RowBounds
mybatis的内存分页,在ResultSetHandler中使用,由外部方法层层传递进来,即通过RowBounds设置的参数对ResultSet进行 skip limit,只取想要页数的记录行
但是关键问题是基于内存的分页,而不是物理分页,所以基本上都不会用到
MetaObject
上面我们已经提到了,MetaObject是mybatis提供的方法使用反射的工具类,将对象Object扔进去,就可以很简单的使用反射;自己项目中如果有需要也可以直接使用,很方便
MetaObject metaObject = configuration.newMetaObject(parameterObject);
metaObject.getValue("name");
需要注意的是此对象并不属于我们StatementHandler,只是这里用到比较多,所以我们就放到这里一起讲一下
4.Executor
熟悉mysql、mssql等关系型数据库隔离级别的同学都知道,数据库的隔离级别分为4类,由低到高:
1.Read Uncommitted 读未提交
2.Read Committed 读已提交
3.Repeatable Read 可重复读
4.Serializable 串行
隔离级别越高则处理速度越慢,隔离级别越低则处理速度越快。
mysql默认隔离级别是Repeatable Read 可重复读;即在同一个事务范围内,同样的查询语句得到的结果一致。
mybatis的又一大亮点:同一个事务范围内,基于内存实现可重复读。直接在mybatis这里就处理好了,都不用到数据库,这样减轻了数据库压力,且速度更快。
所以mybatis在这里引入了缓存和一些其他操作,而它的媒介就是Executor,是对StatementHandler再做一层封装
Executor executor = configuration.newExecutor(transaction);
executor.query(configuration.getMappedStatement("findActiveBlogLike"), parameterObject, rowBounds, Executor.NO_RESULT_HANDLER);
executor.commit()
Executor里的伪代码:
List<E> list;
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);//通过关键对象创建唯一的缓存key
list = localCache.getObject(key);//通过缓存key查缓存
if (list == null) {
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
list = handler.<E>query(stmt, resultHandler);
localCache.putObject(key, list);//存至缓存
}
return list;
就是我们上面所说的,对StatementHandler进行包装,做一些逻辑封装
然后Executor有哪几种呢?主要还是知道这些对象是如何演变过来的,剩下的其实代码里都能看的很明确了
Executor | 说明 |
BaseExecutor | 下面三种Executor的父类,基础方法都在这里,查询方法实现了基于内存的一级缓存 |
SimpleExecutor | 继承自BaseExecutor,默认的Executor |
ResuseExecutor | 继承自BaseExecutor,重用Statement,即同一个Executor内Statement不释放重复使用 |
BatchExecutor | 继承自BaseExecutor,针对增删改的批处理,调用增删改方法时,只是statement.addBatch(),最终还要通过调用commit方法触发批处理 |
CachingExecutor | 在一级缓存的基础上增加二级缓存,二级缓存查不到的情况再去上面几种Executor中进行查询 |
Transaction
为什么mybatis要抽象出Transaction事务对象,其实一方面是为了集中connection的管理,另一方面也是为了能够适应趋势解决事物发展过程中的问题,后面mybatis-spring中我们会详细介绍。
spring关于事务的管理有:
DataSourceTransactionManager、PlatformTransactionManager等
mybatis这里同样也有自己的事务管理 Transaction接口的实现:JdbcTransaction 、SpringManagedTransaction等
相比spring表面看起来只是后缀少了个单词 Manager而已
简单点去理解,就是connection都是放在Transaction对象这里进行管理,要操作数据库连接都统一从这里操作;
例如非托管的Transaction伪代码如下:
protected Connection connection; public Connection getConnection(){
if (connection == null) {
connection = dataSource.getConnection();
}
return connection;
}
如果是受spring 托管的事务,则上面dataSource.getConnection() 变成 DataSourceUtils.getConnection();
一级缓存
一级缓存:默认开启,且不能关闭,同一个Executor内(同一个事务)相同参数、sql语句读到的结果是一样的,都不用到数据库,这样减轻了数据库压力,且速度更快。
二级缓存
CacheExecutor,可基于内存或第三方缓存实现
要注意的是二级缓存的key 是通过 mapper.xml 里的namespace进行分组,例如:
<mapper namespace="UserMapper">
<cache eviction="FIFO" size="512" readOnly="true"/>
这样所有该mapper <select>产生的cacheKey,都统一放在"UserMapper"这个namespace下汇总
mapper.xml里面的<select|insert|update|delete> flushCache属性设置为true时,会清空该namespace下所有cacheKey的缓存
flushCache属性在<select> 标签中默认值为 false,在<insert|update|delete>标签中默认值为 true。
然后如果其他mapper想共用同一个缓存namespace,如下声明就可以了
<mapper namespace="BlogMapper">
<cache-ref namespace="UserMapper"/>
5.SqlSession
mybatis为什么要有session的概念? 上面使用Executor进行crud已经可以满足我们绝大部分业务需求了,为什么还要弄出个session的概念?
这里主要还是为了强调会话的概念,由会话来控制事务的范围,类似web 的session更方便使用者理解
那既然这样,把上面Executor名字改成SqlSession不就行了?这样其实也不好,因为对应的BatchExecutor、CachingExecutor改成BatchSqlSession、CachingSqlSession的话感觉有点混乱了,不符合session干的事情
使用SqlSession后代码如下:
SqlSession session = sqlSessionFactory.openSession();//内部构造executor等对象
session.selectList("findActiveBlogLike",parameterObject);//内部使用Executor进行执行
session.commit();
session.close();
其实跟上面Executor的代码相比,也差不多,只不过SqlSessoin是通过factory工厂来创建,但是原理还是通过configuration创建transaction、executor等对象
Executor executor = configuration.newExecutor(transaction);
executor.query(configuration.getMappedStatement("findActiveBlogLike"), parameterObject, rowBounds, Executor.NO_RESULT_HANDLER);
executor.commit();
executor.close();
到这里可以这样理解,SqlSession就是为了更方便理解和使用而产生的对象,其方法本质还是交由Executor去执行。
到目前为止整体的架构如下(右键新标签页打开可查看大图)
SqlSessionFactory
SqlSession的工厂类,需要的参数主要就是Configuration对象,其实意思很明确了,就是SqlSession需要使用Configuration对象,创建SqlSession代码如下
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSession session = sqlSessionFactory.openSession();
不过configuration的构建其实还是挺麻烦的,上面Configuration已经提到,然后后面mybatis-spring有提供SqlSessionFactoryBean(包含Configuration的构建)方便我们更快捷的构建SqlSessionFactory
6.MapperProxy
熟悉mybatis的朋友都知道xml中每段<select|insert|update|delete>与dao接口方法是一对一的,其实早在ibatis的年代是没有将两者关联起来的
java.lang.reflect.Proxy
那么实现这一功能的核心是什么呢,就是java的Proxy,通过session.getMapper(xxx.class)方法每次都会给接口生成一个代理Proxy的实现
实现后的效果:
try (SqlSession session = sqlSessionFactory.openSession()) {
IBlogMapper mapper = session.getMapper(IBlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
这里我们就不分析Proxy的原理了,还是不明白的同学可以百度搜索了解一下,如下是mybatis中使用proxy的代码:
DefaultSqlSession:
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
经由Configuration和MapperRegistry、MapperProxyFactory,最终执行返回:
protected T newInstance(MapperProxy<T> mapperProxy) {
MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
不过需要注意的是getMapper(xxx.class)的使用前提的addMapper(xxx.class);否则不会生成代理;
addMapper可以由如下两种形式触发:
1.configuration.addMapper(xxx.Class);//基于java注解形式
2.xmlMapperBuilder.parse();//基于mapper.xml配置,详细代码如下
Configuration configuration = new Configuration(environment);
Resource[] mapperLocations = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml");
if (!isEmpty(mapperLocations)) {
for (Resource mapperLocation : mapperLocations) {
...
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
...
}
}
后面结合mybatis-spring时使用SqlSessionFactoryBean时就有帮我们实现了我们上面这段代码
MapperMethod
MapperProxy最后执行方法时,都会交给MapperMethod去执行,接口的每个方法method都会生成一个对应的MapperMethod去执行
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
然后只要在MapperMethod里调用SqlSession对应的方法就算完成了:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//通过接口方法名找到对应的MappedStatement,判断MappedStatement的标签类型是其中哪种<select|insert|update|delete>
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
//调用对应的sqlSesion方法,传递MappedStatement id和请求参数,这里的command.getName即MappedStatement的id(前缀会自动加命名空间来区分唯一)
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
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;
}
其中就是select类型的方法复杂些,需要判断接口里的参数来去调用对应的SqlSession方法。
简单点理解,就是调用mapper接口的方法,最后会被代理实现为调用对应的 sqlSession.select() 或 sqlSession.insert() 等对应的方法。
流程图如下(右键新标签页打开可查看大图)
最后调用的这个sqlSession从哪来?
在sqlSession.getMapper(xxx.class)时,会将sqlSession存到代理MapperProxy的属性,然后MapperProxy调用MapperMethod时,会传递给MapperMethod去使用,即
//通过Proxy为接口生成并返回代理实现类MapperProxy,并将当前sqlSession存至代理实现类MapperProxy的属性
IBlogMapper mapper = session.getMapper(IBlogMapper.class);
//调用具体方法时,MapperProxy会调用MapperMethod来判断执行对应的sqlSession.select 或 insert等方法,且此sqlSession就是上面生成代理类的sqlSession,是同一个
Blog blog = mapper.selectBlog(101);
如果是通过SqlSessionTemplate(后面mybatis-spring内容).getMapper(),则后面调用的sqlSession就是SqlSessionTemplate对象
然后这里还有一点小细节,我们可以在生成代理实现类MapperProxy时,就可以遍历接口的方法来提前生成好所有的MapperMethod【饿汉】,但是其实mybatis是在具体调用接口方法时,才生成对应的MapperMethod并缓存到内存【懒汉】 ;具体利弊我们这里就不做分析了。
7.Mybatis的插件
首先我们为什么需要插件,哪里需要用到插件?其本质也是通过Proxy做一层代理
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
} public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
} public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
} }
Interceptor示例:
public class XXXInterceptor implements Interceptor {
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public Object intercept(Invocation invocation) {
}
}
Plugin代码:
public class Plugin implements InvocationHandler {
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
} public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
}
我们上面已经接触了很多使用Proxy的场景了,这里又是熟悉的配方,熟悉的味道。
一共有四个地方可以使用插件,即可以被代理,当然被代理对象的所有方法都可以被拦截:
Executor
StatementHandler
ParameterHandler
ResultSetHandler
mybatis比较经典的插件使用还是 pagehelper ,然后关于 插件拦截的使用规范 在 pagehelper官方文档 中也讲的很透彻很详细,我相信在弄懂了本文mybatis原理后再去看 pagehelper这类插件源码也会比较容易懂
8.Mybatis的扩展
由于我们基本上每个表都要用到一些增删改查方法,然后我们生成代码时,总是会生成一堆类似的代码,xml文件、mapper接口中存在大量相似代码,有没有办法把这些代码抽出来?
这时候mybatis-plus就出现了,其原理其实就是在mybatis 构建Configuration对象时做了加工,帮我们把增删改查的MappedStatement添加进去;当然mybatis-plus还包含很多其它便捷的功能,但是也是也是基于mybatis做扩展。
还是那句话,我们把mybatis原理分析清楚了,这块也就更容易去理解了,感兴趣的同学可以从mybatis-plus的MybatisSqlSessionFactoryBean为源头进去看
二、mybatis-spring抽象出来的关键对象
我们要知道mybatis 和 mybatis-spring是分开的两个项目,然后又可以无缝的结合起来进行使用,但是为了便于我们理解,所以我们是分开进行分析,这样更有利于吸收
与spring结合之前我们必须得熟悉一下spring的数据访问与实务管理
1.事务管理的发展史
其实spring关于数据访问、事务管理已经做得很好了,但是其中的发展史是怎样的,对于理解mybatis的事务管理非常重要
我们简单概括一下关于事务的发展过程中的几个典型问题,尽量能够让大家回顾一下发展过程:
1.局部事务的管理绑定了具体的数据访问方式
问题描述:即connection-passing问题,不同方法想要共用事务需要在方法间传递connection,如果使用jdbc则传递connection对象,如果使用hibernate则需要传递session或transaction对象,不同的数据访问形式要用不同的api来控制局部事务,这样我们的方法就业务层就没办法和数据访问解耦
解决方法:connection绑定到线程ThreadLocal,在业务开始方法获取连接,业务结束方法提交、释放连接
2.事务管理代码与业务逻辑代码相互混杂
问题描述:上面问题1虽然解决了方法间传递数据库连接的问题,但是事务的管理还是在业务代码里,且需要合理控制,否则也会有问题
解决方法:面向切面编程,事务的切面管理(spring @Transactional)
如果还是不是很理解的朋友, 推荐去看一下《spring 揭密》一书里的数据访问和事务管理相关章节,增加这一块的感知和认识,会有助于平滑的理解mybatis-spring的事务管理
2.Spring 之 DataSourceUtils、@Transactional
使用spring事务,注册相关的bean:
@Bean
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager dstm = new DataSourceTransactionManager();
dstm.setDataSource(dataSource);
return dstm;
}
@Bean
public BasicDataSource dataSource() {
BasicDataSource bds = new BasicDataSource();
bds.setDriverClassName("");
bds.setUrl("");
bds.setUsername("");
bds.setPassword("");
return bds;
}
具体的使用,注意TransactionManager和DataSourceUtils里使用的dataSource是同一个,不然事务不生效:
@Transactional
public void methodA(){
//简单理解就是从ThreadLocal获取数据库连接,如果没有就从DataSource获取后set到ThreadLocal
Connection connection = DataSourceUtils.getConnection(dataSource);
PreparedStatement statement = connection.prepareStatement("insert into blog xxx");
statement.executeUpdate();
methodB();
}//@Transactional切面after:当前ThreadLocal的connection自动commit,并release DataSourceUtils ThreadLocal中的connection public void methodB(){
Connection connection = DataSourceUtils.getConnection(dataSource);
PreparedStatement statement = connection.prepareStatement("insert into log xxx");
statement.executeUpdate();
}
如果方法不在@Transactional事务控制范围内
需要注意的是如果方法不在@Transactional事务控制范围内,通过DataSourceUtils.getConnection还是会存在ThreadLocal,只不过ThreadLocal中的connection就需要我们手动去 commit和release,当然DataSourceUtils有方法供我们调用。
DataSourceUtils中的伪代码:
private final ThreadLocal<Connection> tlConnection = new ThreadLocal<Connection>(); public static Connection getConnection(DataSource dataSource){
if (tlConnection.get() == null) {
tlConnection.set(dataSource.getConnection());
}
return tlConnection.get();
} public static void releaseConnection(){
tlConnection.get().close();
tlConnection.set(null);
}
@Transactional切面实现的伪代码,需要结合TransactionManager和DataSourceUtils来使用,这里简化如下:
@After
public void after(JoinPoint joinPoint){
commitConnection();
DataSourceUtils.releaseConnection();
}
结合spring事务时,connection数据库连接在线程中的生命周期如下,即随着事务开始而开始,随时事务结束而结束
要注意ThreadLocal中set Connection是在业务代码中第一次获取connection时,而不是@Transactional切面的before方法,在必须时才去获取数据库连接,而不是提前占用
使用spring的数据访问和事务管理就解决了我们上面所提到的两个问题:
1.局部事务的管理绑定了具体的数据访问方式
2.事务管理代码与业务逻辑代码相互混杂
其实mybatis项目一直抽象到SqlSession,都没有解决事务管理发展的那两个问题
多个方法如果想要共用SqlSession需要通过参数传递,且事务的提交也要我们自己写在业务代码里,如下:
public void methodA(){
SqlSession session = sqlSessionFactory.openSession();
session.insert("insertBlog",xxx);
methodB(session);
}
public void methodB(SqlSesion session){
session.insert("insertUser",xxx);
session.commit();
session.close();
}
3.SpringManagedTransaction
我们上文已经知道mybatis的Transaction对象是用来获取、操作connection,但是也仅限于单个Executor、SqlSession内部,没有放到线程ThreadLocal里去,要想共用同一个connection事务,还是必须参数传递SqlSession或者Connection对象(即上面的问题1),如何解决?我们把Transaction里的connection放到ThreadLocal不就解决了吗?
那我们直接把Transaction对象里的getConnection方法改一下不就行了
private final ThreadLocal<Connection> tlConnection = new ThreadLocal<Connection>(); public Connection getConnection(){
if (tlConnection.get() == null) {
tlConnection.set(this.dataSource.getConnection());
}
return tlConnection.get();
}
发现是不是跟DataSourceUtils的getConnection方法一模一样,所以结合spring的数据访问的话,可以精简成:
public Connection getConnection(){
return DataSourceUtils.getConnection(this.dataSource);
}
上面这段伪代码其实就是SpringManagedTransaction所干的事情
4.SqlSessionUtils
然后我们结合@Transactional使用,我们来看看代码:
@Transactional
public void methodA(){
TransactionFactory transactionFactory = new SpringManagedTransactionFactory();
Transaction transaction = transactionFactory.newTransaction(dataSource); Connection connection = transaction.getConnection();
PreparedStatement statement = connection.prepareStatement("insert into blog xxx");
statement.executeUpdate();
methodB(transaction);
}
public void methodB(Transaction transaction){
Connection connection = transaction.getConnection();
PreparedStatement statement = connection.prepareStatement("insert into user xxx");
statement.executeUpdate();
}
上面代码解决了问题2,但是没解决问题1,是不用传递connection了,但是现在又要传递transaction。
类似connection,我们创建一个TransactionUtils工具类将transaction也绑定到ThreadLocal不就解决问题了?
@Transactional
public void methodA(){
Connection connection = TransactionUtils.getTransaction().getConnection();
PreparedStatement statement = connection.prepareStatement("insert into blog xxx");
statement.executeUpdate();
methodB(transaction);
}
public void methodB(){
Connection connection = TransactionUtils.getTransaction().getConnection();
PreparedStatement statement = connection.prepareStatement("insert into user xxx");
statement.executeUpdate();
}
TransactionUtils的伪代码:
private final ThreadLocal<Transaction> tlTransaction = new ThreadLocal<Transaction>(); public static Transaction getTransaction(){
if (tlTransaction.get() == null) {
TransactionFactory transactionFactory = new SpringManagedTransactionFactory();
Transaction transaction = transactionFactory.newTransaction(dataSource);
tlTransaction.set(transaction);
}
return tlTransaction.get();
}
public static void releaseTransaction(){
tlTransaction.get().connection.close();
tlTransaction.set(null);
}
问题并没有结束,我们要用的是mybatis的SqlSession,你这样不是又回到原始的jdbc了,行我们继续改,同样类似DataSourceUtils我们再建个SqlSessionUtils行了吧:
@Transactional
public void methodA(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory);
sqlSession.insert("insertBlog",xxx);
methodB();
}
public void methodB(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory);
sqlSession.insert("insertUser",xxx);
}
SqlSessionUtils里的伪代码:
private final ThreadLocal<SqlSession> tlSqlSession = new ThreadLocal<SqlSession>(); public static SqlSession getSqlSession(SqlSessionFactory factory){
if (tlSqlSession.get() == null) {
SqlSession sqlSession = factory.openSession();
tlSqlSession.set(sqlSession);
}
return tlSqlSession.get();
}
现在SqlSession里的connection已经通过SpringManagedTransaction打通spring的DataSourceUtils存到ThreadLocal,且@Transactional注解切面after会自动connection.commit(); 且释放ThreadLocal资源(SqlSession)
但是还有一个问题:
同一个spring事务我们是使用相同的SqlSession了,但是我们想要的是@Transactional注解切面after自动实现sqlSession.commit() 而不是 connection.commit();其实SqlSession.commit()主要也是实现connection.commit(),这个确实是一点小瑕疵,但是确实是不影响使用。
这样SqlSession的生命周期就实现了类似spring事务里Connection的生命周期,且同connection一样,ThreadLocal中set SqlSession是在业务代码中第一次获取SqlSession时,而不是@Transactional切面的before方法,在必须时才去获取,而不是提前获取资源。
需要注意的是源码中SqlSessionUtils不是直接将SqlSession存在ThreadLocal,而是和spring的DataSourceUtils一样,通过spring的TransactionSynchronizationManager来存储到ThreadLocal,这里为了便于理解我们直接进行了简化。
如果不使用@Transactional注解进行事务管理的话怎么使用SqlSessionUtils
SqlSession依然会帮我们存到ThreadLocal,不过同DataSourceUtils一样就需要我们手动commit和release;因为没人帮我们干这个事情了,需要我们自己处理。当然SqlSessionUtils有提供方法供我们自己调用。
例如下面代码,如果这样写是不是就有问题了?就没人帮我们commit和close connection了!
public void methodC(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory);
sqlSession.insert("insertXXX",xxx);
}
需要改成如下格式:
public void methodC(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory);
sqlSession.insert("insertXXX",xxx);
sqlSession.commit();
SqlSessionUtils.closeSqlSession(sqlSession,sqlSessionFactory);
}
这下问题又麻烦了:
1.methodC可能会被其他方法受spring事务控制的方法调用,这样其也会被纳入spring事务范围管理,不需要自己提交connection。
例如如果被上面methodA方法内部调用,@Transactional切面after会在methodA的所有代码(当然包括methodC的代码)执行完后自动提交connection
2.如果直接调用methodC,其本身又不在spring事务管理范围,需要自己提交connection。
我们有没有办法判断当前方法是否在@Transactional事务范围内,如果在事务范围内,就不处理,交由事务去提交;如果不在事务范围内,就自己提交?
5.SqlSessionTemplate
上述问题我们做一下判断,伪代码如下:
public void methodC(){
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
sqlSession.insert("insertXXX",xxx);
if(isSqlSessionTransactional(sqlSession,sqlSessionFactory)){//判断当前sqlSession是否在spring @Transactional事务管理范围内
//donothing
}else{
sqlSession.commit();
sqlSession.close();
}
}
如何判断当前sqlSession是否在spring @Transactional事务管理范围内呢?如果感兴趣的话可以直接去看一下源码,我们这里就不啰嗦了
然后上面这段判断代码我们不可能每个方法里都写一遍吧,有没有办法提取出来,我们就不绕弯子了,直接看优雅的SqlSessionTemplate:
public void methodC(){
SqlSessionTemplate sqlSessionTemplate=new SqlSessionTemplate(sqlSessionFactory);
sqlSessionTemplate.insert("insertXXX",xxx);
}
又是基于Proxy代理,在执行 SqlSession方法时,都交由代理去处理,SqlSessionTemplate的伪代码:
public class SqlSessionTemplate implements SqlSession, DisposableBean {
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
...
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
} public int insert(String statement, Object parameter) {
return this.sqlSessionProxy.insert(statement, parameter);
} private class SqlSessionInterceptor implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SqlSessionUtils.getSqlSession(sqlSessionFactory);
Object result = method.invoke(sqlSession, args);//执行sqlSession对应的方法
if(isSqlSessionTransactional(sqlSession,sqlSessionFactory)){//判断当前sqlSession是否在spring @Transactional事务管理范围内
//donothing
}else{
sqlSession.commit();
sqlSession.close();
}
}
} }
终于,经历了这么多,mybatis-spring终于能够与spring的事务管理比较完美的融合了?
问题仍然还没结束,我们目前的操作也仅限于SqlSession的方法操作,我们上面基于Mapper接口的操作呢,回顾我们上面MapperProxy、MapperMethod,MapperMethod是调用SqlSession相应的方法,怎么才能对接上SqlSessionTemplate
那还不简单:
SqlSessionTemplate sqlSessionTemplate =new SqlSessionTemplate(sqlSessionFactory);
IBlogMapper blogMapper = sqlSessionTemplate.getMapper(IBlogMapper.class);
blogMapper.selectBlog(101);
6.MapperScannerConfigurer
现在我们结合mybatis-spring来使用SqlSession已经优雅了很多,我们也可以基于MapperProxy来实现上面的MethodA、MethodB的代码,这样就省去了字符串硬编码,这种方式会更好:
@Transactional
public void methodA(){
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
IBlogMapper blogMapper = sqlSession.getMapper(IBlogMapper.class);
blogMapper.insertBlog(xxx);
methodB();
}
public void methodB(){
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
IUserMapper userMapper = sqlSession.getMapper(IUser.class);
userMapper.insertUser(xxx);
}
我们知道SqlSessionTemplate是基于proxy代理形式实现了对应的功能,那么我们在结合spring使用的时候,能否把这个代理注册成spring的bean呢,就是把sqlSession.getMapper(xxx.class)注册成spring的bean,这样我们就能够使用如下@Autowired这样更优雅的编码:
@Autowired
IBlogMapper blogMapper; @Autowired
IUserMapper userMapper; @Transactional
public void methodA(){
blogMapper.insertBlog(xxx);
methodB();
}
public void methodB(){
userMapper.insertUser(xxx);
}
怎样注册spring bean呢,我们以IBlogMapper接口举例:
public interface IBlogMapper {
List<Blog> findActiveBlogLike(Map map);
}
手动注册实现类:
public class BlogMapper implements IBlogMapper {
@Autowired
SqlSessionFactory sqlSessionFactory; @Override
public List<Blog> findActiveBlogLike(Map map) {
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
List<Blog> list = sqlSessionTemplate.selectList("findActiveBlogLike",map);
return list;
}
}
不对啊,这里没有用到MapperProxy代理实现啊,而是自己手动去判断和映射接口需要使用sqlsession的哪个方法了,完全没MapperProxy和MapperMethod的事情啊?这肯定不是我们想要的!
Spring 之 BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor
要想给spring动态的注册bean,这就又到了spring bean的生命周期的知识了,我们这里就直接看mybatis-spring使用的什么了,就不啰嗦spring bean生命周期了
@Bean
MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("xxx");
msc.setAnnotationClass(Mapper.class);//可以设置只注册添加了mybatis @Mapper注解的接口
msc.setSqlSessionFactoryBeanName("sqlSessionFactory");
return msc;
}
MapperScannerConfigurer的实现:
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor ... {
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
...
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
...
ClassPathMapperScanner的实现:
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
...
processBeanDefinitions(beanDefinitions);
...
return beanDefinitions;
} private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class; private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName(); definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
definition.setBeanClass(this.mapperFactoryBeanClass);
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory",
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
}
...
}
}
即扫描我们设置的basepackage下的所有符合过滤器规则的接口(例如可以设置只扫描返回带有mybatis @Mapper注解的接口),然后注册成为spring bean,不过注册的bean并不是MapperProxy,而是MapperFactoryBean,好吧,继续往里面看
MapperFactoryBean
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
} public SqlSession getSqlSession() {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
MapperFactoryBean这里实现了FactoryBean接口,实际注册的bean会通过getObject方法返回最终的实现类,终于到了我们的MapperProxy了
@MapperScan @MapperScans
这两个mybatis-spring的注解其实就是用于自动帮我们注册MapperScannerConfigurer 的spring Bean
SqlSessionFactoryBean
我们之前声明SqlSessionFactory时要写一堆代码,现在这个工作交给SqlSessionFactoryBean,其也继承了spring FactoryBean接口,即通过getObject方法返回实际注册的对象:SqlSessionFactory
7.mybatis-spring-boot-starter
mybatis-spring-boot-starter其实就是帮我们做一些自动化的配置,和spring-boot-starter的初衷一样,这一块其实没有什么好讲的,所以我们就附属到mybatis-spring的一个小章节里
该项目pom里引用了mybatis-spring-boot-autoconfigure,其spring.factories如下
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
MybatisLanguageDriverAutoConfiguration
就是帮我们自动设置LanguageDriver,例如FreeMarkerLanguageDriver、ThymeleafLanguageDriver等,mybatis默认是XMLLanguageDriver
MybatisAutoConfiguration
这里主要自动帮我们注册了SqlSessionFactory、SqlSessionTemplate、MapperScannerConfigurer的Bean
主要还是MapperScannerConfigurer的Bean,就省去了我们之前还要手动去注册MapperScannerConfigurer Bean,不过这里有设置MapperScannerConfigurer 只扫描带有mybatis @Mapper注解的接口。
到目前为止我们绝大多数场景只需要注册一个SqlSessionFactoryBean为 spring bean就可以了
读懂源码不难,讲出来通俗易懂很难,写出来通俗易懂是难上加难,文章写出来不易,还望各位点点推荐,也欢迎评论区交流,你的互动也是我更新和维护的动力。
mybatis是怎样炼成的的更多相关文章
- 老杜告诉你java小白到大神是怎么炼成的(转载)
老杜告诉你java小白到大神是怎么炼成的 1. 学习前的准备 一个好的学习方法(应该怎么学习更高效): 一个合格的程序员应该具备两个能力 有一个很好的指法速度(敲代码快) 有一个很好的编程思想(编程思 ...
- fir.im Weekly - 论个人技术影响力是如何炼成的
每个圈子都有一群能力强且懂得经营自己的人,技术圈也是如此.本期 fir.im Weekly 一如往期精选了一些实用的 iOS,Android 开发工具和源码分享,还有一些关于程序员的成长 Tips 和 ...
- 我的 Github 个人博客是怎样炼成的
Joey's Blog 长大后才发现政府建造 GFW 真是太 TM 机智了,由于本人自制力较差,且不说 91porn, youporn 等两性知识网站的超强战斗力,单单一个Youtube就可以让我瞬间 ...
- 自由是有代价的:聊聊这几年尝试的道路 要想生活好,别看哲学书和思想书。简单看看可以,看多了问题就大了。还是要去研究研究些具体的问题。别jb坐在屋子里,嘴里念着海子的诗,脑袋里想康德想的事情,兜里屁都没有,幻想自己是大国总理,去想影帝是怎么炼成的。
自由是有代价的:聊聊这几年尝试的道路 现在不愿意写过多的技术文章了,一点是现在做的技术比较偏,写出来看的人也不多,二来是家庭事务比较繁多,没以前那么有时间写了.最近,园子里多了一些写经历的文章,我也将 ...
- 2星|《10W+走心文案是怎样炼成的》:标题党。实际是台湾创意总监的一些人生感悟和两三个很一般的创意文案
10W+走心文案是怎样炼成的 作者是台湾人,曾在台湾奥美担任创意总监,做过一些广告.本书是他的一些经验介绍. 总体来说是标题党,作者的广告基本是电视广告,跟文案也有关系,估计播放量也很容易过10W+, ...
- 测度论--长度是怎样炼成的[zz]
http://www.58pic.com/newpic/27882296.html http://www.58pic.com/newpic/27893137.html http://699pic.co ...
- AI算法工程师炼成之路
AI算法工程师炼成之路 面试题: l 自我介绍/项目介绍 l 类别不均衡如何处理 l 数据标准化有哪些方法/正则化如何实现/onehot原理 l 为什么XGB比GBDT好 l 数据清洗的方法 ...
- 开会不用把人都轰进一个小黑屋子——《Office妖精是怎样炼成的》续2
<Office妖精是怎样炼成的>http://blog.sina.com.cn/s/articlelist_1446470001_6_1.html 一本不是技术图书却含有技术内容的图书,一 ...
- 学习型的“文山表海无限发展公司”——《Office妖精是怎样炼成的》续1
本篇无故事情节版:https://www.cnblogs.com/officeplayer/p/14841590.html <Office妖精是怎样炼成的>http://blog.sina ...
随机推荐
- 现代软件工程讲义 如何提出靠谱的项目建议 NABCD
互联网时代对于创新者来说, 既是一个伟大的时代, 又是一个糟糕的时代. 你有很多机会做出影响世界的产品, 但是, 似乎任何想法都被别人想到过了, 做出来了, 上市了, 移植到各种平台上去了- 那么 ...
- C语言编程入门题目--No.12
题目:判断101-200之间有多少个素数,并输出所有素数. 1.程序分析:判断素数的方法:用一个数分别去除2到sqrt(这个数),如果能被整除, 则表明此数不是素数,反之是素数. 2.程序源代码: # ...
- RF(For 循环)
一.介绍:RobotFrameWork 支持 FOR 循环语句,语法和 Python 的语法基本相同,但 RobotFrameWork 中,"FOR" 关键字前面需要增加一个 &q ...
- Prometheus monitor RabbitMQ
Install docker-compose sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2 ...
- RocketMQ搭建全过程
RocketMQ下载地址:https://mirrors.tuna.tsinghua.edu.cn/apache/rocketmq/4.3.0/rocketmq-all-4.3.0-bin-relea ...
- GitHub 热点速览 Vol.18:刷 LeetCode 的正确姿势
作者:HelloGitHub-小鱼干 摘要:找对路子,事半功倍,正如本周 GitHub Trending #刷 LeetCode# 主题想表达的那般,正确的学习姿势方能让人走得更远,走进大厂
- Java——异常那些事
异常的基本定义 异常情形是指阻止当前方法或者作用域继续执行的问题.在这里一定要明确一点:异常代码某种程度的错误,尽管Java有异常处理机制,但是我们不能以“正常”的眼光来看待异常,异常处理机制的原因就 ...
- Cell Phone Network G
最小点队的题意:https://www.luogu.com.cn/problem/P2899 与战略游戏不同的是,这里要求占领所有的点而不是边. 1自己被自己染色(有信号塔) 这时我们可以想一下,u被 ...
- 汽车安全攻击篇:智能网联系统的短板,如何防护汽车的安全C
我们在<速度与激情>里,经常可以看到主角们利用网络侵入汽车网络系统,然后任意的操纵这些车辆,看电影的时候会被画面所震撼到,这两年"自动驾驶"随着特斯拉的车已经越来越普及 ...
- 给大家发个Python和Django的福利吧,不要钱的那种~~~
前言一: 这篇是一个发放福利的文章,但是发放之前,我还是想跟大家聊聊我为什么要发这样的福利. 我第一份工作是做的IT桌面支持,日常工作就是给同事修修电脑.装装软件.开通账号.维护内部系统之类的基础工作 ...