码迷,mamicode.com
首页 > 其他好文 > 详细

MyBatis详细源码解析(中篇)

时间:2020-12-19 13:17:09      阅读:1      评论:0      收藏:0      [点我收藏+]

标签:isp   互动   ann   指定   text   hash   gets   attr   end   

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;
}

MyBatis详细源码解析(中篇)

标签:isp   互动   ann   指定   text   hash   gets   attr   end   

原文地址:https://www.cnblogs.com/SunnyGao/p/14136757.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!