万字长文了解Mybatis执行流程

前言

关于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方法,在此之前,还会处理缓存,关于缓存的文章,可以看看我以前的博客。

MyBatis一二级缓存

在这个方法中,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);
                    }
                }
            }
        }

    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值