mybatis启动流程之xml解析
测试环境搭建
数据库建表
CREATE DATABASE IF NOT EXISTS db_mybatis CHARACTER SET utf8;
-- 创建数据表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(32) NOT NULL COMMENT '用户名称',
`birthday` DATETIME DEFAULT NULL COMMENT '生日',
`sex` CHAR(1) DEFAULT NULL COMMENT '性别',
`address` VARCHAR(256) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
java实体类
public class User {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
}
mybatis-config配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="com.wjc.entity"></package>
</typeAliases>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/db_mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/wjc/mapper/UserMapper.xml"/>
</mappers>
</configuration>
mapper接口与mapper映射文件
package com.wjc.mapper;
import com.wjc.entity.User;
import java.util.List;
public interface UserMapper {
List<User> findAll();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wjc.mapper.UserMapper">
<sql id="selectSql">
select * from user
</sql>
<select id="findAll" resultType="user">
<include refid="selectSql"/>
</select>
</mapper>
因为mybatis还是使用maven来构建项目的,所以我将测试环境放在了test目录下,以下为我的测试环境项目结构
测试类:
public class SelectTest {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = build.openSession();
UserMapper userMapper = session.getMapper(UserMapper.class);
List<User> users = userMapper.findAll();
}
}
mybatis-config文件解析
首先我们来看一下Resources,这是mybatis开发的一个工具类用于读取资源文件,但是底层还是通过AppClassLoader来加载资源文件,关于java的类加载机制我们在以后的文章中再来解释,这里就简单认为这个Resources的getResourceAsStream()就是为了从classpath目录下获取配置文件的输入流即可。
从SqlSessionFactoryBuilder的build方法才开始去解析配置文件
上面的代码是一个空壳方法,只是做了方法调用
XMLConfigBuilder是mybatis中解析xml的解析器,但是底层是通过java dom技术来解析的,mybatis只是在这个基础上做了一些封装,build方法会返回DefaultSqlSessionFactory这个也就是我们通常所见的SqlSessionFactory,里面封装了我们的解析出来的配置文件环境,解析xml的代码都在parser.parse()方法中
这里就有一点需要注意,因为解析xml配置文件会比较耗时,所以如果重复解析mybatis就会抛出异常XMLConfigBuilder中定义了一个布尔变量parsed默认是false,当调用过一次parse方法后就马上赋值为true,如果同一份配置文件再一次得解析就会抛出异常,因为mybatis-config.xml配置文件的根节点是configuration,所以将这个节点作为root节点传入到parseConfiguration方法中,再往下解析。
以上其实就做了一些环境初始化的准备工作,parseConfiguration方法才是真正开始解析配置文件。
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
//别名其实就是把我们指定的别名对应的class存储在一个map中
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
//objectFactory - 自定义实例化对象的行为,mybatis针对resultMap结果集的时候需要封装对象,如果我们要为封装的对象赋值就可以重写ObjectFactory
//然后为返回的对象做一些默认的我们需要的操作,实现ObjectFactory接口重写setProperties和create方法,很少用
objectFactoryElement(root.evalNode("objectFactory"));
//配合MateObject做反射用的,方便反射操作实体类的对象
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
//动态切换数据库id
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
这里着重探讨一下typeAliases与mappers的解析
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
通过package来注册别名
方法参数Xnode parent其实就是标签的节点,因为对于typeAliases而言其中的package与typeAlias标签都是子标签,也就是子节点,所以只要我们配置了别名parent != null是一定为true的。
获取子标签进行for循环解析,因为typeAliases标签中可以有两个子标签同时存在,这在配置文件的dtd约束文件中有明确表示
如果别名中配置的是package标签,代表对某一包下的所有类配置别名String typeAliasPackage = child.getStringAttribute("name");
获取package标签中配置的属性值,也就是包路径。configuration.getTypeAliasRegistry()
获取别名注册器,里面提供了解析、注册、获取别名的一些方法
这个解析器默认就已经通过无参构造方法在实例化的时候注册了总共72个别名映射,这里就仅展示一部分
通过源码我们也可以发现,这就是为什么当返回值为String、Integer的时候不需要在resultType中配置全路径的原因
继续调用注册别名方法
public void registerAliases(String packageName) {
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for (Class<?> type : typeSet) {
// Ignore inner classes and interfaces (including package-info.java)
// Skip also inner classes. See issue #6
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
根据配置文件中的属性,我们这里传入的packageName的参数是"com.wjc.entity"包路径,ResolverUtil这个类的方法比较复杂这里就简单说说它的功能,ResolverUtil的find方法会去解析所有我们所定义的包路径下的文件,
不管是不是.class文件都会先解析出来,然后这里传入的Test的是一个接口主要实现了match方法,在调用registerAliases的时候我们传入了Object.class,目的在于如果说我们在该包下定义了一个不是由.java文件编译而成.class文件的时候可以通过matches方法来判断,因为Object是一个超类
接下来将该包下解析出来的所有class对象放入到set集合中,可以为了去处重复的类,判断不是匿名内部类和接口就调用注册方法,将当前遍历的class存放到别名缓存中。
如果该类上有@Alias注解,就获取注解中配置的别名值,如果没有配置那么就使用类的名字作为别名传入到注册别名的方法中,这里可以看出无论我们是否配置了别名,最终mybatis都会将别名转换成小写字母,然后判断别名列表中是否有当前的别名,如果存在当前的别名,并且别名所对应的class又是同一个,那么会被mybatis认为是在重复注册,就抛出异常,否则就成功注册了别名。
typeAliases其实就是一个HashMap
通过typeAlias来注册别名
<typeAliases>
<package name="com.wjc.entity"></package>
<typeAlias type="com.wjc.entity.User" alias="user"/>
</typeAliases>
上面我们说了通过package来配置别名,package配置别名逻辑要比typeAlias标签类配置要复杂的多,因为typeAlias配置别名是一对一的配置,首先获取typeAlias标签中type和alias的属性值,通过系统类加载器去加载该类,如果该类不存在就会抛ch出ClassNotFoundException异常,如果正常加载到了这个类,那么还是调用别名解析器的注册方法来注册别名,这里的逻辑就和package来配置别名是一模一样的了,就不再赘述。
mapper映射文件解析
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
/**
* 解析mapper节点也分为两种情况,mapper标签中可以配置package和mapper
*<mappers>
* <package name="com.wjc.mapper"/>
* <mapper resource="com/wjc/mapper/UserMapper.xml"/>
*</mappers>
*/
for (XNode child : parent.getChildren()) {
//判断是否配了package,然后再去解析mapper,因为这涉及到解析注解还是xml文件
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//解析mapper,但是mybatis提供了三种方式去配置文件地址
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
//class - 其实就是以注解的方式去解析,因为通过class引入mapper,是在UserMapper上加Mapper注解
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
//解析resource,这三个值只能有一个值是有值的
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//解析resource的xml配置
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.");
}
}
}
}
}
在解析mapper映射文件的时候同样要分两种情况
<mappers>
<mapper resource="com/wjc/mapper/UserMapper.xml"/>
<package name="com.wjc.mapper"/>
</mappers>
1.通过mapper标签进行配置,而mapper标签来配置映射文件的路径又有三种情况,有resource标签、url标签、class标签,前两者都是指向mapper.xml的路径,而后者是指向一个mappper接口,通过class标签配置其实就是注解开发的形式
2.通过package标签进行配置,配置mapper接口所在的包路径。
通过package来配置映射文件
这里无需多言就是获取package标签中的name属性,也就是mapper所在包的路径,然后调用addMappers方法,将name属性值作为方法参数,这里又是一个方法调用
还是一个方法调用,但是多了一个Object参数,我们可以大胆猜测,这里通过包路径去获取mapper接口的逻辑应该与之前解析别名的时候获取包下的实体类逻辑是一致的,也要通过这个超类来做类型匹配判断。
果然如此,这三行代码一模一样,那么下面的逻辑就简单了,
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);
//解析mapper对象
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
首先判断当前遍历到的class是不是一个接口,不是接口就直接就什么都不处理,然后hasMapper(type)
判断当前的class是否已经被解析过了,是否已经被存放在knownMappers这个map集合中了
//TODO
通过mapper resource来解析mapper
这里我使用的是resource标签来配置映射文件路径,mybatis会以classpath目录下,然后根据我们所配置的路径去找映射文件,因为mybatis事先无法知道用户会采用哪一种方式来指定文件路径,所以不论是否存在这个属性值,mybatis都会将resource、url、class都去解析一遍,如果没有配置该值,那么就为null,然后会会逐一判断,这里我们也可以发现在mapper标签中的三个子标签resource、url、class只能配置其中的一个,因为在判断的时候是判断其他两个为null,其中一个为空,比如我配置了resource:
所以如果配置了多个那么这三个if语句块都不会进入最后就会抛出异常
接下来就聊一聊具体的解析方法,resource、url标签的解析方法是一样的。因为这两个标签的不同之处仅仅在于指定的路径不同,而class标签则不一样,因为指定的是一个接口,不是一个映射文件。
调用mapperParser.parse方法开始解析,configuration中缓存了当前mybatis环境的所有配置
在实例化XMLConfigBuilder的时候就已经实例化了,我们解析出来的resultMap、别名等等,所有的配置信息都保存在configuration中。而在我们解析mapper文件的时候这里实例化XMLMapperBuilder的时候就不会再去实例化一个新的Configuration对象了,接下来继续说解析mapper文件。
首先会判断当前的mapper文件是否已经解析过了,Configuration中维护了一个HashSet里面保存的是resource的路径,在解析完一个resource后就会调用相应的load方法,将当前解析的resource路径添加到集合中,为了避免重复解析
接下来调用configurationElement方法开始解析:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//设置当前全局的命名空间
builderAssistant.setCurrentNamespace(namespace);
//解析缓存
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析静态sql代码块
/**
*<sql id="selectSql">
* select * from user
*</sql>
*
*<select id="findAll" resultType="user">
* <include refid="selectSql"/>
*</select>
*/
sqlElement(context.evalNodes("/mapper/sql"));
//解析完sql代码块以后与用到代码块的sql语句进行拼接
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
解析完成后会封装成MapperStatement对象,首先解析mapper标签中的命名空间,也就是映射文件所指向的mapper接口
解析parameterMap比较简单,就是从各个标签中获取到属性值,然后每一个parameterMap都会构建一个ParameterMapping对象保存parameterMap的属性
private void parameterMapElement(List<XNode> list) {
for (XNode parameterMapNode : list) {
String id = parameterMapNode.getStringAttribute("id");
String type = parameterMapNode.getStringAttribute("type");
Class<?> parameterClass = resolveClass(type);
List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
List<ParameterMapping> parameterMappings = new ArrayList<>();
for (XNode parameterNode : parameterNodes) {
String property = parameterNode.getStringAttribute("property");
String javaType = parameterNode.getStringAttribute("javaType");
String jdbcType = parameterNode.getStringAttribute("jdbcType");
String resultMap = parameterNode.getStringAttribute("resultMap");
String mode = parameterNode.getStringAttribute("mode");
String typeHandler = parameterNode.getStringAttribute("typeHandler");
Integer numericScale = parameterNode.getIntAttribute("numericScale");
ParameterMode modeEnum = resolveParameterMode(mode);
Class<?> javaTypeClass = resolveClass(javaType);
//jdbcType - String -> varchar java参数类型与数据库的对应转换
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
parameterMappings.add(parameterMapping);
}
builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
}
}
解析resultMap:
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
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);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
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);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
这里主要就看一下再解析type别名的时候的一个小细节,type属性指向的是java pojo类的全路径名,当然如果配置了别名这里就直接获取别名,通过别名去解析类文件即可,调用resolveClass
方法,将type属性值传入
protected <T> Class<? extends T> resolveClass(String alias) {
if (alias == null) {
return null;
}
try {
return resolveAlias(alias);
} catch (Exception e) {
throw new BuilderException("Error resolving class. Cause: " + e, e);
}
}
这里的代码又是和别名解析是类似的,如果type属性为null,就直接返回null,不为null就再次调用resolveAlias
方法
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
// issue #748
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
if (typeAliases.containsKey(key)) {
value = (Class<T>) typeAliases.get(key);
} else {
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
这里我配置的type属性值是user,因为之前配置了别名,在typeAliases缓存中已经存在了user的别名配置所以这里可以直接获取到,如果没有配置别名也没关系,比如我们提供了type属性值为com.wjc.entity.User
,那么mybatis会通过app类加载器去加载该路径下的User类。解析完这些属性以后,就会去解析sql语句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
方法不仅会拼接静态sql代码块还会去解析占位符,这也就是给经常被提及的面试题的一个回答,#与$符号的区别,解析静态sql代码块比较简单就是将我们所编写的代码块的id与sql语句放入到一个map中,key - id,value - sql语句
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
//将节点的名称转换成大写,并且判断是否是查询操作,因为查询操作和增删改有区别
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//是否刷新缓存,默认值:增删改刷新 查询不刷新
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//是否使用二级缓存,默认值:增删改不使用 查询使用
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//是否需要处理嵌套查询结果 group by 语句是否要封装成map对象,很少用
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
//替换Include标签为对应的sql标签里面的值,实现了拼接sql语句块
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
//解析参数类型
Class<?> parameterTypeClass = resolveClass(parameterType);
//解析配置的自定义脚本语言驱动(没用过),比如我们在sql语句中#{}占位符mybatis会为我们解析成标准的sql语句
//而这个解析规则我们可以自己去重新定义
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
//解析selectKey(除了经常用来返回自增的主键值),还可以用来做嵌套查询
/**
*<selectKey resultType="int" keyColumn="id" order="AFTER" keyProperty="userId">
* select last_insert_id();
*</selectKey>
*/
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//设置主键自增规则
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//解析Sql 根据sql语句判断是否需要动态解析 如果没有动态sql语句且只有#{}的时候,直接静态解析占位符,使用?占位
//当有${}时这里就不解析,等到执行时再解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
这里在实例化XMLScriptBuilder的时候会实例化几个handler后面在解析sql语句的时候会用到
我们的动态sql标签就是通过这些处理器来实现的,实例化XMLScriptBuilderXMLScriptBuilderu就调用builder的parseScriptNode
方法来解析,首先判断当前的sql语句是否是动态sql语句
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
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("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
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();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
判断的原理也很简单,以本例中的测试环境为例,当前的select标签下是否有子标签,这里稍稍修改了一下测试环境
<select id="findAll" resultType="user" parameterType="user">
<include refid="selectSql"/>
<where>
<if test="username != null">
and username like #{username}
</if>
<if test="sex != null">
and sex = #{sex}
</if>
</where>
</select>
根据标签名选择相应的handler,然后递归执行解析过程,where标签里面还有if标签,又回去解析if标签调用if标签处理器
这里就是一些字符串的操作就没什么好多聊的,如果说是动态的sql那么isDynamic会被赋值为true,然会方法返回后就会实例化DynamicSqlSource对象,不会去处理动态sql
下面再来看看非动态sql需要解析的情况
apply方法底层最终调用了lambda表示,再往下跟踪
其实就是调用了字符串拼接的方法,这样的设计就是因为静态的sql语句在这里就直接解析并且用占位符来替换,而动态sql需要到使用的时候再去解析,这里做的只是预编译而已,所谓预编译其实就是解析select标签中的一些属性值封装成java pojo类
静态sql的解析:
调用parse方法
接下来方法调用的parse又是字符串的拼接,替换操作
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
//open token - #{
//close token - }
//handleToken 方法返回?
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
//遍历里面所有的#{}
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
如果是$符号会直接被当成文本来处理
解析完成后