SQL解析

Mybatis在初始化的时候,会读取xml中的SQL,解析后会生成SqlSource对象,SqlSource对象分为两种。

  • DynamicSqlSource,动态SQL,获取SQL(getBoundSQL方法中)的时候生成参数化SQL。

  • RawSqlSource,原始SQL,创建对象时直接生成参数化SQL。

因为RawSqlSource不会重复去生成参数化SQL,调用的时候直接传入参数并执行,而DynamicSqlSource则是每次执行的时候参数化SQL,所以RawSqlSourceDynamicSqlSource的性能要好的。

解析的时候会先解析include标签和selectkey标签,然后判断是否是动态SQL,判断取决于以下两个条件:

  • SQL中有动态拼接字符串,简单来说就是是否使用了${}表达式。注意这种方式存在SQL注入,谨慎使用。
  • SQL中有trimwheresetforeachifchoosewhenotherwisebind标签

相关代码如下:

protected MixedSqlNode parseDynamicTags(XNode node) {
// 创建 SqlNode 数组
List<SqlNode> contents = new ArrayList<>();
// 遍历 SQL 节点的所有子节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 当前子节点
XNode child = node.newXNode(children.item(i));
// 如果类型是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 时
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
// 获得内容
String data = child.getStringBody("");
// 创建 TextSqlNode 对象
TextSqlNode textSqlNode = new TextSqlNode(data);
// 如果是动态的 TextSqlNode 对象(是否使用了${}表达式)
if (textSqlNode.isDynamic()) {
// 添加到 contents 中
contents.add(textSqlNode);
// 标记为动态 SQL
isDynamic = true;
// 如果是非动态的 TextSqlNode 对象
} else {
// 创建 StaticTextSqlNode 添加到 contents 中
contents.add(new StaticTextSqlNode(data));
}
// 如果类型是 Node.ELEMENT_NODE,其实就是XMl中<where>等那些动态标签
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// 根据子节点的标签,获得对应的 NodeHandler 对象
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) { // 获得不到,说明是未知的标签,抛出 BuilderException 异常
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 执行 NodeHandler 处理
handler.handleNode(child, contents);
// 标记为动态 SQL
isDynamic = true;
}
}
// 创建 MixedSqlNode 对象
return new MixedSqlNode(contents);
}

参数解析

Mybais中用于解析Mapper方法的参数的类是ParamNameResolver,它主要做了这些事情:

  • 每个Mapper方法第一次运行时会去创建ParamNameResolver,之后会缓存

  • 创建时会根据方法签名,解析出参数名,解析的规则顺序是

    1. 如果参数类型是RowBounds或者ResultHandler类型或者他们的子类,则不处理。

    2. 如果参数中有Param注解,则使用Param中的值作为参数名

    3. 如果配置项useActualParamName=true,argn(n>=0)标作为参数名,如果你是Java8以上并且开启了-parameters`,则是实际的参数名

      如果配置项useActualParamName=false,则使用n(n>=0)作为参数名

相关源代码:

public ParamNameResolver(Configuration config, Method method) {
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
int paramCount = paramAnnotations.length;
// 获取方法中每个参数在SQL中的参数名
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
// 跳过RowBounds、ResultHandler类型
if (isSpecialParameter(paramTypes[paramIndex])) {
continue;
}
String name = null;
// 遍历参数上面的所有注解,如果有Param注解,使用它的值作为参数名
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
// 如果没有指定注解
if (name == null) {
// 如果开启了useActualParamName配置,则参数名为argn(n>=0),如果是Java8以上并且开启-parameters,则为实际的参数名
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
// 否则为下标
if (name == null) {
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}

而在使用这个names构建xml中参数对象和值的映射时,还进行了进一步的处理。

public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
// 无参数,直接返回null
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
// 一个参数,并且没有注解,直接返回这个对象
return args[names.firstKey()];
} else {
// 其他情况则返回一个Map对象
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 先直接放入name的键和对应位置的参数值,其实就是构造函数中存入的值
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// 防止覆盖 @Param 的参数值
if (!names.containsValue(genericParamName)) {
// 然后放入GENERIC_NAME_PREFIX + index + 1,其实就是param1,params2,paramn
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}

另外值得一提的是,对于集合类型,最后还有一个特殊处理

private Object wrapCollection(final Object object) {
// 如果对象是集合属性
if (object instanceof Collection) {
StrictMap<Object> map = new StrictMap<Object>();
// 加入一个collection参数
map.put("collection", object);
// 如果是一个List集合
if (object instanceof List) {
// 额外加入一个list属性使用
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
// 数组使用array
StrictMap<Object> map = new StrictMap<Object>();
map.put("array", object);
return map;
}
return object;
}

由此我们可以得出使用参数的结论:

  • 如果参数加了@Param注解,则使用注解的值作为参数
  • 如果只有一个参数,并且不是集合类型和数组,且没有加注解,则使用对象的属性名作为参数
  • 如果只有一个参数,并且是集合类型,则使用collection参数,如果是List对象,可以额外使用list参数。
  • 如果只有一个参数,并且是数组,则可以使用array参数
  • 如果有多个参数,没有加@Param注解的可以使用argn或者n(n>=0,取决于useActualParamName配置项)作为参数,加了注解的使用注解的值。
  • 如果有多个参数,任意参数只要不是和@Param中的值覆盖,都可以使用paramn(n>=1)

延迟加载

Mybatis是支持延迟加载的,具体的实现方式根据resultMap创建返回对象时,发现fetchType=“lazy”,则使用代理对象,默认使用Javassist(MyBatis 3.3 以上,可以修改为使用CgLib)。代码处理逻辑在处理返回结果集时,具体代码调用关系如下:

PreparedStatementHandler.query=> handleResultSets =>handleResultSet=>handleRowValues=>handleRowValuesForNestedResultMap=>getRowValue

getRowValue中,有一个方法createResultObject创建返回对象,其中的关键代码创建了代理对象:

if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
}

另一方面,getRowValue会调用applyPropertyMappings方法,其内部会调用getPropertyMappingValue,继续追踪到getNestedQueryMappingValue方法,在这里,有几行关键代码:

// 如果要求延迟加载,则延迟加载
if (propertyMapping.isLazy()) {
// 如果该属性配置了延迟加载,则将其添加到 `ResultLoader.loaderMap` 中,等待真正使用时再执行嵌套查询并得到结果对象。
lazyLoader.addLoader(property, metaResultObject, resultLoader);
// 返回已定义
value = DEFERED;
// 如果不要求延迟加载,则直接执行加载对应的值
} else {
value = resultLoader.loadResult();
}

这几行的目的是跳过属性值的加载,等真正需要值的时候,再获取值。

Executor

Executor是一个接口,其直接实现的类是BaseExecutorCachingExecutorBaseExecutor又派生了BatchExecutorReuseExecutorSimpleExecutorClosedExecutor。其继承结构如图:

其中ClosedExecutor是一个私有类,用户不直接使用它。

  • BaseExecutor:模板类,里面有各个Executor的公用的方法。
  • SimpleExecutor:最常用的Executor,默认是使用它去连接数据库,执行SQL语句,没有特殊行为。
  • ReuseExecutor:SQL语句执行后会进行缓存,不会关闭Statement,下次执行时会复用,缓存的key值是BoundSql解析后SQL,清空缓存使用doFlushStatements。其他与SimpleExecutor相同。
  • BatchExecutor:当有连续InsertUpdateDelete的操作语句,并且语句的BoundSql相同,则这些语句会批量执行。使用doFlushStatements方法获取批量操作的返回值。
  • CachingExecutor:当你开启二级缓存的时候,会使用CachingExecutor装饰SimpleExecutorReuseExecutorBatchExecutor,Mybatis通过CachingExecutor来实现二级缓存。

缓存

一级缓存

Mybatis一级缓存的实现主要是在BaseExecutor中,在它的查询方法里,会优先查询缓存中的值,如果不存在,再查询数据库,查询部分的代码如下,关键代码在17-24行:

@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());
// 已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// queryStack + 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 {
// queryStack - 1
queryStack--;
}
if (queryStack == 0) {
// 执行延迟加载
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
// 清空 deferredLoads
deferredLoads.clear();
// 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}

而在queryFromDatabase中,则会将查询出来的结果放到缓存中。

// 从数据库中读取操作
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 在缓存中,添加占位对象。此处的占位符,和延迟加载有关,可见 `DeferredLoad#canLoad()` 方法
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 执行读操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 从缓存中,移除占位对象
localCache.removeObject(key);
}
// 添加到缓存中
localCache.putObject(key, list);
// 暂时忽略,存储过程相关
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

而一级缓存的Key,从方法的参数可以看出,与调用方法、参数、rowBounds分页参数、最终生成的sql有关。

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 创建 CacheKey 对象
CacheKey cacheKey = new CacheKey();
// 设置 id、offset、limit、sql 到 CacheKey 对象中
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
// 设置 ParameterMapping 数组的元素对应的每个 value 到 CacheKey 对象中
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic 这块逻辑,和 DefaultParameterHandler 获取 value 是一致的。
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
// 设置 Environment.id 到 CacheKey 对象中
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}

通过查看一级缓存类的实现,可以看出一级缓存是通过HashMap结构存储的:

/**
* 一级缓存的实现类,部分源代码
*/
public class PerpetualCache implements Cache {
/**
* 缓存容器
*/
private Map<Object, Object> cache = new HashMap<>(); @Override
public void putObject(Object key, Object value) {
cache.put(key, value);
} @Override
public Object getObject(Object key) {
return cache.get(key);
} @Override
public Object removeObject(Object key) {
return cache.remove(key);
}
}

通过配置项,我们可以控制一级缓存的使用范围,默认是Session级别的,也就是SqlSession的范围内有效。也可以配制成Statement级别,当本次查询结束后立即清除缓存。

当进行插入、更新、删除操作时,也会在执行SQL之前清空以及缓存。

二级缓存

Mybatis二级缓存的实现是依靠CachingExecutor装饰其他的Executor实现。原理是在查询的时候先根据CacheKey查询缓存中是否存在值,如果存在则返回缓存的值,没有则查询数据库。

CachingExecutorquery方法中,就有缓存的使用:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
// 如果需要清空缓存,则进行清空
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 暂时忽略,存储过程相关
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从二级缓存中,获取结果
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果不存在,则从数据库中查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 缓存结果到二级缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
// 如果存在,则直接返回结果
return list;
}
}
// 不使用缓存,则从数据库中查询
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

那么这个Cache是在哪里创建的呢?通过调用的追溯,可以找到它的创建:

public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 创建 Cache 对象
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 添加到 configuration 的 caches 中
configuration.addCache(cache);
// 赋值给 currentCache
currentCache = cache;
return cache;
}

从方法的第一行可以看出,Cache对象的范围是namespace,同一个namespace下的所有mapper方法共享Cache对象,也就是说,共享这个缓存。

另一个创建方法是通过CacheRef里面的:

public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true; // 标记未解决
// 获得 Cache 对象
Cache cache = configuration.getCache(namespace);
// 获得不到,抛出 IncompleteElementException 异常
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
// 记录当前 Cache 对象
currentCache = cache;
unresolvedCacheRef = false; // 标记已解决
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}

这里的话会通过CacheRef中的参数namespace,找到那个Cache对象,且这里使用了unresolvedCacheRef,因为Mapper文件的加载是有顺序的,可能当前加载时引用的那个namespace的Mapper文件还没有加载,所以用这个标记一下,延后加载。

二级缓存通过TransactionalCache来管理,内部使用的是一个HashMap。Key是Cache对象,默认的实现是PerpetualCache,一个namespace下共享这个对象。Value是另一个Cache的对象,默认实现是TransactionalCache,是前面那个Key值的装饰器,扩展了事务方面的功能。

通过查看TransactionalCache的源码我们可以知道,默认查询后添加的缓存保存在待提交对象里。

public void putObject(Object key, Object object) {
// 暂存 KV 到 entriesToAddOnCommit 中
entriesToAddOnCommit.put(key, object);
}

只有等到commit的时候才会去刷入缓存。

public void commit() {
// 如果 clearOnCommit 为 true ,则清空 delegate 缓存
if (clearOnCommit) {
delegate.clear();
}
// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
flushPendingEntries();
// 重置
reset();
}

查看clear代码,只是做了标记,并没有真正释放对象。在查询时根据标记直接返回空,在commit才真正释放对象:

public void clear() {
// 标记 clearOnCommit 为 true
clearOnCommit = true;
// 清空 entriesToAddOnCommit
entriesToAddOnCommit.clear();
} public Object getObject(Object key) {
// issue #116
// 从 delegate 中获取 key 对应的 value
Object object = delegate.getObject(key);
// 如果不存在,则添加到 entriesMissedInCache 中
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
// 如果 clearOnCommit 为 true ,表示处于持续清空状态,则返回 null
if (clearOnCommit) {
return null;
// 返回 value
} else {
return object;
}
}

rollback会清空这些临时缓存:

public void rollback() {
// 从 delegate 移除出 entriesMissedInCache
unlockMissedEntries();
// 重置
reset();
} private void reset() {
// 重置 clearOnCommit 为 false
clearOnCommit = false;
// 清空 entriesToAddOnCommit、entriesMissedInCache
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}

根据二级缓存代码可以看出,二级缓存是基于namespace的,可以跨SqlSession。也正是因为基于namespace,如果在不同的namespace中修改了同一个表的数据,会导致脏读的问题。

插件

Mybatis的插件是通过代理对象实现的,可以代理的对象有:

  • Executor:执行器,执行器是执行过程中第一个代理对象,它内部调用StatementHandler返回SQL结果。
  • StatementHandler:语句处理器,执行SQL前调用ParameterHandler处理参数,执行SQL后调用ResultSetHandler处理返回结果
  • ParameterHandler:参数处理器
  • ResultSetHandler:返回对象处理器

这四个对象的接口的所有方法都可以用插件拦截。

插件的实现代码如下:

// 创建 ParameterHandler 对象
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
// 创建 ParameterHandler 对象
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 应用插件
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
} // 创建 ResultSetHandler 对象
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
// 创建 DefaultResultSetHandler 对象
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
// 应用插件
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
} // 创建 StatementHandler 对象
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 创建 RoutingStatementHandler 对象
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 应用插件
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
} /**
* 创建 Executor 对象
*
* @param transaction 事务对象
* @param executorType 执行器类型
* @return Executor 对象
*/
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 获得执行器类型
executorType = executorType == null ? defaultExecutorType : executorType; // 使用默认
executorType = executorType == null ? ExecutorType.SIMPLE : executorType; // 使用 ExecutorType.SIMPLE
// 创建对应实现的 Executor 对象
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);
}
// 如果开启缓存,创建 CachingExecutor 对象,进行包装
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 应用插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

可以很明显的看到,四个方法内都有interceptorChain.pluginAll()方法的调用,继续查看这个方法:

/**
* 应用所有插件
*
* @param target 目标对象
* @return 应用结果
*/
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}

这个方法比较简单,就是遍历interceptors列表,然后调用器plugin方法。interceptors是在解析XML配置文件是通过反射创建的,而创建后会立即调用setProperties方法

我们通常配置插件时,会在interceptor.plugin调用Plugin.wrap,这里面通过Java的动态代理,拦截方法的实现:

/**
* 创建目标类的代理对象
*
* @param target 目标类
* @param interceptor 拦截器对象
* @return 代理对象
*/
public static Object wrap(Object target, Interceptor interceptor) {
// 获得拦截的方法映射
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 获得目标类的类型
Class<?> type = target.getClass();
// 获得目标类的接口集合
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 若有接口,则创建目标对象的 JDK Proxy 对象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap)); // 因为 Plugin 实现了 InvocationHandler 接口,所以可以作为 JDK 动态代理的调用处理器
}
// 如果没有,则返回原始的目标对象
return target;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 获得目标方法是否被拦截
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 如果是,则拦截处理该方法
return interceptor.intercept(new Invocation(target, method, args));
}
// 如果不是,则调用原方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}

而拦截的参数传了Plugin对象,Plugin本身是实现了InvocationHandler接口,其invoke方法里面调用了interceptor.intercept,这个方法就是我们实现拦截处理的地方。

注意到里面有个getSignatureMap方法,这个方法实现的是查找我们自定义拦截器的注解,通过注解确定哪些方法需要被拦截:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}

通过源代码我们可以知道,创建一个插件需要做以下事情:

  1. 创建一个类,实现Interceptor接口。
  2. 这个类必须使用@Intercepts@Signature来表明要拦截哪个对象的哪些方法。
  3. 这个类的plugin方法中调用Plugin.wrap(target, this)
  4. (可选)这个类的setProperties方法设置一些参数。
  5. XML中<plugins>节点配置<plugin interceptor="你的自定义类的全名称"></plugin>

可以在第三点中根据具体的业务情况不进行本次SQL操作的代理,毕竟动态代理还是有性能损耗的。

通过源代码分析Mybatis的功能的更多相关文章

  1. MyBatis架构设计及源代码分析系列(一):MyBatis架构

    如果不太熟悉MyBatis使用的请先参见MyBatis官方文档,这对理解其架构设计和源码分析有很大好处. 一.概述 MyBatis并不是一个完整的ORM框架,其官方首页是这么介绍自己 The MyBa ...

  2. mybatis源代码分析:mybatis延迟加载机制改进

    在上一篇博客<mybatis源代码分析:深入了解mybatis延迟加载机制>讲诉了mybatis延迟加载的具体机制及实现原理. 可以看出,如果查询结果对象中有一个属性是需要延迟加载的,那整 ...

  3. Android源代码分析之拍照、图片、录音、视频和音频功能

    Android源代码分析之拍照.图片.录音.视频和音频功能   //选择图片 requestCode 返回的标识 Intent innerIntent = new Intent(Intent.ACTI ...

  4. Mybatis结合Spring注解自己主动扫描源代码分析

    作为一个想做架构师的程序猿,必须是一个优秀的程序猿.在引入某一个框架的时候,必需要研究源代码,将新的开源框架的风险变为可控性. 1.Spring结合Mybatis最经常使用的配置. <!--理论 ...

  5. Mybatis Laz-Load功能实现代码赏析(原创)

    对于Mybatis 拥有的Lazy Load(有中文翻译成延迟加载)功能,应该很同学都有听说过,今天主要与大家一起来解读一下Mybatis在Lazy Load功能的实现的代码.Lazy Load实现的 ...

  6. android-plugmgr源代码分析

    android-plugmgr是一个Android插件加载框架,它最大的特点就是对插件不需要进行任何约束.关于这个类库的介绍见作者博客,市面上也有一些插件加载框架,但是感觉没有这个好.在这篇文章中,我 ...

  7. 转:SDL2源代码分析

    1:初始化(SDL_Init()) SDL简介 有关SDL的简介在<最简单的视音频播放示例7:SDL2播放RGB/YUV>以及<最简单的视音频播放示例9:SDL2播放PCM>中 ...

  8. 转:RTMPDump源代码分析

    0: 主要函数调用分析 rtmpdump 是一个用来处理 RTMP 流媒体的开源工具包,支持 rtmp://, rtmpt://, rtmpe://, rtmpte://, and rtmps://. ...

  9. 转:ffdshow 源代码分析

    ffdshow神奇的功能:视频播放时显示运动矢量和QP FFDShow可以称得上是全能的解码.编码器.最初FFDShow只是mpeg视频解码器,不过现在他能做到的远不止于此.它能够解码的视频格式已经远 ...

随机推荐

  1. 小书MybatisPlus第2篇-条件构造器的应用及总结

    一.条件构造器Wrapper Mybatis Plus为我们提供了如下的一些条件构造器,我们可以利用它们实现查询条件.删除条件.更新条件的构造. 条件构造器用于给如下的Mapper方法传参,通常情况下 ...

  2. 基于图嵌入的高斯混合变分自编码器的深度聚类(Deep Clustering by Gaussian Mixture Variational Autoencoders with Graph Embedding, DGG)

    基于图嵌入的高斯混合变分自编码器的深度聚类 Deep Clustering by Gaussian Mixture Variational Autoencoders with Graph Embedd ...

  3. json转化为C#、Java、TypeScript、VisualBasic、Python实体类

    效果展示: 源码下载地址:https://github.com/doyoulaikeme/DotNetSample/tree/master/DotNetSample2

  4. css 实现动态二级菜单

    动态实现简单的二级菜单 当鼠标放到一级标签上时,鼠标会变成小手的形状 展示二级菜单,源码如下,复制即可直接使用 <!DOCTYPE html> <html lang="en ...

  5. (5)webpack中url-loader的使用

    为什么要使用url-loader 在前面已经介绍了css文件可以使用第三方loader去加载. 在一个项目中,也不仅仅只有html,js和css文件,还有图片文件,字体文件等等.当我们给一个css样式 ...

  6. Python3 迭代器深入解析

    第6章 函数 6.1 函数的定义和调用 6.2 参数传递 6.3 函数返回值 6.4 变量作用域 6.5 匿名函数(lambda) 6.6 递归函数 6.7 迭代器 6.8 生成器 6.9 装饰器 6 ...

  7. Active Directory - Creating users via PowerShell

    Method1: Create a user by executing the following PowerShell Script. New-ADUser -name 'Michael Jorda ...

  8. DC-1靶机实战和分析

    前言 我们都知道,对靶机的渗透,可以宽阔自己的解题思路,练习并熟悉相关操作命令,提高自己的能力.下面我就对Vulnhub的DC-1靶机进行渗透,靶机设置了5个flag,咱们依次找到它.并通过图文形式讲 ...

  9. 高精度进制转换(poj1220)

    常规短除法原理 高精度进制转换是对于特别大的数字来说的,当数字特别大时,难以进行除法和取余的操作,此时通过字符串模拟的办法可以解决. #include <iostream> #includ ...

  10. web自动化 -- Keys(键盘操作)

    Keys没啥好讲的 语法:Keys.CONTRAL    等等类似. 下方就是可以  Keys.   跟的键 那些 \ue000  就是对应的  Windows系统中的键盘码,pywin32 也一样的 ...