MyBatis官方文档:https://mybatis.org/mybatis-3/zh/
对于MyBatis,其工作流程实际上分为两部分:第一,构建,也就是解析我们写的xml配置,将其变成它所需要的对象。第二,执行,在构建完成的基础上,去执行我们的SQL,完成与Jdbc的交互
数据库配置如Mybatis学习笔记一样,我的项目结构如下图所示
创建mybatis-config.xml
文件
<configuration>
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
文件
<mapper namespace="test">
<select id="getEmp" resultType="Employee">
select * from employee
select>
mapper>
创建测试文件
public static void main(String[] args) throws IOException {
// 加载mybatis框架主配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 读取解析配置文件内容,创建SqlSessionFacory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 这种操作不推荐,推荐使用mapper代理方式,后面有提到
// 执行数据库操作
List<Employee> list = sqlSession.selectList("test.getEmp");
System.out.println(list);
// 释放资源
sqlSession.close();
// sqlSession.commit();
}
Configuration 是整个MyBatis的配置体系集中管理中心,前面所学Executor、StatementHandler、Cache、MappedStatement…等绝大部分组件都是由它直接或间接的创建和管理。其主要作用如下
Configuration 配置信息来源于xml和注解,每个文件和注解都是由若干个配置元素组成,并呈现嵌套关系,总体关系如下图所示,关于各配置的使用请参见官网给出文档:https://mybatis.org/mybatis-3/zh/configuration.html#properties
无论是xml 注解这些配置元素最弱都要被转换成JAVA配置属性或对象组件来承载。其对应关系如下:
XML文件解析流程
整体解析流程是从XmlConfigBuilder 开始,然后逐步向内解析,直到解析完所有节点。我们通过一个MappedStatement 解析过程即可了解到期整体解析流程
注解配置解析
注解解析底层实现是通过反射获取Mapper接口当中注解元素实现。有两种方式一种是直接指定接口名,一种是指定包名然后自动扫描包下所有的接口类。这些逻辑均由Mapper注册器(MapperRegistry)实现。其接收一个接口类参数,并基于该参数创建针对该接口的动态代理工厂,然后解析内部方法注解生成每个MapperStatement 最后添加至Configuration 完成解析。
进入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) {
// Intentionally ignore. Prefer previous error.
}
}
}
在进入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.");
}
//如果没有解析过,则继续往下解析,并且将标识符置为true
parsed = true;
//解析节点,即从根节点开始解析,名字必须是configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
在进入parseConfiguration()
方法,可以看到这个方法已经在解析
下的节点了,例如
,
,
和
等,同时返回了Configuration
对象
// root即是完整的xml内容
private void parseConfiguration(XNode root) {
try {
//解析下的节点
propertiesElement(root.evalNode("properties"));
//
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 别名解析
// 所谓别名其实就是把你指定的别名对应的class存储在一个Map当中
typeAliasesElement(root.evalNode("typeAliases"));
//插件
pluginElement(root.evalNode("plugins"));
//自定义实例化对象的行为
objectFactoryElement(root.evalNode("objectFactory"));
//MateObject 方便反射操作实体类的对象
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
//
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// typeHandlers
typeHandlerElement(root.evalNode("typeHandlers"));
//主要 指向我们存放SQL的xxxxMapper.xml文件
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
对于db.properties
替代,在parseConfiguration()
方法中的propertiesElement(root.evalNode("properties"));
就是对外部配置文件的替换修改,它首先形成Properties对象对其替换
Mybatis别名设置若存在标签,则mapper.xml别名默认是类名(忽略大小写),同时将其存放于TYPE_ALIASES
这个HashMap中,同时里面已经存在很多内置别名,可以直接使用
environmentsElement(root.evalNode("environments"));
方法将数据库相关信息配置(例如事务,数据库账号密码等)存入enviroment对象,最终和configuration相关联存入其对象中
mybatis-config.xml
文件中我们一定会写一个叫做的标签,这个标签中的
节点存放了我们对数据库进行操作的SQL语句,这里就详细详解一下mapper的执行过程
<mappers>
<package name="org.demo.po"/>
<mapper resource="mappers/EmployeeMapper.xml" />
<mapper class="org.demo.mapper.EmployeeMapper.java"/>
<mapper url=""/>
mappers>
这是
标签的几种配置方式,通过这几种配置方式,可以帮助我们更容易理解mappers的解析
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
//遍历解析mappers下的节点
for (XNode child : parent.getChildren()) {
//首先解析package节点
if ("package".equals(child.getName())) {
//获取包名
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//如果不存在package节点,那么扫描mapper节点
//resource/url/mapperClass三个值只能有一个值是有值的
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//优先级 resource>url>mapperClass
if (resource != null && url == null && mapperClass == null) {
//如果mapper节点中的resource不为空
ErrorContext.instance().resource(resource);
//那么直接加载resource指向的XXXMapper.xml文件为字节流
InputStream inputStream = Resources.getResourceAsStream(resource);
//通过XMLMapperBuilder解析XXXMapper.xml,可以看到这里构建的XMLMapperBuilde还传入了configuration,所以之后肯定是会将mapper封装到configuration对象中去的。
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//解析
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//如果url!=null,那么通过url解析
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) {
//如果mapperClass!=null,那么通过加载类构造Configuration
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
文件中的各种节点,包括
、
、
(已过时)、
、
、还有增删改查节点,和上面相同的是,我们也挑一个主要的来说,因为解析过程都大同小异。
public void parse() {
//判断文件是否之前解析过
if (!configuration.isResourceLoaded(resource)) {
//解析mapper文件节点(主要)(下面贴了代码)
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//绑定Namespace里面的Class对象
bindMapperForNamespace();
}
//重新解析之前解析不了的节点,先不看,最后填坑。
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
//解析mapper文件里面的节点
// 拿到里面配置的配置项 最终封装成一个MapperedStatemanet
private void configurationElement(XNode context) {
try {
//获取命名空间 namespace,这个很重要,后期mybatis会通过这个动态代理我们的Mapper接口
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
//如果namespace为空则抛一个异常
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//解析缓存节点
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
//解析parameterMap(过时)和resultMap
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析节点
//select * from test (可重用的代码段)
//
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);
}
}
这里解析其中一项举例,解析增删改查节点,进入
buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
方法
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
//解析xml
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 {
//解析xml节点
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
//xml语句有问题时 存储到集合中 等解析完能解析的再重新解析
configuration.addIncompleteStatement(statementParser);
}
}
}
进入statementParser.parseStatementNode();
方法,解析里面的xml节点
public void parseStatementNode() {
//获取
String id = context.getStringAttribute("id");
//获取databaseId 用于多数据库,这里为null
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//获取节点名 select update delete insert
String nodeName = context.getNode().getNodeName();
//根据节点名,得到SQL操作的类型
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);
//是否需要处理嵌套查询结果 group by
// 三组数据 分成一个嵌套的查询结果
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
//替换Includes标签为对应的sql标签里面的值
includeParser.applyIncludes(context.getNode());
//获取parameterType名
String parameterType = context.getStringAttribute("parameterType");
//获取parameterType的Class
Class<?> parameterTypeClass = resolveClass(parameterType);
//解析配置的自定义脚本语言驱动 这里为null
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
//解析selectKey
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: and were parsed and removed)
//设置主键自增规则
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;
}
/************************************************************************************/
//解析Sql(重要) 根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//获取StatementType,可以理解为Statement和PreparedStatement
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
Class<?> resultTypeClass = resolveClass(resultType);
//获取resultMap的id
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");
//将刚才获取到的属性,封装成MappedStatement对象(代码贴在下面)
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结果了
//将刚才获取到的属性,封装成MappedStatement对象
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 = namespace
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//通过构造者模式+链式变成,构造一个MappedStatement的构造者
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
MappedStatement statement = statementBuilder.build();
//将MappedStatement对象封装到Configuration对象中
configuration.addMappedStatement(statement);
return statement;
}
OGNL表达示
OGNL全称是对象导航图语言(Object Graph Navigation Language)是一种JAVA表达示语言,可以方便的存取对象属和方法,已用于逻辑判断。其支持以下特性:获取属性属性值,以及子属性值进行逻辑计;表达示中可直接调用方法(如果是无参方法,可以省略括号);通过下标访问数组或集合;遍历集合
每个动态元素都会有一个与之对应的脚本类,即会产生许多SqlNode脚本如if
对应ifSqlNode
、forEarch
对应ForEachSqlNode
以此类推下去。同时脚本之间是呈现嵌套关系的,比如if
元素中会包含一个MixedSqlNode
,而MixedSqlNode
下又会包含1至1至多个其它节点,最后组成一课脚本语法树。最后SqlNode的接口非常简单,就只有一个apply
方法,方法的作用就是执行当前脚本节点逻辑,并把结果应用到DynamicContext
当中去。
这里要注意下面三个脚本
StaticTextSqlNode
表示一段纯静态文本如: select * from user
TextSqlNode
表示一个通过参数拼装的文本如:select * from ${user}
MixedSqlNode
表示多个节点的集合SqlSource 是基于XML解析而来,解析的底层是使用Dom4j 把XML解析成一个个子节点,在通过 XMLScriptBuilder 遍历这些子节点最后生成对应的Sql源。在上层定义上每个Sql映射(MappedStatement)中都会包含一个SqlSource 用来获取可执行Sql(BoundSql
)。SqlSource又分为原生SQL源与动态SQL源,以及第三方源
生成SQL语句代码,首先这里会通过节点获取到我们的SQL语句,假设SQL语句中只有
${}
,那么直接就什么都不做,在运行的时候直接进行赋值。而如果扫描到了#{}
字符串之后,会进行替换,将#{}
替换为 ?
。
这里会生成一个GenericTokenParser,这个对象可以传入一个openToken和closeToken,如果是#{}
,那么openToken就是#{
,closeToken就是 }
,然后通过parse方法中的handler.handleToken()
方法进行替换。在这之前由于已经进行过SQL是否含有#{}
的判断了,所以在这里如果是只有${}
,那么handler就是BindingTokenParser的实例化对象,如果存在#{}
,那么handler就是ParameterMappingTokenHandler
的实例化对象。
mapperElement()
> mapperParser.parse()
> 进入XMLMapperBuilder类 configurationElement()
> buildStatementFromContext()
> buildStatementFromContext()
> statementParser.parseStatementNode();
//XMLStatementBuilder类parseStatementNode方法
//解析Sql(重要)根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
/*进入createSqlSource方法*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
//进入这个构造
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
//进入parseScriptNode
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<>();
//获取select标签下的子标签
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) {
//如果是查询
//获取原生SQL语句 这里是 select * from test where id = #{id}
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//检查sql是否是${}
if (textSqlNode.isDynamic()) {
//如果是${}那么直接不解析
contents.add(textSqlNode);
isDynamic = true;
} else {
//如果不是,则直接生成静态SQL
//#{} -> ?
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
//如果是增删改
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,传入#{}作为开始和结束,然后调用其parse方法,即可将#{}换为 ?
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 "";
}
// search open token
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;
//遍历里面所有的#{} select ? ,#{id1} ${}
while (start > -1) {
if (start > 0 && src[start - 1] == '\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
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] == '\') {
// this close token is escaped. remove the backslash and continue.
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) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//使用占位符 ?
//注意handler.handleToken()方法,这个方法是核心
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();
}
//BindingTokenParser 的handleToken
//当扫描到${}的时候调用此方法 其实就是不解析 在运行时候在替换成具体的值
@Override
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
//ParameterMappingTokenHandler的handleToken
//全局扫描#{id} 字符串之后 会把里面所有 #{} 调用handleToken 替换为?
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
MyBatis需要做的就是,先判断这个节点是用来干什么的,然后再获取这个节点的id、parameterType、resultType等属性,封装成一个MappedStatement对象,由于这个对象很复杂,所以MyBatis使用了构造者模式来构造这个对象,最后当MappedStatement对象构造完成后,将其封装到Configuration对象中。
MyBatis需要对配置文件进行解析,最终会解析成一个Configuration对象
但是最终MappedStatement对象会封装到Configuration对象中,合二为一,成为一个单独的对象,也就是Configuration
我在开发xxxx项目的时候、使用Mybatis开发项目,我对Mybatis的认识是:它其实是一个orm持久层框架,其实就对jdbc一个封装而得的框架,使用好处其实就可以把jdbc从连接开辟事务管理以及连接关闭和sql执行,对数据的映射pojo整个过程进行一个封装而已。它的整个执行的过程:
核心
将SqlSessionFactoryBuilder中通过build方法创建和装配好Configuration对象通过构造函数进行下传,传递到SqlSession中,最后开辟SqlSession会话对象
源码分析
public static void main(String[] args) throws IOException {
// 加载mybatis框架主配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 读取解析配置文件内容,创建SqlSessionFacory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
/*---------开始注入执行------------*/
// 获取sqlSession对象
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 {
//解析config.xml(mybatis解析xml是用的 java dom) dom4j sax...
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//parse(): 解析config.xml里面的节点
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
//注入到SqlSessionFactory
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 {
//对SqlSession对象进行进一步加工封装
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
//构建SqlSession对象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
首先需要修改部分文件内容
// 创建EmployeeMapper、代理mapper
package org.demo.mapper;
import org.demo.po.Employee;
import java.util.List;
public interface EmployeeMapper {
List<Employee> getEmp();
}
//同时修改EmployeeMapper.xml的命名空间namespace为
//
//最后修改测试类
public static void main(String[] args) throws IOException {
// 加载mybatis框架主配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 读取解析配置文件内容,创建SqlSessionFacory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取Mapper
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接口
//getMapper方法最终会调用到这里,这个是MapperRegistry的getMapper方法
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//MapperProxyFactory 在解析的时候会生成一个map map中会有我们的DemoMapper的Class
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)) {
//解析mapper文件
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//这里:绑定Namespace里面的Class对象*
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);
//这里将接口class传入
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 {
//这里将接口信息put进konwMappers。
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) {
//首先会调用这个newInstance方法
//动态代理逻辑在MapperProxy里面
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
//通过这里调用下面的newInstance方法
return newInstance(mapperProxy);
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//jdk自带的动态代理
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 {
//这就是一个很标准的JDK动态代理了
//执行的时候会调用invoke方法
try {
if (Object.class.equals(method.getDeclaringClass())) {
//判断方法所属的类
//是不是调用的Object默认的方法
//如果是 则不代理,不改变原先方法的行为
return method.invoke(this, args);
} else if (method.isDefault()) {
//对于默认方法的处理
//判断是否为default方法,即接口中定义的默认方法。
//如果是接口中的默认方法则把方法绑定到代理对象中然后调用。
//这里不详细说
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//如果不是默认方法,则真正开始执行MyBatis代理逻辑。
//获取MapperMethod代理对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
//执行
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
//动态代理会有缓存,computeIfAbsent 如果缓存中有则直接从缓存中拿
//如果缓存中没有,则new一个然后放入缓存中
//因为动态代理是很耗资源的
return methodCache.computeIfAbsent(method,
k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
}
最后执行逻辑,这里首先会判断SQL的类型:SELECT|DELETE|UPDATE|INSERT,判断SQL类型为SELECT之后,就开始判断返回值类型,根据不同的情况做不同的操作。然后开始获取参数>执行SQL
//execute() 这里是真正执行SQL的地方
public Object execute(SqlSession sqlSession, Object[] args) {
//判断是哪一种SQL语句
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()) {
//返回Map
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
//返回Cursor
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;
//param是我们传入的参数,如果传入的是Map,那么这个实际上就是Map对象
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
//如果有分页
RowBounds rowBounds = method.extractRowBounds(args);
//执行SQL的位置
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
//如果没有
//执行SQL的位置
result = sqlSession.selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
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) {
//如果参数上没有加注解例如@Param,且参数只有一个,则直接返回参数
return args[names.firstKey()];
} else {
//如果参数上加了注解,或者参数有多个。
//那么MyBatis会封装参数为一个Map,但是要注意,由于jdk的原因,我们只能获取到参数下标和参数名,但是参数名会变成arg0,arg1.
//所以传入多个参数的时候,最好加@Param,否则假设传入多个String,会造成#{}获取不到值的情况
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
//entry.getValue 就是参数名称
param.put(entry.getValue(), args[entry.getKey()]);
//如果传很多个String,也可以使用param1,param2.。。
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
进入sqlSession.selectList("test.getEmp");
方法,可以发现在调用sqlsession执行的selectList、insert、update、delete的时候,其实就是根据执行的statement名字,到Configuration的mapperStatements对应的map中去找到有没有一个对应的 MapperStatement对象,如果找到就返回这个对象,然后给后续执行一个依据和参考
执行器的实现有三种:SimpleExecute、ReuseExecute和BatchExecute,这三种执行器有个抽象的基础执行器BaseExecutor,用于维护缓存和事务;此外通过装饰器形式添加了一个缓存执行器CachingExecutor,用于处理二级缓存
SimpleExecute 简单执行器(默认)
SimpleExecutor是执行器的默认实现,主要完成了“执行”功能,在利用StatementHandler 完成。每次调用执行方法 都会构建一个StatementHandler,并预行参数,然后执行
默认情况是executor是CachingExecutor。这个执行器是二级缓存的执行器,如果在配置文件xxxxMapper.xml文件中申明了
节点的话,就是使用CachingExecutor;如果没有,就会委托SimpleExecutor(默认类型是simple,在configuration创建的时候指定)执行器去执行你的SQL语句,然后这里会执行的结果放入loaclCache一级缓存中。
ReuseExecute 可重用执行器
ReuseExecutor 区别在于他会将在会话期间内的Statement进行缓存,并使用SQL语句作为Key。所以当执行下一请求的时候,不在重复构建Statement,而是从缓存中取出并设置参数,然后执行(参数不同也可以重用)
BatchExecute 批处理执行器
BatchExecutor 顾名思议,它就是用来作批处理的。但会将所 有SQL请求集中起来,最后调用Executor.flushStatements() 方法时一次性将所有请求发送至数据库
执行SQL的核心方法就是selectList,即使是selectOne,底层实际上也是调用了selectList方法,然后取第一个而已
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//MappedStatement:解析XML时生成的对象, 解析某一个SQL 会封装成MappedStatement,里面存放了我们所有执行SQL所需要的信息
MappedStatement ms = configuration.getMappedStatement(statement);
//查询,通过executor
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 {
//获取sql语句
BoundSql boundSql = ms.getBoundSql(parameterObject);
//生成一个缓存的key
//key = ms.id + rowBounds.getOffset()+rowBounds.getOffset()+sql+参数+上下文坏境的id
//这里是-1954235241:110303602:test.getEmp:0:2147483647:select * from employee:development
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 cache = ms.getCache();
if (cache != null) {
//如果Cache不为空则进入
//如果有需要的话,就刷新缓存(有些缓存是定时刷新的,需要用到这个)
flushCacheIfRequired(ms);
//如果这个statement用到了缓存(二级缓存的作用域是namespace,也可以理解为这里的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); // issue #578 and #116
}
//返回
return list;
}
}
//如果cache根本就不存在,那么直接查询一级缓存。并委托delegate(默认简单执行器)查询
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 {
//查询栈+1
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 {
//查询栈-1
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
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;
//先往一级缓存中put一个占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//调用doQuery方法查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//往缓存中put真实数据
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
//SimpleExecutor类
//真实数据库查询
@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也是MyBatis四大对象之一
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//#{} -> ? 的SQL在这里初始化
stmt = prepareStatement(handler, ms.getStatementLog());
//参数赋值完毕之后,才会真正地查询。
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
总结,一级缓存和二级缓存的key是一样的,一级缓存默认开启,二级缓存需要设置开启。这里CacheExecutor使用的是装饰者模式,即在不改变原有类结构和继承的情况下,通过包装原对象去扩展一个新功能。
StatementHandler即为JDBC处理器,基于JDBC构建JDBC Statement并设置参数,然后执行Sql。每调用会话当中一次SQl,都会有与之相对应的且唯一的Statement实例,一个SQL请求会经过会话,然后是执行器,最由StatementHandler执行jdbc最终到达数据库,这三者之间比例是1:1:n。
StatementHandler接口定义了JDBC操作的相关方法如下,
// 基于JDBC 声明Statement
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
// 为Statement 设置方法
void parameterize(Statement statement)
throws SQLException;
// 添加批处理(并非执行)
void batch(Statement statement)
throws SQLException;
// 执行update操作
int update(Statement statement)
throws SQLException;
// 执行query操作
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
StatementHandler
有三个子类SimpleStatementHandler
、PreparedStatementHandler
、CallableStatementHandler
,分别对应JDBC中的Statement、PreparedStatement、CallableStatement。
参数处理即将Java Bean转换成数据类型。总共要经历过三个步骤,ParamNameResolver
(参数转换)、ParameterHandler
(参数映射)、TypeHandler
(参数赋值)
参数转换
所有转换逻辑均在ParamNameResolver中实现
@Select("select * from employee where id = #{id}")
@Options
Employee getEmpById(@Param("id") Integer id);
参数映射
映射是指Map中的key如何与SQL中绑定的参数相对应。以下这几种情况
参数赋值
通过TypeHandler
为PrepareStatement
设置值,通常情况下一般的数据类型MyBatis都有与之相对应的TypeHandler
MetaObject相当于一个工具类,里面还包括分词器等,可以参考MetaObject详解
读取ResultSet数据,并将每一行转换成相对应的对象。用户可在转换的过程当中可以通过ResultContext
来控制是否要继续转换,转换后的对象都会暂存在ResultHandler
中最后统一封装成list返回给调用方,结果集转换中99%的逻辑DefaultResultSetHandler
中实现。整个流程可大致分为以下阶段:
//PreparedStatementHandler,这里是真正查询
@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已经完成了对数据的查询,那么现在还存在最后一个问题,那就是结果集处理,换句话来说,就是将结果集封装成对象,这里会创建一个处理结果集的对象
//DefaultResultSetHandler
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
//resultMap可以通过多个标签指定多个值,所以存在多个结果集
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
//拿到当前第一个结果集
ResultSetWrapper rsw = getFirstResultSet(stmt);
//拿到所有的resultMap
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
//resultMap的数量
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
//循环处理每一个结果集
while (rsw != null && resultMapCount > resultSetCount) {
//开始封装结果集 list.get(index) 获取结果集
ResultMap resultMap = resultMaps.get(resultSetCount);
//传入resultMap处理结果集 rsw 当前结果集(主线)
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) {
//判断resultHandler是否为空,如果为空建立一个默认的。
//结果集处理器
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
//处理行数据
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
//关闭结果集
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
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
//通过每行的结果集,然后将其直接封装成一个Object对象
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
//创建一个空的Map存值
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
//创建一个空对象装行数据
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())){
//通过反射操作返回值
//此时metaObject.originalObject = rowValue
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
//判断是否需要自动映射,默认自动映射,也可以通过resultMap节点上的autoMapping配置是否自动映射
//这里是自动映射的操作。
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;
}
//在getRowValue中会判断是否是自动映射的,我们这里没有使用ResultMap,所以是自动映射(默认),那么就进入applyAutomaticMappings()方法,而这个方法就会完成对象的封装。
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)) {
// gcode issue #377, call setter on nulls (value is not 'found')
//在这里赋值
metaObject.setValue(mapping.property, value);
}
}
}
return foundValues;
}
我们可以看到这个方法会通过遍历参数列表从而通过metaObject.setValue(mapping.property, value);
对返回对象进行赋值,所有的赋值操作在内部都是通过一个叫ObjectWrapper
的对象完成的,先看看中代码的metaObject.setValue()
方法
//MetaObject类,工具类
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) {
// don't instantiate child path if value is null
return;
} else {
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
metaValue.setValue(prop.getChildren(), value);
} else {
//这个方法最终会调用objectWrapper.set()对结果进行赋值
objectWrapper.set(prop, value);
}
}
objectWrapper有两个实现:BeanWrapper和MapWrapper,如果是自定义类型,那么就会调用BeanWrapper的set方法。MapWrapper的set方法实际上就是将属性名和属性值放到map的key和value中,而BeanWrapper则是使用了反射,调用了Bean的set方法,将值注入。
//MapWrapper的set方法
public void set(PropertyTokenizer prop, Object value) {
if (prop.getIndex() != null) {
Object collection = resolveCollection(prop, map);
setCollectionValue(prop, collection, value);
} else {
//实际上就是调用了Map的put方法将属性名和属性值放入map中
map.put(prop.getName(), value);
}
}
//BeanWrapper的set方法
public void set(PropertyTokenizer prop, Object value) {
if (prop.getIndex() != null) {
Object collection = resolveCollection(prop, object);
setCollectionValue(prop, collection, value);
} else {
//在这里赋值,通过反射赋值,调用setXX()方法赋值
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);
}
}
映射是指返回的ResultSet列与Java Bean 属性之间的对应关系。通过ResultMapping进行映射描述,在用ResultMap封装成一个整体,包括手动映射和自动映射
property | 属性名(必填) |
---|---|
column | 列名(必填) |
jdbcType | jdbc类型(可自动推导) |
javaType | java类型(可自动推导) |
typeHandler | 类型处理器(可自动推导) |
懒加载的参考文章之一
懒加载是为了改善在映射结果集解析对象属性时,大量的嵌套子查询的并发效率问题,当设置懒加载后,只有在使用指定属性时才会触发子查询,从而实现分散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用于执行加载操作,执行前如果原执行器关闭会创建一个新的。
映射是指返回的ResultSet列与Java Bean 属性之间的对应关系。通过ResultMapping进行映射描述,在用ResultMap封装成一个整体。映射分为简单映射与复合嵌套映射,联合查询分为一对一查询和一对多查询
流程说明
所有映射流程的解析都是在DefaultResultSetHandler当中完成。主要方法如下:
handleRowValuesForNestedResultMap()
嵌套结果集解析入口,在这里会遍历结果集中所有行。并为每一行创建一个RowKey对象。然后调用getRowValue()获取解析结果对象。最后保存至ResultHandler中(注:调用getRowValue前会基于RowKey获取已解析的对象,然后作为partialObject参数发给getRowValue)
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中存在两个缓存,一级缓存和二级缓存
flushCache
和localCacheScope
对其做相应控制。缓存命中参数
触发清空缓存
因为Spring 对SqlSession进行了封装,通过SqlSessionTemplae ,使得每次调用Sql,都会重新构建一个SqlSession,解决方法是
二级缓存也称作是应用级缓存,与一级缓存不同的,是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据,在流程上是先访问二级缓存,在访问一级缓存。二级缓存的更新,必须是在会话提交之后,同时要提交之后才能命中缓存
缓存空间声明
二级默认缓存默认是不开启的,需要为其声明缓存空间才可以使用,通过@CacheNamespace
或 在xml配置
。声明之后该缓存为该Mapper所独有,其它Mapper不能访问。如需要多个Mapper共享一个缓存空间可通过@CacheNamespaceRef
或
进行引用同一个缓存空间。@CacheNamespace 详细配置见下表:
配置 | 说明 |
---|---|
implementation | 指定缓存的存储实现类,默认是用HashMap存储在内存当中 |
eviction | 指定缓存溢出淘汰实现类,默认LRU ,清除最少使用 |
flushInterval | 设置缓存定时全部清空时间,默认不清空。 |
size | 指定缓存容量,超出后就会按eviction指定算法进行淘汰 |
readWrite | true即通过序列化复制,来保证缓存对象是可读写的,默认true |
blocking | 为每个Key的访问添加阻塞锁,防止缓存击穿 |
properties | 为上述组件,配置额外参数,key对应组件中的字段名。 |
缓存其它配置
除@CacheNamespace 还可以通过其它参数来控制二级缓存
字段 | 配置域 | 说明 |
---|---|---|
cacheEnabled | 在mybatis设置 | 二级缓存全局开关,默认开启 |
useCache | ||
flushCache |
注意:若*Mapper.xml
和mapper
接口同时设置SQL查询,并同时配置了缓存,那么两个缓存空间是不一致,需要用缓存引用ref使用同一个缓存空间
这里MyBatis抽像出Cache接口,其只定义了缓存中最基本的功能方法:
然后上述中每一个功能都会对应一个组件类,并基于装饰者加责任链的模式,将各个组件进行串联。在执行缓存的基本功能时,其它的缓存逻辑会沿着这个责任链依次往下传递。
原本会话是通过Executor实现SQL调用,这里基于装饰器模式使用CachingExecutor对SQL调用逻辑进行拦截,以嵌入二级缓存相关逻辑。这里SqlSession会话可以对应多个暂存区,而多个暂存区对应一个缓存空间
查询操作query
当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。
请注意,这里的查询是实时从缓存空间读取的,而变更,只会记录在暂存区
更新操作update
当执行update操作时,同样会基于查询的语句和参数组成缓存KEY,然后在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。
如果在查询操作中配置了flushCache=true ,也会执行相同的操作。
提交操作commit
当会话执行commit操作后,会将该会话下所有暂存区的变更,更新到对应二级缓存空间去。
这个类是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。
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;
//未命中的缓存集合,防止击穿缓存,并且如果查询到的数据为null,说明要通过数据库查询,有可能存在数据不一致,都记录到这个地方
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) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
//如果取出的是空,那么放到未命中缓存,并且在查询数据库之后putObject中将本应该放到真实缓存中的键值对放到待提交事务缓存
entriesMissedInCache.add(key);
}
//如果不为空
// issue #146
//查看缓存清空标识是否为false,如果事务提交了就为true,事务提交了会更新缓存,所以返回null。
if (clearOnCommit) {
return null;
} else {
//如果事务没有提交,那么返回原先缓存中的数据,
return object;
}
}
@Override
public void putObject(Object key, Object object) {
//如果返回的数据为null,那么有可能到数据库查询,查询到的数据先放置到待提交事务的缓存中
//本来应该put到缓存中,现在put到待提交事务的缓存中去。
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
//如果事务提交了,那么将清空缓存提交标识设置为true
clearOnCommit = true;
//清空entriesToAddOnCommit
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
//如果为true,那么就清空缓存。
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) {
//把未命中的一起put
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);
}
}
}
}
二级缓存不能存在一直增多的数据
由于二级缓存的影响范围不是SqlSession而是namespace,所以二级缓存会在你的应用启动时一直存在直到应用关闭,所以二级缓存中不能存在随着时间数据量越来越大的数据,这样有可能会造成内存空间被占满。
二级缓存有可能存在脏读的问题(可避免)
由于二级缓存的作用域为namespace,那么就可以假设这么一个场景,有两个namespace操作一张表,第一个namespace查询该表并回写到内存中,第二个namespace往表中插一条数据,那么第一个namespace的二级缓存是不会清空这个缓存的内容的,在下一次查询中,还会通过缓存去查询,这样会造成数据的不一致。所以当项目里有多个命名空间操作同一张表的时候,最好不要用二级缓存,或者使用二级缓存时避免用两个namespace操作一张表。
插件机制是为了对MyBatis现有体系进行扩展而提供的入口。底层通过动责任链模式+ JDK动态代理实现。插件的核心是拦截四个接口的子对象,拦截以后会进入到intercept
方法中进行业务的处理,而Invocation对象可以获取到四个接口的具体
//注意interceptorChain.pluginAll()方法
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 {
//intercept方法:如果自定插件实现Interceptor覆盖intercept方法,
//这个方法是一个核心方法,里面参数Invocation对象,这个对象可以通过反射调度原来的对象的方法。
Object intercept(Invocation invocation) throws Throwable;
//target被拦截的对象,它的作用把拦截的target对象变成一个代理对象
Object plugin(Object target);
//允许plugin在注册的时候,配置插件需要的参数,这个参数可以在mybatsi的核心配置文件中注册插件的时候,一起配置到文件中
void setProperties(Properties properties);
}
首先创建自定义插件
@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(); //方法参数
// do something ...... 方法拦截前执行代码块
Object result = invocation.proceed();
// do something .......方法拦截后执行代码块
return result;
}
// 生成代理对象,可自定义生成代理对象,这样就无需配置@Intercepts注解。另外需要自行判断是否为拦截目标接口。
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>
核心代码
插件对象的创建InterceptorChain
//SqlSessionFactoryBuilder类的build方法
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) {
//pluginAll方法是把具体的四大接口的具体实现类,生成动态代理的方法。
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"))
//XMLConfigBuilder类
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
//这里循环解析配置文件中的所有定义的插件
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
//如果插件有配置属性。获取到配置的属性,然后把属性的值,注册到Properties对象中
Properties properties = child.getChildrenAsProperties();
//同时获取到具体的注册的插件对象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//调用插件,并赋值插件中的属性
interceptorInstance.setProperties(properties);
//注册到interceptorChain中
configuration.addInterceptor(interceptorInstance);
}
}
}
// Configuration类
//这个集合就是把下面解析的插件,进行注册和收集的容器
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
Mybatis插件使用机制就是:** jdk动态代理+责任链的设计模式**。插件的运行和注册会分为几个阶段:
1、定义阶段,定义插件类然后实现Interceptor
接口,覆盖这个接口三个方法,分别是:plugin
方法,interceptor
方法,setProperties
方法
2、注册阶段,写入到mybatis配置文件中,如果是spring整合myabtis化,就使用配置类来进行插件的注册
3、同时在定义的时候,会通过@Intercepts
注解和签名,来告诉插件具体要拦截那些类执行的方法,mybatis对四个接口实现类都会进行拦截
4、运行和执行阶段,定义了执行器的插件后,在初始化sqlsession
的时候会确定一个执行器,而执行器在创建的时候,会调用executor = (Executor)interceptorChain.pluginAll(executor)
。这个方法的作用就是把执行器对象变成一个代理对象,而代理对象的生成,是通过插件的的plugin方法进行生成和创建,具体的话是通过代理类Plugin中的wrap方法创建而生成,生成executor代理对象之后,当代理执行器执行方法的时候,就进入Plugin代理类中invoke方法中进行业务处理
首先设定一个Page类,其包含total、size、index 3个属性,在Mapper接口中声明该参数即表示需要执行自动分页逻辑。总体实现步骤包含3个:
分页条件是 1.是否为查询方法,2.查询参数中是否带上Page参数。在intercept 方法中可直接获得拦截目标StatementHandler ,通过它又可以获得BoundSql 里面就包含了SQL 和参数。遍历参数即可获得Page。
// 带上分页参数
StatementHandler target = (StatementHandler) invocation.getTarget();
// SQL包 sql、参数、参数映射
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);
}
实现逻辑是 将原查询SQL作为子查询进行包装成子查询,然后用原有参数,还是能过原来的参数处理器进行赋值。关于执行是采用JDBC 原生API实现。MyBatis执行器,从而绕开了一二级缓存。
private int selectCount(Invocation invocation) throws SQLException {
int count = 0;
StatementHandler target = (StatementHandler) invocation.getTarget();
// SQL包 sql、参数、参数映射
String countSql = String.format("select count(*) from (%s) as _page", target.getBoundSql().getSql());
// JDBC
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;
}
最后一项就是修改原来的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的源码分析-执行过程(一)