【Mybatis】配置类解析的源码分析

Mybaits整体体系图

Mybaits整体体系图.png

Mybaits功能架构

  • API接口层:提供给外部使用的接口API,程序员可以通过这些本地API来操作数据库。
  • 数据处理层:负责具体的SQL处理、SQL解析、SQL执行和执行结果映射处理...
  • 基础支撑层:通用基础功能,包括连接管理、事务管理、配置加载、缓存处理...

mybatis对传统的JDBC的解决方案

  • 数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
    解决:在SqlMapConfig.xml中配置数据连接池,使用连接池管理数据库链接。
  • Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
    解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
  • 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
    解决:Mybatis自动将java对象映射至sql语句,通过statement中的parameterType定义输入参数的类型。
  • 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
    解决:Mybatis自动将sql执行结果映射至java对象,通过statement中的resultType定义输出结果的类型。

本文使用的全局配置文件

<?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>
    <!--SqlSessionFactoryBuilder中配置的配置文件的优先级最高;config.properties配置文件的优先级次之;properties标签中的配置优先级最低 -->
    <properties resource="org/mybatis/example/config.properties">
      <property name="username" value="username"/>
      <property name="password" value="password"/>
    </properties>

    <!--一些重要的全局配置-->
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <typeAliases>

    </typeAliases>

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!--默认值为 false,当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果-->
            <!--如果某些查询数据量非常大,不应该允许查出所有数据-->
            <property name="pageSizeZero" value="true"/>
        </plugin>
    </plugins>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://10.59.97.10:3308/Test"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <databaseIdProvider type="DB_VENDOR">
        <property name="MySQL" value="mysql" />
        <property name="Oracle" value="oracle" />
    </databaseIdProvider>

    <mappers>
	<!-- 1、必须保证接口名称和XML名称相同,还必须在同一个包中。-->
	<package name = "com.cangqiong"/>
	<!-- 2、不保证同接口同包。-->
	<mapper resouse = "mapper/Test.xml"/>
	<!-- 3、保证接口名称和XML名称相同,还必须在同一个包中。-->
	<mapper class= "com.cangqiong.Test"/>
	<!-- 4、使用本地路径或者网络路径。-->
	<mapper url= "file:E/test/TestMapper.xml"/>
    </mappers>
</configuration>

本文使用的Mybatis使用方式

public static void main(String[] args) {
        String resource = "mybatis-config.xml";
        Reader reader;
        try {
            //将XML配置文件构建为Configuration配置类
            reader = Resources.getResourceAsReader(resource);
            // 通过加载配置文件流构建一个SqlSessionFactory,默认使用DefaultSqlSessionFactory
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
            // 数据源执行器,默认使用DefaultSqlSession
            SqlSession session = sqlMapper.openSession();
            try {
                // 执行查询,底层执行jdbc
                //User user = (User)session.selectOne("com.zhangwei.mapper.selectById", 1);

                UserMapper mapper = session.getMapper(UserMapper.class);
                System.out.println(mapper.getClass());
                User user = mapper.selectById(1L);
                System.out.println(user.getUserName());
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                session.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
}

解析全局配置源码分析

/**
 * 全局配置类解析入口
 * 源码位置:org.apache.ibatis.session.SqlSessionFactoryBuilder.build(Reader)
 */
public SqlSessionFactory build(Reader reader) {
	return build(reader, null, null);
}

/**
 * 全局配置类解析入口,三个参数的构造方法
 * 源码位置:org.apache.ibatis.session.SqlSessionFactoryBuilder.build(Reader, String, Properties)
 */
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
	try {
		// 继续java中sax的方向进行解析,Mybatis对其进行了增强
		XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
		// 具体的即系流程
		return build(parser.parse());
	} catch (Exception e) {
		throw ExceptionFactory.wrapException("Error building SqlSession.", e);
	} finally {
		ErrorContext.instance().reset();
		try {
			reader.close();
		} catch (IOException e) {
			// Intentionally ignore. Prefer previous error.
		}
	}
}

/**
 * 解析配置文件的外部方法
 * 源码位置:org.apache.ibatis.builder.xml.XMLConfigBuilder.parse()
 */
public Configuration parse() {
	// 判断是否解析过,解析过直接抛异常。所以Mybatis的配置文件只有一个并且只能解析一次!
	if (parsed) {
		throw new BuilderException("Each XMLConfigBuilder can only be used once.");
	}
	// 标记已经解析了,下次进来抛异常
	parsed = true;
	// 解析configuration节点,配置文件的核心
	parseConfiguration(parser.evalNode("/configuration"));
	// 返回解析好的数据
	return configuration;
}

/**
 * 解析配置文件的最核心的方法,解析配置文件的configuration节点
 * 源码位置:org.apache.ibatis.session.SqlSessionFactoryBuilder.build(Reader, String, Properties)
 */
private void parseConfiguration(XNode root) {
	try {
		// issue #117 read properties first
		// 解析properties标签,并set到Configration对象中
		// 在properties配置属性后,在Mybatis的配置文件中就可以使用${key}的形式使用了。
		propertiesElement(root.evalNode("properties"));

		// 解析setting标签的配置
		Properties settings = settingsAsProperties(root.evalNode("settings"));
		// 添加vfs的自定义实现,这个功能不怎么用
		loadCustomVfs(settings);

		// 配置类的别名,配置后就可以用别名来替代全限定名
		// mybatis默认设置了很多别名,参考附录部分
		typeAliasesElement(root.evalNode("typeAliases"));

		// 解析拦截器和拦截器的属性,set到Configration的interceptorChain中
		// MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
		// Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
		// ParameterHandler (getParameterObject, setParameters)
		// ResultSetHandler (handleResultSets, handleOutputParameters)
		// StatementHandler (prepare, parameterize, batch, update, query)
		pluginElement(root.evalNode("plugins"));

		// Mybatis创建对象是会使用objectFactory来创建对象,一般情况下不会自己配置这个objectFactory,使用系统默认的objectFactory就好了
		objectFactoryElement(root.evalNode("objectFactory"));
		objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
		reflectorFactoryElement(root.evalNode("reflectorFactory"));

		// 设置在setting标签中配置的配置
		settingsElement(settings);

		// 解析环境信息,包括事物管理器和数据源,SqlSessionFactoryBuilder在解析时需要指定环境id,如果不指定的话,会选择默认的环境;
		// 最后将这些信息set到Configration的Environment属性里面
		environmentsElement(root.evalNode("environments"));

		// 数据源ID,用于区分不同的数据库,目前很少用
		databaseIdProviderElement(root.evalNode("databaseIdProvider"));

		// 无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。解析typeHandler。
		// 简单说就是JAVA类型与数据库类型的对应关系
		typeHandlerElement(root.evalNode("typeHandlers"));
		// 解析Mapper
		mapperElement(root.evalNode("mappers"));
	} catch (Exception e) {
		throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
	}
}

Mapper的解析

/**
 * mapper信息解析入口
 * 源码位置:org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement(XNode)
 */
private void mapperElement(XNode parent) throws Exception {
	// 解析的节点不为null
	if (parent != null) {
		// 遍历每个节点
		for (XNode child : parent.getChildren()) {
			// package的解析方式
			if ("package".equals(child.getName())) {
				String mapperPackage = child.getStringAttribute("name");
				configuration.addMappers(mapperPackage);
			} else {
				String resource = child.getStringAttribute("resource");
				String url = child.getStringAttribute("url");
				String mapperClass = child.getStringAttribute("class");
				if (resource != null && url == null && mapperClass == null) {
					// resouse的解析方式
					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) {
					// url的解析方式
					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的解析方式
					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源码分析

/**
 * 包路径解析Mapper的入口
 * 源码位置:org.apache.ibatis.session.Configuration.addMappers(String)
 */
public void addMappers(String packageName) {
	mapperRegistry.addMappers(packageName);
}

/**
 * 继续调用
 * 源码位置:org.apache.ibatis.binding.MapperRegistry.addMappers(String)
 */
public void addMappers(String packageName) {
	addMappers(packageName, Object.class);
}

/**
 * 继续调用
 * 源码位置:org.apache.ibatis.binding.MapperRegistry.addMappers(String, Class<?>)
 */
public void addMappers(String packageName, Class<?> superType) {
	ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
	resolverUtil.find(new ResolverUtil.IsA(superType), packageName);

	// 得到所有的包路径内的文件
	Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();

	// 遍历包中的所有文件
	for (Class<?> mapperClass : mapperSet) {
		addMapper(mapperClass);
	}
}

/**
 * 添加单个Mapper
 * 源码位置:org.apache.ibatis.binding.MapperRegistry.addMapper(Class<T>)
 */
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 {
			// 添加到缓存。。值是一个MapperProxyFactor,后面会使用jdk的动态代理去参数代理对象
			knownMappers.put(type, new MapperProxyFactory<T>(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);
			}
		}
	}
}

/**
 * 解析mapper文件
 * 源码位置:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse()
 */
public void parse() {
	// 得到mapper路径
	String resource = type.toString();
	// 是否已经解析mapper对应的xml
	if (!configuration.isResourceLoaded(resource)) {
		// 根据文件接口名,获取对应的xml文件
		loadXmlResource();
		// 标记位已经解析
		configuration.addLoadedResource(resource);
		// 设置解析的nameSpace
		assistant.setCurrentNamespace(type.getName());
		// 解析缓存
		parseCache();
		parseCacheRef();

		// 得到里面的方法,看是不是用了注解
		Method[] methods = type.getMethods();
		for (Method method : methods) {
			try {
				// issue #237
				// 判断是不是用了注解
				if (!method.isBridge()) {
					// 用了注解的解析为MappedStatement
					parseStatement(method);
				}
			} catch (IncompleteElementException e) {
				configuration.addIncompleteMethod(new MethodResolver(this, method));
			}
		}
	}

	// 解析方法
	parsePendingMethods();
}

加载XML文件源码分析

/**
 * 加载XML文件
 * 源码位置:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.loadXmlResource()
 */
private void loadXmlResource() {
	// Spring may not know the real resource name so we check a flag
	// to prevent loading again a resource twice
	// this flag is set at XMLMapperBuilder#bindMapperForNamespace
	if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
		// 更觉接口名称找xml的名称
		String xmlResource = type.getName().replace('.', '/') + ".xml";
		InputStream inputStream = null;
		try {
			// 读取文件
			inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
		} catch (IOException e) {
			// ignore, resource is not required
		}
		if (inputStream != null) {
			// 创建解析器
			XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(),
					xmlResource, configuration.getSqlFragments(), type.getName());
			// 解析XML
			xmlParser.parse();
		}
	}
}

/**
 * 解析XML文件
 * 源码位置:org.apache.ibatis.builder.xml.XMLMapperBuilder.parse()
 */
public void parse() {
	// 判断当前的mapper是否在加载过
	if (!configuration.isResourceLoaded(resource)) {
		// 加载mapper节点
		configurationElement(parser.evalNode("/mapper"));
		// 资源保存在configuration中
		configuration.addLoadedResource(resource);
		bindMapperForNamespace();
	}

	parsePendingResultMaps();
	parsePendingCacheRefs();
	parsePendingStatements();
}

/**
 * 加载mapper节点
 * 源码位置:org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XNode)
 */
private void configurationElement(XNode context) {
	try {
		// 得到节点中namespace的值
		String namespace = context.getStringAttribute("namespace");
		// 没有值,抛异常
		if (namespace == null || namespace.equals("")) {
			throw new BuilderException("Mapper's namespace cannot be empty");
		}
		// 将namespace进行存储(里面判断是否与类名相同)
		builderAssistant.setCurrentNamespace(namespace);
		// 解析缓存引用
		cacheRefElement(context.evalNode("cache-ref"));
		// 解析cache标签。二级缓存
		cacheElement(context.evalNode("cache"));
		// 解析parameterMap。3.5之后不推荐使用了
		parameterMapElement(context.evalNodes("/mapper/parameterMap"));
		// 解析resultMap节点
		resultMapElements(context.evalNodes("/mapper/resultMap"));
		// 解析sql节点
		sqlElement(context.evalNodes("/mapper/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);
	}
}

二级缓存解析逻辑

/**
 * 源码位置:org.apache.ibatis.builder.xml.XMLMapperBuilder.cacheElement(XNode)
 */
private void cacheElement(XNode context) throws Exception {
	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);
		// 设置刷新间隔
		Long flushInterval = context.getLongAttribute("flushInterval");
		// 得到缓存的大小
		Integer size = context.getIntAttribute("size");
		// 判断是不是只读
		boolean readWrite = !context.getBooleanAttribute("readOnly", false);
		// 得到调用方式
		boolean blocking = context.getBooleanAttribute("blocking", false);
		Properties props = context.getChildrenAsProperties();
		// 构建缓存,加入到缓存链,组成装饰者模式
		builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
	}
}

解析增删改查节点

/**
 * 源码位置:org.apache.ibatis.builder.xml.XMLMapperBuilder.buildStatementFromContext(List<XNode>)
 */
private void buildStatementFromContext(List<XNode> list) {
	// 解析增删改查节点
	if (configuration.getDatabaseId() != null) {
		buildStatementFromContext(list, configuration.getDatabaseId());
	}
	buildStatementFromContext(list, null);
}

/**
 * 解析增删改查节点
 * 源码位置:org.apache.ibatis.builder.xml.XMLMapperBuilder.buildStatementFromContext(List<XNode>, String)
 */
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
	// 循环每一个节点!
	for (XNode context : list) {
		// 创建XMLStatement构建器对象
		final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant,
				context, requiredDatabaseId);
		try {
			// 解析具体的节点:将所有属性解析出来,sql解析出来
			statementParser.parseStatementNode();
		} catch (IncompleteElementException e) {
			configuration.addIncompleteStatement(statementParser);
		}
	}
}

结束语

  • 获取更多本文的前置知识文章,以及新的有价值的文章,让我们一起成为架构师!
  • 目前已经完成了并发编程、MySQL、spring源码、Mybatis的源码。可以在公众号下方菜单点击查看之前的文章!
  • 接下来的目标是深入分析JVM、tomcat、redis
  • 这个公众号,无广告!!!每日更新!!!
    作者公众号.jpg
posted @ 2022-03-28 21:53  程序java圈  阅读(63)  评论(0编辑  收藏  举报