当前位置: 欣欣网 > 码农

面试官:谈谈 Mybatis 源码的执行流程

2024-05-07码农

架构师(JiaGouX)

我们都是架构师!
架构未来,你来不来?

Mybatis 源码执行流程大致分为下面几步:

  • 1、获取配置文件流;

  • 2、解析XML配置文件,构建 SqlSessionFactory

  • 3、利用 SqlSessionFactory 创建 SqlSession 会话;

  • 4、利用 SqlSession 创建 Mapper接口 的代理对象 MapperProxy

  • 5、然后在执行 Mapper接口 的 SQL查询时,转而利用代理对象执行 JDBC的SQL底层操作。

  • 下面的5个小结会对着5个步骤分别分析。

    我们这里只是直接利用Mybatis框架进行操作,没有说利用Spring整合Mybatis之后操作数据库,其实Spring整合Mybatis也是对下面几步的封装。

    @TestpublicvoidtestMyBatisBuild()throws IOException {// 1、获取配置文件流; InputStream input = Resources.getResourceAsStream("SqlSessionConfig.xml");// 2、解析XML配置文件,构建 `SqlSessionFactory`; SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);// 3、利用`SqlSessionFactory`创建 `SqlSession` 会话; SqlSession sqlSession = sessionFactory.openSession();// 4、利用`SqlSession` 创建 `Mapper接口` 的代理对象 `MapperProxy`; TestMapperDao mapper = sqlSession.getMapper(TestMapperDao. class);// 5、然后在执行 `Mapper接口`的 SQL查询时,转而利用代理对象执行 JDBC的SQL底层操作。 System.out.println(mapper.selectByPrimaryKey(1));}


    一、获取配置文件流


    // 1、获取配置文件流;InputStream input = Resources.getResourceAsStream("SqlSessionConfig.xml");

    这个没什么好说的,就是将配置文件以流的形式读入内存。


    二、构建 SqlSessionFactory


    // 2、解析XML配置文件,构建 `SqlSessionFactory`;SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);

    其实这里 .build(input) 利用了设计模式的建造者模式构建对象;我们进入 .build(input) 方法:

    public classSqlSessionFactoryBuilder{public SqlSessionFactory build(InputStream inputStream){return build(inputStream, null, null); }/** * 该方法的作用就是:建造一个SqlSessionFactory对象 * 建造一个SqlSessionFactory对象 需要经历下面三步: * (1)传入配置文件,创建一个 XMLConfigBuilder类准备对配置文件展开解析。 * (2)解析配置文件,得到配置文件对应的 Configuration对象。 * (3)根据 Configuration对象,获得一个 DefaultSqlSessionFactory。 * @param inputStream Mybatis配置文件 * @param environment 环境变量 * @param properties 属性信息 * @return */public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties){try {//不用说,下面两句是核心代码 XMLConfigBuilder(XML配置文件的 构造者)// 1.传入配置文件,创建一个XMLConfigBuilder类 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);// 2、解析配置文件,得到配置文件对应的Configuration对象// 3、根据Configuration对象,获得一个DefaultSqlSessionFactoryreturn build(parser.parse()); } catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset();try { inputStream.close(); } catch (IOException e) {// Intentionally ignore. Prefer previous error. } } }}


    流程分为三步:

  • 1、传入配置文件,创建一个XMLConfigBuilder类;

  • 2、解析配置文件,得到配置文件对应的Configuration对象;

  • 3、利用Configuration对象,获得一个DefaultSqlSessionFactory。

  • 下面将分三小节分别讲解。

    2.1 创建XMLConfigBuilder类

    // 1.传入配置文件,创建一个XMLConfigBuilder类
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

    public classXMLConfigBuilderextendsBaseBuilder{
    publicXMLConfigBuilder(InputStream inputStream, String environment, Properties props){
    //
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
    }
    }

    进入: new XPathParser(inputStream, true, props, new XMLMapperEntityResolver())

  • 我们主要是进入到 下面类型的第一个构造方法,然后再看一下 this.document

  • this.document = createDocument(new InputSource(inputStream)); 这里就是将 配置文件的输入流构建成 原生XML Document对象。

  • 下面同时也列出了 XPathParser类中的一些属性信息;目的是为了对 XML整个解析过程有更清晰的认识。

  • public classXPathParser{// 代表要解析的整个XML文档privatefinal Document document;// 是否开启验证privateboolean validation;// EntityResolver,通过它可以声明寻找DTD文件的方法,例如通过本地寻找,而不是只能通过网络下载dtd文件private EntityResolver entityResolver;// MyBatis配置文件中的properties信息private Properties variables;// javax.xml.xpath.XPath工具private XPath xpath; //这玩意是解析XML的publicXPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver){ commonConstructor(validation, variables, entityResolver);// document : 就是 将xml文件节点,内容解析,构建成Document对象this.document = createDocument(new InputSource(inputStream)); }private Document createDocument(InputSource inputSource){// important: this must only be called AFTER common constructor 重要提示:这只能在公共构造函数之后调用// 也就是说在构造方法中,先调用下面的commonConstructor()方法对XPathParser该类中的属性进行初始化操作之后;// 才能调用该方法,原因也很简单,下面会用到该类中的属性信息。try {// DOM文档创建器的工厂 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(validation); factory.setNamespaceAware(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(false); factory.setCoalescing(false); factory.setExpandEntityReferences(true);// DOM文档创建器 DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(entityResolver); builder.setErrorHandler(new ErrorHandler() {@Overridepublicvoiderror(SAXParseException exception)throws SAXException {throw exception; }@OverridepublicvoidfatalError(SAXParseException exception)throws SAXException {throw exception; }@Overridepublicvoidwarning(SAXParseException exception)throws SAXException { } });return builder.parse(inputSource); // 这里就是将xml内容解析成 Document对象 } catch (Exception e) {thrownew BuilderException("Error creating document instance. Cause: " + e, e); } } }好,我们继续回到 XMLConfigBuilder

    好,我们继续回到 XMLConfigBuilder 类中 this

    private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {// 初始化父类的构造方法 BaseBuilder,// 初始化:configuration(配置类) , typeAliasRegistry(类型别名注册表),typeHandlerRegistry(类型处理器注册表)super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration");// 一些属性信息可能是以 配置文件的方式写的,这里的props就是属性信息this.configuration.setVariables(props);this.parsed = false;this.environment = environment;this.parser = parser; }

    这里面比较重要的一行:

    // 初始化XMLConfigBuilder父类的构造方法 BaseBuilder,// 初始化:configuration(配置类) , typeAliasRegistry(类型别名注册表),typeHandlerRegistry(类型处理器注册表)// 创建 Configuration对象,这里先初始化Configuration里面的 typeAliasRegistry注册表// 然后放入 BaseBuilder构造方法 super(new Configuration());

    OK ,XMLConfigBuilder对象构建完成。

    2.2 构建Configuration对象

    parser.parse() 这里是 解析XML配置文件,继续初始化 Configuration对象。

    parse() 还是在 XMLConfigBuilder 对象中。

    public classXMLConfigBuilderextendsBaseBuilder{/** * ★ 解析配置文件的入口方法 * @return Configuration对象 */public Configuration parse(){// 不允许重复解析if (parsed) {thrownew BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true;// 开始解析,根节点是configuration// parser.evalNode("/configuration") 会将 XML节点解析,构建成Node对象,然后再封装成Mybatis定义的XNode对象// 然后parseConfiguration(parser.evalNode("/configuration")) 继续解析,并放入到 configuration对象对应的属性中! parseConfiguration(parser.evalNode("/configuration"));// 这个 configuration属性 可是在BaseBuilder中的!!!return configuration; }}

    我们查看:parseConfiguration(parser.evalNode("/configuration"));

    下面我们就可以看到,XML文件中各种节点属性信息、值都会被解析,然后放入到 configuration 对象中。

    /** * 这里是解析配置文件的起始方法,解析的所有信息都放入到了configuration中 * @param root */privatevoidparseConfiguration(XNode root) {try {//issue #117 read properties first propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); loadCustomLogImpl(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); //所有的 settings 节点中的属性都放在这这里解析了// read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) {thrownew BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); }}

    以解析 mappers 节点为例,分析对一个节点是如何解析的: mapperElement(root.evalNode("mappers"));

    /** * 对配置文件中的 mappers节点进行解析 * ege: * <mappers> * <mapper resource="com.yogurt.example.mapper.TestMapperDao"/> * <package name="com.yogurt.example.mapper"/> * </mappers> * @param parent mappers节点内容 */privatevoid mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {//下面对 mappers节点下不同的子节点进行不同的处理//1.对package子节点的处理if ("package".equals(child.getName())) {// 取出包的路径String mapperPackage = child.getStringAttribute("name");// 全部加入Mappers中 configuration.addMappers(mapperPackage); } else { //2.对 mapper 字节的处理// 下面三个 resource,url,mapper class只能有一个生效,// 就是获取的 mapper节点上的属性,用于获取 xxxMapper.xml的路径,// 然后下面的(if/else if/else)就是针对节点属性不同进行不同的处理。String resource = child.getStringAttribute("resource");String url = child.getStringAttribute("url");String mapper class = child.getStringAttribute(" class");if (resource != null && url == null && mapper class == null) { // resource ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource);// ★使用XMLMapperBuilder解析Mapper文件解析: ★ 这应该解析的是对应路径中的接口 继续解析(就是对XML的接口映射文件继续解析) XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } elseif (resource == null && url != null && mapper class == null) { // url ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url);// ★使用XMLMapperBuilder解析Mapper文件 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } elseif (resource == null && url == null && mapper class != null) { // 配置的不是Mapper文件,而是Mapper接口 class<?> mapperInterface = Resources. classForName(mapper class); configuration.addMapper(mapperInterface); } else {thrownew BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } }}

    2.3 获取DefaultSqlSessionFactory对象

    build(parser.parse());

    public classSqlSessionFactoryBuilder{/** * 根据配置信息建造一个SqlSessionFactory对象; * 这里可以看到构建的对象是SqlSessionFactory子类实现类: DefaultSqlSessionFactory * @param config 配置信息 * @return SqlSessionFactory对象 */public SqlSessionFactory build(Configuration config){returnnew DefaultSqlSessionFactory(config); }}

    OK, SqlSessionFactory 构建完成。


    三、创建 SqlSession 会话

    // 3、利用 SqlSessionFactory 创建 SqlSession 会话;SqlSession sqlSession = sessionFactory.openSession();

    public classDefaultSqlSessionFactoryimplementsSqlSessionFactory{@Overridepublic SqlSession openSession(){return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit){ Transaction tx = null;try {// 找出要使用的指定环境final Environment environment = configuration.getEnvironment();// 从环境中获取事务工厂final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);// 从事务工厂中生产事务 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 创建执行器final Executor executor = configuration.newExecutor(tx, execType);// 创建DefaultSqlSession对象returnnew DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally {// ErrorContext 类是一个错误上下文,它能够提前将一些背景信息保存下来。// 这样在真正发生错误时,便能将这些背景信息提供出来,进而给我们的错误排查带来便利。//// 这里整个 初始化Session完成,就将该上下文信息重置(清理) ErrorContext.instance().reset(); } }}



    四、创建代理对象 MapperProxy


    TestMapperDao mapper = sqlSession.getMapper(TestMapperDao. class);

    我们可以看到:sqlSession 类型是 : DefaultSqlSession

    往里跟: sqlSession.getMapper(TestMapperDao. class);

    /** * SqlSession的默认实现。请注意,此类不是线程安全的 * * 可以看到该类提供了 查询、增加、更新、删除、提交、回滚等方法。 * 其实这就就是提供给用户,对外的。 * 执行工作不在这里执行,而是交给了executor包 * * session包是整个 MyBatis应用的对外接口包,而 executor包是最为核心的执行器包。 * DefaultSqlSession 类做的主要工作则非常简单——把接口包的工作交给执行器包处理。 */public classDefaultSqlSessionimplementsSqlSession{// 配置信息privatefinal Configuration configuration;// 执行器 (下面的操作语句都交由执行去执行)privatefinal Executor executor;// 是否自动提交privatefinalboolean autoCommit;// 缓存是否已经污染privateboolean dirty;// 游标列表private List<Cursor<?>> cursorList;@Overridepublic <T> T getMapper( class<T> type){return configuration.getMapper(type, this); }}

    继续往下跟: configuration.getMapper(type, this);

    public Configuration{public <T> T getMapper( class<T> type, SqlSession sqlSession) {// 在 mapper的注册表中 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口 生成一个代理实现return mapperRegistry.getMapper(type, sqlSession); }}

    跟:

    /** * 用于解决:抽象方法与数据库操作节点之间的关联,就是通过该MapperRegistry里面的 knownMappers属性 * 1.因为 MapperMethod 解决了 如果通过一个方法去执行SQL(就是将一个SQL转为为一个方法) * 2.因为 MapperProxy、MapperProxyFactory 通过动态代理解决了 * 映射接口(ege:UserMapper.java)的方法去执行(数据库操作的方法); * (因为 UserMapper.java本身是一个接口,没有具体的实现类,如果找到SQL数据库操作,就是通过MapperProxyFactory、MapperProxy) * */public classMapperRegistry{privatefinal Configuration config;// knownMappers维护了 映射接口与MapperProxyFactory关联起来// key: 映射接口 (UserMapper. class)// value: 对应的 MapperProxyFactory对象// 而这个MapperProxyFactory 作为代理工厂,里面有封装了 MapperProxy,这个MapperProxy,一个对一个,对应MapperMethod;// 同时MapperProxyFactory工厂里面还有一个 methodCache(这是一个map)维护了当前 代理接口的所有方法(MapperMethod)privatefinal Map< class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();/** * 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口 生成一个代理实现 * @param type 映射接口 * @param sqlSession sqlSession * @param <T> 映射接口类型 * @return 代理实现对象 *//** * TestMapperDao mapper = sqlSession.getMapper(TestMapperDao. class); * 这里sqlSession.getMapper(***. class)最终也会来到这里 */@SuppressWarnings("unchecked")public <T> T getMapper( class<T> type, SqlSession sqlSession){// 1、找出指定映射接口的代理工厂// ★ 这里我们主要看属性: knownMappers的 (key,value)值final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {thrownew BindingException("Type " + type + " is not known to the MapperRegistry."); }try {//2、通过mapperProxyFactory返回一个对应映射接口的代理器实例return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) {thrownew BindingException("Error getting mapper instance. Cause: " + e, e); } }}

    1、 knownMappers.get(type); 我们代码跟到这里看一下:

    可以看到,每一个 Mapper接口都有与之对应的代理工厂。

    2、利用对应的代理工厂创建代理对象: mapperProxyFactory.newInstance(sqlSession);

    跟着代码可以来到 MapperProxyFactory类,来到代理创建的位置。

    public classMapperProxyFactory<T> {// mapperInterface:// 对应SQL的Java映射接口(ege: UserMapper.java)// methodCache:// 该map缓存的是映射接口的方法,但是这里没有put操作,// 也就说,这里一个mapperInterface,一个methodCache,而且methodCache里面只对应mapperInterface里面的一个方法,没有多个。privatefinal class<T> mapperInterface;privatefinal Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();public T newInstance(SqlSession sqlSession){//这里就是为 映射接口创建代理对象final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy); }/** * 这里返回的应该是一个 代理(映射接口)对象 */@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy){// 三个参数分别是:// 创建代理对象的类加载器、要代理的接口(映射接口)、代理类的处理器(映射接口中的方法所绑定的SQL)。return (T) Proxy.newProxyInstance(mapperInterface.get classLoader(), new class[] { mapperInterface }, mapperProxy); }}

    至此,代理对象创建完成,代理对象的类型为 :MapperProxy,我们可以返回 main 方法中查看,可以看到此时的 TestMapperDao 已经是代理类型了,那么,下一步,执行 mapper.selectByPrimaryKey(1) 就是由代理对象执行了。


    五、代理对象执行目标方法


    // 5、然后在执行 `Mapper接口`的 SQL查询时,转而利用代理对象执行 JDBCSQL底层操作。System.out.println(mapper.selectByPrimaryKey(1));

    /** * 首先是MapperMethod已经完成方法和SQL语句之间绑定,将数据库操作(SQL * 语句)转化(这里的转化有反转之意)为MapperMethod中的execute()方法。 * * 该类基于动态代理将针对 映射接口的方法(mapper.java中的方法)调用转接成了 * 对MapperMethod对象execute方法的调用,进而实现了数据库操作。 * 该类实现了InvocationHandler,这意味着当使用它的实例替代被代理对象后, * 对被代理对象的方法调用会被转接到MapperProxy中invoke方法上 (√ 没毛病) */public classMapperProxy<T> implementsInvocationHandler, Serializable{privatestaticfinallong serialVersionUID = -6424540398559729838L;privatefinal SqlSession sqlSession;// mapperInterface:映射接口,methodCache:映射接口中的方法所对应的SQL方法privatefinal class<T> mapperInterface;// 该Map的键为映射接口中的方法,值为MapperMethod对象。// 通过该属性,完成了MapperProxy内(即映射接口内)方法和MapperMethod的绑定// 也就是一个 抽象方法(ege:UserMapper.java中的一个抽象方法)对应一个MapperMethod对象// 这个map作为:映射接口方法和MapperMethod,第一次调用会put进入,再次调用的话就直接在该map中寻找了privatefinal Map<Method, MapperMethod> methodCache;/** * 这里是 代理方法 * @param proxy * @param method 被代理的方法 * @param args 被代理方法的参数 * @return 方法执行结果 * @throws Throwable */@Overridepublic Object invoke(Object proxy, Method method, Object[] args)throws Throwable {try {if (Object. class.equals(method.getDeclaring class())) { // 继承自Object的方法// 直接执行原有方法return method.invoke(this, args); } elseif (method.isDefault()) {// 执行默认方法return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t); }// 1、找到对应的MapperMethod对象final MapperMethod mapperMethod = cachedMapperMethod(method);// 调用MapperMethod中的execute方法// 2、就是用于执行数据库操作的方法!return mapperMethod.execute(sqlSession, args); }}

    5.1 找到对应的MapperMethod对象

    5.2 用于执行数据库操作的方法

    mapperMethod.execute(sqlSession, args);

    /** * 每个 MapperMethod对象都对应了一个数据库操作节点(即一次SQL操作,换句话说,一个接口映射方法就对应一个MapperMethod), * 调用 MapperMethod实例中的 execute方法就可以触发节点中的 SQL语句。 * * MapperMethod类将一个数据库操作语句和一个Java方法绑定在一起: * MethodSignature属性保存了这个方法的详细信息;SqlCommand属性持有这个方法对应的SQL语句。 * 所以,对接方法的操作:MethodSignature,对接SQL的操作在SqlCommand; * 所以,只要调用 MapperMethod对象的 execute方法,就可以触发具体的数据库操作,★于是数据库操作就被转化为了方法。 */public class MapperMethod {// SqlCommand、MethodSignature这两个是该类(MapperMethod)中的两个内部类// SqlCommand内部类指代一条SQL语句// ege:// {// "name":"org.apache.ibatis.binding.BoundBlogMapper.selectBlogUsingConstructorWithResultMap",// "type":"SELECT"// }private final SqlCommand command;// MethodSignature 内部类的属性详细描述了一个方法的细节。private final MethodSignature method;/** * 每个MapperMethod对象都对应了一个数据库操作节点,调用MapperMethod实例中的execute方法就可以触发节点中的SQL语句。 * 这个方法会被执行: * 可以看到,这里里面由于 增、删、改、查 * @param sqlSession * @param args * @return */publicObject 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()) { // 方法返回值为void,且有结果处理器 executeWithResultHandler(sqlSession, args); result = null; } elseif (method.returnsMany()) { //多条结果查询 result = executeForMany(sqlSession, args); } elseif (method.returnsMap()) { //Map结果查询 result = executeForMap(sqlSession, args); } elseif (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.get class()))) { result = Optional.ofNullable(result); } }break;case FLUSH: //清缓存 result = sqlSession.flushStatements();break;default: //未知thrownew BindingException("Unknown execution method for: " + command.getName()); }if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {// 查询结果为null,但返回类型为基本类型。因此返回变量无法接收查询结果,抛出异常。thrownew BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); }return result; }}

    我们这里是单条数据结果查询:就是其为例。

    result = sqlSession.selectOne(command.getName(), param);

    sqlSession 类型是 DefaultSqlSession

    public classDefaultSqlSessionimplementsSqlSession{ @Overridepublic <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) {returnlist.get(0); } elseif (list.size() > 1) {thrownew TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else {returnnull; } } @Overridepublic <E> List<E> selectList(String statement, Object parameter) {// 接着跟这一行,走到最终下面的方法return this.selectList(statement, parameter, RowBounds.DEFAULT); }/** * 查询结果列表 * @param <E> 返回的列表元素的类型 * @param statement SQL语句 * @param parameter 参数对象 * @param rowBounds 翻页限制条件 * @return 结果对象列表 */ @Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {// 每一个MappedStatement对象对应了我们设置的一个数据库操作节点,它主要定义了数据库操作语句、输入/输出参数等信息// configuration.getMappedStatement(statement) : 将 要执行的MappedStatement对象从Configuration对象存储的映射文件信息中找出来//// 获取查询语句 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(); } } }


    5.2.1 交由执行器,执行SQL操作

    // 交由执行器进行查询returnexecutor.query(mswrapCollection(parameter), rowBoundsExecutor.NO_RESULT_HANDLER);

    executor 类型为 CachingExecutor ,我们往下跟:

    public classCachingExecutorimplementsExecutor{// 被装饰的执行器privatefinal Executor delegate;// 事务缓存管理器privatefinal TransactionalCacheManager tcm = new TransactionalCacheManager();@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)throws SQLException {//1. BoundSql是经过层层转化后去除掉 if、where等标签的 SQL语句 BoundSql boundSql = ms.getBoundSql(parameterObject);//2. CacheKey是为该次查询操作计算出来的缓存键 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);//3. 执行查询操作return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }}

    1、 boundSql 值:

    2、key值

    CacheKey是为该次查询操作计算出来的缓存键

    3、查询数据库中的数据

    这里涉及到:

    先去 二级缓存 查找数据,如果有,返回:

  • 一级缓存没有,执行数据查询操作。

  • 二级缓存没有,去一级缓存查找,一级缓存有,返回

  • 如果我们设置了缓存的话, 那么我们就要在执行数据库操作之后,将其放入到缓存中。

    5.2.1.1 二级缓存 查找数据

    /** * 查询数据库中的数据 * @param ms 映射语句 * @param parameterObject 参数对象 * @param rowBounds 翻页限制条件 * @param resultHandler 结果处理器 * @param key 缓存的键 * @param boundSql 查询语句 * @param <E> 结果类型 * @return 结果列表 * @throws SQLException */@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// 获取MappedStatement对应的缓存,可能的结果有:该命名空间的缓存、共享的其它命名空间的缓存、无缓存 Cache cache = ms.getCache();// 如果映射文件未设置<cache>或<cache-ref>则,此处cache变量为nullif (cache != null) { // 存在缓存// 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存 flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) { // 该语句使用缓存且没有输出结果处理器// 二级缓存不支持含有输出参数的CALLABLE语句,故在这里进行判断 ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked")// 从缓存中读取结果List<E> list = (List<E>) tcm.getObject(cache, key);// 说明缓存中没有相应数据if (list == null) {// 交给被包装的执行器执行// 首先: 当前类CachingExecutor本身是二级缓存,然后 来到这里说明在二级缓存中没有找到数据,// 这里应该就会调用做为一级缓存的BaseExecutor查询数据库。list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 缓存 被包装执行器返回的结果 tcm.putObject(cache, key, list); // issue #578 and #116 }returnlist; } }// 交由 被包装的实际执行器 执行return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

    5.2.1.2 一级缓存 查找数据

    二级缓存没有的话,执行 delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

    去一级缓存查找数据;

    这里是到了 执行的基类: `org.apache.ibatis.executor.BaseExecutor`

    /** * 查询数据库中的数据; * 该方法大致的流程是:会尝试读取一级缓存,而在缓存中无结果时,则会调用queryFromDatabase方法进行数据库中结果的查询 * @param ms 映射语句 * @param parameter 参数对象 * @param rowBounds 翻页限制条件 * @param resultHandler 结果处理器 * @param key 缓存的键 * @param boundSql 查询语句 * @param <E> 结果类型 * @return 结果列表 * @throws SQLException */@SuppressWarnings("unchecked")@Overridepublic <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) {thrownew ExecutorException("Executor was closed."); }// 新的查询栈且要求清除缓存if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); // 清除一级缓存 }List<E> list;try { queryStack++;// 尝试从本地缓存获取结果list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {// 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上 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();// 如果本地缓存的作用域为STATEMENT,则立刻清除本地缓存if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482 clearLocalCache(); } }returnlist;}

    5.2.1.3 数据库查找数据

    // 本地缓存没有结果,故需要查询数据库list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);/** * 从数据库中查询结果 * @param ms 映射语句 * @param parameter 参数对象 * @param rowBounds 翻页限制条件 * @param resultHandler 结果处理器 * @param key 缓存的键 * @param boundSql 查询语句 * @param <E> 结果类型 * @return 结果列表 * @throws SQLException */private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;// MyBatis先在缓存中放置一个占位符,然后调用doQuery方法实际执行查询操作。// 最后,又把缓存中的占位符替换成真正的查询结果(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); }returnlist;}/** * 该方法中,不论下面的那个实现类实现的次方法,都有这样几步: * 1. 生成Statement对象stmt;Statement类并不是MyBatis中的类,而是JDK java.sql包中的类,Statement类能够执行静态SQL语句并返回结果。 * 2. 通过Configuration的newStatementHandler方法获得了一个StatementHandler对象handler, * 然后将查询操作交给StatementHandler对象进行,StatementHandler是一个语句处理器类,其中封装了很多语句操作方法。 * 3. handler.query(stmt, resultHandler); * 会进入到 StatementHandler接口下面的某一个实现类中(这里进入PreparedStatementHandler) * 里面的 ps.execute(), ps类型是 PreparedStatement(这个已经是 com.mysql.cj.jdbc包中的类负责)的了 */protectedabstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

    我们这里单个查询的实现类是:

    org.apache.ibatis.executor.SimpleExecutor#doQuery

    public classSimpleExecutorextendsBaseExecutor{@Overridepublic <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); stmt = prepareStatement(handler, ms.getStatementLog());// SQL语句的处理方式:// handler有下面4种类型:// CallableStatementHandler,PreparedStatementHandler,// SimpleStatementHandler,RoutingStatementHandlerreturn handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }}


    这里是: PreparedStatementHandler 类型

    public classPreparedStatementHandlerextendsBaseStatementHandler{@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler)throws SQLException { PreparedStatement ps = (PreparedStatement) statement;//这里是 com.mysql.cj.jdbc包中的类负责;//执行的结果,我们可以在 ps对象中找到: (这里是debug模式,查看ps对象中的值)// ->h// ->statement// ->results// ->(catalog:数据库名;// connection:连接对象,里面有数据库明,地址,端口,账号,密码等信息;// rowData:rows记录了该次数据库查询的结果信息) ps.execute();//这里最终数据库查询得到的结果交给 ResultHandler对象处理return resultSetHandler.handleResultSets(ps); }}

    OK,到这里就要调用 JDBC的PreparedStatement执行SQL(数据库操作)了。

    然后再层层返回,执行流程走完。

    完结撒花。


    六、目标方法的注册

    这里单独介绍一下 Mapper接口如何和代理类绑定起来的;以及如何注册到 Configuration中的,因为只有这些Mapper接口信息以及接口中的方法信息注册了,我们才能在会话中用。

    其实就是在 2.2节中解析 mapper 节点的一个步骤:

    // ★使用XMLMapperBuilder解析Mapper文件解析: ★ 这应该解析的是对应路径中的接口 继续解析(就是对XML的接口映射文件继续解析)XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());mapperParser.parse();

    XMLMapperBuilder 就是 Mapper文件方式的解析类。

    我们进入该类的 parse() 方法:

    /** * 解析映射文件 */publicvoidparse() {//该节点是否被解析过if (!configuration.isResourceLoaded(resource)) {// 处理mapper节点 configurationElement(parser.evalNode("/mapper"));// 加入到已解析列表 configuration.addLoadedResource(resource);// 将mapper注册给 configuration bindMapperForNamespace(); }// 下面分别用来处理暂时性失败的<resultMap>、<cache-ref>、SQL语句// ege:// <resultMap id="girlUserMap" type="Girl" extends="userMap">// <result property="email" column="email"/>// </resultMap>// <resultMap id="userMap" type="User" autoMapping="false">// <id property="id" column="id" javaType="Integer"/>// <result property="name" column="name"/>// </resultMap>// 由于映射文件(xxxMapper.xml)节点的解析顺序是由上往上的;// 当先解析girlUserMap的时候,extends="userMap"引用的是id="userMap";// 可是,id="userMap"的resultMap(由于在下面)还未被读入,此时就会出现暂时性的错误。// 这里就是处理这种场景的!!!// (由于已经在第一遍解析时读入了所有节点,因此第二遍解析的时候可以解决这种依赖。) parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements();}作者:烤包子链接:https://juejin.cn/post/7348762390387507240来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    关于 mapper解析过程中的细枝末节,这里不做关注,想看的话,可以到博主源码工程中去看。

    这里直接看 bindMapperForNamespace(); 注册 对应Mapper的代理工厂

    privatevoid bindMapperForNamespace() {Stringnamespace = builderAssistant.getCurrentNamespace();if (namespace != null) { class<?> boundType = null;try { boundType = Resources. classForName(namespace); } catch ( classNotFoundException e) {//ignore, bound type is not required }if (boundType != null) {if (!configuration.hasMapper(boundType)) {// Spring may not know the real resource name so we set a flag// to prevent loading again this resource from the mapper interface// look at MapperAnnotationBuilder#loadXmlResource configuration.addLoadedResource("namespace:" + namespace);// 这里注册!!! configuration.addMapper(boundType); } } }}

    跟: configuration.addMapper(boundType);

    这里,我们要注意,这里是 Configuration类中的 方法,并且这里是 mapperRegistry 属性中添加 mapper 的代理工厂。

    好的, 找到了,与第四节创建代理对象时,先从 mapperRegistry 注册表中获取对应mapper的代理工厂构成对应关系了。

    public class Configuration {// 这里是注册 mapper的代理工厂的。public <T> void addMapper( class<T> type) { mapperRegistry.addMapper(type); }// 第四节时候用的这个方法获取的!!!// 与第四节创建代理对象时,先从`mapperRegistry`注册表中获取对应mapper的代理工厂构成对应关系了。public <T> T getMapper( class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession); } }

    跟: mapperRegistry.addMapper(type);

    注意看里面的 knownMappers 属性。

    /** * 用于解决:抽象方法与数据库操作节点之间的关联,就是通过该MapperRegistry里面的 knownMappers属性 * 1.因为 MapperMethod 解决了 如何通过一个方法去执行SQL(就是将一个SQL转为为一个方法) * 2.因为 MapperProxy、MapperProxyFactory 通过动态代理解决了 * 映射接口(ege:UserMapper.java)的方法去执行(数据库操作的方法); * (因为 UserMapper.java本身是一个接口,没有具体的实现类,如何找到SQL数据库操作,就是通过MapperProxyFactory、MapperProxy) * */public class MapperRegistry {// knownMappers维护了 映射接口与MapperProxyFactory关联起来// key: 映射接口 (UserMapper. class)// value: 对应的 MapperProxyFactory对象// 而这个MapperProxyFactory 作为代理工厂,里面又封装了 MapperProxy,这个MapperProxy一个对一个,对应MapperMethod;// 同时MapperProxyFactory工厂里面还有一个methodCache(这是一个map)维护了当前 代理接口的所有方法(MapperMethod)private final Map< class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();/** * 该方法就是为映射接口 创建代理工厂并将该工厂加入 knownMappers中 * 然后后面在 上面的getMapper()方法中使用该工厂(是这样使用的:在getMapper()方法中①+②(②就是通过代理工厂创建代理对象)) */public <T> void addMapper( class<T> type) {// 只有是接口的时候才可以添加if (type.isInterface()) {//判断是否已经注册过了if (hasMapper(type)) {thrownew BindingException("Type " + type + " is already known to the MapperRegistry."); }boolean loadCompleted = false;try {//★添加 Mapper对应的代理的工厂 knownMappers.put(type, new MapperProxyFactory<>(type));// It's important that the type is added before the parser is run// otherwise the binding may automatically be attempted by the// mapper parser. If the type is already known, it won't try.// 必须在运行解析器之前添加类型,否则映射器解析器可能会自动尝试绑定。如果类型已知,则不会尝试。// 这里应该是注解方式的处理 : 是的没错// 这里是构造一个 MapperAnnotationBuilder,// 如果 映射接口中有注解方式的SQL语句:@Select/@SelectProvider...删改查;就会用该类中的parse()方法进行解析//// ★不,不,解析的不止是注解方式的,还有配置文件方式,parse()会有判断步骤分开处理。 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally {if (!loadCompleted) { knownMappers.remove(type); } } } }}

    这里,可以看到注册了 Mapper接口对应的代理工厂。

    //★添加 Mapper对应的代理的工厂knownMappers.put(type, new MapperProxyFactory<>(type));

    其实这里就完结了!!!

    如喜欢本文,请点击右上角,把文章分享到朋友圈
    如有想了解学习的技术点,请留言给若飞安排分享

    因公众号更改推送规则,请点「在看」并加「星标」 第一时间获取精彩技术分享

    ·END·

    相关阅读:

    作者:烤包子

    来源:https://juejin.cn/post/7348762390387507240

    版权申明:内容来源网络,仅供学习研究,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

    架构师

    我们都是架构师!

    关注 架构师(JiaGouX),添加「星标」

    获取每天技术干货,一起成为牛逼架构师

    技术群请 加若飞: 1321113940 进架构师群

    投稿、合作、版权等邮箱: [email protected]