MyBatis的運行的核心原理解析(一)

一.MyBatis的運行分爲兩部分:

  • 讀取配置文件緩存道Configuration對象,用來建立SqlSessionFactoryjava

  • sqlSession的執行node

以上兩部分種第一部分的SqlSessionFactory的建立時比較容易理解的,可是SqlSession的執行過程要複雜許多。算法

二.涉及到的技術

  • 動態代理技術(JDK,CGLIB)
  • 反射技術

三.SqlSessionFactory的構建過程

  • 使用org.apache.ibatis.builder.xml.XMLConfigBuilder解析XML文件的配置文件,讀出配置參數,並將幾乎全部讀取的配置數據設置進org.apache.ibatis.session.Configuration類裏面sql

  • 使用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文件的配置項都會保存進這個類裏,並且有且只有一份(單例)。它會作以下的初始化:apache

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個部分組成的:session

  • MapperStatement,它保存映射器的一個節點(select|insert|update|delete)。包括許多咱們配置的SQL,SQL的id,緩存信息,resultMap,parameterType,resultType,languageDriver等重要配置內容。app

  • SqlSource,他是提供BoundSql對象的地方,它是MapperStatement的一個屬性。ide

  • 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是很危險的,須要謹慎處理.
相關文章
相關標籤/搜索