mybatis是怎样炼成的
RoyTian 人气:0前言
一些个人感受:不管分析什么源码,如果我们能摸索出作者的心路历程,跟着他的脚步一步一步往前走,这样才能接近事实的真相,也能更平滑更有趣的学习到知识。跟福尔摩斯探案一样,作者都经历了些什么,为什么他要这样去设计这样去做,留给我们的只有无声的代码和那一段孤独的日子。
阅读顺序建议是从上往下阅读,如果直接跳转到某一节,没有基于上面的分析推理的话可能会不容易理解。
一切的一切要从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"/> <https://img.qb5200.com/download-x/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
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事务管理范围内 /https://img.qb5200.com/download-x/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事务管理范围内 /https://img.qb5200.com/download-x/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就可以了
读懂源码不难,讲出来通俗易懂很难,写出来通俗易懂是难上加难,文章写出来不易,还望各位点点推荐,也欢迎评论区交流,你的互动也是我更新和维护的动力。
加载全部内容