一.MyBatis的运行分为两部分:
-
读取配置文件缓存道Configuration对象,用来创建SqlSessionFactory
-
sqlSession的执行
以上两部分种第一部分的SqlSessionFactory的创建时比较容易理解的,但是SqlSession的执行过程要复杂许多。
二.涉及到的技术
- 动态代理技术(JDK,CGLIB)
- 反射技术
三.SqlSessionFactory的构建过程
-
使用org.apache.ibatis.builder.xml.XMLConfigBuilder解析XML文件的配置文件,读出配置参数,并将几乎所有读取的配置数据设置进org.apache.ibatis.session.Configuration类里面
-
使用org.apache.ibatis.session.Configuration对象去构建SqlSessionFactory(是一个接口,实际上使用的是org.apache.ibatis.session.defaults.DefaultSqlSessionFactory),其实大部分情况使用默认的org.apache.ibatis.session.defaults.DefaultSqlSessionFactory就可以了,没有必要自己创建SqlSessionFactory
四.构建你Configuration
- 在SqlSessionFactory的构建种,Configuration是最重要的,它有如下的作用
- 读入配置文件,包括基础配置的XML文件和映射器的XML文件.
- 初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,例如,插件,映射器,ObjectFactory和typeHandler对象
- 提供单例,为后续创建SessionFactory服务并提供配置的参数
- 执行一些重要的对象方法,初始化配置信息
以上的几步就可以概括了Configuration类的作用,虽然可以分为四部概括它,然而它并不是一个简单的类,在源码中可以看到几乎所有的配置都可以在这个类里面找到。Configuration是通过XMLConfigBuilder去构建的,所有读取到的XML文件的配置项都会保存进这个类里,而且有且只有一份(单例)。它会做如下的初始化:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
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);
}
}
五.映射器的内部组成
由于插件需要频繁的访问映射器的内部组成,这里有必要深入的了解下映射器的内部组成
一般而言,一个映射器是由3个部分组成的:
-
MapperStatement,它保存映射器的一个节点(select|insert|update|delete)。包括许多我们配置的SQL,SQL的id,缓存信息,resultMap,parameterType,resultType,languageDriver等重要配置内容。
-
SqlSource,他是提供BoundSql对象的地方,它是MapperStatement的一个属性。
-
BoundSql,它是建立SQL和参数的地方,他有3个常用的属性:SQL,parameterObject,parameterMappings。 他们的类型关系图如下:
MappedStatement对象关联的东西很多,大多数情况下是不需要去修改它的,容易导致错误,SqlSource是一个接口,它主要作用是根据参数和其他的规则组装SQL,这些都是很复杂的东西,MyBatis本身已经实现了它,一般不需要修改。对于SQL的而言,主要的规则都体现在BoundSql类对象上,在插件中往往需要拿到它进而可以拿到当前运行的SQL和参数一级参数规则,做出适当的修改,满足我们特殊的需求。
MapperStatement创建过程解析
Mapper接口是用来声明持久层的方法,而Mapper配置对于的xml,决定了方法的执行的内容,决定持久层方法的行为。在MyBatis启动时,会解析这些包含SQL的XML文件,并将其包装成为MapperStatement对象,并将MapperStatement注册到全局的configuration对象上。 讲解过程如下: 在基础配置文件中,要加载映射文件*Mapper.xml的时候
<mappers>
<!--<mapper resource="com/batis/bean/CityResidentMapper.xml"/>-->
<!--<package name="com.batis.mapper"></package>-->
<!--<mapper class="com.batis.mapper.CityResidentMapper"></mapper>-->
<mapper resource="com/batis/mapper/CityResidentMapper.xml"/>
<mapper resource="com/batis/mapper/IdentificationCardMapper.xml"/>
<mapper resource="com/batis/mapper/PersonalHobbyMapper.xml"/>
</mappers>
此时MyBatis会将加载的配置进行解析,如下:
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");
if (resource != null && url == null && mapperClass == null) {
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) {
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.");
}
}
}
}
}
从源码可以知道,映射文件可以时使用package标签配合name属性的的方式,也可以使用mapper标签配置resource和name属性的方式引入并且使用MapperRegistry注册.注册的容器是一个map,Map<Class<?>,MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>,MapperProxyFactory<?>>();key是mapper接口的完整类名,value是mapper的代理工厂。注册完成后,还要做解析XML的文件操作,具体操作如下:
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);
}
}
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<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);
}
}
}
}
解析代码如下:
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();//加载文件
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
MyBatis将类名中的"."替换成为"/",然后加上后缀".xml",拼接成为XML资源路径,然后判断是否已经加载过XML文件,然后使用xmlMapperBuilder建造这解析XML文件中的元素.
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())) {
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());
//开始解析
xmlParser.parse();
}
}
}
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
//开始解析文件的配置
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
-
resource是创建建造者的构造参数,type.getClass(),就是mapper的类型。判断然后还没有加载mapper,就开始解析XML文件中的mapper节点.
-
解析时,先设置命名空间。然后解析cache-ref元素,可以使用其他命名空间的的缓存。在configuration对象上有一个cacheRefMap用来维护引用缓存的关系。并且引用其他命名空间的引用指向助手类的currentCache属性上。如果被指向的命名空间还未加载,则抛出异常,并且往configuration对象上添加未处理的缓存引用chcheRef。
private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { 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")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } }
以下围绕上面这几个解析方法展开学习:
-
解析cache-ref元素,可以使用其他命名空间的的缓存。在configuration对象上有一个cacheRefMap用来维护引用缓存的关系。并且引用其他命名空间的引用指向助手类的currentCache属性上。如果被指向的命名空间还未加载,则抛出异常,并且往configuration对象上添加未处理的缓存引用chcheRef。
private void cacheRefElement(XNode context) { if (context != null) { configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); try { //引用其他命名空间的引用指向助手类的currentCache属性上 cacheRefResolver.resolveCacheRef(); } catch (IncompleteElementException e) { //往configuration对象上添加未处理的缓存引用chcheRef configuration.addIncompleteCacheRef(cacheRefResolver); } } }
-
解析缓存元素,可以使用type属性配置自定义的缓存,否则使用默认的PERPETUAL。然后用别名注册器注册缓存类。接下来注册缓存的回收算法,缓存大小,过期时间,是否只读等属性。然后由助手类通过反射创建一个具体的Cache对象。然后注册到configuration全局对象上。
private void cacheElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); 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); } }
-
parameterMapElement方法解析parameterMap是个不推荐使用的方法,但是resultMapElements解析的resultMap和sqlElement解析的SQL是有价值的。解析resultMap的元素比较多,解析完成后,还会根据解析到的映射关系创建一个结果处理器对象resultMapResolver,后面对数据库操作时,用来处理列和属性的类型转换。
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception { ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier()); String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType")))); String extend = resultMapNode.getStringAttribute("extends"); Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); Class<?> typeClass = resolveClass(type); Discriminator discriminator = null; List<ResultMapping> resultMappings = new ArrayList<ResultMapping>(); resultMappings.addAll(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<ResultFlag>(); if ("id".equals(resultChild.getName())) { flags.add(ResultFlag.ID); } resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); } } //结果处理器对象resultMapResolver ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); try { return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } }
解析SQL片段,用来复用的SQL,助手类将SQL片段的ID前面加上当前命名孔家和一点,用来区分其他命名空间,然后将SQL片段加载到configuration全局对象的sqlFragments对象上保存.
private void sqlElement(List<XNode> list) throws Exception {
if (configuration.getDatabaseId() != null) {
sqlElement(list, configuration.getDatabaseId());
}
sqlElement(list, null);
}
private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
sqlFragments.put(id, context);
}
}
}
紧接着就是创建statement对象,也是最后的关键步骤,首先需要解析XML配置文件的各个属性,然后处理<incluede></incluede>和<selectKey></selectKey>片段,根据include标签中的refid到全局配置中取对应的SQL片段。根据selectKey的配置信息,创建一个 MapperStatement,并且添加到全局配置中,然后移除selectKey节点
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
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);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
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))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
后面的操作,也是根据配置的属性,然后通过建造者创建mappedStatement对象。并添加到configuration全局对象上。
BoundSql提供3个主要属性:parameterMappings,parameterObject和sql。
- parameterObject是参数本身(系我们传递的简单对象,POJO,Map或者@param注解的参数),很常用
- 传递简单对象(包括int,string,float,double等),比如当我们传递int类型时,MyBatis会把参数Integer对象传递,类似的long,String,float,double也是如此。
- 如果我们传递的时POJO或者Map,那么这个parameterObject就是你传入的POJO或者Map不变。
- 我们可以传递多个参数,如果没有@Param注解,那么MyBatis就会把parameterObject变为一个Map<String,Object>对象,其键值的关系就是按顺序来规划的,类似与这样的形式{"1":param1,"2":p2,"3":p3,...},所以编写的时候我们都可以使用#{p1}或者#{1}去引用第一个参数.
- parameterMappings,他是一个List,每一个元素都时ParameterMapping的对象。这个对象会描述我们的参数。参数包括属性,名称,表达式,javaType,jdbcType,typeHandler等重要的信息,我们一般不需要改变它。通过它可以实现参数和SQL的结合,以便PreparedStatement能够通过它找到parameterObject对象的属性并设置参数,使得程序准确运行。
- 如果我们使用@Param注解,那么MyBatis就会把parameterObject也会变为一个Map<String,Object>对象,类似于没有@Param注解,只是把其数字的键值对应为@Param注解的的键值,比如@Param("key1")String p1,@Param("key2")int p2,@Param("key3")Role p3,那么这个parameterObject对象就越是一个Map<String,Object>,它的键值包含:{"key1":p1,"key2":p2,"key3":p3}。
- sql属性就是我们书写的映射器里卖弄的一条SQL,在大多数时候午休修改它,只有咋i插件的情况下,我们需要进行改写,改写SQL是很危险的,需要谨慎处理.