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 请求,相关内容将在下一篇(MyBatis 源码解析(二):SqlSession 执行流程)分析。
如果我的文章对您有帮助,不妨点个赞支持一下(^_^)