一、前言
MyBatis官方文档:https://mybatis.org/mybatis-3/zh/
1、介绍
对于MyBatis,其工作流程实际上分为两部分:第一,构建,也就是解析我们写的xml配置,将其变成它所需要的对象。第二,执行,在构建完成的基础上,去执行我们的SQL,完成与Jdbc的交互
2、快速上手
数据库配置如Mybatis学习笔记一样,我的项目结构如下图所示 创建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 resource ="db.properties"></properties>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
<typeAliases>
<package name="org.demo.po"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/EmployeeMapper.xml" />
</mappers>
</configuration>
创建db.properties 外部文件
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/learnmybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=root
创建EmployeeMapper.xml 文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test">
<select id="getEmp" resultType="Employee">
select * from employee
</select>
</mapper>
创建测试文件
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Employee> list = sqlSession.selectList("test.getEmp");
System.out.println(list);
sqlSession.close();
}
二、Mybatis的构建
1、核心流程
1.1 介绍
Configuration 是整个MyBatis的配置体系集中管理中心,前面所学Executor、StatementHandler、Cache、MappedStatement…等绝大部分组件都是由它直接或间接的创建和管理。其主要作用如下
- 存储全局配置信息,其来源于settings(设置)
- 初始化并维护全局基础组件
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- plugins(插件)
- environments(环境配置)
- cache(二级缓存空间)
- 初始化并维护MappedStatement
- 组件构造器,并基于插件进行增强
- newExecutor(执行器)
- newStatementHandler(JDBC处理器)
- newResultSetHandler(结果集处理器)
- newParameterHandler(参数处理器)
Configuration 配置信息来源于xml和注解,每个文件和注解都是由若干个配置元素组成,并呈现嵌套关系,总体关系如下图所示,关于各配置的使用请参见官网给出文档:https://mybatis.org/mybatis-3/zh/configuration.html#properties
无论是xml 注解这些配置元素最弱都要被转换成JAVA配置属性或对象组件来承载。其对应关系如下:
- 全配置(config.xml) 由Configuration对像属性承载
- sql映射<select|insert…> 或@Select 等由MappedStatement对象承载
- 缓存<cache…> 或@CacheNamespace 由Cache对象承载
- 结果集映射 由ResultMap 对象承载
1.2 配置文件解析
- XMLConfigBuilder :解析config.xml文件,会直接创建一个configuration对象,用于解析全局配置
- XMLMapperBuilder :解析Mapper.xml文件,内容包含等
- MapperBuilderAssistant:Mapper.xml解析辅助,在一个Mapper.xml中Cache是对Statement(sql声明)共享的,共享组件的分配即由该解析实现
- XMLStatementBuilder:SQL映射解析 即<select|update|insert|delete> 元素解析成MapperStatement
- SqlSourceBuilder:Sql数据源解析,将声明的SQL解析可执行的SQL
- XMLScriptBuilder:解析动态SQL数据源当中所设置 SqlNode脚本集
XML文件解析流程
整体解析流程是从XmlConfigBuilder 开始,然后逐步向内解析,直到解析完所有节点。我们通过一个MappedStatement 解析过程即可了解到期整体解析流程 注解配置解析
注解解析底层实现是通过反射获取Mapper接口当中注解元素实现。有两种方式一种是直接指定接口名,一种是指定包名然后自动扫描包下所有的接口类。这些逻辑均由Mapper注册器(MapperRegistry)实现。其接收一个接口类参数,并基于该参数创建针对该接口的动态代理工厂,然后解析内部方法注解生成每个MapperStatement 最后添加至Configuration 完成解析。
1.3 源码分析
进入build方法,可以看见代码将xml文件传入并返回了一个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) {
}
}
}
在进入build对象的parse() 方法,这个方法初始化Configuration对象,并且解析xml文件,把解析内容放入到Configuration对象中。其中就包括别名的映射,在初始化阶段别名映射会自动注册一些常用的别名,如果我们自己也配置也自动注册到Configuration对象的TypeAliasRegistry的TYPE_ALIASES的map中,并且把数据源和事务解析以后放入到Environment,给后续的执行提供数据链接和事务管理。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
在进入parseConfiguration() 方法,可以看到这个方法已经在解析<configuration> 下的节点了,例如<settings> ,<typeAliases> ,<environments> 和<mappers> 等,同时返回了Configuration 对象
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
2、Configuration对象详解
2.1 配置文件dataSource 内容替换
对于db.properties 替代,在parseConfiguration() 方法中的propertiesElement(root.evalNode("properties")); 就是对外部配置文件的替换修改,它首先形成Properties对象对其替换
2.2 typeAliasesElement别名设置
Mybatis别名设置若存在标签,则mapper.xml别名默认是类名(忽略大小写),同时将其存放于TYPE_ALIASES 这个HashMap中,同时里面已经存在很多内置别名,可以直接使用
2.3 数据库相关内容载入
environmentsElement(root.evalNode("environments")); 方法将数据库相关信息配置(例如事务,数据库账号密码等)存入enviroment对象,最终和configuration相关联存入其对象中
2.4 mapper解析(重要?)
mybatis-config.xml 文件中我们一定会写一个叫做的标签,这个标签中的<mapper> 节点存放了我们对数据库进行操作的SQL语句,这里就详细详解一下mapper的执行过程
<mappers>
<package name="org.demo.po"/>
<mapper resource="mappers/EmployeeMapper.xml" />
<mapper class="org.demo.mapper.EmployeeMapper.java"/>
<mapper url=""/>
</mappers>
这是<mappers> 标签的几种配置方式,通过这几种配置方式,可以帮助我们更容易理解mappers的解析
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
我们的配置文件中写的是通过resource来加载mapper.xml 的,所以会通过XMLMapperBuilder 来进行解析,我们在进去它的parse() 方法。在这个parse()方法中,调用了一个configuationElement 代码,用于解析XXXMapper.xml 文件中的各种节点,包括<cache> 、<cache-ref> 、<paramaterMap> (已过时)、<resultMap> 、<sql> 、还有增删改查节点,和上面相同的是,我们也挑一个主要的来说,因为解析过程都大同小异。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
这里解析其中一项举例,解析增删改查节点<select> <insert> <update> <delete> ,进入buildStatementFromContext(context.evalNodes("select|insert|update|delete")) 方法
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
进入statementParser.parseStatementNode(); 方法,解析里面的xml节点
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
processSelectKeyNodes(id, parameterTypeClass, langDriver);
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
具体的MappedStatement对象,这里每一个方法id对应存储一个MappedStatement对象,这样在执行的时候就可以直接通过id获得映射的MappedStatement对象了,即可以直接执行获取mysql结果了
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
3、动态SQL构建
3.1 动态SQL解析
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
OGNL表达示
OGNL全称是对象导航图语言(Object Graph Navigation Language)是一种JAVA表达示语言,可以方便的存取对象属和方法,已用于逻辑判断。其支持以下特性:获取属性属性值,以及子属性值进行逻辑计;表达示中可直接调用方法(如果是无参方法,可以省略括号);通过下标访问数组或集合;遍历集合
3.2 动态SQL脚本
每个动态元素都会有一个与之对应的脚本类,即会产生许多SqlNode脚本如if 对应ifSqlNode 、forEarch 对应ForEachSqlNode 以此类推下去。同时脚本之间是呈现嵌套关系的,比如if 元素中会包含一个MixedSqlNode ,而MixedSqlNode 下又会包含1至1至多个其它节点,最后组成一课脚本语法树。最后SqlNode的接口非常简单,就只有一个apply 方法,方法的作用就是执行当前脚本节点逻辑,并把结果应用到DynamicContext 当中去。
这里要注意下面三个脚本
StaticTextSqlNode 表示一段纯静态文本如: select * from user TextSqlNode 表示一个通过参数拼装的文本如:select * from ${user} MixedSqlNode 表示多个节点的集合
3.3 SqlSource(SQL数据源)
SqlSource 是基于XML解析而来,解析的底层是使用Dom4j 把XML解析成一个个子节点,在通过 XMLScriptBuilder 遍历这些子节点最后生成对应的Sql源。在上层定义上每个Sql映射(MappedStatement)中都会包含一个SqlSource 用来获取可执行Sql(BoundSql )。SqlSource又分为原生SQL源与动态SQL源,以及第三方源
- roviderSqlSource :第三方法SQL源,每次获取SQL都会基于参数动态创建静态数据源,然后在创建BoundSql
- DynamicSqlSource:动态SQL源包含了SQL脚本,每次获取SQL都会基于参数又及脚本,动态创建创建BoundSql
- RawSqlSource:不包含任何动态元素,原生文本的SQL。但这个SQL是不能直接执行的,需要转换成BoundSql
- StaticSqlSource:包含可执行的SQL,以及参数映射,可直接生成BoundSql。前面三个数据源都要先创建StaticSqlSource然后才创建BoundSql
3.4 源码流程
生成SQL语句代码,首先这里会通过<select> 节点获取到我们的SQL语句,假设SQL语句中只有${} ,那么直接就什么都不做,在运行的时候直接进行赋值。而如果扫描到了#{} 字符串之后,会进行替换,将#{} 替换为 ? 。
这里会生成一个GenericTokenParser,这个对象可以传入一个openToken和closeToken,如果是#{} ,那么openToken就是#{ ,closeToken就是 } ,然后通过parse方法中的handler.handleToken() 方法进行替换。在这之前由于已经进行过SQL是否含有#{} 的判断了,所以在这里如果是只有${} ,那么handler就是BindingTokenParser的实例化对象,如果存在#{} ,那么handler就是ParameterMappingTokenHandler 的实例化对象。
mapperElement() > mapperParser.parse() > 进入XMLMapperBuilder类 configurationElement() > buildStatementFromContext() > buildStatementFromContext() > statementParser.parseStatementNode();
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
进入sqlSource = new RawSqlSource() >sqlSourceParser.parse()
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\') {
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\') {
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
@Override
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
4、构建总结
4.1 总结
MyBatis需要做的就是,先判断这个节点是用来干什么的,然后再获取这个节点的id、parameterType、resultType等属性,封装成一个MappedStatement对象,由于这个对象很复杂,所以MyBatis使用了构造者模式来构造这个对象,最后当MappedStatement对象构造完成后,将其封装到Configuration对象中。
MyBatis需要对配置文件进行解析,最终会解析成一个Configuration对象
- Configuration对象,保存了mybatis-config.xml的配置信息。
- MappedStatement,保存了XXXMapper.xml的配置信息。
但是最终MappedStatement对象会封装到Configuration对象中,合二为一,成为一个单独的对象,也就是Configuration
4.2 面试回答
我在开发xxxx项目的时候、使用Mybatis开发项目,我对Mybatis的认识是:它其实是一个orm持久层框架,其实就对jdbc一个封装而得的框架,使用好处其实就可以把jdbc从连接开辟事务管理以及连接关闭和sql执行,对数据的映射pojo整个过程进行一个封装而已。它的整个执行的过程:
- 首先会引入mybatis依赖,然后会定义个xml核心配置文件放入类路径resouces,这个文件里面就描述了数据源、mapper映射、别名的映射、数据类型转换、插件、属性配置等。定义好以后,那么接下就是创建一个SqlSessionFactory对象,但是在创建这个对象之前,我们会进行xml文件的解析,解析过程中会使用SqlSessionFacotoryBuilder里面提供了一个build方法。这个方法的做了一个非常核心的事情:初始化Configuration对象,并且把对应类的属性的对象全部初始化,并且解析核心xml文件
- 把解析核心的xml配置文件的内容放入到Configuration对象中属性中,其中就包括别名的映射,在初始化阶段别名映射会自动注册一些常用的别名。如果我们自己也配置也会自动注册到
Configuration对象的TypeAliasRegistry的map中 - 并且把在配置文件中的数据源和事务解析以后放入到Environment,给后续的执行,提供数据链接和事务管理
- 然后在解析xxxMapper.xml配置文件,根据配置文件解析的规则,会解析里面对应的节点。比如:<select<update<insert <delete<dql <cache<cache-ref <resultMap等,然后把每个解析的节点放入到一个叫MapperStament对象,sql语句就放入到这个对象SqlSource中
- 并且把解析的每一个节点对应的MapperStatment同时放入到Configuration全局的
Map (mapperedStatments)中,以节点的id和命名空间+id做为key,以MapperStatement对象做value,给后续执行提供一个参考和方向
三、Mybatis的执行
1、SqlSession对象生成
1.1 Xml对象直接生成
核心
将SqlSessionFactoryBuilder中通过build方法创建和装配好Configuration对象通过构造函数进行下传,传递到SqlSession中,最后开辟SqlSession会话对象
源码分析
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Employee> list = sqlSession.selectList("test.getEmp");
System.out.println(list);
sqlSession.close();
}
这里进入SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 方法后,Configuration是DefaultSqlSessionFactory 的一个属性。而SqlSessionFactoryBuilder 在build 方法中实际上就是调用XMLConfigBuilder对xml文件进行解析生成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) {
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
通过调用sqlSessionFactory.openSession(); 方法来获取SqlSession 对象,而openSession中实际上就是对SqlSession做了进一步的加工封装,包括增加了事务、执行器等
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
1.2 mapper 代理对象的生成
首先需要修改部分文件内容
package org.demo.mapper;
import org.demo.po.Employee;
import java.util.List;
public interface EmployeeMapper {
List<Employee> getEmp();
}
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
List<Employee> list = mapper.getEmp();
System.out.println(list);
sqlSession.close();
}
源码分析
从SqlSession的getMapper()方法进入,可以看到这里mapperProxyFactory对象会从一个叫做knownMappers的对象中以type为key取出值,这个knownMappers是一个HashMap,存放了我们的EmployeeMapper对象,而这里的type,就是我们上面写的Mapper接口
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
对于knownMappers生成,在configuration对象在解析的时候,会调用parse()方法,这个方法内部有一个bindMapperForNamespace 方法,而就是这个方法帮我们完成了knownMappers的生成,并且将我们的Mapper接口put进去
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
我们在getMapper之后,获取到的是一个Class,之后的代码就简单了,就是生成标准的代理类了,调用newInstance()方法。到这里,就完成了代理对象(MapperProxy)的创建,很明显的,MyBatis的底层就是对我们的接口进行代理类的实例化,从而操作数据库。
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
查看动态代理调用的方法逻辑,进入MapperProxy类,发现实现了InvocationHandler接口。
在方法开始代理之前,首先会先判断是否调用了Object类的方法,如果是,那么MyBatis不会去改变其行为,直接返回,如果是默认方法,则绑定到代理对象中然后调用,如果都不是,那么就是我们定义的mapper接口方法了,那么就开始执行。执行方法需要一个MapperMethod对象,这个对象是MyBatis执行方法逻辑使用的,MyBatis这里获取MapperMethod对象的方式是,首先去方法缓存中看看是否已经存在了,如果不存在则new一个然后存入缓存中,因为创建代理对象是十分消耗资源的操作。总而言之,这里会得到一个MapperMethod对象,然后通过MapperMethod的excute()方法,来真正地执行逻辑。
public class MapperProxy<T> implements InvocationHandler, Serializable {
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method,
k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
}
最后执行逻辑,这里首先会判断SQL的类型:SELECT|DELETE|UPDATE|INSERT,判断SQL类型为SELECT之后,就开始判断返回值类型,根据不同的情况做不同的操作。然后开始获取参数>执行SQL
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
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);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
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;
}
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
2、执行SQL前述
进入sqlSession.selectList("test.getEmp"); 方法,可以发现在调用sqlsession执行的selectList、insert、update、delete的时候,其实就是根据执行的statement名字,到Configuration的mapperStatements对应的map中去找到有没有一个对应的 MapperStatement对象,如果找到就返回这个对象,然后给后续执行一个依据和参考
3、执行SQL语句——执行器
3.1 执行架构
https://zhuanlan.zhihu.com/p/299020451
- 执行器:Executor, 处理流程的头部,主要负责缓存、事务、批处理。一个执行可用于执行多条SQL。它和SQL处理器是1对N的关系
- Sql处理器:StatementHandler 用于和JDBC打道,比如基于SQL声明Statement、设置参数、然后就是调用Statement来执行。它只能用于一次SQL的执行
- 参数处理器:ParameterHandler,用于解析SQL参数,并基于参数映射,填充至PrepareStatement。同样它只能用于一次SQL的执行
- 结果集处理器:ResultSetHandler,用于读取ResultSet 结果集,并基于结果集映射,封装成JAVA对象。他也只用用于一次SQL的执行
3.2 执行器Executor
执行器的实现有三种:SimpleExecute、ReuseExecute和BatchExecute,这三种执行器有个抽象的基础执行器BaseExecutor,用于维护缓存和事务;此外通过装饰器形式添加了一个缓存执行器CachingExecutor,用于处理二级缓存
-
SimpleExecute 简单执行器(默认) SimpleExecutor是执行器的默认实现,主要完成了“执行”功能,在利用StatementHandler 完成。每次调用执行方法 都会构建一个StatementHandler,并预行参数,然后执行 默认情况是executor是CachingExecutor。这个执行器是二级缓存的执行器,如果在配置文件xxxxMapper.xml文件中申明了<cache/> 节点的话,就是使用CachingExecutor;如果没有,就会委托SimpleExecutor(默认类型是simple,在configuration创建的时候指定)执行器去执行你的SQL语句,然后这里会执行的结果放入loaclCache一级缓存中。 -
ReuseExecute 可重用执行器 ReuseExecutor 区别在于他会将在会话期间内的Statement进行缓存,并使用SQL语句作为Key。所以当执行下一请求的时候,不在重复构建Statement,而是从缓存中取出并设置参数,然后执行(参数不同也可以重用) -
BatchExecute 批处理执行器 BatchExecutor 顾名思议,它就是用来作批处理的。但会将所 有SQL请求集中起来,最后调用Executor.flushStatements() 方法时一次性将所有请求发送至数据库
3.3 **SimpleExecute **简单执行器源码分析(重点?)
执行SQL的核心方法就是selectList,即使是selectOne,底层实际上也是调用了selectList方法,然后取第一个而已
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
selectList内部调用了Executor对象执行SQL语句,首先进入的是CachingExecutor执行器,若没有开启二级缓存,那么委托简单执行器
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list);
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
首先MyBatis在查询时,不会直接查询数据库,而是会进行二级缓存的查询,由于二级缓存的作用域是namespace,也可以理解为一个mapper,所以还会判断一下这个mapper是否开启了二级缓存,如果没有开启,则进入一级缓存继续查询。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
如果一级缓存localCache里查到了,那么直接就返回结果了,如果一级缓存没有查到结果,那么最终会进入数据库进行查询
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
总结,一级缓存和二级缓存的key是一样的,一级缓存默认开启,二级缓存需要设置开启。这里CacheExecutor使用的是装饰者模式,即在不改变原有类结构和继承的情况下,通过包装原对象去扩展一个新功能。
4、执行SQL语句——数据库查询
执行流程
4.1 StatementHandler介绍
StatementHandler即为JDBC处理器,基于JDBC构建JDBC Statement并设置参数,然后执行Sql。每调用会话当中一次SQl,都会有与之相对应的且唯一的Statement实例,一个SQL请求会经过会话,然后是执行器,最由StatementHandler执行jdbc最终到达数据库,这三者之间比例是1:1:n。
StatementHandler接口定义了JDBC操作的相关方法如下,
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
void parameterize(Statement statement)
throws SQLException;
void batch(Statement statement)
throws SQLException;
int update(Statement statement)
throws SQLException;
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
StatementHandler 有三个子类SimpleStatementHandler 、PreparedStatementHandler 、CallableStatementHandler ,分别对应JDBC中的Statement、PreparedStatement、CallableStatement。
4.2 参数处理和转换
参数处理即将Java Bean转换成数据类型。总共要经历过三个步骤,ParamNameResolver (参数转换)、ParameterHandler (参数映射)、TypeHandler (参数赋值)
参数转换
所有转换逻辑均在ParamNameResolver中实现
@Select("select * from employee where id = #{id}")
@Options
Employee getEmpById(@Param("id") Integer id);
- 单个参数的情况下且没有设置@param注解会直接转换,勿略SQL中的引用名称
- 多个参数情况:优先采用@Param中设置的名称,如果没有则用参数序号代替 即"param1、parm2等"
参数映射
映射是指Map中的key如何与SQL中绑定的参数相对应。以下这几种情况
- 单个原始类型:直接映射,勿略SQL中引用名称
- Map类型:基于Map key映射
- Object:基于属性名称映射,支持嵌套对象属性访问
参数赋值
通过TypeHandler 为PrepareStatement 设置值,通常情况下一般的数据类型MyBatis都有与之相对应的TypeHandler
4.3 结果集封装
MetaObject相当于一个工具类,里面还包括分词器等,可以参考MetaObject详解
读取ResultSet数据,并将每一行转换成相对应的对象。用户可在转换的过程当中可以通过ResultContext 来控制是否要继续转换,转换后的对象都会暂存在ResultHandler 中最后统一封装成list返回给调用方,结果集转换中99%的逻辑DefaultResultSetHandler 中实现。整个流程可大致分为以下阶段:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
在SQL执行阶段,MyBatis已经完成了对数据的查询,那么现在还存在最后一个问题,那就是结果集处理,换句话来说,就是将结果集封装成对象,这里会创建一个处理结果集的对象
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
closeResultSet(rsw.getResultSet());
}
}
调用handleRwoValues()方法进行行数据的处理
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
skipRows(resultSet, rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())){
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
boolean foundValues = false;
if (!autoMapping.isEmpty()) {
for (UnMappedColumnAutoMapping mapping : autoMapping) {
final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
metaObject.setValue(mapping.property, value);
}
}
}
return foundValues;
}
我们可以看到这个方法会通过遍历参数列表从而通过metaObject.setValue(mapping.property, value); 对返回对象进行赋值,所有的赋值操作在内部都是通过一个叫ObjectWrapper 的对象完成的,先看看中代码的metaObject.setValue() 方法
public void setValue(String name, Object value) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
if (value == null) {
return;
} else {
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
metaValue.setValue(prop.getChildren(), value);
} else {
objectWrapper.set(prop, value);
}
}
objectWrapper有两个实现:BeanWrapper和MapWrapper,如果是自定义类型,那么就会调用BeanWrapper的set方法。MapWrapper的set方法实际上就是将属性名和属性值放到map的key和value中,而BeanWrapper则是使用了反射,调用了Bean的set方法,将值注入。
public void set(PropertyTokenizer prop, Object value) {
if (prop.getIndex() != null) {
Object collection = resolveCollection(prop, map);
setCollectionValue(prop, collection, value);
} else {
map.put(prop.getName(), value);
}
}
public void set(PropertyTokenizer prop, Object value) {
if (prop.getIndex() != null) {
Object collection = resolveCollection(prop, object);
setCollectionValue(prop, collection, value);
} else {
setBeanProperty(prop, object, value);
}
}
private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
try {
Invoker method = metaClass.getSetInvoker(prop.getName());
Object[] params = {value};
try {
method.invoke(object, params);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} catch (Throwable t) {
throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
}
}
4.4 结果集映射
映射是指返回的ResultSet列与Java Bean 属性之间的对应关系。通过ResultMapping进行映射描述,在用ResultMap封装成一个整体,包括手动映射和自动映射
property | 属性名(必填) |
---|
column | 列名(必填) | jdbcType | jdbc类型(可自动推导) | javaType | java类型(可自动推导) | typeHandler | 类型处理器(可自动推导) |
4.5 懒加载
懒加载的参考文章之一
懒加载是为了改善在映射结果集解析对象属性时,大量的嵌套子查询的并发效率问题,当设置懒加载后,只有在使用指定属性时才会触发子查询,从而实现分散SQL请求的目的
配置方式
在mybais主配置文件中配置开启侵入式加载和深度加载,也可以在xml映射文件中配置fetchType ,有效值为 lazy 和 eager 。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled ,使用属性的值
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="true"/>
内部原理
代理过程发生在结果集解析创建对象之后(DefaultResultSetHandler.createResultObject ),如果对应的属性设置了懒加载,则会通过**ProxyFactory **创建代理对象,该对象继承自原对象,然后将对象的值全部拷贝到代理对象,并设置相应MethodHandler (原对象直接抛弃)
通过对Bean的动态代理,重写所有属性的getXxx方法,代理之后Bean会包含一个MethodHandler,内部在包含一个Map用于存放待执行懒加载,执行前懒加载前会移除。LoadPair用于针对反序列化的Bean准备执行环境。ResultLoader用于执行加载操作,执行前如果原执行器关闭会创建一个新的。
4.6 嵌套映射
映射是指返回的ResultSet列与Java Bean 属性之间的对应关系。通过ResultMapping进行映射描述,在用ResultMap封装成一个整体。映射分为简单映射与复合嵌套映射,联合查询分为一对一查询和一对多查询 流程说明
所有映射流程的解析都是在DefaultResultSetHandler当中完成。主要方法如下:
handleRowValuesForNestedResultMap()
嵌套结果集解析入口,在这里会遍历结果集中所有行。并为每一行创建一个RowKey对象。然后调用getRowValue()获取解析结果对象。最后保存至ResultHandler中(注:调用getRowValue前会基于RowKey获取已解析的对象,然后作为partialObject参数发给getRowValue)
该方法最终会基于当前行生成一个解析好对象。具体职责包括,1.创建对象、2.填充普通属性和3.填充嵌套属性。在解析嵌套属性时会以递归的方式在调用getRowValue获取子对象。最后一步4.基于RowKey 暂存当前解析对象(如果partialObject参数不为空 只会执行 第3步。因为1、2已经执行过了)
applyNestedResultMappings()
解析并填充嵌套结果集映射,遍历所有嵌套映射,然后获取其嵌套ResultMap。接着创建RowKey 去获取暂存区的值。然后调用getRowValue 获取属性对象。最后填充至父对象(如果通过RowKey能获取到属性对象,它还是会去调用getRowsValue,因为有可能属下还存在未解析的属性)
MyBatis循环依赖问题
mybatis解决循环依赖主要是利用一级缓存和内置的queryStack标识。mybatis中BaseExecutor执行器对一级缓存进行管控,利用queryStack标识对最终结果进行处理,一级缓存对没有操作的查询缓存key进行空参填充,在嵌套子查询中会判断是否命中一级缓存,然后将其添加到延迟队列(非懒加载),直到整个查询结束再对其进行延迟队列的加载,填充所有数据
其源码主要在DefaultResultSetHandler类中,方法调用手动映射,具体为applyPropertyMappings >getPropertyMappingValue >getNestedQueryMappingValue >ResultLoader - 结果集加载器>再次进入BaseExecutor的query中,对queryStack进行累加,直到跳出整个查询
四、Mybatis的缓存
1、缓存概述
myBatis中存在两个缓存,一级缓存和二级缓存
- 一级缓存:也叫做会话级缓存,生命周期仅存在于当前会话,不可以直接关关闭。但可以通过
flushCache 和localCacheScope 对其做相应控制。 - 二级缓存:也叫应用级缓存,缓存对象存在于整个应用周期,而且可以跨线程使用。
2、一级缓存
2.1 缓存命中与清空
缓存命中参数
- SQL与参数相同
- 同一个会话
- 相同的MapperStatement ID
- RowBounds行范围相同
触发清空缓存
- 手动调用clearCache,注意clearLocalCache 不是清空某条具体数据,而是清当前会话下所有一级缓存数据
- 执行提交回滚(commit、Rolback)
- 执行任意增删改update
- 配置flushCache=true
- 缓存作用域为Statement(即子查询,子查询依赖一级缓存)
2.2 集成Spring一级缓存失效
因为Spring 对SqlSession进行了封装,通过SqlSessionTemplae ,使得每次调用Sql,都会重新构建一个SqlSession,解决方法是
- 开启事务,因为一旦开启事务,Spring就不会在执行完SQL之后就销毁SqlSession,因为SqlSession一旦关闭,事务就没了,一旦我们开启事务,在事务期间内,缓存会一直存在
- 使用二级缓存
3、二级缓存
3.1 简介
二级缓存也称作是应用级缓存,与一级缓存不同的,是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据,在流程上是先访问二级缓存,在访问一级缓存。二级缓存的更新,必须是在会话提交之后,同时要提交之后才能命中缓存
3.2 二级缓存使用
缓存空间声明
二级默认缓存默认是不开启的,需要为其声明缓存空间才可以使用,通过@CacheNamespace 或 在xml配置<Cache/> 。声明之后该缓存为该Mapper所独有,其它Mapper不能访问。如需要多个Mapper共享一个缓存空间可通过@CacheNamespaceRef 或<cache-ref namespace=""/> 进行引用同一个缓存空间。@CacheNamespace 详细配置见下表:
配置 | 说明 |
---|
implementation | 指定缓存的存储实现类,默认是用HashMap存储在内存当中 | eviction | 指定缓存溢出淘汰实现类,默认LRU ,清除最少使用 | flushInterval | 设置缓存定时全部清空时间,默认不清空。 | size | 指定缓存容量,超出后就会按eviction指定算法进行淘汰 | readWrite | true即通过序列化复制,来保证缓存对象是可读写的,默认true | blocking | 为每个Key的访问添加阻塞锁,防止缓存击穿 | properties | 为上述组件,配置额外参数,key对应组件中的字段名。 |
缓存其它配置
除@CacheNamespace 还可以通过其它参数来控制二级缓存
字段 | 配置域 | 说明 |
---|
cacheEnabled | 在mybatis设置 | 二级缓存全局开关,默认开启 | useCache | <select | update | flushCache | <select | update |
注意:若*Mapper.xml 和mapper 接口同时设置SQL查询,并同时配置了缓存,那么两个缓存空间是不一致,需要用缓存引用ref使用同一个缓存空间
3.3 责任链设计
这里MyBatis抽像出Cache接口,其只定义了缓存中最基本的功能方法:
然后上述中每一个功能都会对应一个组件类,并基于装饰者加责任链的模式,将各个组件进行串联。在执行缓存的基本功能时,其它的缓存逻辑会沿着这个责任链依次往下传递。
3.4 执行流程
原本会话是通过Executor实现SQL调用,这里基于装饰器模式使用CachingExecutor对SQL调用逻辑进行拦截,以嵌入二级缓存相关逻辑。这里SqlSession会话可以对应多个暂存区,而多个暂存区对应一个缓存空间 查询操作query
当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。
请注意,这里的查询是实时从缓存空间读取的,而变更,只会记录在暂存区
更新操作update
当执行update操作时,同样会基于查询的语句和参数组成缓存KEY,然后在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。
如果在查询操作中配置了flushCache=true ,也会执行相同的操作。
提交操作commit
当会话执行commit操作后,会将该会话下所有暂存区的变更,更新到对应二级缓存空间去。
3.5 缓存源码事务分析
这个类是MyBatis用于缓存事务管理的类
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
TransactionalCacheManager中封装了一个Map,用于将事务缓存对象缓存起来,这个Map的Key是我们的二级缓存对象,而Value是一个叫做TransactionalCache。
- 其中在getObject()方法中存在两个分支:如果发现缓存中取出的数据为null,那么会把这个key放到entriesMissedInCache中,这个对象的主要作用就是将我们未命中的key全都保存下来,防止缓存被击穿,并且当我们在缓存中无法查询到数据,那么就有可能到一级缓存和数据库中查询,那么查询过后会调用putObject()方法,这个方法本应该将我们查询到的数据put到真实缓存中,但是现在由于存在事务,所以暂时先放到entriesToAddOnCommit中;如果发现缓存中取出的数据不为null,那么会查看事务提交标识(clearOnCommit)是否为true,如果为true,代表事务已经提交了,之后缓存会被清空,所以返回null,如果为false,那么由于事务还没有被提交,所以返回当前缓存中存的数据
- 事务提交成功时有以下几步:清空真实缓存、将本地缓存(未提交的事务缓存 entriesToAddOnCommit)刷新到真实缓存、将所有值复位
- 回滚步骤:清空真实缓存中未命中的缓存、将所有值复位
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
private final Cache delegate;
private boolean clearOnCommit;
private final Map<Object, Object> entriesToAddOnCommit;
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object getObject(Object key) {
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
3.6 使用经验
二级缓存不能存在一直增多的数据
由于二级缓存的影响范围不是SqlSession而是namespace,所以二级缓存会在你的应用启动时一直存在直到应用关闭,所以二级缓存中不能存在随着时间数据量越来越大的数据,这样有可能会造成内存空间被占满。
二级缓存有可能存在脏读的问题(可避免)
由于二级缓存的作用域为namespace,那么就可以假设这么一个场景,有两个namespace操作一张表,第一个namespace查询该表并回写到内存中,第二个namespace往表中插一条数据,那么第一个namespace的二级缓存是不会清空这个缓存的内容的,在下一次查询中,还会通过缓存去查询,这样会造成数据的不一致。所以当项目里有多个命名空间操作同一张表的时候,最好不要用二级缓存,或者使用二级缓存时避免用两个namespace操作一张表。
五、Mybatis插件
1、核心原理
插件机制是为了对MyBatis现有体系进行扩展而提供的入口。底层通过动责任链模式+ JDK动态代理实现。插件的核心是拦截四个接口的子对象,拦截以后会进入到intercept 方法中进行业务的处理,而Invocation对象可以获取到四个接口的具体
- Executor:执行器
- StatementHandler:JDBC处理器
- ParameterHandler:参数处理器
- ResultSetHandler:结果集处理器
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
Interceptor类核心方法代码
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
2、源码分析
2.1 插件类创建
首先创建自定义插件
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private int number;
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
Method method = invocation.getMethod();
Object[] args = invocation.getArgs();
Object result = invocation.proceed();
return result;
}
public Object plugin(Object target) {
return Plugin.wrap(target,this);
}
public void setProperties(Properties properties) {
this.number = Integer.parseInt(properties.getProperty("number", String.valueOf(100)));
}
}
在config.xml 中添加插件配置,注意配置顺序
<plugins>
<plugin interceptor="org.demo.plugin.ExamplePlugin">
<property name="number" value="1"/>
</plugin>
</plugins>
2.2 源码分析
构建图
核心代码
插件对象的创建InterceptorChain
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
XMLConfigBuilder 会实例化一个Configuration对象,在创建Configuration对象,会调用构造函数,InterceptorChain对象的创建,就是在Configuration的构造函数中进行了初始化,如InterceptorChain interceptorChain = new InterceptorChain();
进入InterceptorChain 代码
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);
}
}
在parse()方法解析的时候,parseConfiguration()中解析插件pluginElement(root.evalNode("plugins"))
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
插件的执行流程
2.3 源码总结
Mybatis插件使用机制就是:** jdk动态代理+责任链的设计模式**。插件的运行和注册会分为几个阶段:
1、定义阶段,定义插件类然后实现Interceptor 接口,覆盖这个接口三个方法,分别是:plugin 方法,interceptor 方法,setProperties 方法
- intercept方法:如果自定插件实现Interceptor覆盖intercept方法,这个方法是一个核心方法,里面参数Invocation对象,这个对象可以通过反射调度原来的对象的方法。
- plugin方法:target被拦截的对象。它的作用:把拦截的target对象变成一个代理对象
- setProperties方法:允许plugin在注册的时候,配置插件需要的参数,这个参数可以在mybats的核心配置文件中注册插件的时候,一起配置到文件中
2、注册阶段,写入到mybatis配置文件中,如果是spring整合myabtis化,就使用配置类来进行插件的注册
3、同时在定义的时候,会通过@Intercepts 注解和签名,来告诉插件具体要拦截那些类执行的方法,mybatis对四个接口实现类都会进行拦截
4、运行和执行阶段,定义了执行器的插件后,在初始化sqlsession 的时候会确定一个执行器,而执行器在创建的时候,会调用executor = (Executor)interceptorChain.pluginAll(executor) 。这个方法的作用就是把执行器对象变成一个代理对象,而代理对象的生成,是通过插件的的plugin方法进行生成和创建,具体的话是通过代理类Plugin中的wrap方法创建而生成,生成executor代理对象之后,当代理执行器执行方法的时候,就进入Plugin代理类中invoke方法中进行业务处理
3、分页插件举例
3.1 分页插件原理
首先设定一个Page类,其包含total、size、index 3个属性,在Mapper接口中声明该参数即表示需要执行自动分页逻辑。总体实现步骤包含3个:
- 检测是否满足分页条件
- 自动求出当前查询的总行数
- 修改原有的SQL语句 ,添加 limit offset 关键字。
3.2 检测是否满足分页条件
分页条件是 1.是否为查询方法,2.查询参数中是否带上Page参数。在intercept 方法中可直接获得拦截目标StatementHandler ,通过它又可以获得BoundSql 里面就包含了SQL 和参数。遍历参数即可获得Page。
StatementHandler target = (StatementHandler) invocation.getTarget();
BoundSql boundSql = target.getBoundSql();
Object parameterObject = boundSql.getParameterObject();
Page page = null;
if (parameterObject instanceof Page) {
page = (Page) parameterObject;
} else if (parameterObject instanceof Map) {
page = (Page) ((Map) parameterObject).values().stream().filter(v -> v instanceof Page).findFirst().orElse(null);
}
3.3 查询总行数
实现逻辑是 将原查询SQL作为子查询进行包装成子查询,然后用原有参数,还是能过原来的参数处理器进行赋值。关于执行是采用JDBC 原生API实现。MyBatis执行器,从而绕开了一二级缓存。
private int selectCount(Invocation invocation) throws SQLException {
int count = 0;
StatementHandler target = (StatementHandler) invocation.getTarget();
String countSql = String.format("select count(*) from (%s) as _page", target.getBoundSql().getSql());
Connection connection = (Connection) invocation.getArgs()[0];
PreparedStatement preparedStatement = connection.prepareStatement(countSql);
target.getParameterHandler().setParameters(preparedStatement);
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
count = resultSet.getInt(1);
}
resultSet.close();
preparedStatement.close();
return count;
}
3.4 修改原SQL
最后一项就是修改原来的SQL,前面我是可以拿到BoundSql 的,但它没有提供修改SQL的方法,这里可以采用反射强行为SQL属性赋值。也可以采用MyBatis提供的工具类SystemMetaObject来赋值
String newSql= String.format("%s limit %s offset %s", boundSql.getSql(),page.getSize(),page.getOffset());
SystemMetaObject.forObject(boundSql).setValue("sql",newSql);
参考文章:
手把手带你阅读Mybatis源码(一)构造篇
手把手带你阅读Mybatis源码(二)执行篇
手把手带你阅读Mybatis源码(三)缓存篇
源码阅读网
MyBatis源码解析大合集
[学相伴飞哥]Mybatis的源码分析-执行过程(一)
|