MyBatis 源码解析(一):初始化和动态代理
简介
MyBatis 是 Java 开发中非常流行的 ORM 框架,其封装了 JDBC 并且解决了 Java 对象与输入参数和结果集的映射,同时又能够让用户方便地手写 SQL 语句。MyBatis 的行为类似于以下几行代码:
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, usr, password);
PraparedStatement st = conn.prepareStatement(sql);
st.setInt(0, 1);
st.execute();
ResultSet rs = st.getResultSet();
while (rs.next()) {
String result = rs.getString(colname);
}
上面是 JDBC 的使用流程,MyBatis 其实就是对上面的代码进行分解包装。本文将对 MyBatis 的代码进行分析,探究其中的逻辑。
基本用法
首先从 MyBatis 的基本用法开始,下面是 MyBatis 官网的入门示例:
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
其中 mabatis-config.xml
是 MyBatis 的核心配置文件,其中包括数据源、事务管理器、别名以及 SQL 对应的 Mapper 文件等,如下所示:
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<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>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
有了 SqlSessionFactory
后就可以创建 SqlSession
来调用 select
以及 update
等方法请求数据了:
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
} finally {
session.close();
}
配置文件解析
我们按照上面的代码流程开始分析源码,首先是配置文件的解析:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream)
,SqlSessionFactoryBuilder
显然是为了构建 SqlSessionFactory
,而且是从配置文件的输入流构建,代码如下:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 创建 XMLConfigBuilder
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// parse.parse() 进行解析
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
首先是创建了一个 XMLConfigBuilder
对象,它是用来解析 Config 文件的。XMLConfigBuilder
继承自 BaseBuilder
,BaseBuilder
中有个 Configuration
类型的变量,这个类需要重点关注,Config 文件中解析出来的所有信息都保存在这个变量中。
创建了 XMLConfigBuilder
后调用了其 parse
方法:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 在这个函数中解析
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
这里主要逻辑在 parseConfiguration
中:
private void parseConfiguration(XNode root) {
try {
// 解析 properties
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
// 解析 type alias
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 setting
settingsElement(root.evalNode("settings"));
// 解析 environment
environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 mapper
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
这里解析了 config 文件中所有的标签,包括 properties
、settings
以及 mappers
等,下面挑几个看一下。
settings
settings 是对 MyBatis 的一些配置项,包括缓存的开启以及是否使用驼峰转换(mapUnderscoreToCamelCase)等,代码如下:
private void settingsElement(XNode context) throws Exception {
if (context != null) {
// 将配置项保存到 Properties 中
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
MetaClass metaConfig = MetaClass.forClass(Configuration.class);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
// 默认开启缓存
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));
}
}
可以看出,settings 的子节点保存在 Properties
中,然后校验是否有不合法的子节点,最后提取出其中的属性保存到 Configuration
中,上面提到这个类专门用于保存 Config 文件解析出的信息。
从上面也可以看到 MyBatis 的一些默认属性,例如一级缓存如果没有配置,那么默认是开启的。
environments
environments 包含了数据源(dataSource) 和事务管理器(transactionManager) 的配置,代码如下:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
// 解析 transactionManager
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 解析 dataSource
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 设置 environment 到 configuration
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties props = context.getChildrenAsProperties();
// 通过反射实例化
TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a TransactionFactory.");
}
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties props = context.getChildrenAsProperties();
// 通过反射实例化
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
其中主要是两部分,第一部分解析 transactionManager
,第二部分解析 dataSource
。从 transactionManagerElement
和 dataSourceElement
中可以看出通过对应 Class
文件的 newInstance
实例化出对应的工厂对象。最终解析出的 transactionManager
和 dataSource
依然是设置到 Configuration
中。
mappers
mappers 对应了具体的 SQL Mapper 文件,也是我们要分析的重点。
mappers 标签可以多种子标签,上面的示例中是 mapper
配合 resource
:
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
我们下面看一下此种形式在源码中的解析:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
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");
// 这个分支解析 resource 形式的标签
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建 XMLMapperBuilder
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.");
}
}
}
}
}
resource
标签解析的对应分支是 (resource != null && url == null && mapperClass == null)
,其中创建了一个 XMLMapperBuilder
对象然后调用 parse
方法进行解析:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解析 mapper 下面的标签,包括 namespace、cache、parameterMap、resultMap 等
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
// namespace 对应 Mapper 对应接口的全名(包名 + 类名)
String namespace = context.getStringAttribute("namespace");
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
// 解析生成 ParameterMap
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析生成 ResultMap
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
// 每一个 sql 语句生成一个 MappedStatement
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);
}
}
configurationElement
用于解析具体的子标签,如 namespace
、cache
、parameterMap
、resultMap
以及 select|insert|update|delete
等。
namespace
对应了 Mapper 接口类的包名 + 类名,通过 namespace
可以唯一定位一个 Class
文件,解析的 namespace
保存在 builderAssistant
中,后面会用到。
parameterMap
和 resultMap
解析会生成 ParameterMap
和 ResultMap
对象。每个 SQL 语句解析会生成 MappedStatement
。
在上面的 parse
方法中,解析完标签后调用了 bindMapperForNamespace
,这个实现了加载 namespace
对应的 Class
,并且为每个 Class
创建了代理类工厂对象(MapperProxyFactory
)。
MapperProxyFactory
MapperProxyFactory
用于为 Mapper 接口类创建代理对象,代理对象指的是
BlogMapper mapper = session.getMapper(BlogMapper.class)
生成的对象。
下面从 bindMapperForNamespace
开始:
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 加载类
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
// 添加 mapper 和 MapperProxyFactory
configuration.addMapper(boundType);
}
}
}
}
其中先从 builderAssistant
取出 namespace
,然后加载对应的 Class
(boundType = Resources.classForName(namespace)
)。最后调用 configuration.addMapper(boundType)
添加到 configuration
中。configuration.addMapper(boundType)
很关键,看代码:
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 {
// 添加到 Map<Class<?>, MapperProxyFactory<?>> 中
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);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
关键的一行是 knownMappers.put(type, new MapperProxyFactory<T>(type))
,其中 knownMappers
的类型是 Map<Class<?>, MapperProxyFactory<?>>
,即 key 是 Class
,value 是 MapperProxyFactory
。这里的 MapperProxyFactory
即是动态代理对象的工厂,下面是其 newInstance
方法的代码:
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<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
从中可以看出,这里用的是 Java 的动态代理,Proxy.newProxyInstance
方法生成指定接口的代理对象,这个方法的第三个参数是用于方法拦截的对象,这里是 MapperProxy
的实例。
由此可以知道,具体的执行 SQL 语句的操作是由这个类拦截并且执行的,看看这个类的 invoke
方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
如果是 Object
类中声明的方法,则直接执行,否则调用 MapperMethod
的 execute
,其中便是 JDBC 相关的逻辑了。限于篇幅,具体内容留到下一篇文章再看。
在分析完配置文件的解析后,再回到 XMLConfigBuilder
中:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 构建 SqlSessionFactory
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
在 parser.parse()
执行完后,生成一个 Configuration
对象,最后调用 build
构建 SqlSessionFactory
,代码如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
可以看到,最终创建的是 DefaultSqlSessionFactory
,这个类内部持有 Configuration
,并且提供了多个重载的 openSession
方法用于创建 SqlSession
。
到这里,初始化部分就结束了。
总结
MyBatis 的初始化流程主要是解析配置文件,将相关信息保存在 Configuration
中,同时对每个 namespace
代表的 Class
生成代理对象工厂。最后,利用 Configuration
生成了一个 DefaultSqlSessionFactory
,通过这个对象可以创建 SqlSession
执行 SQL 请求,相关内容将在下一篇分析。