目录
前言
关于Mybatis以前也写过几篇,但是没有写过他的执行过程,这几天又重温了一下,记录在本章中,Mybatis的执行过程比Tomcat简单多了,在以前学习Tomcat源码时,用了三四天才简单梳理明白他处理一个请求的流程,而Mybatis可以在短短几小时就可以明白。
在本章会按照一个查询的流程来跟踪,删除、更新的流程也大同小异。
上路吧。
流程图
图可能画的不好也不太详细,大概看一看,没有参考相关文章,可能有误,但是应该差不了多少。
Configuration
这个类在API层面是用不到的,但是在内部这个类却保存这几乎所有的配置,在XMLConfigBuilder解析配置文件的时候,会把里面的节点信息转换成java对象,设置到Configuration里面,如在解析mapper时,会把标签(select|insert|update|delete)封装成一个MappedStatement对象,添加到Configuration下的字段mappedStatements(这是个Map对象,key是namespace+"."+标签的id,value是MappedStatement)。
在解析过程中,Mybatis会解析出所有的mapper标签的namespace属性值,转换成一个Class对象,然后添加到一个集合中,也就是MapperRegistry,这是一个非常重要的字段,内部结构简单,也存有一个Map,刚才的添加其实就是添加到这里了,key是dao接口类名,value是MapperProxyFactory,MapperProxyFactory是为这个dao接口生成代理对象的工厂。
public <T> void addMapper(Class<T> type) {
//如果不是接口,抛出异常
if (type.isInterface()) {
if (this.hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//向Map中添加
this.knownMappers.put(type, new MapperProxyFactory(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
this.knownMappers.remove(type);
}
}
}
}
动态代理
下面简单说一下动态代理的使用,我们知道,接口是需要有实现,否则不能调用,那在Mybatis中为何我们能直接调用Dao接口返回数据呢?这其实是依靠动态代理完成的。
public interface TestMapper {
public List<Test> listAll(int i);
public String getName();
default void test(){
System.out.println("test");
}
}
通过JDK的Proxy,就生成了目标对象的代理对象,可以选择继续调用目标方法,也可以进行拦截、修改,但是要注意返回值要和目标方法的一样。
TestMapper o = (TestMapper)Proxy.newProxyInstance(TestMapper.class.getClassLoader(), new Class[]{TestMapper.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())){
return method.invoke(this,args);
}
return "name";
}
});
System.out.println(o.toString());
System.out.println(o.getName());
com.mybatis.Main$1@2a84aee7
name
getMapper()
这是个基础的用法,我们在学习Mybatis的时候应该就是从这个方法开始,那就从getMapper方法入手。
String resource = "mybatis/config/config.xml";
//拿到config的InputStream
InputStream inputStream = Resources.getResourceAsStream(resource);
//SqlSessionFactoryBuilder构建出一个SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//通过SqlSessionFactory返回一个SqlSession,我们大多数时候都会和SqlSession打交道
SqlSession sqlSession = sqlSessionFactory.openSession();
//返回TestMapper的动态代理兑现
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
List<Test> tests = mapper.listAll(3);
getMapper一路跟踪来到这里,位于MapperRegistry类中,参数type就是dao接口的类,对应上面代码就是TestMapper.class
,然后从已知的集合对象中取出MapperProxyFactory,这就需要事先在这个集合中添加,否则都会返回null,添加的过程是在解析xml时候完成的,详细可以看XMLMapperBuilder类,XMLMapperBuilder的父类BaseBuilder持有configuration,XMLMapperBuilder在解析过程中就会把xml信息放入到configuration中。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//从已知的接口集合信息中获取MapperProxyFactory,MapperProxyFactory用来给接口生成代理对象。
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
//如果传入的mapper接口在集合中没有,也就是没有在mappers节点下声明,就抛出异常。
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
//生成代理对象。
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
然后通过newInstance返回一个代理对象,这就看到了文章上面动态代理示例的代码,关键点就是Proxy.newProxyInstance
,所有调用dao方法时都会进入MapperProxy下的invoke方法,Mybatis正式查询、删除等操作就是从这个方法下开始。
protected T newInstance(MapperProxy<T> mapperProxy) {
//JDK动态代理生成代理对象
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
//MapperProxy实现了InvocationHandler接口
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
MapperProxy下的invoke方法中判断是不是Object中的方法,如果是,则默认处理,否则调用cachedInvoker方法返回值的invoke方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return Object.class.equals(method.getDeclaringClass()) ?
method.invoke(this, args) : this.cachedInvoker(method)
.invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
MapperMethod
通过简单的处理后会构造MapperMethod进行下一步,进入其execute方法,这里不管是什么操作都会进入,进入后才更具不同操作进行不同方法处理,MapperMethod封装中Mapper方法的信息,Sql语句的信息,command字段表示SQL的名称和类型,类型就是SELECT、UPDATE、DELETE、INSERT,method是对应方法的信息。
另外execute除了SELECT操作,其他都会走rowCountResult方法,将int值转换成mapper接口中对应方法的返回值。
而SELECT操作又分为很多种返回值,ResultHandler、List、Map,会更具不同返回值的结果走不同方法。
public class MapperMethod {
private final MapperMethod.SqlCommand command;
private final MapperMethod.MethodSignature method;
}
如果是List<x>
的话,会走executeForMany方法。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(this.command.getType()) {
case INSERT:
xxxxx
break;
case UPDATE:
xxxxx
break;
case DELETE:
xxxxx
break;
case SELECT:
xxxxx
break;
case FLUSH:
xxxxx
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
}
executeForMany
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
//参数列表转换
Object param = this.method.convertArgsToSqlCommandParam(args);
List result;
if (this.method.hasRowBounds()) {//是否指定了RowBounds参数。
RowBounds rowBounds = this.method.extractRowBounds(args);
//调用sqlSession查询。
result = sqlSession.selectList(this.command.getName(), param, rowBounds);
} else {
//调用sqlSession查询。
result = sqlSession.selectList(this.command.getName(), param);
}
//将结果转换为数组或Collection集合。
if (!this.method.getReturnType().isAssignableFrom(result.getClass())) {
return this.method.getReturnType().isArray() ? this.convertToArray(result) : this.convertToDeclaredCollection(sqlSession.getConfiguration(), result);
} else {
return result;
}
}
DefaultSqlSession
由于上面的sqlSession默认实现是DefaultSqlSession,所以在来到DefaultSqlSession的selectList下,参数statement是mapper文件中的namespace+'.'+标签Id
,就是指明要调用那个sql标签下的语句,parameter是方法的参数,但是parameter是经过处理了的,会更具方法的个数,生成不同的对象,如果个数是1的话,那么parameter就是这个参数的实例,如果参数有多个的话,会封装成一个ParamMap对象,key也是有规律的,在后面会单独说,RowBounds是用来分页用的。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
//根据`namespace+'.'+标签Id`获取MappedStatement对象。
MappedStatement ms = this.configuration.getMappedStatement(statement);
//查询
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}
return var5;
}
是不是我们也可以直接调用DefaultSqlSession下的selectList就行?当然没问题,但是我很少这样写。
String resource = "mybatis/config/config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
List<Object> objects = sqlSession.selectList("com.mybatis.TestMapper.listAll",1);
System.out.println(objects);
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.TestMapper">
<select id="listAll" resultType="com.mybatis.Test">
select * from tb_test where 1 and #{id}
</select>
</mapper>
其实在这里,有个知识点,就是参数问题,如果参数只有一个,xml中通过#{xx}
不管取写什么,都能拿到,但如果有多个,就需要指明了。
String resource = "mybatis/config/config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
MapperMethod.ParamMap<Object> objectParamMap = new MapperMethod.ParamMap<>();
objectParamMap.put("id",1);
objectParamMap.put("name","name");
List<Object> objects = sqlSession.selectList("com.mybatis.TestMapper.listAll",objectParamMap);
System.out.println(objects);
Executor
Mybatis的核心之一,定义了数据库的基本操作,从上面可以看到还是走了query方法进行查询,在进入之前用通过wrapCollection
方法把参数包装了一遍,经过层层调用,会走到SimpleExecutor下的doQuery方法,在此之前,还会处理缓存,关于缓存的文章,可以看看我以前的博客。
在这个方法中,prepareStatement会创建一个Statement,也就是PrepareStatement(使用JDBC的时候经常使用的一个类),还会解析参数,把方法的参数拼接到最终sql中。
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
List var9;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var9 = handler.query(stmt, resultHandler);
} finally {
this.closeStatement(stmt);
}
return var9;
}
然后就看到了我们熟悉的代码,最后让结果处理器进行结果处理并且返回,这里就结束了。
PreparedStatement ps = (PreparedStatement)statement;
ps.execute();
return this.resultSetHandler.handleResultSets(ps);
ResultSetHandler
Mybatis会按照配置文件中定义的映射规则映射成相应的结果对象,这是一个非常强大的功能,也是核心之一,ResultSetHandler是个接口,他有三个方法,唯一的实现是DefaultResultSetHandler。
public interface ResultSetHandler {
//处理结果集,返回集合
<E> List<E> handleResultSets(Statement var1) throws SQLException;
//处理结果集,返回游标
<E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException;
//处理储存过程的输出参数
void handleOutputParameters(CallableStatement var1) throws SQLException;
}
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(this.mappedStatement.getId());
//用于保存结果
List<Object> multipleResults = new ArrayList();
int resultSetCount = 0;
//获取第一个ResultSet对象,ResultSet可能存在多个,这里只获取第一个。
ResultSetWrapper rsw = this.getFirstResultSet(stmt);
List<ResultMap> resultMaps = this.mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
this.validateResultMapsCount(rsw, resultMapCount);
//遍历resultMaps集合
while(rsw != null && resultMapCount > resultSetCount) {
//获取结果集对应的ResultMap对象。
ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount);
this.handleResultSet(rsw, resultMap, multipleResults, (ResultMapping)null);
rsw = this.getNextResultSet(stmt);//获取下一个
this.cleanUpAfterHandlingResultSet();
++resultSetCount;
}
String[] resultSets = this.mappedStatement.getResultSets();
if (resultSets != null) {
while(rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = (ResultMapping)this.nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = this.configuration.getResultMap(nestedResultMapId);
this.handleResultSet(rsw, resultMap, (List)null, parentMapping);
}
rsw = this.getNextResultSet(stmt);
this.cleanUpAfterHandlingResultSet();
++resultSetCount;
}
}
return this.collapseSingleResultList(multipleResults);
}
补充
下面是补充的一些地方。
参数转换
在进行查询前,还会进行一步参数转换,就是convertArgsToSqlCommandParam方法,convertArgsToSqlCommandParam会把方法参数转换成ParamMap或者单个对象返回,
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
Object param = this.method.convertArgsToSqlCommandParam(args);
//.............
result = sqlSession.selectList(this.command.getName(), param);
}
最终会调用到ParamNameResolver#getNamedParams方法中进行转换,ParamNameResolver实例化的时候是在MapperMethod的构造方法的MapperMethod.MethodSignature的构造方法中,MethodSignature维护着接口中定义的方法相关信息,比如返回值类型,他还持有ParamNameResolver,所以上面的this.method就是指MethodSignature对象,convertArgsToSqlCommandParam就是调用他持有的ParamNameResolver的getNamedParams方法。
ParamNameResolver 使用name字段(是个SortedMap<Integer, String>
类型)记录了参数在参数列表中的位置索引与参数名称之间的对应关系,key表示参数在参数列表中的索引位置,value表示参数名称,参数名称可以通过@Param注解指定,如果没有指定@Param注解,则使用参数索引作为其名称。如果参数列表中包含RowBounds类型或ResultHandler 类型的参数,则这两种类型的参数不会被记录到name 集合中。
public ParamNameResolver(Configuration config, Method method) {
//获取参数列表中每一个参数类型
Class<?>[] paramTypes = method.getParameterTypes();
//获取参数列表上的注解
Annotation[][] paramAnnotations = method.getParameterAnnotations();
SortedMap<Integer, String> map = new TreeMap();
int paramCount = paramAnnotations.length;
for(int paramIndex = 0; paramIndex < paramCount; ++paramIndex) {
//如果参数是RowBounds类型或ResultHandler,跳出
if (!isSpecialParameter(paramTypes[paramIndex])) {
String name = null;
Annotation[] var9 = paramAnnotations[paramIndex];
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
Annotation annotation = var9[var11];
if (annotation instanceof Param) {
this.hasParamAnnotation = true;
name = ((Param)annotation).value();
break;
}
}
//如果没有@param注解,则根据配置决定是否使用参数实际名作为其名称
if (name == null) {
if (config.isUseActualParamName()) {
name = this.getActualParamName(method, paramIndex);
}
//使用参数索引作为其名称
if (name == null) {
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
}
this.names = Collections.unmodifiableSortedMap(map);
}
具体的转换过程:
public Object getNamedParams(Object[] args) {
int paramCount = this.names.size();
if (args != null && paramCount != 0) {
//如果参数是一个,直接返回
if (!this.hasParamAnnotation && paramCount == 1) {
return args[(Integer)this.names.firstKey()];
} else {
Map<String, Object> param = new ParamMap();
int i = 0;
for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {
Entry<Integer, String> entry = (Entry)var5.next();
//将参数名与实参对应关系放入集合
param.put((String)entry.getValue(), args[(Integer)entry.getKey()]);
String genericParamName = "param" + (i + 1);
//如果@Param注解指定的参数名称就是param+索引格式,则不需要添加
if (!this.names.containsValue(genericParamName)) {
param.put(genericParamName, args[(Integer)entry.getKey()]);
}
}
return param;
}
} else {
return null;
}
}
设置SQL参数
BoundSql是保存Sql语句的对象,但是SQL语句中可能包含“?”占位符,每个占位符都对应了BoundSql.parameterMappings集合中的每一个元素,在这个ParameterMapping中记录了对应的参数名称和参数相关的属性。
具体处理参数问题的是在DefaultParameterHandler中的setParameters方法,这个方法就调用PreparedStatement.setXX()为SQL语句绑定实参。
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId());
//SQL中的参数列表
List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();
if (parameterMappings != null) {
for(int i = 0; i < parameterMappings.size(); ++i) {
ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
//过滤掉储存过程中的输出参数
if (parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();//获取参数名称
Object value;//记录绑定的实参
if (this.boundSql.hasAdditionalParameter(propertyName)) {
value = this.boundSql.getAdditionalParameter(propertyName);
} else if (this.parameterObject == null) {
value = null;
} else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {
value = this.parameterObject;
} else {
MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);
value = metaObject.getValue(propertyName);
}
//获取parameterMapping中设置的TypeHandler
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = this.configuration.getJdbcTypeForNull();
}
try {
//为SQL语句绑定相应的实参。
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (SQLException | TypeException var10) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10);
}
}
}
}
}