3.1 MyBatis初始化
MyBatis 初始化过程中,除了会读取 mybatis-config.xml 配置文件以及映射配置文件,还会加载配置文件指定的类,处理类中的注解,创建一些配置对象,最终完成框架中各个模块的初始化。
3.1.1 建造者模式
建造者模式(也被称为“生成器模式”)将一个复杂对象的构建过程与它的表示分离,从而使得同样的构建过程可以创建不同的表示。用户只需要了解复杂对象的类型和内容,而无须关注复杂对象的具体构造过程。
主要角色
- 建造者(Builder)接口,定义建造者构建产品对象的各部分的行为
- 具体建造者(ConcreteBuilder),直接创建产品对象。必须实现两类方法,即建造方法和获取方法
- 导演(Director),调用具体建造者创建需要的产品对象
- 产品(Product),用户需要使用的复杂对象
优点:
- 导演角色不需要知晓产品类的内部细节
- 将复杂产品的创建过程分散到了不同的构造步骤中
- 每个具体建造者都可以创建出完整的产品对象,建造都之间相互独立,新产品出现时,只需添加新的具体建造者即可,符合开闭原则
3.1.2 BaseBuilder
MyBatis 初始化的主要工作是加载并解析 mybatis-config.xml 配置文件,映射文件以及相关的注解信息。其入口是 SqlSessionFactoryBuilder.build() 方法
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// 读取配置文件
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 解析配置文件得到 Configuration 对象,创建 DefaultSqlSessionFactory 对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
其中用来解析 XML 的 XMLConfigBuilder,就是继承自 BaseBuilder,其子类如图
public abstract class BaseBuilder {
// Configuration 是 MyBatis 初始化过程的核心对象,MyBatis 中几乎全部的配置信息都会保存到 Configuration 对象中
protected final Configuration configuration;
// mybatis-config.xml 配置文件中的别名标签
protected final TypeAliasRegistry typeAliasRegistry;
// mybatis-config.xml 配置文件中使用 <typeHandler> 标签添加的 TypeHandler
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
}
3.1.3 XMLConfigBuilder
XMLConfigBuilder 主要负责解析 mybatis-config.xml 配置文件
public class XMLConfigBuilder extends BaseBuilder {
// 标识是否已经解析过 mybatis-config.xml 配置文件
private boolean parsed;
// 用于解析 mybatis-config.xml 配置文件的 XPathParser 对象
private final XPathParser parser;
// 标识 <environment> 配置的名称,默认读取 <environment> 标签的 default 属性
private String environment;
// ReflectorFactory 负责创建和缓存 Reflector 对象
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
}
XMLConfigBuilder 通过调用 parseConfiguration 方法实现解析功能,具体实现如下
private void parseConfiguration(XNode root) {
try {
// 解析 <properties> 标签
propertiesElement(root.evalNode("properties"));
// 解析 <settings> 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 设置 vfsImpl 字段
loadCustomVfs(settings);
// 加载日志实现类
loadCustomLogImpl(settings);
// 解析 <typeAliases> 标签
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 <plugins> 标签
pluginElement(root.evalNode("plugins"));
// 解析 <objectFactory> 标签
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 <objectWrapperFactory> 标签
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 <reflectorFactory> 标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 设置 settings 字段
settingsElement(settings);
// 解析 <environments> 标签
environmentsElement(root.evalNode("environments"));
// 解析 <databaseIdProvider> 标签
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 <typeHandlers> 标签
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 <mappers> 标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
3.1.4 XMLMapperBuilder
XMLMapperBuilder 负责解析映射配置文件,parse 方法是解析入口
public class XMLMapperBuilder extends BaseBuilder {
private final XPathParser parser;
private final MapperBuilderAssistant builderAssistant;
private final Map<String, XNode> sqlFragments;
private final String resource;
}
parse
public void parse() {
// 判断是否已经加载过该映射文件
if (!configuration.isResourceLoaded(resource)) {
// 处理<mapper>节点
configurationElement(parser.evalNode("/mapper"));
// 将resource添加到Configuration.loadedResources集合中,它是一个 HashSet<String> 集合,其中存储了已经加载过的映射文件的路径
configuration.addLoadedResource(resource);
// 注册 Mapper 接口
bindMapperForNamespace();
}
// 解析 configurationElement 方法中解析失败的<resultMap>节点
parsePendingResultMaps();
// 解析 configurationElement 方法中解析失败的<cache-ref>节点
parsePendingCacheRefs();
// 解析 configurationElement 方法中解析失败的 SQL 语句节点
parsePendingStatements();
}
configurationElement
private void configurationElement(XNode context) {
try {
// 获取 namespace 属性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 解析 <cache-ref> 节点
cacheRefElement(context.evalNode("cache-ref"));
// 解析 <cache> 节点
cacheElement(context.evalNode("cache"));
// 解析 <parameterMap> 节点,已废弃,不推荐使用
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析 <resultMap> 节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 <sql> 节点
sqlElement(context.evalNodes("/mapper/sql"));
// 解析 <select>、<insert>、<update>、<delete> 节点
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);
}
}
3.1.5 XMLStatementBuilder
XMLStatementBuilder 负责解析 SQL 节点定义的 SQL 语句。也就是上述 configurationElement 方法的最后一步 buildStatementFromContext 实际上是调用 XMLStatementBuilder 实现的。
public class XMLStatementBuilder extends BaseBuilder {
private final MapperBuilderAssistant builderAssistant;
private final XNode context;
private final String requiredDatabaseId;
}
MyBatis 使用 SqlSource 接口表示映射文件或注解中定义的 SQL 语句
public interface SqlSource {
// 根据映射文件或注解描述的 SQL 语句,以及传入的参数,返回可执行的 SQL
BoundSql getBoundSql(Object parameterObject);
}
MyBatis 使用 MappedStatement 表示映射配置文件中定义的 SQL 节点
public final class MappedStatement {
// 节点中的 id 属性(包括命名空间前缀)
private String resource;
// 对应一条 SQL 语句
private SqlSource sqlSource;
// SQL 的类型,INSERT、UPDATE、DELETE、SELECT
private SqlCommandType sqlCommandType;
// ……
}
XMLStatementBuilder 的 parseStatementNode 方法是解析 SQL 节点的入口函数
public void parseStatementNode() {
// 获取 SQL 节点的 id 和 databaseId 属性,若 databaseId 属性值与当前使用的数据库不匹配,则不加载该 SQL 节点;
// 若存在相同 id 且 databaseId 属性值不为空的 SQL 节点,则不加载该 SQL 节点;
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取 SQL 节点的 nodeName 属性值,即 SQL 类型,如 SELECT、INSERT、UPDATE、DELETE 等;
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 在解析 SQL 节点之前,先解析 <include> 节点,将 <include> 节点中的 SQL 片段合并到当前 SQL 节点中;
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 处理 <selectKey> 节点,该节点用于生成主键;
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 完成 SQL 节点的解析,后面单独分析
}
3.1.6 绑定Mapper接口
每个映射配置文件的命名空间可以绑定一个 Mapper 接口,并注册到 MapperRegistry 中。通过 XMLMapperBuilder.bindMapperForNamespace() 实现映射配置文件与对应 Mapper 接口的绑定。
private void bindMapperForNamespace() {
// 获取映射配置文件的命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 解析命名空间对应的类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
// 是否已经加载了 boundType 接口
if (boundType != null && !configuration.hasMapper(boundType)) {
// 追加 namespace 前缀,并添加到 configuration.loadedResources 集合中
configuration.addLoadedResource("namespace:" + namespace);
// 调用 MapperRegistry.addMapper() 方法,注册 boundType 接口
configuration.addMapper(boundType);
}
}
}
3.1.7 处理incomplete*集合
XMLMapperBuilder.configurationElement() 方法在解析映射配置文件前面的节点时,可能会引用后面还未解析的节点,导致解析失败并抛出 IncompleteElementException。
MyBatis 会捕获这些异常并做针对性处理,前面提到的 parsePendingResultMaps()、parsePendingCacheRefs()、parsePendingStatements() 三个方法就是为此而产生的。
private void parsePendingStatements() {
// 获取 Configuration.incompleteStatements 集合
Collection<XMLStatementBuilder> incompleteStatements = configuration.getIncompleteStatements();
// 加锁,避免并发问题
synchronized (incompleteStatements) {
// 遍历 incompleteStatements 集合
Iterator<XMLStatementBuilder> iter = incompleteStatements.iterator();
while (iter.hasNext()) {
try {
// 重新解析 Statement 节点
iter.next().parseStatementNode();
iter.remove();
} catch (IncompleteElementException e) {
// Statement is still missing a resource...
}
}
}
}
3.2 SqlNode&SqlSource
映射配置文件中定义的 SQL 会被解析成 SqlSource 对象,SQL 语句中定义的动态 SQL 节点、文本节点等,则由 SqlNode 接口的相应实现表示。
public interface SqlSource {
// 通过解析得到 BondSql 对象,BondSql 对象封装了包含“?”占位符的 SQL 语句,以及绑定的实参
BoundSql getBoundSql(Object parameterObject);
}
- DynamicSqlSource,负责处理动态 SQL 语句
- RawSqlSource,负责处理静态语句
- StaticSqlSource,以上二者处理后的 SQL 会封装成 StaticSqlSource 返回
3.2.1 组合模式
组合模式是将对象组合成树形结构,以表示“部分——整体”的层次结构(一般是树形结构),用户可以像处理一个简单对象一样来处理一个复杂对象。
- 抽象组件(Component),定义了树形结构中所有类的公共行为(operation),以及一些用于管理子组件的方法(add,remove,getChild)
- 树叶(Leaf),表示叶子节点,叶子节点没有子节点
- 树枝(Composite),有子组件的组件
- 调用者(Client),通过 Component 接口操纵整个树形结构
优点:
- 帮助调用者屏蔽对象的复杂性
- 通过增加树中节点的方式,添加新的 Component 对象,从而实现扩展,符合开闭原则
3.2.2 OGNL 表达式简介
MyBatis 中涉及的 OGNL 表达式的功能主要是:存取 Java 对象树中的属性、调用 Java 对象树中的方法
OGNL 表达式有三个重要概念:
-
表达式
OGNL 表达式执行的所有操作都是根据表达式解析得到的。例如:“对象名.方法名”表示调用指定对象的指定方法。
-
root 对象
OGNL 表达式指定了具体的操作,而 root 对象指定了需要操作的对象
-
OgnlContext(上下文对象)
OgnlContext 类继承了 Map 接口,可以存放除 root 对象之外的其他对象。在使用 OGNL 表达式操作非 root 对象时,需要使用 #前缀,而操作 root 对象则不需要使用 #前缀。
示例代码:
在 MyBatis 中,使用 OgnlCache 对原生的 OGNL 进行了封装。OGNL 表达式的解析过程是比较耗时的,为了提高效率,OgnlCache 使用 expressionCache 字段(ConcurrentHashMap<String, Object>)对解析后的 OGNL 表达式进行缓存。
private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>();
public static Object getValue(String expression, Object root) {
try {
// 创建一个 OGNL 上下文对象,OgnlClassResolver 替代了 OGNL 中原有的 DefaultClassResolver
// 其主要功能是使用前面介绍的 Resource 工具类定位资源
Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
private static Object parseExpression(String expression) throws OgnlException {
// 从缓存中获取 OGNL 表达式对应的节点对象
Object node = expressionCache.get(expression);
if (node == null) {
// 如果缓存中不存在,则解析 OGNL 表达式
node = Ognl.parseExpression(expression);
// 将解析结果放入缓存
expressionCache.put(expression, node);
}
return node;
}
3.2.3 DynamicContext
DynamicContext 是用于记录解析动态 SQL 语句之后产生的 SQL 语句片段的容器。
public class DynamicContext {
// 参数上下文
private final ContextMap bindings;
// 在 SqlNode 解析动态 SQL 时,会将解析后的 SQL 语句片段添加到该属性中保存,最终拼凑出一条完整的 SQL
private final StringJoiner sqlBuilder = new StringJoiner(" ");
// 追加 SQL 片段
public void appendSql(String sql) {
this.sqlBuilder.add(sql);
}
// 获取解析后的、完整的 SQL 语句
public String getSql() {
return this.sqlBuilder.toString().trim();
}
}
3.2.4 SqlNode
public interface SqlNode {
// 根据用户传入的实参,解析该 SqlNode 所记录的动态 SQL 节点,并调用 DynamicContext.appendSql() 方法将解析后的 SQL 片段追加到 DynamicContext.sqlBuilder 中保存
// 当 SQL 节点下所有 SqlNode 完成解析后,我们就可以从 DynamicContext 中获取一条动态生成的、完整的 SQL 语句
boolean apply(DynamicContext context);
}
SqlNode 接口有多个实现类,每个实现类对应一个动态 SQL 节点。
StaticTextSqlNode&MixedSqlNode
StaticTextSqlNode 中使用 text 字段(String类型)记录了对应的非动态 SQL 语句节点。
MixedSqlNode 中使用 contents 字段(List类型)记录其子节点对应的 SqlNode 对象集合。
TextSqlNode
TextSqlNode 表示的是包含“${}”占位符的动态 SQL 节点。
IfSqlNode
IfSqlNode 对应的动态 SQL 节点是 节点。使用 OGNL 检测表达式是否成立,并根据结构决定是否执行 apply() 方法。
TrimSqlNode&WhereSqlNode&SetSqlNode
TrimSqlNode 对应节点,会根据子节点的解析结果,添加或删除相应的前缀或后缀。
WhereSqlNode 是 TrimSqlNode 的子类,对应节点
SetSqlNode 是 TrimSqlNode 的子类,对应节点
ForEachSqlNode
ForEachSqlNode 对应节点。
ChooseSqlNode
ChooseSqlNode,对应节点。
VarDeclSqlNode
VarDeclSqlNode 表示动态 SQL 语句中的 节点,该节点可以从 OGNL 表达式中创建一个变量并将其记录到上下文中。
<select id="getUserInfo" parameterType="com.example.User" resultType="com.example.UserInfo">
<bind name="name" value="'%' + name + '%'"/>
<bind name="age" value="age * 2"/>
SELECT * FROM user_info WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE #{name}
</if>
<if test="age != null">
AND age >= #{age}
</if>
</select>
3.2.5 SqlSourceBuilder
SqlSourceBuilder 主要完成了两方面的操作,一方面是解析 SQL 语句中的“#{}”占位符中定义的属性,另一方面是将 SQL 语句中的“#{}”占位符替换成“?”占位符。
3.2.6 DynamicSqlSource
DynamicSqlSource 负责解析动态 SQL 语句。
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 创建 DynamicContext 对象,parameterObject 为传入的参数
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 通过 rootSqlNode.apply 方法调用整个树形结构中全部 SqlNode.apply 方法,
// 每个 SqlNode.apply 方法会将解析后的 SQL 语句片段追加到 context 中,最终通过 context.getSql() 获取完整的 SQL 语句
rootSqlNode.apply(context);
// 创建 SqlSourceBuilder 对象,解析 SQL 语句中的 #{} 占位符,将 SQL 语句中的 #{} 占位符替换成 ? 占位符
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 创建 BoundSql 对象,并将DynamicContext.bindings 中的参数添加到 BoundSql.additionalParameters 集合中
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
3.2.7 RawSqlSource
RawSqlSource 负责处理静态 SQL 语句。
3.3 ResultSetHandler
MyBatis 将结果集按照映射配置文件中定义的映射规则,例如节点、resultType 属性等,映射成相应的结果对象。这一过程是由 ResultSetHandler 完成的。
public interface ResultSetHandler {
// 处理结果集,生成相应的结果对象集合
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
// 处理结果集,返回相应的游标对象
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
// 处理存储过程的输出参数
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
DefaultResultSetHandler 是 MyBatis 提供的 ResultSetHandler 的唯一实现。
3.3.1 handleResultSets()方法
通过 select 语句查询数据库得到的结果集由 handleResultSets() 方法进行处理。
3.3.2 ResultSetWrapper
DefaultResultSetHandler 在获取 ResultSet 对象之后,将其封装成 ResultSetWrapper 对象进行处理。
3.3.3 简单映射
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
// 默认的 ResultContext 对象
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
// 根据 RowBounds 中的 offset 定位到指定的记录
skipRows(resultSet, rowBounds);
// 检测已经处理的行数是否达到上限(RowBounds 中的 limit)以及 ResultSet 中是否还有更多的记录
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
// 根据该行记录以及 ResultMap.discriminator 中的配置,解析出对应的 ResultMap
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
// 根据最终确定的 ResultMap,对该行记录进行映射,得到映射结果
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
// 将映射结果存储到 resultHandler.resultList 中
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
3.3.4 嵌套映射
private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
// 创建 DefaultResultContext 对象
final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
// 根据 RowBounds 中的 offset 定位到指定的记录
skipRows(resultSet, rowBounds);
Object rowValue = previousRowValue;
// 检测 ResultSet 中是否还有更多的记录
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
// 通过 resolveDiscriminatedResultMap() 方法解析出最终确定的 ResultMap
final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
// 为当前行记录创建 CacheKey 对象
final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
// 从 nestedResultObjects 中获取该行记录对应的嵌套映射的映射结果
Object partialObject = nestedResultObjects.get(rowKey);
// 检测 resultOrdered 是否为 true
if (mappedStatement.isResultOrdered()) {
if (partialObject == null && rowValue != null) {
// 主结果对象发生变化
// 清空 nestedResultObjects 集合
nestedResultObjects.clear();
// 保存主结果对象,也就是嵌套的外层对象
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
// 完成该行记录的映射,将映射结果存储到 nestedResultObjects 集合中
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
} else {
// 完成该行记录的映射,将映射结果存储到 nestedResultObjects 集合中
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
if (partialObject == null) {
// 保存结果对象
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
}
// 对 resultOrdered 为 true 的情况进行特殊处理,调用 storeObject() 方法保存结果对象
if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
previousRowValue = null;
} else if (rowValue != null) {
previousRowValue = rowValue;
}
}
3.3.5 嵌套查询&延迟加载
“延迟加载”是指嵌套查询时,作为被嵌套属性的某个对象,直到真正被使用时,才会执行数据库操作加载到内存中。一个属性是否延迟加载,有两个地方配置:
-
中明确地配置了【fetchType】
<resultMap id="detailedBlogResultMap" type="Blog"> <!-- 对象属性的映射,同时也是一个嵌套映射 --> <association property="author" resultMap="authorResultMap" fetchType="eager"/> <!-- 集合属性的映射,也是一个匿名的嵌套映射 --> <collection property="posts" ofType="Post" fetchType="lazy"> <id column="post_id" property="id"/> <result column="post_content" property="content"/> </collection> </resultMap>
-
mybatis-config.xml 配置文件
<settings> <!-- 打开延迟加载的功能 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 将积极的延迟加载改变为消极的加载模式 --> <setting name="aggressiveLazyLoading" value="false"/> </settings>
MyBatis 中延迟加载是通过动态代理实现的,但是由于被代理对象通过是普通的 JavaBean,没有实现任何接口,所以无不使用 JDK 动态代理。MyBatis 提供了另外两种可以为普通 JavaBean 动态生成代理对象的方式。
1. cglib
cglib 采用字节码技术实现动态代理功能,其原理是通过字节码技术为目标类生成一个子类,并在该子类中采用方法拦截的方式拦截父类方法的调用,从而实现代理的功能。
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxy implements MethodInterceptor {
// cglib 中的 Enhancer 对象
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class<?> clazz) {
// 设置创建子类的类
enhancer.setSuperclass(clazz);
// 设置回调
enhancer.setCallback(this);
// 创建子类对象代理
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("前置处理");
// 调用父类方法
Object result = methodProxy.invokeSuper(o, objects);
System.out.println("后置处理");
return result;
}
}
public class CgLibTest {
/**
* 目标方法
* @param str
* @return
*/
public String method(String str) {
System.out.println(str);
return "CgLibTest method(): " + str;
}
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy();
CgLibTest proxyImp = (CgLibTest) proxy.getProxy(CgLibTest.class);
String result = proxyImp.method("test");
System.out.println(result);
/**
* 前置处理
* test
* 后置处理
* CgLibTest method(): test
*/
}
}
2. Javassist
Javassist 是一个开源的生成 Java 字节码的类库,其主要优点在于简单、快速,直接使用 API 就能动态修改类的结构,或是动态地生成类。
public class JavassistMain {
public static void main(String[] args) throws Exception {
// 创建类池
ClassPool cp = ClassPool.getDefault();
// 要生成的类名为 com.example.chapter3.section3.JavassistTest
CtClass clazz = cp.makeClass("com.example.chapter3.section3.JavassistTest");
StringBuffer body;
// 创建字段、指定了字段名和字段类型、字段所属的类
CtField field = new CtField(cp.get("java.lang.String"), "prop", clazz);
// 指定该字段为 private
field.setModifiers(Modifier.PRIVATE);
// 设置字段的getter和setter方法
clazz.addMethod(CtNewMethod.setter("setProp", field));
clazz.addMethod(CtNewMethod.getter("getProp", field));
// 设置 prop 字段的初始化值,并将字段添加到类中
clazz.addField(field, CtField.Initializer.constant("MyName"));
// 创建构造方法,指定了构造方法的参数、构造方法所属的类
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
// 设置构造方法的方法体
body = new StringBuffer();
body.append("{\n prop=\"MyName\";\n}");
constructor.setBody(body.toString());
// 将构造方法添加到类中
clazz.addConstructor(constructor);
// 创建 execute方法,指定了方法的返回值类型、方法名、方法所属的类
CtMethod ctMethod = new CtMethod(CtClass.voidType, "execute", new CtClass[]{}, clazz);
// 设置方法的访问修饰符
ctMethod.setModifiers(Modifier.PUBLIC);
// 设置方法体
body = new StringBuffer();
body.append("{\n System.out.println(\"execute(): \" + this.prop);\n}");
ctMethod.setBody(body.toString());
// 将方法添加到类中
clazz.addMethod(ctMethod);
// 生成的类写入文件
clazz.writeFile("D:\\Gitee\\notes\\MyBatis\\MyBatis技术内幕\\MyBatis-Tec-Inside\\src\\main\\java");
// 加载 clazz 类,并创建对象
Class<?> c = clazz.toClass();
Object obj = c.newInstance();
// 调用 execute 方法
Method method = obj.getClass().getMethod("execute", new Class[]{});
method.invoke(obj, new Object[]{});
}
}
执行上述代码后,在指定目录下可以找到生成的 JavassistTest.class 文件
Javassist 也是通过创建目标类的子类方式实现动态代理功能的。
public class JavassistMain2 {
public static void main(String[] args) throws Exception {
ProxyFactory proxyFactory = new ProxyFactory();
// 指定父类,ProxyFactory 会动态生成一个子类
proxyFactory.setSuperclass(JavassistTest.class);
// 设置过滤器,判断哪些方法调用需要被拦截
proxyFactory.setFilter(m -> {
// 拦截 execute 方法
if (m.getName().equals("execute")) {
return true;
}
return false;
});
// 设置拦截处理
proxyFactory.setHandler((self, thisMethod, proceed, params) -> {
System.out.println("前置处理");
Object result = proceed.invoke(self, params);
System.out.println("执行结果:" + result);
System.out.println("后置处理");
return result;
});
// 生成代理类
Class<?> c = proxyFactory.createClass();
JavassistTest javassistTest = (JavassistTest) c.newInstance();
// 调用 execute 方法,会被拦截
javassistTest.execute();
System.out.println(javassistTest.getProp());
}
}
3.3.6 多结果集处理
3.4 KeyGenerator
默认情况下,insert
语句并不会返回自动生成的主键,而是返回插入记录的条数。MyBatis 提供 KeyGenerator 接口来获取插入记录时产生的自增主键。
public interface KeyGenerator {
// 在执行 insert 之前执行,设置属性 order="BEFORE"
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
// 在执行 insert 之后执行,设置属性 order="AFTER"
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
3.4.1 Jdbc3KeyGenerator
Jdbc3KeyGenerator 用于取回数据库生成的自增 id,它对应于 mybatis-config.xml 配置文件中的 useGeneratedKeys 全局配置,以及映射配置文件中 SQL 节点的 useGeneratedKeys 属性。
3.4.2 SelectKeyGenerator
SelectKeyGenerator 用于不支持自动生成自增主键的数据库。
3.5 StatementHandler
StatementHandler 接口是 MyBatis 的核心接口之一,它的功能很多,例如创建 Statement 对象,为 SQL 语句绑定实参,执行 select、insert、update、delete 等多种类型的 SQL 语句,批量执行 SQL 语句,将结果集映射成结果对象。
public interface StatementHandler {
// 从连接中获取一个 Statement
Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
// 绑定 statement 执行时所需的实参
void parameterize(Statement statement) throws SQLException;
// 批量执行 SQL 语句
void batch(Statement statement) throws SQLException;
// 执行 update/insert/delete 语句
int update(Statement statement) throws SQLException;
// 执行 select 语句
<E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(Statement statement) throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
3.5.1 RoutingStatementHandler
RoutingStatementHandler 会根据 MappedStatement 中指定的 statementType 字段,创建对应的 StatementHandler 接口实现。
3.5.2 BaseStatementHandler
BaseStatementHandler 是一个实现了 StatementHandler 接口的抽象类,它只提供了一些参数绑定相关的方法,并没有实现操作数据库的方法。
3.5.3 ParameterHandler
ParameterHandler 只定义了一个 setParameters() 方法,主要负责调用 PreparedStatement.set*() 方法为 SQL 语句绑定实参。
3.5.4 SimpleStatementHandler
SimpleStatementHandler 继承了 BaseStatementHandler 抽象类。它底层使用 java.sql.Statement 对象来完成数据库的相关操作。
3.5.5 PreparedStatementHandler
PreparedStatementHandler 底层依赖于 java.sql.PreparedStatement 对象来完成数据库的相关操作。
3.6 Executor
Executor 是MyBatis 的核心接口之一,其中定义了数据库操作的基本方法。在实际应用中经常涉及的 SqlSession 接口的功能,都是基于 Executor 接口实现的。
public interface Executor {
// 执行 update、insert、delete 三种类型的 SQL 语句
int update(MappedStatement ms, Object parameter) throws SQLException;
// 执行 select 类型的 SQL 语句,返回值分为结果对象列表或游标对象
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
// 批量执行 SQL 语句
List<BatchResult> flushStatements() throws SQLException;
// 提交事务
void commit(boolean required) throws SQLException;
// 回滚事务
void rollback(boolean required) throws SQLException;
// 创建缓存中用到的 CacheKey 对象
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
// 根据 CacheKey 对象查找缓存
boolean isCached(MappedStatement ms, CacheKey key);
// 清空一级缓存
void clearLocalCache();
// 延迟加载一级缓存中的数据
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
// 获取事务对象
Transaction getTransaction();
// 关闭 Executor 对象
void close(boolean forceRollback);
// 检测 Executor 是否已关闭
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
3.6.1 模板方法模式
在模板方法模式中,一个算法可以分为多个步骤,这些步骤的执行次序在一个被称为“模板方法”的方法中定义,而算法的每个步骤都对应着一个方法,这些方法被称为“基本方法”。
模板方法模式可以将模板方法以及固定不变的基本方法统一封装到父类中,而将变化的部分封装到子类中实现,这样就由父类控制整个算法的流程,而子类实现算法的某些细节,实现了这两方面的解耦。
3.6.2 BaseExecutor
BaseExecutor 是一个实现了 Executor 接口的抽象类,主要提供了缓存管理和事务管理的基本功能,继承 BaseExecutor 的子类只要实现四个基本方法来完成数据库的相关操作即可。
public abstract class BaseExecutor implements Executor {
// 实现事务的提交、回滚和关闭操作
protected Transaction transaction;
// 其中封装的 Executor 对象
protected Executor wrapper;
// 延迟加载队列
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// 一级缓存,用于缓存该 Executor 对象查询结果
protected PerpetualCache localCache;
// 一级缓存,用于缓存输出类型的参数
protected PerpetualCache localOutputParameterCache;
// 用来记录嵌套查询的层数
protected Configuration configuration;
}
1. 一级缓存简介
一级缓存是会话级别的缓存,在 MyBatis 中每创建一个 SqlSession 对象,就表示开启一次数据库会话。MyBatis 通过在 Executor 对象中建立一个简单的缓存来实现“一级缓存”。它会将每次查询的结果对象缓存起来,在执行查询操作时,会先查询一级缓存,如果其中存在完全一样的查询语句,则直接从一级缓存中取出相应的结果对象并返回给用户,避免访问数据库,降低了数据库的压力。
一级缓存的生命周期与 SqlSession 相同,但其中的缓存对象的对象存活时间受很多方面影响。
一级缓存是默认开启的,一般情况下,用户不需要进行特殊配置。
2. 一级缓存的管理
3. 事务相关操作
在 BatchExecutor 实现中,可以缓存多条 SQL 语句,等待合适的时机将缓存的多条 SQL 一并发送到数据库执行。
3.6.3 SimpleExecutor
SimpleExecutor 继承了 BaseExecutor 抽象类,它是最简单的 Executor 接口实现。
3.6.4 ReuseExecutor
ReuseExecutor 提供了 Statement 重用的功能,ReuseExecutor 中通过 statementMap 字段(HashMap<String,Statement)缓存使用过的 Statement 对象。
3.6.5 BatchExecutor
BatchExecutor 实现了批处理多条 SQL 的功能。
3.6.6 CachingExecutor
CachingExecutor 是一个 Executor 接口的装饰器,它为 Executor 对象增加了二级缓存的相关功能。
1. 二级缓存简介
MyBatis 提供的二级缓存是应用级别的缓存,它的生命周期与应用程序的生命周期相同。与二级缓存相关的配置有三个:
(1)mybatis-config.xml 中 cacheEnable
,二级缓存的总开关,默认 true,此时后面两项的配置才会起作用
<settings>
<!-- 使全局的映射器启用或禁用缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
(2)映射配置文件中 cache
或 cache-ref
节点。
如果配置了 标签,在解析时会为该映射配置文件指定的命名空间创建相应的 Cache 对象作为其二级缓存,默认是 PerpetualCache 对象。
如果配置了 标签,则会与指定的 命名空间共享同一个 Cache 对象。
(3) 节点的 useCache 属性,表示查询产生的结果对象是否要保存到二级缓存中,默认 true。 2. TransactionalCache&TransactionalCacheManager TransactionalCache 和 TransactionalCacheManager 是 CachingExcutor 依赖的两个组件。其中TransactionalCache 继承了 Cache 接口,主要用于保存在某个 SqlSession 的某个事务中需要向某个二级缓存中添加的缓存数据。 3. CachingExcutor的实现 CachingExcutor 封装了一个用于执行数据库操作的 Executor 对象,以及一个用于管理缓存的 TransactionalCacheManager 对象。 3.7 接口层 SqlSession 是 MyBatis 核心接口之一,也是 MyBatis 接口层的主要组成部分,对外提供 MyBatis 常用 API。 SqlSessionFactory 负责创建 SqlSession 对象,其中只包含了多个 openSession() 方法的重载。 SqlSession 中定义了常用的数据库操作以及事务的相关操作。 3.7.1 策略模式 某些场景下,系统需要根据输入条件以及运行环境选择不同的算法来完成某一功能,开发人员可以通过硬编码的方式将多种算法通过条件分支语句写到一个类中,但这显然是不符合“开放-封闭”原则的。 使用策略模式,将每一个算法封装起来,由不同的类进行管理,并让它们之间可以相互替换。 Context 表示算法的调用者,Strategy 表示算法的统一接口,ConcreteStrategy1 和 ConcreteStrategy2 表示具体的算法实现。当需要添加新的算法时,可以直接为 Strategy 接口添加新的实现类。开发人员通过 setStrategy() 方法选择要调用的 Strategy 接口实现。这是符合“开闭”原则的。 3.7.2 SqlSession public class DefaultSqlSession implements SqlSession { // 配置对象 private final Configuration configuration; // 底层依赖的 Executor 对象 private final Executor executor; // 是否自动提交事务 private final boolean autoCommit; // 当前缓存中是否有脏数据 private boolean dirty; // 为防止用户忘记关闭已打开的游标对象,会通过 cursorList 字段记录由该 SqlSession 对象生成的游标对象,在 DefaultSqlSession.close() 方法中会统一关闭这些游标对象 private List<Cursor<?>> cursorList; } 3.7.3 DefaultSqlSessionFactory DefaultSqlSessionFactory 提供了两种创建 DefaultSqlSession 对象方式。 一是通过数据源获取数据连接,并创建 Executor 对象以及 DefaultSqlSession 对象。 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // 获取 mybatis-config.xml 配置文件中配置的 Environment 对象 final Environment environment = configuration.getEnvironment(); // 获取 TransactionFactory 对象 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 创建 Transaction 对象 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 根据配置创建 Executor 对象 final Executor executor = configuration.newExecutor(tx, execType); // 创建 DefaultSqlSession 对象 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { // 关闭 Transaction 对象 closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 二是用户提供数据库连接对象,DefaultSqlSessionFactory 会使用该数据库连接对象创建 Executor 对象以及 DefaultSqlSession 对象。 private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) { try { boolean autoCommit; try { // 获取 Connection 对象的 autoCommit 属性 autoCommit = connection.getAutoCommit(); } catch (SQLException e) { // Failover to true, as most poor drivers // or databases won't support transactions autoCommit = true; } // 获取 mybatis-config.xml 配置文件中配置的 Environment 对象 final Environment environment = configuration.getEnvironment(); // 获取 TransactionFactory 对象 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 创建 Transaction 对象 final Transaction tx = transactionFactory.newTransaction(connection); // 根据配置创建 Executor 对象 final Executor executor = configuration.newExecutor(tx, execType); // 创建 DefaultSqlSession 对象 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 3.7.4 SqlSessionManager SqlSessionManager 同时实现了 SqlSession 接口和 SqlSessionFactory 接口,也就同时提供了创建 SqlSession 对象以及 SqlSession 操纵数据库的功能。 public class SqlSessionManager implements SqlSessionFactory, SqlSession { // 底层封装的 SqlSessionFactory 对象 private final SqlSessionFactory sqlSessionFactory; // ThreadLocal 变量,记录一具与当前线程绑定的 SqlSession 对象 private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>(); // localSqlSession 中记录的 SqlSession 对象的代理对象,在 SqlSessionManager 初始化时,会使用 JDK 动态代理的方式为 localSqlSession 创建代理对象 private final SqlSession sqlSessionProxy; } 3.8 本章小结 MyBatis 初始化流程 OGNL 表达式、静态/动态 SQL 语句、用户传入的实参等信息的处理 结果集映射 主键生成器 Executor 接口及其实现