目录
前言
本文以查询的情况为例,主要分析mybatis的处理过程。mybatis处理增删改查主要分为了select和update两种方式,insert和delete操作也使用update操作进行处理。
一、整体流程
在mapper代理对象构建完成之后,调用查询方法,依次经过DefaultSqlSession,Executor和StatementHandler的相关操作,获得返回数据,下面将进行具体分析。
二、查询操作
1.MapperProxy
MapperProxy中为每个method创建一个MapperMethodInvoker代理对象,并将其放入缓存中,以后可以直接获取。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
//执行PlainMethodInvoker的invoke方法,本来已经是一个代理对象,然后调用另一个代理对象的invoke方法,为啥啊
//Map<Method, MapperMethodInvoker> methodCache有缓存的作用,存储的新建的代理对象
//前面是对mapper对象创建代理对象,后面是为方法创建代理对象,并加入缓存,invoke方法中调用MapperMethod的execute方法
//创建方法代理对象费时的是MapperMethod对象创建,缓存节省时间,不一开始就全部创建是因为可能不会全部用到,多次调用同一方法不用重复创建
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
MapperProxy中持有methodCache,里面存储着当前这个代理对象中的方法生成的cacheInvoker对象,cacheInvoker对象中保存了方法的返回值,参数,注解等信息以及对应的sql信息,在下次调用时不必再次重新解析生成,可以直接使用。这里我有些好奇methodCache是如何保证并发问题,后来看到是一个ConcurrentHashmap,有点意思的。
private final Map<Method, MapperMethodInvoker> methodCache;
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// A workaround for https://bugs.openjdk.java.net/browse/JDK-8161372
// It should be removed once the fix is backported to Java 8 or
// MyBatis drops Java 8 support. See gh-1929
MapperMethodInvoker invoker = methodCache.get(method);
if (invoker != null) {
return invoker;
}
//为method创建MapperMethodInvoker代理对象并加入缓存中
return methodCache.computeIfAbsent(method, m -> {
//生成MapperMethod对象,记录sqlcommand和MethodSignature
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
});
}
}
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
invoke方法会直接执行mapperMethod的execute方法,在该方法中,对参数进行解析,并构建parameterObject,会根据方法的类别分别调用sqlSession的不同方法进行处理。
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
值得注意的是,对于select方法,其实sql返回的都是list,mybatis是通过方法声明的返回值来确定通过何种方式查询,其实底层都是通过selectLIst方法查询,对于单个对象就是取其中的第一个。如果使用一个对象接收一个本来是list的结果,如果上面的逻辑判断,会取第一个,好像没有问题,但是mybatis在取值的时候做了判断,如果返回对象个数超过1就会报错,个数为0会返回null。
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
2.DefaultSqlSession
本文以查询为例,会调用DefaultSqlSession的selectList方法,首先通过类名+方法名作为id查询对应的MapperStatement,然后调用executor的query方法进行处理。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//在mapperStatement中选取,id是类名+方法名
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
3.Executor
在sqlSession中会调用CachingExecutor的query方法,首先是获取boundsql,会完成对#符和$符的初步处理,对于$符,会直接以字符串拼接的方式替换为真实值;对于#符,会替换为?,作为预编译语句,后面还会进一步解析。然后使用mapperStatement,parameterObject,rowBounds和boundSql构建key。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
/*
boundSql 就是执行的sql,在此处会对$符和#进行解析,$返回真实参数,#返回占位符?,后面再进行解析
*/
BoundSql boundSql = ms.getBoundSql(parameterObject);
/*
创建缓存,缓存的key值由mapperStatement,参数,rowBounds,和boundsql共同决定
*/
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 将sql语句、缓存key都传入query方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
在query方法中,在有二级缓存的情况下,首先从二级缓存中查找数据,在无法查到的情况下,会调用BaseExecutor的query方法,在一级缓存中查询,并将查询结果写入二级缓存中;在没有二级缓存的情况下,会直接在一级缓存中查询。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
/*
mybatis的二级缓存
*/
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);
}
在BeseExecutor的query方法中,首先会从一级缓存中查询数据,如果没有查询到,会调用queryFromDatabase方法,从数据库中进行查询,拿到结果之后写入缓存中。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
/*
去localCache本地缓存中查,一级缓存
*/
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 没有命中缓存,去数据库中查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
在queryFromDatabase中,首先在localCache以当前key插入一个默认值,然后在数据库中进行查询,再将结果更新到缓存中。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 真正去数据库查询数据
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 将查询的结果缓存在本地
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
在doQuery方法中,会调用prepareStatement方法对sql中的占位符进行处理,也就是对#符的进一步处理,而且在其中对一些特殊字符进行了处理,避免了sql注入的情况。然后调用StatementHandler的query方法得到最终结果。其实,对于参数的处理也是由statementHandler实现的,为啥要在executor中调用还要考虑。
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 创建语句处理器,同时创建了参数处理器和结果集处理器,可以进行拦截器的代理,
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//语句预处理,进行#符的再次解析,将解析结果写入preparedStatement中,这里为了防止sql注入,对特殊字符进行了处理,对单引号会在单引号后面添加一个单引号
//此处在executor中调用statementHandler的parameterHandler进行参数处理就有点意思,为啥不在后面statementHandler的处理中进行
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
4.statementHandler
在statementHandler的query方法中,使用PreparedStatement的execute方法执行查询,并将结果进行处理之后返回。
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
//预编译sql语句执行 JDBC的execute方法
/*
执行这个PreparedStatement对象中的SQL语句,该对象可以是任何类型的SQL语句。
一些准备好的语句返回多个结果;execute方法可以处理这些复杂的语句,
也可以处理由executeQuery和executeUpdate方法处理的更简单的语句。
execute方法返回一个布尔值来表示第一个结果的形式。
您必须调用方法getResultSet或getUpdateCount来检索结果;
必须调用getMoreResults才能移动到任何后续结果。
*/
ps.execute();
//使用resultSetHandler处理结果集,参数resultHandler根本没有用啊
return resultSetHandler.handleResultSets(ps);
}
三、查询操作
关于更新操作此处不做过多介绍,在executor的更新过程中,会将缓存清空。
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
//清空缓存
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//清空缓存
clearLocalCache();
return doUpdate(ms, parameter);
}
在PreparedStatementHandler中的update方法中,在更新完成之后,可以将主键写回到dto中。
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
//获取主键生成器
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
//处理主键
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
总结
本文对mybatis在运行过程中的一些操作进行了源码上的分析,主要是对sqlSession,executor和statementHandler的相关操作,重点分析了查询流程,并介绍了更新流程的不同之处。