• MyBatis详细源码解析(中篇)


    XMLStatementBuilder类中的parseStatementNode方法是真正开始解析指定的SQL节点。

    从上文中可知context就是SQL标签对应的XNode对象,该方法前面大部分内容都是从XNode对象中获取各个数据。其实该方法的大致意思就是解析这个SQL标签里的所有数据(SQL语句以及标签属性),并把所有数据通过addMappedStatement这个方法封装在MappedStatement这个对象中。这个对象中封装了一条SQL所在标签的所有内容,比如这个SQL标签的id、SQL语句、输入值、输出值等,我们要清楚一个SQL的标签就对应一个MappedStatement对象。

    public void parseStatementNode() {
        // 通过XNode对象获取标签的各个数据
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            return;
        }
        String nodeName = context.getNode().getNodeName();
    	
        // 省略其他内容...
        
        // 获取SQL语句并封装成一个SqlSource对象
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        String keyColumn = context.getStringAttribute("keyColumn");
        String resultSets = context.getStringAttribute("resultSets");
    
        // 将SQL标签的所有数据添加至MappedStatement对象中
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                            resultSetTypeEnum, flushCache, useCache, resultOrdered,
                                            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }
    

    讲解一下关于SQL语句的获取,我们着重关注这一行代码:

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    

    这里LanguageDriver接口实现类是XMLLanguageDriver,再进入createSqlSource方法,又是熟悉的XxxBuilder对象,很明显是用来解析SQL内容的。parseScriptNode方法最终会创建一个RawSqlSource对象,里面存储一个BoundSql对象,而BoundSql对象才是真正存储SQL语句的类。

    在创建RawSqlSource对象的时候,会调用GenericTokenParser类中的parse方法来解析SQL语句中的通用标记(GenericToken),比如“#{ }”,并且将所有的通用标记都变为“?”占位符。

    // XMLLanguageDriver类
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }
    
    // XMLScriptBuilder类
    public SqlSource parseScriptNode() {
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource;
        if (isDynamic) {
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
            // 创建的是RawSqlSource对象,里面存有一个BoundSql对象
            sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
    }
    
    // 真正存储SQL语句以及参数的对象
    public class BoundSql {
        private final String sql;
        private final List<ParameterMapping> parameterMappings;
        private final Object parameterObject;
        private final Map<String, Object> additionalParameters;
        private final MetaObject metaParameters;
        
        // 省略其他内容...
    }
    

    进入到addMappedStatement方法,又是一个很长的方法。很明显这里又使用了建造者模式,依靠MappedStatement类中的一个内部类Builder来构造MappedStatement对象。Configuration类中使用了一个Map集合来存储所有的MappedStatement对象,Key值就是这个SQL标签的id值,我们这里应该就是“getPaymentById”,Value值就是我们创建的对应的MapperStatement对象。

    有个地方需要注意一下,在创建MapperStatement对象前会对id(即接口方法名)进行处理,在id前加上命名空间,也就成了该接口方法的全限定名,因此我们在调用selectOne等方法时应该填写接口方法的全限定名。

    其实我们解析XML文件的目的就是把每个XML文件中的所有增、删、改、查SQL标签解析成一个个MapperStatement,并把这些对象装到Configuration的Map中备用。

    public MappedStatement addMappedStatement(
        String id,
        SqlSource sqlSource,
        StatementType statementType,
        SqlCommandType sqlCommandType,
        Integer fetchSize,
        Integer timeout,
        String parameterMap,
        Class<?> parameterType,
        String resultMap,
        Class<?> resultType,
        ResultSetType resultSetType,
        boolean flushCache,
        boolean useCache,
        boolean resultOrdered,
        KeyGenerator keyGenerator,
        String keyProperty,
        String keyColumn,
        String databaseId,
        LanguageDriver lang,
        String resultSets) {
    
        if (unresolvedCacheRef) {
            throw new IncompleteElementException("Cache-ref not yet resolved");
        }
    
        // 修改存入Map集合的Key值为全限定名
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    
        // 创建MappedStatement的构造器
        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
            .resource(resource)
            .fetchSize(fetchSize)
            .timeout(timeout)
            .statementType(statementType)
            .keyGenerator(keyGenerator)
            .keyProperty(keyProperty)
            .keyColumn(keyColumn)
            .databaseId(databaseId)
            .lang(lang)
            .resultOrdered(resultOrdered)
            .resultSets(resultSets)
            .resultMaps(getStatementResultMaps(resultMap, resultType, id))
            .resultSetType(resultSetType)
            .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
            .useCache(valueOrDefault(useCache, isSelect))
            .cache(currentCache);
    
        ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
        if (statementParameterMap != null) {
            statementBuilder.parameterMap(statementParameterMap);
        }
    
        // 创建MappedStatement对象,并添加至Configuration对象中
        MappedStatement statement = statementBuilder.build();
        configuration.addMappedStatement(statement);
        return statement;
    }
    

    继续回到XMLMapperBuilder类中的parse方法,当解析完一个XML文件后就会把该文件的路径存入loadedResources集合中。

    接下来我们看看bindMapperForNamespace方法,看名字就知道它的作用是通过命名空间绑定mapper。一开始获取名称空间,名称空间一般都是我们mapper的全限定名,它通过反射获取这个mapper的Class对象。Configuration中维护了一个名为knownMappers的Map集合,Key值是我们刚才通过反射创建的Class对象,Value值则是通过动态代理创建的Class对象的代理对象。

    因为knownMappers集合中还没有存入我们创建的Class对象,所以会进入判断语句,它先把名称空间存到我们刚才存XML文件路径名的Set集合中,表示该命名空间已经加载过,然后再把mapper的Class对象存到knownMappers集合中。

    public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
            configurationElement(parser.evalNode("/mapper"));
            // 将解析完的XML文件路径存入Configuration的loadedResources集合中
            configuration.addLoadedResource(resource);
            // 通过名称空间绑定mapper
            bindMapperForNamespace();
        }
        
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
    }
    
    private void bindMapperForNamespace() {
        // 获取到Mapper接口的全限定名
        String namespace = builderAssistant.getCurrentNamespace();
        if (namespace != null) {
            Class<?> boundType = null;
            try {
                // 通过全限定名创建该Mapper接口的Class对象
                boundType = Resources.classForName(namespace);
            } catch (ClassNotFoundException e) {
                // ignore, bound type is not required
            }
            if (boundType != null && !configuration.hasMapper(boundType)) {
    			// 将命名空间的名称存入已加载的Set集合中
                configuration.addLoadedResource("namespace:" + namespace);
                // 将Class对象存入Map集合中
                configuration.addMapper(boundType);
            }
        }
    }
    
    public class Configuration {
        protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    
        public <T> void addMapper(Class<T> type) {
            mapperRegistry.addMapper(type);
        }
    }
    
    public class MapperRegistry {
        private final Configuration config;
        private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
        
        // 添加已注册的Mapper接口的Class对象
        public <T> void addMapper(Class<T> type) {
            if (type.isInterface()) {
                if (hasMapper(type)) {
                    throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
                }
                boolean loadCompleted = false;
                try {
                    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 parser = new MapperAnnotationBuilder(config, type);
                    parser.parse();
                    loadCompleted = true;
                } finally {
                    if (!loadCompleted) {
                        knownMappers.remove(type);
                    }
                }
            }
        }
        
        // 省略其他内容...
    }
    

    在存入Class对象的代理对象后,后面还有一步MapperAnnotationBuilder类的解析工作,我们进入到parse方法,可以看到它会使用Class对象的字符串进行判断该是否已经解析过该Class对象,通常这里是不会进入判断的(下面的mapperClass方式才会进来)。

    public void parse() {
        String resource = type.toString();
        // 判断该Mapper接口的Class对象是否已经解析过
        if (!configuration.isResourceLoaded(resource)) {
            loadXmlResource();
            configuration.addLoadedResource(resource);
    		
            // 省略其他内容...
        }
        parsePendingMethods();
    }
    

    基于resource的方式讲解完毕了,接下来就是url和mapperClass。url的方式和resource一样,这里就不再赘述了。关于mapperClass,这种方式的解析步骤其实和resource是相反的,即先通过反射创建Mapper接口的Class对象,再通过Class对象的全限定名来寻找对应的XML映射文件,

    else if (resource == null && url == null && mapperClass != null) {
        // 反射创建Mapper接口的Class对象
        Class<?> mapperInterface = Resources.classForName(mapperClass);
        // 将该Class对象添加至knownMappers集合中
        configuration.addMapper(mapperInterface);
    }
    
    // Configuration类
    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }
    

    mapperClass方式解析XML映射文件和resource方式略有不同,mapperClass首先使用的是MapperAnnotationBuilder类进行解析,虽然看名字貌似该类只是负责注解的映射,但是其实暗藏玄机。

    // MapperRegistry类
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                knownMappers.put(type, new MapperProxyFactory<>(type));
                // 使用的是MapperAnnotationBuilder解析器
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                // mapperClass方式进行XML映射文件的解析
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
    

    我们进入到该类的parse方法,与resource方式不同的是这里会进入该判断,并且执行了loadXmlResource方法,这个方法就是通过Class对象的名称来加载XML映射文件。loadXmlResource方法中首先也是进行一个判断,这里肯定是没有加载该XML映射文件的命名空间的。后面有一行代码非常关键,它将Class对象的名称中所有的“.”替换为了“/”,然后拼接上了XML文件的后缀,这表示MyBatis会在Mapper接口的同层级目录来寻找对应的XML映射文件。后面的步骤就和之前resource方式一样了,通过XMLMapperBuilder类来解析。

    这也解释了为啥使用mapperClass方式时,XML映射文件要与Mapper接口放在同一层级目录下。

    // MapperAnnotationBuilder
    public void parse() {
        String resource = type.toString();
        if (!configuration.isResourceLoaded(resource)) {
            // 加载XML映射文件
            loadXmlResource();
            // 加载完成后同样存入Map集合中
            configuration.addLoadedResource(resource);
            assistant.setCurrentNamespace(type.getName());
            parseCache();
            parseCacheRef();
            for (Method method : type.getMethods()) {
                if (!canHaveStatement(method)) {
                    continue;
                }
                if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
                    && method.getAnnotation(ResultMap.class) == null) {
                    parseResultMap(method);
                }
                try {
                    parseStatement(method);
                } catch (IncompleteElementException e) {
                    configuration.addIncompleteMethod(new MethodResolver(this, method));
                }
            }
        }
        parsePendingMethods();
    }
    
    // 通过Class对象的名称来加载XML映射文件
    private void loadXmlResource() {
        if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
            // 构建XML映射文件路径
            String xmlResource = type.getName().replace('.', '/') + ".xml";
            // 后续步骤和resource方式一致
            InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
            if (inputStream == null) {
                try {
                    inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
                } catch (IOException e2) {
                    // ignore, resource is not required
                }
            }
            if (inputStream != null) {
                XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
                xmlParser.parse();
            }
        }
    }
    

    至此,单文件映射的加载已经讲解完毕,接下里进入多文件映射的加载!


    最后再讲讲多文件映射的加载。它首先或得XML所在的包名,然后调用configuration的addMappers对象,是不是有点眼熟,单文件映射是addMapper,多文件映射就是addMappers。

    if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
    }
    

    进入addMappers方法,就是通过ResolverUtil这个解析工具类找出该包下的所有mapper的名称并通过反射创建mapper的Class对象装进集合中,然后循环调用addMapper(mapperClass)这个方法,这就和单文件映射的Class类型一样了,把mapper接口的Class对象作为参数传进去,然后生成代理对象装进集合然后再解析XML。

    // Configuration类
    public void addMappers(String packageName) {
        mapperRegistry.addMappers(packageName);
    }
    
    // MapperRegistry类
    public void addMappers(String packageName) {
        addMappers(packageName, Object.class);
    }
    
    public void addMappers(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        for (Class<?> mapperClass : mapperSet) {
            // 后续步骤和单映射文件加载一致
            addMapper(mapperClass);
        }
    }
    
    // ResolveUtil类
    public ResolverUtil<T> find(Test test, String packageName) {
        String path = getPackagePath(packageName);
        try {
            List<String> children = VFS.getInstance().list(path);
            for (String child : children) {
                if (child.endsWith(".class")) {
                    addIfMatching(test, child);
                }
            }
        } catch (IOException ioe) {
            log.error("Could not read package: " + packageName, ioe);
        }
        return this;
    }
    
    protected void addIfMatching(Test test, String fqn) {
        try {
            String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
            ClassLoader loader = getClassLoader();
            if (log.isDebugEnabled()) {
                log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
            }
    
            // 反射创建Class对象并存入Set集合中
            Class<?> type = loader.loadClass(externalName);
            if (test.matches(type)) {
                matches.add((Class<T>) type);
            }
        } catch (Throwable t) {
            log.warn("Could not examine class '" + fqn + "'" + " due to a "
                     + t.getClass().getName() + " with message: " + t.getMessage());
        }
    }
    

    终于把MyBatis的初始化步骤讲完了,这里只是重点讲解了<mappers>节点的解析,还有许多节点比如<environments>、<settings>、<typeAliases>等都大同小异。


    第三步

    这一步的主要目的就是通过之前初始化的SqlSessionFactory实现类来开启一个SQL会话。

    SqlSession sqlSession = sqlSessionFactory.openSession();
    

    可以看出这里SqlSessionFactory实现类为DefaultSqlSessionFactory类。

    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
    

    我们知道SqlSession是我们与数据库互动的顶级接口,所有的增删改查都要通过SqlSession,所以进入DefaultSqlSessionFactory类的openSession方法,而openSession方法又调用了openSessionFromDataSource方法。

    因为我们解析的XML主配置文件把所有的节点信息都保存在了Configuration对象中,它开始直接获得Environment节点的信息,这个节点配置了数据库的连接信息和事务信息。

    之后通过Environment创建了一个事务工厂TransactionFactory,这里其实是实现类JdbcTransactionFactory。然后通过事务工厂实例化了一个事务对象Transaction,这里其实是实现类JdbcTransaction。在JdbcTransaction类中存有我们配置的数据库环境相关信息,例如数据源、数据库隔离界别、数据库连接以及是否自动提交事务。

    public class DefaultSqlSessionFactory implements SqlSessionFactory {
        private final Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            this.configuration = configuration;
        }
    
        @Override
        public SqlSession openSession() {
            return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
        }
        
        private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
            Transaction tx = null;
            try {
                // 通过Configuration对象获取数据库环境对象
                final Environment environment = configuration.getEnvironment();
                // 从数据库环境对象中获取事务工厂对象,调用方法在下面
                final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
                // 根据数据库环境信息创建事务对象
                tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
                final Executor executor = configuration.newExecutor(tx, execType);
                return new 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.instance().reset();
            }
        }
        
        // 从数据库环境对象中获取事务工厂对象,如果没有就新建一个
        private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
            if (environment == null || environment.getTransactionFactory() == null) {
                return new ManagedTransactionFactory();
            }
            return environment.getTransactionFactory();
        }
        
        // 省略其他内容...
    }
    

    默认情况下,不会传入Connection参数,而是等到使用时才通过DataSource创建Connection对象。

    public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
        dataSource = ds;
        level = desiredLevel;
        autoCommit = desiredAutoCommit;
    }
    
    public Connection getConnection() throws SQLException {
        // 使用时才创建Connection
        if (connection == null) {
            openConnection();
        }
        return connection;
    }
    
    protected void openConnection() throws SQLException {
        if (log.isDebugEnabled()) {
            log.debug("Opening JDBC Connection");
        }
        connection = dataSource.getConnection();
        if (level != null) {
            connection.setTransactionIsolation(level.getLevel());
        }
        setDesiredAutoCommit(autoCommit);
    }
    

    重点来了,最后他创建了一个执行器Executor ,我们知道SqlSession是与数据库交互的顶层接口,SqlSession中会维护一个Executor来负责SQL生产和执行和查询缓存等。

    由源码可知最终它是创建一个SqlSession实现类DefaultSqlSession,并且维护了一个Executor实例。

    // DefaultSqlSessionFactory类中关于创建Excutor和SqlSession的代码片段
    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
    
    public class DefaultSqlSession implements SqlSession {
        private final Configuration configuration;
        // 维护了一个Executor实例
        private final Executor executor;
        private final boolean autoCommit;
        private boolean dirty;
        private List<Cursor<?>> cursorList;
        
        // 省略其他内容...
    }
    

    我们再来看看这个执行器的创建过程,其实就是判断生成哪种执行器,defaultExecutorType默认指定使用SimpleExecutor。

    MyBatis有三种的执行器:

    1. SimpleExecutor(默认)。

    2. ReuseExecutor。

    3. BatchExecutor。

    SimpleExecutor:简单执行器。

    是MyBatis中默认使用的执行器,每执行一Update或Select,就开启一个Statement对象(或PreparedStatement对象),用完就直接关闭Statement对象(或PreparedStatment对象)。

    ReuseExecutor:可重用执行器。

    这里的重用指的是重复使用Statement(或PreparedStatement),它会在内部使用一个Map把创建的Statement(或PreparedStatement)都缓存起来,每次执行SQL命令的时候,都会去判断是否存在基于该SQL的Statement对象,如果存在Statement对象(或PreparedStatement对象)并且对应的Connection还没有关闭的情况下就继续使用之前的Statement对象(或PreparedStatement对象),并将其缓存起来。

    因为每一个SqlSession都有一个新的Executor对象,所以我们缓存在ReuseExecutor上的Statement作用域是同一个SqlSession。

    BatchExecutor:批处理执行器。

    用于将多个SQL一次性输出到数据库。

     public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        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);
        }
        if (cacheEnabled) {
            executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
    
  • 相关阅读:
    C# 类型的创建
    C# 中4个访问符和8个修饰符详解
    C#命名空间详解namespace
    ContextMenuStrip 添加在窗体。点击右键不能显示问题解答
    C# 学习笔记 C#基础
    React-Navigation web前端架构
    Css animation 与 float 、flex 布局问题
    javaScript 工作必知(十一) 数组常用方法实现
    Vue 父子组件传值 props
    kafka 参数配置 1
  • 原文地址:https://www.cnblogs.com/SunnyGao/p/14136757.html
Copyright © 2020-2023  润新知