Mybatis原理2 — mapper映射文件的解析

本文详细介绍了Mybatis配置项,尤其是mapper.xml映射文件的解析过程。内容涉及Configuration、Environment、Mapper、缓存、映射节点、Statement节点及动态标签等,阐述了如何将XML配置解析并封装到Configuration中,以便Mybatis与数据库进行交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

mybatis配置项

Mybtis的全局配置项Configuration或者叫全局配置缓存,整个mybtais框架就是围绕其进行;Configuration封装了数据源连接实例DataSource(用于与db连接交互),封装了执行sql用于statement交互实例执行等

Configuration的配置项封装主要有两部分,第一部分就是环境配置项,比如由XMLConfigBuilder进行parse解析xml配置,也可以通过SpringBoot自动装配MybatisProperties;第二部分就是statement相关,主要就是sql执行命令的封装,使用动态代理以mapper接口的形式通过Statement与db进行交互,mybatis.xml方式是配置mappers节点,也可以通过集成spring的方式@MapperScan注解扫描注册mapper接口的代理类为bean

以mybatis.xml为例解析Configuration全局的配置项

全局配置文件中有mappers属性可以解析mapper.xml文件也保存到Configuration中,将增删改查标签的每一个标签每一个属性也解析出来,封装到一个MapperedStatement中。

//构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = this.build(configBuilder.parse());
//mybatis的xml解析器
private XPathParser parser;

public Configuration parse() {
    //parser.evalNode返回的是xnode实例,parseConfiguration方法从根节点开始解析子节点
    this.parseConfiguration(this.parser.evalNode("/configuration"));
    return this.configuration;	
}

//configuration的子节点
private void parseConfiguration(XNode root) {
    //setting对应的就是configuration的set属性,开启缓存,设置defaultExecutorType(默认REUSE)
 	Properties settings = settingsAsPropertiess(root.evalNode("settings"));
    //解析是否配置了外部properties,例如本例中配置的jdbc.propertis
 	propertiesElement(root.evalNode("properties"));
    //查看数据库环境配置
 	environmentsElement(root.evalNode("environments"));
    //查看是否使用多种数据库
 	databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    //查看是否配置了新的类型处理器,如果跟处理的类型跟默认的一致就会覆盖。
 	typeHandlerElement(root.evalNode("typeHandlers"));
   	//查看是否配置SQL映射文件,有四种配置方式,resource,url,class以及自动扫包package。
 	mapperElement(root.evalNode("mappers"));
}

其中最常用的节点是properties,environments和mappers

Properties

properties节点:构建成一个Properties缓存实例,本质上就是一个全局的map缓存,其他节点比如enviroment也存在property子节点用于封装其应用缓存

  <!-- 方式一:使用resource属性加载properties文件--> 
  <properties resource="dbConfig.properties"></properties> 
  
  <!-- 方式二: property子节点配置项,environments的子节点也可以配置property -->
  <properties>
      <property name="driver" value="com.mysql.jdbc.Driver"/>
      <property name="url" value="jdbc:mysql://localhost:3306/test1"/>
      <property name="username" value="root"/>
      <property name="password" value="root"/>
  </properties>
  
  <!-- 方式三: 直接构建一个Properties实例 -->    
  InputStream in = Resources.getResourceAsStream("dbConfig.properties");
  properties.load(in);
  sqlSessionFactory = new SqlSessionFactoryBuilder().build(resource,properties);  

解析流程

//解析properties的具体方法
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        Properties defaults = context.getChildrenAsProperties();
        //resource和url属性
        String resource = context.getStringAttribute("resource");
        //获取properties节点上 url属性的值
        String url = context.getStringAttribute("url");
        //resource和url不能同时配置     
        //解析properties文件将属性封装到Properties
        if (resource != null) {
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            defaults.putAll(Resources.getUrlAsProperties(url));
        }

        //如果已经存则会覆盖相同key的值
        Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }

        //解析器也存一份
        parser.setVariables(defaults);
        //set进configuration对象
        configuration.setVariables(defaults);
    }
}

Enviroment

environments节点:可以配置多个environment子节点,每个environment配置一套环境, 通过配置environments的default属性就能选择对应的environment了

sqlSessionFactory = new SqlSessionFactoryBuilder().build(environment);

//在构建SqlSessionFactory时指定default环境
<environments default="first">
    //配置第一种环境
    <environment id="first">
         <!--采用JDBC事务管理-->
    	<transactionManager type="JDBC">
        	<property name="autoCommit" value="false"/>
        </transactionManager>
 		<dataSource type="POOLED">
			<property name="driver" value="${driver}"/>
     		<property name="url" value="${url}"/>
			<property name="username" value="${username}"/>
			<property name="password" value="${password}"/>
		</dataSource> 
    </environment>
    //配置第二种环境
    <environment id="second">
    </environment>
</environments>

//遍历environments所有子节点去匹配default对应的环境
private void environmentsElement(XNode context) throws Exception {
    //environment对应的时构建实例时指定的环境名称,也就是default的值
    this.environment = context.getStringAttribute("default");
    //开始遍历所有的environment节点	
    for(XNode child:context.getChildren()){
        //只有enviroment节点有id属性,确保唯一
        String id = child.getStringAttribute("id");
        //id匹配default中的值去找对应的environment,此时才去构建SqlSessionFactory需要的环境
        if (this.isSpecifiedEnvironment(id)) {
            //environment有两个子节点:transactionManager用于配置事务,dataSource时配置连接池
            TransactionFactory txFactory = this.transactionManagerElement(child.evalNode("transactionManager"));
            DataSourceFactory dsFactory = this.dataSourceElement(child.evalNode("dataSource"));
            DataSource dataSource = dsFactory.getDataSource();
            Builder environmentBuilder = (new Builder(id)).transactionFactory(txFactory).dataSource(dataSource);
            this.configuration.setEnvironment(environmentBuilder.build());
        } 
    }
}

transactionManager:事务管理,JDBC类型的JdbcTransaction实现,通过使用jdbc提供的方式来管理事务,通过Connection提供的事务管理方法来进行事务管理.;MANAGED类型的ManagedTransaction实现,通过容器来进行事务管理,对事务提交和回滚并不会做任何操作

//transactionManager节点有一个type属性,有多个通用的property子节点
private TransactionFactory transactionManagerElement(XNode context) {
    //type:JDBC和MANAGED, 配置为JDBC则直接使用JDBC的事务,配置为MANAGED则是将事务托管给容器
    String type = context.getStringAttribute("type");
    //获取transactionManager节点的Properties节点的值
    Properties props = context.getChildrenAsProperties();
    //根据type名称实例化一个对应的TransactionFactory
    TransactionFactory factory = (TransactionFactory)this.resolveClass(type).newInstance();
    factory.setProperties(props);
    return factory;
}

dataSource:数据源类型有:UNPOOLED就是单连接,POOLED连接池,JNDI类型就是spi机制,从context.xml配置中“jdbc/mybatis-jndi"服务名称对应的dataSource

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    String type = context.getStringAttribute("type");
    Properties props = context.getChildrenAsProperties();
    //"POOLED"实例化PooledDataSourceFactory
    DataSourceFactory factory = (DataSourceFactory)this.resolveClass(type).newInstance();
    factory.setProperties(props);
    return factory;
} 

Environment通过环境id,TransactionFactory和DataSource构建

public final class Environment {
  private final String id;
  private final TransactionFactory transactionFactory;
  private final DataSource dataSource;
}
//进一步加工得到对应的DataSource
DataSource dataSource = dsFactory.getDataSource();
          Builder environmentBuilder = (new Builder(id)).transactionFactory(txFactory).dataSource(dataSource);
          this.configuration.setEnvironment(environmentBuilder.build());

Mapper

初始化mapper映射文件的方式

<!-- 方式一:通过resource指定,一个mapper节点对应一个 -->
<mappers>
	<mapper resource="mapper/PersonMapper.xml"/>
</mappers>

<!-- 第二种方式, 通过class指定接口,进而将接口与对应的xml文件形成映射关系
不过,使用这种方式必须保证接口与mapper文件同名(不区分大小写), 
比如PersonMapper,那么意味着mapper文件为PersonMapper.xml 
-->
<mapper class="cn.mytest.mapper.PersonMapper"/>

<!-- 第三种方式,直接指定包,自动扫描,与方法二同理-->
<package name="cn.mytest.mapper"/>

<!-- 第四种方式:通过url指定mapper文件位置 -->
<mapper url="file://........"/>

mappers节点初始化解析

//mappers可子节点就2个,package和mapper
if ("package".equals(child.getName())) {
    resource = child.getStringAttribute("name");
    //依然将解析的数据保存在configuration中,这个过程有点复杂
    this.configuration.addMappers(resource);
} else {
    //map子节点下三种方式添加mapper映射文件
    resource = child.getStringAttribute("resource");
    String url = child.getStringAttribute("url");
    String mapperClass = child.getStringAttribute("class");
    XMLMapperBuilder mapperParser;
    InputStream inputStream;
    //resource不为空,第一种方式
    if (resource != null && url == null && mapperClass == null) {
        inputStream = Resources.getResourceAsStream(resource);
        //mapper映射文件都是通过解析器XMLMapperBuilder解析
        mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
        mapperParser.parse();
    } else if (resource == null && url != null && mapperClass == null) {
        inputStream = Resources.getUrlAsStream(url);
        //url的传参和resource一样都是作为解析映射文件的路径的表示
        mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
        mapperParser.parse();
    } else {
        Class<?> mapperInterface = Resources.classForName(mapperClass);
        this.configuration.addMapper(mapperInterface);
    }
}

XMLMapperBuilder:基于xml解析器的mapper解析器

//需要先构建xml解析器XPathParser
//XMLMapperEntityResolver用于寻找mapper的dtd文件:../mybatis-3-mapper.dtd
//configuration.getVariables(),properties节点中配置解析的内容,引入外部资源文件
public XMLMapperBuilder(InputStream inputStream, Configuration configuration...) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()), configuration, resource, sqlFragments);
}

public class XPathParser {
    private Document document ; // document对象,xml解析的就是document中的Node节点
    private boolean validation; // 是否开启验证
    private EntityResolver entityResolver; //用于加载本地DTD文件
    private Properties variables; 
    private XPath xpath ; // XML查询语言,XPaht与XML的关系相当于SQL语言与数据库之间的关系
}

回到XMLMapperBuilder的parse()方法:

public void parse() {
    //如果是第一次加载resource/url映射文件
    if (!this.configuration.isResourceLoaded(this.resource)) {
        //解析映射文件下的mapper标签,并对解析出来的标签信息加以封装,然后添加到全局配置中
        //这一步就会将所有的mapper映射文件解析并缓存到Configuration中
        this.configurationElement(this.parser.evalNode("/mapper"));
        //将resouce添加到Set<String> loadedResources中,以防止重新加载resource
        this.configuration.addLoadedResource(this.resource);
        //找到namespace对应的mapper接口,并且addMapper到全局配置中
        this.bindMapperForNamespace();
    }
    //解析未完成的ResultMap,CacheRef和DML标签
    this.parsePendingResultMaps();
    this.parsePendingChacheRefs();
    this.parsePendingStatements();
}

private void bindMapperForNamespace() {
    //解析器助理器中获取namespace命名空间的值
    String namespace = this.builderAssistant.getCurrentNamespace();
    //类加载Mapper.xml需要绑定java类型,这一步在解析mappers指定接口时已经做过了
    Class boundType = Resources.classForName(namespace);

    //判断Mybatis全局配置信息中有没有注册boundType,boundType就是对应的mapper接口
    if (boundType != null && !this.configuration.hasMapper(boundType)) {
        //parse()方法中addLoadedResource添加是mapper/PersonMapper.xml映射文件
        //namespace:添加的是cn.mytest.mapper.StudentMapper接口全限定名
        this.configuration.addLoadedResource("namespace:" + namespace);
        //回到configuration
        this.configuration.addMapper(boundType);
    }
}
//运用了注册模式,本质上configuration就是注册中心,会将封装多种业务注册类,虽然都访问configuration的方法,但是实际都是由不同的业务注册类转发执行
public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
}

MapperRegistry:看类名就知道是处理mapper的注册器

public <T> void addMapper(Class<T> type) {
    //mapper必须是一个接口
    if (type.isInterface()) {
        //knownMappers就是namespace和mapperProxyFactory的映射map
        //knownMappers.containsKey(type),如果config中已经注册了接口
        if (this.hasMapper(type)) {
            throw new BindingException("already known to the MapperRegistry.");
        }
        //MapperProxyFactory.newInstance(sqlSession)构建代理mapper类
        this.knownMappers.put(type, new MapperProxyFactory(type));
        //MapperAnnotationBuilder中也有MapperBuilderAssistant属性,注解扫描
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        //后面会对比MapperAnnotationBuilder和XMLMapperBuilder的parse()方法
        parser.parse();
    }
}

//构建MapperProxyFactory实例
public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();

    public MapperProxyFactory(Class<T> mapperInterface) {
        //new MapperProxyFactory(type)构建实例时又将mapper接口注入进去
        this.mapperInterface = mapperInterface;
    }
} 

MapperAnnotationBuilder:主要作用于解析注解配置项

//构建MapperAnnotationBuilder时的入参是全局配置configuration和映射接口type
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
    //假设入参type.getName='cn.mytest.mapper.PersonMapper',那么resource='cn/mytest/mapper/PersonMapper.java (best guess)'
    String resource = type.getName().replace('.', '/') + ".java (best guess)";
    //辅助器去执行
    this.assistant = new MapperBuilderAssistant(configuration, resource);
    //初始化@Select等注解类型
    this.sqlAnnotationTypes.add(Select.class);
    this.sqlAnnotationTypes.add(Insert.class);
}

//解析映射接口,接口的所有方法和注解
public void parse() {
    //重写过的toString:比如"interface cn.mytest.mapper.PersonMapper"
    //return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))+ getName();
    String resource = this.type.toString();
    //判断是否被加载过,如果配置的是mapper接口且之前没有被加载则执行
    if (!this.configuration.isResourceLoaded(resource)) {
        //加载对应接口的映射文件,又调用了XMLMapperBuilder的parse()方法去解析resultmap等子标签
        this.loadXmlResource();
        //将resource添加到已加载的资源集合中,以防止重新加载resource
        this.configuration.addLoadedResource(resource);
        //设置构建器助理的当前命名空间为当前接口的全限定名
        this.assistant.setCurrentNamespace(this.type.getName());
        //接口上面的注解也会有对应的映射标签,后面都是处理解析接口方法上的注解
        //解析CacheNamespace注解,构建一个Cache对象,并保存到Mybatis全局配置信息中
        this.parseCache();
        //解析CacheNamespace注解,引用CacheRef对应的Cache对象
        this.parseCacheRef();
        Method[] methods = this.type.getMethods();
        for(Method method : methods) {
            //遍历接口的方法,根据方法的注解去构建MapperStatement对象
            this.parseStatement(method);
        }
    }
    //解析未完成解析的Method
    this.parsePendingMethods();
}

//loadXmlResource()根据接口简单名称去加载同名的映射文件,最终又会调用parse()方法,形成递归
private void loadXmlResource() {
    //是否有加载过mapper接口
    if (!this.configuration.isResourceLoaded("namespace:" + this.type.getName())) {
        String xmlResource = this.type.getName().replace('.', '/') + ".xml";
        //加载映射文件
        InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
        //递归,构建XMLMapperBuilder,然后解析xml文件,接口和映射文件相互找到
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(...);
        xmlParser.parse();
    }
}

其他节点

typeAliases节点:映射的实体的type需要用全限定名来表示,配置好别名之后运用在reSultMap映射

<typeAliases>
    <!--单个起别名-->
    <typeAlias type="cn.mytest.domain.Person" alias="Hahaha"/>
    <!--默认为类的简单类名,即Person-->
    <package name="cn.mytest.domain"/>
</typeAliases>
<!-- 通用查询映射结果 -->
<resultMap id="resultMap" type="Hahaha">

<!-- registerAlias("map", Map.class) -->
<select id="selectData" resultType="map">
    select * from person
</select>

解析typeAliases节点

//typeAliases标签子节点
//如果子节点是package, 那么就获取package节点的name属性, mybatis会扫描指定的package
if ("package".equals(child.getName())) {
    alias = child.getStringAttribute("name");
    this.configuration.getTypeAliasRegistry().registerAliases(alias);
} else {
    //就两个子节点,不是package,就只有typeAlias,解析其type和alias属性值
    alias = child.getStringAttribute("alias");
    String type = child.getStringAttribute("type");
    //动态加载类Class.forName(type),registerAlias入参是字节码对象
    Class<?> clazz = Resources.classForName(type);
    if (alias == null) {
        //没有指定别名,就按默认指定clazz.getSimpleName为别名
        this.typeAliasRegistry.registerAlias(clazz);
    } else {
        this.typeAliasRegistry.registerAlias(alias, clazz);
    }
}

//指定别名
public void registerAlias(Class<?> type) {
    String alias = type.getSimpleName();
    //获取javaBean上注解设置的别名 @Alias("user")
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
        alias = aliasAnnotation.value();
    } 
    registerAlias(alias, type);
}

//不指定alias,则默认为类的简单名称
public void registerAlias(String alias, Class<?> value) {
    //将别名转换成小写,所以大小写不敏感的
    String key = alias.toLowerCase(Locale.ENGLISH); 
    //Map<String, Class<?>> TYPE_ALIASES 别名映射缓存
    TYPE_ALIASES.put(key, value);
}

typehandler节点:入参和出参都会经过typehandler类型处理器将值以合适的方式转换成映射类型,默认会根据参数选择合适的TypeHandler处理

//用于处理jdbc与java的数据类型映射关系
public interface TypeHandler<T> {
  // 保存操作,数据入库之前时数据处理,将映射类型转为jdbc类型
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType);
  //数据库响应返回数据后,封装成映射的类型
  T getResult(ResultSet rs, String columnName) throws SQLException;
  T getResult(ResultSet rs, int columnIndex) throws SQLException;
  T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}

//mybatis框架为每种数据类型做了默认的关系对应,父类BaseTypeHandler
//javaType:基本数据类型和包装类型,JdbcType:在enum JdbcType中
/* MyBatis 内置的 TypeHandler,在TypeHandlerRegistry中内置的类型处理器,比如:
	1. BigDecimalTypeHandler:用于 java 类型 BigDecimal,jdbc 类型 REAL、DECIMAL、NUMERIC
	2. DateTypeHandler:用于 java 类型 Date,jdbc 类型 TIMESTAMP
    3. StringTypeHandler:用于 java 类型 string,jdbc 类型 CHAR、VARCHAR
*/

typeHandlers有两个子节点:package和typeHandler

<typeHandlers>
	//不能和typeHandler标签同时存在
	<package name="cn.mytest.demo"/>
    //handler属性直接配置要指定的TypeHandler
    <typeHandler handler=""/>
    //javaType配置java类型, 如果配上javaType, 那么指定的typeHandler就只作用于指定的类型
    //jdbcType配置数据库基本数据类型,如果配上jdbcType, 那么指定的typeHandler就只作用于指定的类型
    //也可两者都配置
    <typeHandler javaType="" jdbcType="VARCHAR" handler="StringTypeHandler"/>
</typeHandlers>

if ("package".equals(child.getName())) {
	//package只有一个属性name
	typeHandlerPackage = child.getStringAttribute("name");
	this.typeHandlerRegistry.register(typeHandlerPackage);
} else {
	//typeHandler标签的三个属性
	typeHandlerPackage = child.getStringAttribute("javaType");
	String jdbcTypeName = child.getStringAttribute("jdbcType");
    String handlerTypeName = child.getStringAttribute("handler");
    //获取别名对应的类
    Class<?> javaTypeClass = this.resolveClass(typeHandlerPackage);
    //获取JdbcType枚举类的值:也就是数据库的类型,比如vaechar等
    JdbcType jdbcType = this.resolveJdbcType(jdbcTypeName);
    Class<?> typeHandlerClass = this.resolveClass(handlerTypeName);
    //注册typeHandler, typeHandler通过TypeHandlerRegistry这个类管理
    //将映射关系缓存到 Map<JdbcType, TypeHandler<?>> TYPE_HANDLER_MAP中
	this.typeHandlerRegistry.register(...);
}

//注册的映射关系
public TypeHandlerRegistry() {
    //比如BooleanTypeHandler就能处理多种数据转换
    this.register((Class)Boolean.class, (TypeHandler)(new BooleanTypeHandler()));
    //getPrimitiveClass("boolean"),获取基本类型字节码对象
    this.register((Class)Boolean.TYPE, (TypeHandler)(new BooleanTypeHandler()));
    this.register((JdbcType)JdbcType.BOOLEAN, (TypeHandler)(new BooleanTypeHandler()));
    this.register((JdbcType)JdbcType.BIT, (TypeHandler)(new BooleanTypeHandler()));
}

自定义TypeHandler,继承BaseTypeHandler

//通过 @MappedTypes指定类型
@MappedJdbcTypes(JdbcType.VARCHAR)  
public class MyTypeHandler extends BaseTypeHandler<Person> {
  //插入数据前的类型转换,也就是说存入时转换成vachar,取出来时自动转换成Person
  public void setNonNullParameter(PreparedStatement preparedStatement, int i, Person person, JdbcType jdbcType) throws SQLException {
    //给第i个参数设置值
    preparedStatement.setString(i,person.toString());
  }

  public Person getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
    //根据列名获取列的数据
    resultSet.getString(columnName);
    return new Person();
  }

  public Person getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
    //根据列的顺序获取列的数据
    resultSet.getString(columnIndex);
    return new Person();
  }
}

//如果定义时已经通过注解指定了jdbcType, 此处就不用再配置jdbcType
<typeHandler handler="MyTypeHandler"/>

plugins节点:其实就是interceptor, 它可以拦截Executor 、ParameterHandler 、ResultSetHandler 、StatementHandler 的部分方法处理逻辑;Executor就是真正执行sql语句的东西, ParameterHandler 是处理传入参数的,当不显示配置typeHandler的时候,mybatis会根据参数类型自动选择合适的typeHandler执行,其实就是ParameterHandler 在选择;ResultSetHandler 就是处理返回结果的

<plugins>
    <plugin interceptor="org.mybatis.example.ExamplePlugin">
    	<property name="someProperty" value="100"/>
    </plugin>
</plugins>

//可选项,如果xml不配置,默认为空不做拦截
this.interceptorChain = new InterceptorChain();
List<Interceptor> interceptors = sqlSessionFactory.getConfiguration().getInterceptors();

//初始化plugins 节点
private void pluginElement(XNode parent) throws Exception {
    //可以配置多个plugin
    for(XNode child:parent.getChildren()){
        //plugin节点就一个属性interceptor
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        //InterceptorChain中List<Interceptor>
        this.configuration.addInterceptor(interceptorInstance);
    }
}

databaseIdProvider节点:多数据库支持,根据databaseId进行切换

//DB_VENDOR是VendorDatabaseIdProvider类的别名
<databaseIdProvider type="DB_VENDOR">
    <property name="SQL Server" value="sqlserver"/>
    <property name="MySQL" value="mysql"/>
    <property name="Oracle" value="oracle" />
</databaseIdProvider>

<select id="selectOne" databaseId="oracle">
    <!-- ... -->
</select>

// 通过数据源确定使用的databaseId,之后 SQL 也只会加载这种 databaseId 的 SQL ,其他类型都会被忽略
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);

mapper.xml映射文件的解析

在初始化mybatis配置项的mapper配置,加载并通过XMLMapperBuilder的parse()解析所有mapper.xml映射文件,本质上就是对xml文件的节点进行解析然后封装到Configuration中;

private void configurationElement(XNode context) {
  //root标签就是mapper:<mapper namespace="cn.mytest.mapper.StudentMapper">
  String namespace = context.getStringAttribute("namespace");
  //MapperBuilderAssistant保存了命名空间的值
  this.builderAssistant.setCurrentNamespace(namespace);
  //解析mapper标签的所有子节点
  this.cacheRefElement(context.evalNode("cache-ref"));
  this.cacheElement(context.evalNode("cache"));
  this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  this.resultMapElements(context.evalNodes("/mapper/resultMap"));
  this.sqlElement(context.evalNodes("/mapper/sql"));
  this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}

缓存节点

缓存节点主要是cache-ref和cache,根据namespace进行缓存的存取configuration.getCache(namespace),缓存Cache本质就是双层Map,每个namespace对应一个cache实例;从封装结构看,Configuration封装所有的namespace的cache,每个namespace也会存在一个cache(默认是PerpetualCache)封装所有当前namespace内的查询缓存

//Configuration
Map<String, Cache> caches;
//PerpetualCache
Map<Object, Object> cache;

cache-ref节点:共享引入的命名空间的缓存配置和实例

private void cacheRefElement(XNode context) {
    //cacheRefMap.put(namespace, referencedNamespace)将这个namespace的缓存添加到配置中
    this.configuration.addCacheRef(this.builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
    //构建一个CacheRef处理器
    CacheRefResolver cacheRefResolver = new CacheRefResolver(this.builderAssistant, context.getStringAttribute("namespace"));
    //找到缓存引用CacheRef,通过builderAssistant的useCacheRef方法
    cacheRefResolver.resolveCacheRef();
}

//useCacheRef()找到引用的对应的cache对象并赋值给currentCache,构建MapperStatement对象时,将currentCache传进去
public Cache useCacheRef(String namespace) {
    //获取全局配置中namespace对应的缓存(可能保存了很多cache)
    Cache cache = configuration.getCache(namespace);
    //如果找到了则设置成当前缓存
    currentCache = cache;
    return cache;
}

cache节点:命名空间的缓存配置

<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache>

eviction:缓存的回收策略
    * LRU - 最近最少使用,移除最长时间不被使用的对象,对应实现类:LruCache
    * FIFO - 先进先出,按对象进入缓存的顺序来移除它们,对应实现类:FifoCache
    * SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象,对应实现类:LruCache
    * WEAK - 弱引用,更积极地移除基于垃圾收集器和弱引用规则的对象,对应实现类:WeakCache
    默认的是LRU 
flushInterval:缓存刷新间隔
	缓存多长时间清空一次,默认不清空,设置一个毫秒值
readOnly:是否只读
	true:只读:不安全,速度快,为了加快获取数据,不考虑被修改就直接从缓存获取数据
	false:读写(默认):考虑会有修改,通过序列化&反序列化的技术将数据复制,返回复制的副本
size:缓存存放多少个元素
type:指定自定义缓存的全类名(实现Cache接口即可),默认是PERPETUAL

Cache配置项解析,构建Cache的实例

//cache标签里有5个属性值:type,eviction,flushInterval,size,readOnly
private void cacheElement(XNode context) throws Exception {
    //自定义缓存的全类名(实现Cache接口),PERPETUAL是别名,对应PerpetualCache缓存类
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = this.typeAliasRegistry.resolveAlias(type);
    //eviction:缓存的回收策略,默认是LRU,对应的是LruCache
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    //缓存刷新间隔,缓存多长时间清空一次,默认不清空
    Long flushInterval = context.getLongAttribute("flushInterval");
    Integer size = context.getIntAttribute("size");
    //默认为false
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    //cache还有property子节点,自定义cache的属性
    Properties props = context.getChildrenAsProperties();
    //构建cache实例封装必要参数,然后添加到全局配置中
    this.builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
}

映射节点

用于实体和db类型的映射关系,主要是resultMap和parameterMap节点,从命名可以知道resultMap用于返参的映射,parameterMap用于入参的映射,但是这两个映射配置本质上都是一样的,基本类型映射jdbc类型

parameterMap节点:入参字段进行映射匹配,一般常用的就是parameterType是以实体的字段直接作为入参字段,不再配置映射关系

//property即实体属性
<parameterMap id="PersonParameterMap" type="cn.mytest.domain.Person">
    <parameter property="personMoney" resultMap="PersonResultMap"/>
    <parameter property="personName" resultMap="PersonResultMap"/>
</parameterMap>

//根据id查询,实体类和数据库对应,所以用parameterType也一样 
<select id="selectById" parameterType="int" resultMap="PersonResultMap">
    select * from person where id=#{id}
</select>

//用parameterType需要实体字段和入参字段名称一致
<delete id="deleteById" parameterType="xxx.domain.Book"> 
    delete from person where id=#{id}
</delete>

//入参是一个对象
<insert id="save" parameterMap="PersonParameterMap">
    insert into person values (#{personName},#{personMoney})
</insert>

构建ParameterMap实例对象并添加到Configuration:Map<String, ParameterMap> parameterMaps中

private void parameterMapElement(List<XNode> list) {
    for (XNode parameterMapNode : list) {
        //namespace中的标签id
        String id = parameterMapNode.getStringAttribute("id");
        //type属性,也就是对应的实体类
        String type = parameterMapNode.getStringAttribute("type");
        Class<?> parameterClass = resolveClass(type);
        //解析子节点parameter
        List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
        //ParameterMapping属性则是parameter标签的属性
        List<ParameterMapping> parameterMappings = new ArrayList<>();
        //parameter属性:property,javaType实体中属性java类型,jdbcType数据库对应的类型枚举,resultMap,typeHandler类型处理器,numericScale表示小数点保留的位数
        for (XNode parameterNode : parameterNodes) {
            // 构建ParameterMapping,一个parameter节点一个ParameterMapping
            ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
            //添加到当前parameterMap集合中
            parameterMappings.add(parameterMapping);
        }
        //构建ParameterMap,并添加到全局配置中
        builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
    }
}

resultMap节点:resultMap或者resultType关联结果集映射

//查询时需要返回类型,即用resultMap,column:数据库中的列,property:对应的实体类中的属性
<resultMap id="PersonResultMap" type="cn.mytest.domain.Person">
    //id元素 ,用于设置主键字段与领域模型属性的映射关系
    <id column="id" property="id"/>
    //result元素 ,用于设置普通字段与领域模型属性的映射关系
    <result column="name" property="personName"/>
    <result column="money" property="personMoney"/>
</resultMap>

//constructor:构造器映射
<resultMap id="getStudentRM" type="EStudnet">
    <constructor>
        //idArg子元素 ,标记该入参为主键
        <idArg column="ID" javaType="_long"/>
        <arg column="Name" javaType="String"/>
        <arg column="Age" javaType="_int"/>
    </constructor>
</resultMap>

//discriminator:动态映射关系信息
<resultMap id="rm" type="EStudent">
    //during字段值为4、5时以juniorHighSchool字段作所为seniorHighSchool信息
    <discriminator column="during" javaType="_int">
        // 形式1:通过resultType设置动态映射信息
        <case value="4" resultType="EStudent">
            <result column="juniorHighSchool" property="seniorHighSchool"/>
        </case>

        // 形式2: 通过resultMap设置动态映射信息
        <case value="5" resultMap="dynamicRM"/>
    </discriminator>
</resultMap>

<!-- autoMapping属性:值范围true(默认值)|false, 设置是否启动自动映射功能,自动映射功能就是自动查找与字段名小写同名的属性名,并调用setter方法。而设置为false后,则需要在`resultMap`内明确注明映射关系才会调用对应的setter方法 -->

和parameterMaps一样构建ResultMap实例并添加到Configuration的resultMaps中,主要是了解一下Nested嵌套结果集的解析;processNestedResultMappings()解析association,collection,case标签的子节点,处理嵌套的ResultMappings

<resultMap id="ResultMap" type="cn.mywork.wms.domain.Grade">
    <id column="id" property="id" />
    <result column="grade_name" property="gradeName" />
    <collection property="studentList" column="id" select="cn.mywork.mapper.StudentMapper.get"/>
</resultMap>

<select id="get" resultMap="GraMap">
	select * from xx
</select>

private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) throws Exception {
    //处理association,collection,case标签
    if ("association".equals(context.getName())||"collection"..|| "case"..) {
        //如果指定了 select 属性,则映射时只需要获取对应 select 语句的 resultMap
        if (context.getStringAttribute("select") == null) {
            //继续解析resultMap的子标签:association,collection,case的属性
            resultMap = resultMapElement(context, resultMappings, enclosingType);
        }
    }
    return resultMap.getId();
}

Statement节点

也就是和db交互的节点,包括select|insert|update|delete等;定义 sql 语句,解析成交互的sql片段

<!--id :唯一的标识符.
	parameterType:入参映射类型,例:com.test.poso.User 或 别名user
	resultType :结果集映射类型 -->
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="Student">
    select * from student where id=#{id}
</select>

解析每个statement标签需要构建对应的XMLStatmentBuilder实例

//先构建全局配置中对应的DatabaseId->requiredDatabaseId
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    //遍历DML标签集合
    for (XNode context : list) {
        //对每个DML标签新建一个XMLStatement构建器
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(...);
        try {
            //解析DML标签,封装成MappedStatement对象,添加到全局配置信息中
            //Map<String, MappedStatement> mappedStatements:key就是namespace+标签id
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            //解析异常时,将XML Statment构建器添加到Mybatis全局配置信息中,后期再尝试解析
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

public void parseStatementNode() {
    //标签id,对应mapper接口的方法名(method)
    String id = context.getStringAttribute("id");
    //检查标签的databaseId是否对应配置的数据库Id
    String databaseId = context.getStringAttribute("databaseId");
    //获取标签名称,也就是sql操作类型,select/insert等
    String nodeName = context.getNode().getNodeName();
    //SqlCommandType枚举值:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //判断是否为select标签
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //获取标签的其他属性,第二个参数标识默认值
    //flushCache表示执行语句时是否刷新缓存,默认是只要不是select语句就会刷新缓存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //useCache表示是否对该语句进行二级缓存,默认是对select语句进行缓存
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    //处理include标签,将include标签替换成对应sql标签下的所有子标签
    //parmeterType:入参类型,可以通过 TypeHandler 推断入参类型,默认值为 unset
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    //lang:指定该DML标签使用的语言驱动,不是数据库驱动,默认是XMLLanguageDriver
    //脚本语言驱动并基于这种语言来编写动态 SQL 查询语句
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    //selectKey标签用于insert和update标签的子节点,类似于include,但是是独立sql语句
    //解析封装到全局配置中Map<String, KeyGenerator> keyGenerators:key值就是keyStatementId
    //然后删除DML标签中的所有selectKey标签
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    KeyGenerator keyGenerator;
    //selectKey标签的Id=DML标签ID+!selectKey    
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    //检查ID是否简写
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        //useGeneratedKeys属性,默认情况下insert标签的该属性为true
        //设置为true时就使用JDBC3KeyGenertor,用于数据库的自增主键,比如 MySQL、PostgreSQL;会将执行SQL后从Statemenet中获取主键放到参数对象对应的属性里
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    //创建标签对应的SqlSource,这是mybatis的核心
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //statementType属性,默认的StatementType是PREPARED,预编译处理
    //STATEMENT有SQL注入的风险,CALLABLE调用存储过程的时候使用
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //预期返回的结果行数和这个设置值相等,默认值为未设置
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //在抛出异常之前,驱动程序等待数据库返回请求结果的秒数
    Integer timeout = context.getIntAttribute("timeout");
    //parameterMap的id,入参类型
    String parameterMap = context.getStringAttribute("parameterMap");
    //结果类型,比如Object
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    //返回的结果映射类型,对应resultMap的id
    String resultMap = context.getStringAttribute("resultMap");
    //默认是ResultSetType.DEFAULT枚举值,依赖驱动
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
        resultSetTypeEnum = configuration.getDefaultResultSetType();
    }

    //GeneratedKeys,返回对应的主键字段,比如 keyProperty="id"
    String keyProperty = context.getStringAttribute("keyProperty");
    //通过生成的键值设置表中的列名,仅在某些数据库(PostgreSQL)是必须的,当主键列不是表中的第一列时
    String keyColumn = context.getStringAttribute("keyColumn");
    //这个设置仅对多结果集的情况适用,将列出语句执行后返回的结果集并给每个结果集一个名称
    String resultSets = context.getStringAttribute("resultSets");

    //最终把解析的属性和子节点全部构建MappedStatement实例添加到全局配置中
    builderAssistant.addMappedStatement(...);
}

总结一下parseStatementNode()主要有三步,将标签中的include标签替换成对应sql标签下的语句,将selectKey标签封装成KeyGenerator对象,然后添加到全局配置信息信息,将标签内容解析完之后封装成MapperStatement对象添加到全局配置信息中:Map<String, MappedStatement> mappedStatements

sql/include 标签:sql标签内的内容可以将其定义为常量,方便调用,而include标签就是用于引用定义的sql标签,include标签只有一个refid属性,通过属性值去找保存在BaserBuilder的sql标签(sql标签解析之后),然后将对应的sql标签的内容嵌入到statement标签内

<select id="selectData" resultType="map">
    select
    <include refid="studentSql">
        <property name="cloumn" value="age">
    </include>
    from person
</select>

<sql id="studentSql">
	#{cloumn}
</sql>

//查询字段
<sql id="Base_Column_List">
    ID,MAJOR,BIRTHDAY,AGE,NAME,HOBBY
</sql>

//搭配include标签使用
<select id="selectData" resultType="map">
    select        
    <include refid="cn.mytest.mapper.StudentMapper.studentSql"/>
    from person
</select>
    

解析sql节点

//<sql id="" databaseId="">,根据databaseId切换数据库
private void sqlElement(List<XNode> list) {
    //sql标签的属性有databaseId用于匹配对应的数据库
    //优先选择Configuration全局配置中配置的databaseId
    if (configuration.getDatabaseId() != null) {
        sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
}

//requiredDatabaseId入参就是应用的数据库标识
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        //对应的数据库Id
        String databaseId = context.getStringAttribute("databaseId");
        //sql标签的ID
        String id = context.getStringAttribute("id");
        //检查ID是否简写,简写就应用当前命名空间,可以引用其他namespace中的sql片段
        id = builderAssistant.applyCurrentNamespace(id, false);
        //找出匹配当前配置的数据库Id,保存在BaserBuilder的configuration中
        //sql标签中的databaseId必须与全局配置的databaseId一致才能被使用
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            //Map<String, XNode> sqlFragments
            sqlFragments.put(id, context);
        }
    }
}

selectKey标签:相当于内嵌了一条select节点,主要是order属性:设置为 BEFORE,那么它会首先选择主键,设置 keyProperty 然后执行插入语句;设置为 AFTER,那么先执行插入语句,然后是 selectKey 元素

<insert id="insert" parameterType="map">
	insert into table1 (name) values (#{name})
    <selectKey resultType="java.lang.Integer" keyProperty="id" order="AFTER">
      //返回自增id
      SELECT LAST_INSERT_ID()
    </selectKey>
</insert>

动态标签

XMLScriptBuilder封装了诸如trim,where,set,foreach,if,choose,when,otherwise,bind动态标签,通过handleNode()方法实现动态标签的解析,解析标签属性并封装到MixedSqlNode,继续迭代解析子节点,直到子节点是文本(静态)节点

//NodeHandler实现类,对应不同的动态标签
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());

IfHandler:对应if和when标签,解析的sql对应的是IfSqlNode

<select id="findUserById" resultType="user">
    select * from user where 
    //只有一个test属性,单引号和双引号都有效,建议单引号
    //<if test='"1" eq custNoType'>
    <if test="id != null">
   		id=#{id}
    </if>
    and deleteFlag=0
</select>

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    //解析if标签下的子节点,封装成MixedSqlNode
    //statement子节点有if标签,同样if标签也可能有其他动态标签或文本节点
    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
    //获取if标签的test属性
    String test = nodeToHandle.getStringAttribute("test");
    //IfSqlNode是sqlNode实现类,封装了mixedSqlNode和test
    IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
    targetContents.add(ifSqlNode);
}

TrimHandler:对应的是trim标签,也具备where和set标签的功能,因为WhereSqlNode,SetSqlNode是继承的TrimSqlNode,handleNode由父类TrimSqlNode实现;trim可以完成sql语句前后缀的拼接和去除,而where只针对前缀and和or的去除,set仅针对后缀逗号的去除

//trim完成where功能:当WHERE后紧随AND或则OR的时候,就去除AND或者OR
<select id="findUserById" resultType="user">
        select * from user where 1 = 1
        <trim prefix="WHERE" prefixOverrides="AND |OR ">
          <if test="name != null"> AND name=#{name}</if>
        </trim>
</select>
//trim完成set功能,去掉最后一个逗号
<update id="updateUser" parameterType="com.dy.entity.User">
		update user
        <trim prefix="SET" suffixOverrides=",">
          <if test="name != null"> name=#{name} , </if>
        </trim>
</update>

//where标签会去除拼接后的sql中第一个and或者是or
<select id="findUserById" resultType="user">
        select * from user 
            <where>
            	//如果id为null,则后续的and会被去除保证sql合法
                <if test="id != null">
                    id = #{id}
                </if>
            	<if test="name != null">
                   and name = #{name}
                </if>   
        	</where>
</select>
//set标签去除最后一个更新字段语句中出现的逗号
<update id="updateUser" parameterType="com.dy.entity.User">
        update user
        <set>
          <if test="name != null">name = #{name},</if> 
          //去除的就是这个拼接条件后面的逗号
          <if test="password != null">password = #{password},</if> 
        </set>
</update>

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    //获取trim标签的prefix属性,给sql语句拼接的前缀
    String prefix = nodeToHandle.getStringAttribute("prefix");
	//获取trim标签的prefixOverrides属性:去除sql语句前面的关键字或者字符
    String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
    //获取trim标签的suffix属性,给sql语句拼接的后缀
    String suffix = nodeToHandle.getStringAttribute("suffix");
    //获取trim标签的suffixOverride,去除sql语句后面的关键字或者字符
    String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
    TrimSqlNode trim = new TrimSqlNode(...);
    targetContents.add(trim);
}

ForEachHandler:对应的是foreach标签,ForEachSqlNode

//默认的有list,array,但是没有默认的map
<insert id="insertData" parameterType="java.util.Map">
    insert into tables
    <foreach collection="dataMap.keys" separator="," item="item" open="(" close=")">
    	${item}
    </foreach>
    values
    <foreach collection="dataMap.keys" item="item" separator="," open="(" close=")">
    	#{dataMap[${item}]}
    </foreach>
</insert>

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    //获取collection属性,循环遍历的集合
    String collection = nodeToHandle.getStringAttribute("collection");
    //获取item属性,设置集合的元素名,类似for(index:list)
    String item = nodeToHandle.getStringAttribute("item");
    //获取index属性,在list和数组中,index是元素的序号,在map中,index是元素的key
    //list入参时设置index="index",map参数设置index="key"
    String index = nodeToHandle.getStringAttribute("index");
    //获取foreach标签的open,close属性,foreach代码的开始和关闭符号,常用在in(),values()时
    String open = nodeToHandle.getStringAttribute("open");
    String close = nodeToHandle.getStringAttribute("close");
	//获取foreach标签的separator属性,元素之间的分隔符
    String separator = nodeToHandle.getStringAttribute("separator");
    ForEachSqlNode forEachSqlNode = new ForEachSqlNode(...);
    targetContents.add(forEachSqlNode);
}

ChooseHandler,OtherwiseHandler:choose,when和otherwise标签一般是同时出现,表示的是switch,case,default;when标签处理器是IfHandler,OtherwiseHandler只用来处理标签内嵌套的sqlNode

//当choose中所有when的条件都不满足时,则获取otherwise中的sql
<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
  	//和if标签是一样的
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    //没有任何属性,只表示else
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    List<SqlNode> whenSqlNodes = new ArrayList<>();
    List<SqlNode> otherwiseSqlNodes = new ArrayList<>();
    //接收处理when标签和otherwise标签
    handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
    //获取otherwise标签
    SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
    //封装whenSqlNodes和defaultSqlNode
    ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
    targetContents.add(chooseSqlNode);
}

BindHandler:对应bind标签,OGNL的SqlNode,可以从 OGNL 表达式中创建一个变量并将其绑定到上下文

<select id="queryTest" resultType="cn.mytest.domain.Employee">
    <!-- bind:可以将OGNL表达式的值绑定到一个变量中,方便后来引用这个变量的值 -->
    <bind name="bindeName" value="'%'+eName+'%'"/> eName是employee中一个属性值
    	SELECT * FROM emp 
    <if test="parameter!=null">
    	where ename like #{bindeName}
    </if>
</select>

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    //获取bind标签的name属性值
    final String name = nodeToHandle.getStringAttribute("name");
    //获取bind标签的value属性值
    final String expression = nodeToHandle.getStringAttribute("value");
    //OGNL解析
    final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
    targetContents.add(node);
}
SqlSource

通过默认的驱动器XMLLanguageDriver创建DML标签对应的SqlSource,简单来说SqlSource就是sql节点的解析封装,其实现有DynamicSqlSource是动态sql ,RawSqlSource就是完整sql

//其主要作用于解析select|update|insert|delete节点为完整的SQL语句
public class XMLLanguageDriver implements LanguageDriver {
    //script:需要解析的节点,当sql中包含有${},或者存在动态SQL标签名,如if,trim等就认为是动态SQL
    //如果是动态返回DynamicSqlSource ,不是动态返回RawSqlSource
    public SqlSource createSqlSource(Configuration configuration, XNode script...) {
        //通过XMLScriptBuilder构建SqlSource
        XMLScriptBuilder builder = new XMLScriptBuilder(..);
        //解析脚本标签节点
        return builder.parseScriptNode();
    }
}

parseScriptNode()分两步,第一步是解析statement标签下的子节点和文本节点,判断是否有动态sql;第二步传入解析的子节点构建一个SqlSource

public SqlSource parseScriptNode() {
    //解析标签下的子节点,封装成MixedSqlNode
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    //DynamicSqlSource解析含有$的sql语句,而RawSqlSource解析含有#的sql语句
    if (isDynamic) {
        //可执行的sql在调用getBoundSql(Parameter)方法时才能组装完成。
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        //调用 SqlSourceBuilder类将"#{xxx}“ 替换为占位符”?",并绑定ParameterMapping
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

//解析dml标签子节点,有动态sql标签,也有文本(select * from xxx)
protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    //获取dml的所有子节点Node
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        //遍历每个子节点并封装成新的xnode,XNode只是对Node实例的封装
        XNode child = node.newXNode(children.item(i));
        //Node.CDATA_SECTION_NODE:代表文档中的 CDATA 部(不会由解析器解析的文本)
        //eg: <![CDATA[and to_char(update_date,'yyyyMMdd') <= #{endDate}]]>
        //Node.TEXT_NODE:代表元素或属性中的文本内容
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            //获取文本节点的文本内容
            String data = child.getStringBody("");
            //文本节点
            TextSqlNode textSqlNode = new TextSqlNode(data);
            //用来校验其中的sql语句是否含有${}字符,有${}时就认为是动态SQL
            //从而根据是否是动态SQL来添加对应SqlNode类型
            if (textSqlNode.isDynamic()) {
                contents.add(textSqlNode);
                isDynamic = true;
            } else {
                //StaticTextSqlNode:静态文本SQL节点,其apply方法使得只是将存储的文本内容直接添加到DynamicContext对象中
                contents.add(new StaticTextSqlNode(data));
            }
            //Node.ELEMENT_NODE:标识为标签  
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { 
            String nodeName = child.getNode().getNodeName();
            //根据标签名找到对应的NodeHandler:比如TrimHandler
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
                throw new BuilderException("");
            }
            //处理和解析动态标签
            handler.handleNode(child, contents);
            //有动态标签也认证为动态sql
            isDynamic = true;
        }
    }
    return new MixedSqlNode(contents);
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值