Mybatis 配置文件解析(二)

一、解析前操作

在上一篇文章《Mybatis配置文件解析(一)》中,介绍了Mybatis一些基础的解析,那些都是很基础的设置,MyBatis最重要的文件解析是SQL配置文件的解析,这篇文章将重点介绍Mybatis是如何解析SQL配置文件的

Mybatis中提供了四种配置SQL文件的方式,但最后一种用的比较少,主要是前面三种

<mappers>
    <!--1.必须保证接口名(例如IUserDao)和xml名(IUserDao.xml)相同,还必须在同一个包中-->
    <package name="com.lizhi.mapper"/>

    <!--2.不用保证同接口同包同名-->
     <mapper resource="com/mybatis/mappers/EmployeeMapper.xml"/>

    <!--3.保证接口名(例如IUserDao)和xml名(IUserDao.xml)相同,还必须在同一个包中-->
    <mapper class="com.mybatis.dao.EmployeeMapper"/>

    <!--4.不推荐:引用网路路径或者磁盘路径下的sql映射文件 file:///var/mappers/AuthorMapper.xml-->
     <mapper url="file:E:/Study/myeclipse/_03_Test/src/cn/sdut/pojo/PersonMapper.xml"/>

</mappers>

Mybatis针对这四种不同的配置方式,提供了不同的解析方式,但是在对xml文件的解析是一样,都会调用XMLMapperBuilder的parse()方法来进行解析,下面我们主要介绍通过package方式配置的SQL文件,这方配置方式的解析是最复杂的

/**
 * package
 *     ·解析mapper接口代理工厂(传入需要代理的接口) 解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers
       ·解析mapper.xml  最终解析成MappedStatement 到:org.apache.ibatis.session.Configuration#mappedStatements
 */
mapperElement(root.evalNode("mappers"));

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        // 获取mappers节点下所有的mapper节点
        for (XNode child : parent.getChildren()) {
            //判断我们mapper是不是通过批量注册的 <package name="com.lizhi.mapper"></package>
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                //判断从classpath下读取我们的mapper <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
                String resource = child.getStringAttribute("resource");
                //判断是不是从我们的网络资源读取(或者本地磁盘得) <mapper url="D:/mapper/EmployeeMapper.xml"/>
                String url = child.getStringAttribute("url");
                //解析这种类型(要求接口和xml在同一个包下) <mapper class="com.tuling.mapper.DeptMapper"></mapper>
                String mapperClass = child.getStringAttribute("class");

                //我们得mappers节点只配置了 <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url == null && mapperClass != null) {
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

1.1 遍历所有Mapper接口

下面,我们重点看一下Configuration的addMappers()方法,在该方法中会去调用MapperRegistry类的addMappers(),在该方法中,会根据指定的包名,得到包下面所有的class文件,然后加载这些Class文件,最后遍历所有的Class文件,调用MapperRegistry的addMapper()方法进行解析

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

在上面,最核心的addMapper()方法位于MapperRegistry类中,那么MapperRegistry又是干什么的呢,我们看下它的源码就一目了然了。该类最重要属性就是knownMappers,它记录了每个Mapper接口,以及为这个接口生成代理对象的代理工厂MapperProxyFactory,我们继续往下面看,在解析的时候,就会去为每个接口生成代理工厂

public class MapperRegistry {

    private final Configuration config;
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

    public MapperRegistry(Configuration config) {
        this.config = config;
    }
}

1.2 生成代理工厂

在addMapper()方法中,首先会判断该类是否为接口,只有接口才会进行下一步解析

然后会生成一个MapperProxyFactory实例的代理工厂放入到knownMappers,然后生成一个MapperAnnotationBuilder对象,来解析对应的xml文件和接口方法的注解

public <T> void addMapper(Class<T> type) {
    // 判断我们传入进来的type类型是不是接口
    if (type.isInterface()) {
        // 判断我们的缓存中有没有该类型
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            // 创建一个MapperProxyFactory 把我们的Mapper接口保存到工厂类中, 该工厂用于创建 MapperProxy
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // mapper注解构造器
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            // 进行解析, 将接口完整限定名作为xml文件地址去解析
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

代理工厂具体的用途,我们通过看MapperProxyFactory源码可以看出来,其中mapperInterface属性存的是接口的Class对象,然后methodCache中的MapperMethod里面是对方法信息的封装,包括方法全限定名、该方法的操作的SQL类型(insert|update|delte|select)以及方法的签名这些信息

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        // 创建我们的代理对象
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        // 创建我们的Mapper代理对象返回
        return newInstance(mapperProxy);
    }

}

其中第一个newInstance(MapperProxy mapperProxy)方法用于创建Mapper接口的代理对象,第二个newInstance(SqlSession sqlSession)方法是供外部使用的,我们通过SqlSession的getMapper()方法调用时,就会调用到该方法,其中MapperProxy实现了InvocationHandler接口,所以在方法调用时,会调用到MapperProxy的invoke()方法,具体的调用流程在后面的文章详细介绍

简单介绍一下SqlSession的getMapper()方法,以DefaultSqlSession类为例

会从configuration的mapperRegistry中,根据接口类型,把扫描时生成的MapperProxyFactory拿出来,然后调用它的newInstance(sqlSession)方法来创建代理对象

public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
}

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 直接去缓存knownMappers中通过Mapper的class类型去找我们的mapperProxyFactory
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    // 缓存中没有获取到 直接抛出异常
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        // 通过MapperProxyFactory来创建我们的实例
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

二、解析SQL配置文件

创建完Mapper接口的代理工厂之后,就是真正要来解析这些文件了,生成一个MapperAnnotationBuilder实例,调用parse()方法进行解析

// mapper注解构造器
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 进行解析, 将接口完整限定名作为xml文件地址去解析
parser.parse();

而在parse()方法内部,首先也是调用loadXmlResource()方法去解析Mapper接口对应的xml文件

public void parse() {
    String resource = type.toString();
    // 是否已经解析mapper接口对应的xml
    if (!configuration.isResourceLoaded(resource)) {
        // 根据mapper接口名获取 xml文件并解析,  解析<mapper></mapper>里面所有东西放到configuration
        loadXmlResource();
        // 添加已解析的标记
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        ……
    }
    parsePendingMethods();
}

2.1 加载XML配置

在loadXmlResource()方法中,会根据接口的名称,来拼接xml配置文件的全限定名,这就是为什么在通过package配置mapper接口的时候,需要让接口和xml文件的路径和名称一模一样

然后创建一个XMLMapperBuilder的实例,调用它的parse()方法,在这里方法里面才是真正在解析xml文件

private void loadXmlResource() {
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        String xmlResource = type.getName().replace('.', '/') + ".xml";
        InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
        if (inputStream == null) {
            inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
        }
        if (inputStream != null) {
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
            xmlParser.parse();
        }
    }
}

2.2 解析命名空间

我们在创建XMLMapperBuilder实例的时候,会创建一个MapperBuilderAssistant实例,该实例相当于一个工具类,会把xml解析出来的属性,封装成对应实例,放入到configuration属性中

而在创建XMLMapperBuilder实例的时候,会先把当前的接口名设置为命名空间的名字,后面再去xml中定义的命名空间作比较

public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
    this(inputStream, configuration, resource, sqlFragments);
    this.builderAssistant.setCurrentNamespace(namespace);
}

取出xml文件mapper节点的namespace属性的值,该值不能为空,然后再判断该值是否于之前设置接口名称一致,不满足就会抛异常

// 解析我们的namespace属性 <mapper namespace="com.tuling.mapper.EmployeeMapper">
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
    throw new BuilderException("Mapper's namespace cannot be empty");
}
// 保存我们当前的namespace  并且判断接口完全类名==namespace
builderAssistant.setCurrentNamespace(namespace);

public void setCurrentNamespace(String currentNamespace) {
    if (currentNamespace == null) {
        throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
    }
	// 判断接口名称于xml中配置的命名空间是否一致
    if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
        throw new BuilderException("Wrong namespace. Expected '"
                                   + this.currentNamespace + "' but found '" + currentNamespace + "'.");
    }

    this.currentNamespace = currentNamespace;
}

2.3 解析缓存配置

2.3.1 解析缓存引用

在SQL的xml文件中,可以通过cache-ref节点来引用其他命名空间的缓存配置,通过namespace属性来指定引用的缓存

/**
 * 解析我们的缓存引用
 * 说明我当前的缓存引用和DeptMapper的缓存引用一致
 * <cache-ref namespace="com.lizhi.mapper.DeptMapper"></cache-ref>
      解析到org.apache.ibatis.session.Configuration#cacheRefMap<当前namespace,ref-namespace>
      异常下(引用缓存未使用缓存):org.apache.ibatis.session.Configuration#incompleteCacheRefs
 */
cacheRefElement(context.evalNode("cache-ref"));

解析缓存引用的时候,首先把缓存引用的依赖关系保存在configuration的cacheRefMap属性,key:当前的mapper的接口名称,value:缓存引用到的mapper的接口名称

然后调用resolveCacheRef()方法去设置缓存,如果根据接口名称找不到缓存配置,就会抛出异常,找到了就设置当前mapper的缓存

private void cacheRefElement(XNode context) {
    if (context != null) {
        configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
        CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
        try {
            cacheRefResolver.resolveCacheRef();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteCacheRef(cacheRefResolver);
        }
    }
}
2.3.2 解析缓存配置

可以通过cache节点来开启缓存,只要配置了该节点,就相当于启用了二级缓存,如果没有配置缓存的属性,Mybatis会使用默认的配置

/**
 * 解析我们的cache节点
 * <cache ></cache>
    解析到:org.apache.ibatis.session.Configuration#caches
           org.apache.ibatis.builder.MapperBuilderAssistant#currentCache
 */
cacheElement(context.evalNode("cache"));

可以通过type属性设置缓存的类型,Mybatis中的二级缓存分为分为好几种类型,采用装饰器的模式,文章后面会对Mybatis的二级缓存做详细说明

private void cacheElement(XNode context) {
    if (context != null) {
        //解析cache节点的type属性
        String type = context.getStringAttribute("type", "PERPETUAL");
        // 根据别名(或完整限定名)  加载为Class
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        //获取缓存过期策略:默认是LRU
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        //flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
        Long flushInterval = context.getLongAttribute("flushInterval");
        //size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
        Integer size = context.getIntAttribute("size");
        //只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        //把缓存节点加入到Configuration中
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

获得这些属性之后,builderAssistant实例会把这些属性,封装成一个Cache对象,然后存在configuration的caches属性中,这是一个Map,KEY为mapper引用的接口名

public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props) {
    Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class)).addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props).build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

2.4 解析resultMap

我们可以通过resultMap节点来定义数据库查询字段名与JavaBean中字段名的映射,以及类型处理器

在一个xml文件中,可以定义多个resultMap节点,只要它们的属性id不一样即可,所以在解析的时候,也是去遍历所有的节点,依次解析

// 解析获取到的所有<resultMap>
resultMapElements(context.evalNodes("/mapper/resultMap"));

// 依次解析
private void resultMapElements(List<XNode> list) throws Exception {
    for (XNode resultMapNode : list) {
        resultMapElement(resultMapNode);
    }
}

首先获取resultMap对应的JavaBean类型,有四种方式可以设置

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 从这里可以看出, 类型可以通过这4个属性设置
    String type = resultMapNode.getStringAttribute("type",
                                                   resultMapNode.getStringAttribute("ofType",
                                                                                    resultMapNode.getStringAttribute("resultType",
                                                                                                                     resultMapNode.getStringAttribute("javaType"))));
    // 根据别名 或 完全类名  获取类型
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
        typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    ……
}

resultMap节点提供了通过指定构造方法的参数来进行映射,这种方式平时不怎么用,主要还是通过id节点来配置

得到所有resultMap节点的子节点之后,遍历这些子节点,然后在buildResultMappingFromContext()方法中获取节点的各种配置,然后封装成一个ResultMapping对象,然后把这些对象先添加到resultMappings列表里面

List<ResultMapping> resultMappings = new ArrayList<>();
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
    if ("constructor".equals(resultChild.getName())) {
        processConstructorElement(resultChild, typeClass, resultMappings);
    } else if ("discriminator".equals(resultChild.getName())) {
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    } else {
        List<ResultFlag> flags = new ArrayList<>();
        if ("id".equals(resultChild.getName())) {
            flags.add(ResultFlag.ID);
        }
        // 解析出所有属性构建为ResultMapping添加到resultMappings中(包括重要的: javaType,jdbcType,column,typeHandler)
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
}

最后获取resultMap节点id属性的值,如果没有指定id属性,通过拼装节点名作为id的属性值

最后调用resolve()方法,把resultMappings封装成一个ResultMap对象,添加到configuration的resultMaps属性中,其中KEY为resultMap节点id的属性值

String id = resultMapNode.getStringAttribute("id",
                                             resultMapNode.getValueBasedIdentifier());
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
// 解析到configuration中
return resultMapResolver.resolve();

2.5 解析SQL片段

我们在使用xml的时候,可以把有些SQL的公共部分抽离,作为一个SQL片段,然后再SQL中通过引用片段来降低冗余;SQL片段还可以配置数据库厂商,通一个SQL语句,使用不同的数据,它们的语法可能是不同,也可以通过SQL片段来定义。具体使用通过sql节点来定义片段

/**
 * 解析我们通过sql片段
 *  解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments
 *   其实等于 org.apache.ibatis.session.Configuration#sqlFragments
 *   因为他们是同一引用,在构建XMLMapperBuilder 时把Configuration.getSqlFragments传进去了
 */
sqlElement(context.evalNodes("/mapper/sql"));

把这些SQL片段添加到XMLMapperBuilder对象的sqlFragments属性中,在使用的时候再解析具体节点的内容

private void sqlElement(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
}

// 与Mybatis定义的数据库厂商id做比较,相同或者SQL片段没有指定数据库厂商时,就进行缓存
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        String databaseId = context.getStringAttribute("databaseId");
        String id = context.getStringAttribute("id");
        id = builderAssistant.applyCurrentNamespace(id, false);
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            sqlFragments.put(id, context);
        }
    }
}

2.6 解析SQL语句

获取所有select|insert|update|delete类型的节点,然后遍历这些节点,对节点配置的参数进行解析

/**
 * 解析我们的select | insert |update |delete节点
 * 解析到org.apache.ibatis.session.Configuration#mappedStatements
 */
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

如果全局配置文件配置了数据库厂商ID,那么在解析SQL语句的时候,也要判断select|insert|update|delete这些节点配置的数据库厂商ID是否匹配,只有匹配了才会继续解析

private void buildStatementFromContext(List<XNode> list) {
    // 判断有没有配置数据库厂商ID
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

创建一个xmlStatement的构建器对象,对SQL节点进行解析

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    // 循环我们的select|delte|insert|update节点
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}
2.6.1 检验数据库商场是否匹配
public void parseStatementNode() {
    // insert|delte|update|select 语句的sqlId
    String id = context.getStringAttribute("id");
    // 判断我们的insert|delte|update|select  节点是否配置了数据库厂商标注,匹配当前的数据库厂商id是否匹配当前数据源的厂商id
    String databaseId = context.getStringAttribute("databaseId");
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }
}
2.6.2 获取缓存相关属性

如果没有配置flushCacheuseCache属性,则根据操作类型来取默认值,如果操作类型为Select,那么flushCache就为false,表示不需要刷新缓存,其他类型就需要刷新缓存

如果没有配置useCache,同样根据操作类型来取默认值,Select操作默认使用缓存,其他操作不使用缓存

// 获得节点名称:select|insert|update|delete
String nodeName = context.getNode().getNodeName();
// 根据nodeName 获得 SqlCommandType枚举
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 判断是不是select语句节点
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 获取flushCache属性,默认值为isSelect的反值:查询:flushCache=false   增删改:flushCache=true
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// 获取useCache属性,默认值为isSelect:查询:useCache=true   增删改:useCache=false
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
2.6.3 解析SQL公用片段

创建一个XMLIncludeTransformer实例,调用applyIncludes()方法对SQL语句引入的SQL片段进行解析

/**
 * 解析我们的sql公用片段
 *     <select id="qryEmployeeById" resultType="Employee" parameterType="int">
          <include refid="selectInfo"></include>
          employee where id=#{id}
      </select>
    将 <include refid="selectInfo"></include> 解析成sql语句 放在<select>Node的子节点中
 */
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

SQL片段的值,可以通过全局配置文件来配置

而在SQL片段内部,也可以使用include标签来导入公共SQL片段,所以在解析SQL片段的时候,要递归进行解析

public void applyIncludes(Node source) {
    Properties variablesContext = new Properties();
    // 拿到之前配置文件解析的<properties>
    Properties configurationVariables = configuration.getVariables();
    // 放入到variablesContext中
    Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
    // 替换Includes标签为对应的sql标签里面的值
    applyIncludes(source, variablesContext, false);
}

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    if (source.getNodeName().equals("include")) {
        // 拿到之前解析的<sql>
        Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
        Properties toIncludeContext = getVariablesContext(source, variablesContext);
        // 递归, included=true
        applyIncludes(toInclude, toIncludeContext, true);
        if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
            toInclude = source.getOwnerDocument().importNode(toInclude, true);
        }
        // <include的父节点=select 。  将<select>里面的<include>替换成 <sql> ,那<include>.getParentNode就为Null了
        source.getParentNode().replaceChild(toInclude, source);
        while (toInclude.hasChildNodes()) {
            // 接下来<sql>.getParentNode()=select.  在<sql>的前面插入<sql> 中的sql语句   ,
            toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
        }
        // <sql>.getParentNode()=select  , 移除select中的<sql> Node 。
        //  不知道为什么不直接replaceChild呢?还做2步 先插再删,
        toInclude.getParentNode().removeChild(toInclude);
        int i=0;
    } else if (source.getNodeType() == Node.ELEMENT_NODE) { // 0
        if (included && !variablesContext.isEmpty()) {
            // replace variables in attribute values
            NamedNodeMap attributes = source.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
                Node attr = attributes.item(i);
                attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
            }
        }
        NodeList children = source.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            // 递归
            applyIncludes(children.item(i), variablesContext, included);
        }
        // included=true 说明是从include递归进来的
    } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
               && !variablesContext.isEmpty()) {
        // 替换sql片段中的 ${<properties解析到的内容>}
        source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
}
2.6.4 解析SQL语句

在解析SQL语句之前,首先需要获得sql脚本语言驱动,可以在select等操作节点,通过lang属性进行配置,如果不配置默认使用XMLLanguageDriver来进行解析,一般也不需要配置

/* 
<settings>
      <setting name="defaultScriptingLanguage" value="lizhiLang"/>
 </settings>
 */
String lang = context.getStringAttribute("lang");
// 获取自定义sql脚本语言驱动 默认:class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
LanguageDriver langDriver = getLanguageDriver(lang);

然后调用createSqlSource()方法来解析SQL,这个时候并不会直接就把SQL解析成可执行的SQL语句,因为这个时候,SQL语句的参数还没确定。

在这一步,只是将SQL语句解析成层次分明的SqlNode对象

/**
 * 通过class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver来解析我们的
 * sql脚本对象  .  解析SqlNode. 注意, 只是解析成一个个的SqlNode, 并不会完全解析sql,因为这个时候参数都没确定,动态sql无法解析
 */
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

真正解析SQL语句的是XMLScriptBuilder,将SQL语句解析后,生成一个MixedSqlNode,然后判断该SQL是动态SQL还是静态SQL,分别生成不同的SqlSource对象

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
}

public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
        // 动态Sql 就是还需要后续执行时根据传入参数动态解析Sql(因为有<if>等,还要拼接${}sql)和参数ParameterMappings   也会在后续执行解析,因为动态条件肯定会有动态参数
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        // 静态Sql源  如果没有动态标签(<if>、<where>等) 以及 没有${}  就是静态Sql源,静态Sql 就是在这里就解析了Sql  和参数ParameterMappings   后续执行就不用解析了
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    // 其实他们的区别就是动态sql 需要在查询的时候解析 因为有动态sql 和拼接${}
    // 静态sql 已经在这里确定好sql. 和参数ParameterMapping,
    return sqlSource;
}

下面介绍一下,parseDynamicTags()方法是如何把SQL解析成SqlNode的

在Mybatis中,它支持动态SQL,所以SqlNode的类型就包括了StaticTextSqlNode、TextSqlNode、ChooseSqlNode、IfSqlNode、TrimSqlNode(SetSqlNode和WhereSqlNode)、ForEachSqlNode、MixedSqlNode

XMLScriptBuilder在实例化的时候,就为这些SqlNode添加了SqlNode的处理器,这些处理器就是为了解析每个标签下的子标签,最终返回一个树型结构的SqlNode,最后再把这些SqlNode,封装成一个MixedSqlNode

protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();  //获得<select>的子节点
    for (int i = 0; i < children.getLength(); i++) {
        XNode child = node.newXNode(children.item(i));
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            String data = child.getStringBody(""); // 获得sql文本
            TextSqlNode textSqlNode = new TextSqlNode(data);
            if (textSqlNode.isDynamic()) {  // 怎样算Dynamic? 其实就是判断sql文本中有${}
                contents.add(textSqlNode);
                isDynamic = true;
            } else {
                contents.add(new StaticTextSqlNode(data));  //静态文本
            }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
            String nodeName = child.getNode().getNodeName();

            /*** 判断当前节点是不是动态sql节点{@link XMLScriptBuilder#initNodeHandlerMap()}*/
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            handler.handleNode(child, contents);  // 不同动态节点有不用的实现
            isDynamic = true;     // 怎样算Dynamic? 其实就是判断sql文本动态sql节点
        }
    }
    return new MixedSqlNode(contents);
}

2.7 解析返回类型

解析resultTyperesultMapresultSetType的属性值

String resultType = context.getStringAttribute("resultType");
/**解析我们查询结果集返回的类型     */
Class<?> resultTypeClass = resolveClass(resultType);
/**
 * 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。
 * 可以使用 resultMap 或 resultType,但不能同时使用。
 */
String resultMap = context.getStringAttribute("resultMap");

String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
    resultSetTypeEnum = configuration.getDefaultResultSetType();
}

2.8 封装MappedStatement

SQL解析完成之后,就会调用addMappedStatement()方法来生成一个MappedStatement对象,MappedStatement对象的id属性值是由mapper接口名+’.’+insert|delte|update|select节点的id属性构成

最后把MappedStatement对象添加到configuration的mappedStatements中,其中KEY为MappedStatement的id属性值

/**
 * 为insert|delete|update|select节点构建成我们的mappedStatment对象
 */
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                    fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                    resultSetTypeEnum, flushCache, useCache, resultOrdered,
                                    keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值