Mybatis原始碼分析(二)XML的解析和Annotation的支援

清幽之地發表於2019-03-10

一、前言

上一節內容我們簡單回顧了Mybatis的整體架構和相關概念知識點,並簡述了本系列所用框架的版本。Mybatis功能強大,花樣繁多。我們不會太關心所有的技術點,而是重點剖析常用的功能點。同Spring相比,Mybatis多以應用為主。從本節開始,我們正式開始原始碼的分析。

二、環境配置

每個基於 MyBatis 的應用都是以一個 SqlSessionFactory 的例項為中心的,SqlSessionFactory 的例項可以通過 SqlSessionFactoryBuilder 獲得。而 SqlSessionFactoryBuilder 則可以從 XML 配置檔案或一個預先定製的 Configuration 的例項構建出 SqlSessionFactory 的例項。例如:

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = 
		new SqlSessionFactoryBuilder().build(inputStream);
複製程式碼

當然,上面這些是Mybatis官方的樣例。不過,我們日常開發中Mybatis都是與Spring一起使用的,交給Spring去搞定這些豈不更好。

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dataSource" />
	<property name="mapperLocations" value="classpath:com/viewscenes/netsupervisor/mapping/*.xml"></property>
	<property name="typeAliasesPackage">
		<array>
			<value>com.viewscenes.netsupervisor.entity</value>
		</array>
	</property>        
</bean> 
複製程式碼

三、初始化

我們來到org.mybatis.spring.SqlSessionFactoryBean,看到它實現了InitializingBean介面。這說明,在這個類被例項化之後會呼叫到afterPropertiesSet()。它只有一個方法

public void afterPropertiesSet() throws Exception {
	this.sqlSessionFactory = buildSqlSessionFactory();
}
複製程式碼

四、兩個介面

1、SqlSessionFactory

SqlSessionFactory是一個介面,它裡面其實就兩個方法:openSession、getConfiguration

package org.apache.ibatis.session;
public interface SqlSessionFactory {
  SqlSession openSession();
  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);
  Configuration getConfiguration();
}
複製程式碼

我們知道,可以通過openSession方法獲取一個SqlSession物件,完成必要資料庫增刪改查功能。但是,SqlSessionFactory屬性也太少了,那些mapper對映檔案、SQL引數、返回值型別、快取等屬性都在哪呢?

2、Configuration

Configuration,你可以把它當成一個資料的大管家。MyBatis所有的配置資訊都維持在Configuration物件之中,基本每個物件都會持有它的引用。正是應了那句話我是革命一塊磚,哪裡需要往哪搬。下面是部分屬性

public class Configuration {
	//環境
	protected Environment environment;
	protected boolean safeRowBoundsEnabled;
	protected boolean safeResultHandlerEnabled = true;
	protected boolean mapUnderscoreToCamelCase;
	protected boolean aggressiveLazyLoading;
	protected boolean multipleResultSetsEnabled = true;
	protected boolean useGeneratedKeys;
	protected boolean useColumnLabel = true;
	protected boolean cacheEnabled = true;
	protected boolean callSettersOnNulls;
	protected boolean useActualParamName = true;
	protected boolean returnInstanceForEmptyRow;
	
	//日誌資訊的字首
	protected String logPrefix;
	
	//日誌介面
	protected Class<? extends Log> logImpl;
	
	//檔案系統介面
	protected Class<? extends VFS> vfsImpl;
	
	//本地Session範圍
	protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
	
	//資料庫型別
	protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
	
	//延遲載入的方法
	protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(
			Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
	
	//預設執行語句超時
	protected Integer defaultStatementTimeout;
	
	//預設的執行器
	protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
	
	//資料庫ID
	protected String databaseId;
	
	//mapper登錄檔
	protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
	
	//攔截器鏈
	protected final InterceptorChain interceptorChain = new InterceptorChain();
	
	//型別處理器
	protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
	
	//型別別名
	protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
	
	//語言驅動
	protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

	//mapper_id 和 mapper檔案的對映
	protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(
			"Mapped Statements collection");
			
	//mapper_id和快取的對映
	protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
	
	//mapper_id和返回值的對映
	protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
	
	//mapper_id和引數的對映
	protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
	
	//資源列表
	protected final Set<String> loadedResources = new HashSet<String>();
	
	未完.......
}
複製程式碼

五、構建SqlSessionFactory

afterPropertiesSet方法只有一個動作,就是buildSqlSessionFactory。它可以分為兩部分來看,先從配置檔案的property屬性中載入各種元件,解析配置到configuration中,然後載入mapper檔案,解析SQL語句,封裝成MappedStatement物件,配置到configuration中。

1、配置property屬性

  • typeAliases

這是一個型別的別名,很好用。比如在user_mapper的方法中,resultType和parameterType想使用實體類對映,而不用寫全限定類名。

//這裡的resultType就是別名
//它對應的是com.viewscenes.netsupervisor.entity.User
<select id="getUserById" resultType="user">
	select * from user where uid=#{uid}
</select>	
複製程式碼

它有兩種配置方式,指定包路徑或者指定類檔案的路徑。

<property name="typeAliasesPackage">
	<array>
		<value>com.viewscenes.netsupervisor.entity</value>
	</array>
</property>
或者
<property name="typeAliases">
	<array>
		<value>com.viewscenes.netsupervisor.entity.User</value>
	</array>
</property>  
複製程式碼

它的解析很簡單,就是拿到類路徑的反射物件,key為預設類名小寫,value為Class物件,註冊到容器中。當然了,你也可以使用@Alias註解來設定別名的名稱。

public void registerAlias(Class<?> type) {
   String alias = type.getSimpleName();
   Alias aliasAnnotation = type.getAnnotation(Alias.class);
   if (aliasAnnotation != null) {
     alias = aliasAnnotation.value();
   } 
   String key = alias.toLowerCase(Locale.ENGLISH);
   TYPE_ALIASES.put(key, type);
}
複製程式碼

TYPE_ALIASES容器就是一個HashMap,裡面已經預設新增了很多的型別別名。

registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
......未完
複製程式碼
  • typeHandlers

它是一個型別的處理器。在資料庫查詢出結果後,應該轉換成Java中的什麼型別?由它來決定。如果Mybatis裡面沒有你想要的,就可以在這裡自定義一個處理器。 這塊內容,在後續章節我將通過一個例項獨立講解。現在,先來看下它的預設處理器。

register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.TINYINT, new ByteTypeHandler());
register(JdbcType.SMALLINT, new ShortTypeHandler());
register(Integer.class, new IntegerTypeHandler());
register(JdbcType.INTEGER, new IntegerTypeHandler());
register(JdbcType.FLOAT, new FloatTypeHandler());
register(JdbcType.DOUBLE, new DoubleTypeHandler());
register(String.class, new StringTypeHandler());
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler());
register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
register(JdbcType.CHAR, new StringTypeHandler());
register(JdbcType.VARCHAR, new StringTypeHandler());
register(JdbcType.CLOB, new ClobTypeHandler());
register(JdbcType.LONGVARCHAR, new ClobTypeHandler());
.....未完
複製程式碼
  • plugins

可以配置一個或多個外掛。外掛功能很強大,在執行SQL之前,在返回結果之後,在插入資料時...都可以讓你有機會插手資料的處理。這個部分最常用的是分頁,在後續章節,筆者將通過分頁和資料同步的例項單獨講解。

<property name="plugins">
	<array>
		<bean class="com.viewscenes.netsupervisor.interceptor.xxxInterceptor"></bean>
		<bean class="com.viewscenes.netsupervisor.interceptor.xxxInterceptor"></bean>
	</array>
</property>
複製程式碼

2、解析mapperLocations

mapperLocations配置的是應用中mapper檔案的路徑,獲取所有的mapper檔案。通過解析裡面的select/insert/update/delete節點,每一個節點生成一個MappedStatement物件。最後註冊到Configuration物件的mappedStatements。key為mapper的namespace+節點id。

先來看一下方法的整體。

public class XMLMapperBuilder extends BaseBuilder {
	private void configurationElement(XNode context) {
		//名稱空間 即mapper介面的路徑
		String namespace = context.getStringAttribute("namespace");
		if (namespace == null || namespace.equals("")) {
			throw new BuilderException("Mapper's namespace cannot be empty");
		}
		//設定當前mapper檔案的名稱空間
		builderAssistant.setCurrentNamespace(namespace);
		//引用快取
		cacheRefElement(context.evalNode("cache-ref"));
		//是否開啟二級快取 
		cacheElement(context.evalNode("cache"));
		//引數
		parameterMapElement(context.evalNodes("/mapper/parameterMap"));
		//返回值
		resultMapElements(context.evalNodes("/mapper/resultMap"));
		//解析sql節點
		sqlElement(context.evalNodes("/mapper/sql"));
		//SQL語句解析
		buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
	}
}
複製程式碼
  • 快取

通過在mapper檔案中宣告</cache>來開啟二級快取。通過獲取快取的配置資訊來構建Cache的例項,最後註冊到configuration。

private void cacheElement(XNode context) throws Exception {
	
	//獲取快取的例項型別,在configuration初始化的時候註冊
	// typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    //typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    //typeAliasRegistry.registerAlias("LRU", LruCache.class);
	String type = context.getStringAttribute("type", "PERPETUAL");
	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);
}
複製程式碼

拿到這些屬性後,將快取設定到configuration。

//二級快取是和mapper繫結的
//所以,這裡的id就是mapper的名稱空間
public void addCache(Cache cache) {
	caches.put(cache.getId(), cache);
}
複製程式碼
  • resultMap、parameterMap

它們表示的是查詢結果集中的列與Java物件中屬性的對應關係。其實,只有在資料庫欄位與JavaBean不匹配的情況下才用到,通常情況下推薦使用resultType/parameterType,也就是直接利用實體類即可。這種方式很簡便,同時遵循約定大於配置,程式碼出錯的可能較小。

中間過程不看了,最後他們也都是註冊到configuration。

public void addParameterMap(ParameterMap pm) {
	parameterMaps.put(pm.getId(), pm);
}
public void addResultMap(ResultMap rm) {
	resultMaps.put(rm.getId(), rm);
}
複製程式碼
  • SQL標籤

SQL標籤可將重複的sql提取出來,使用時用include引用即可,最終達到sql重用的目的。它的解析很簡單,就是把內容放入sqlFragments容器。id為名稱空間+節點ID

  • SELECT、INSERT、UPDATE、DELETE

動態SQL的解析是Mybatis的核心所在。之所以是動態SQL,源自它不同的動態標籤,比如Choose、ForEach、If、Set等,而Mybatis把它們都封裝成不同的類物件,它們共同的介面是SqlNode。

SqlNode

每一種標籤又對應一種處理器。

private void initNodeHandlerMap() {
	nodeHandlerMap.put("trim", new TrimHandler());
	nodeHandlerMap.put("where", new WhereHandler());
	nodeHandlerMap.put("set", new SetHandler());
	nodeHandlerMap.put("foreach", new ForEachHandler());
	nodeHandlerMap.put("if", new IfHandler());
	nodeHandlerMap.put("choose", new ChooseHandler());
	nodeHandlerMap.put("when", new IfHandler());
	nodeHandlerMap.put("otherwise", new OtherwiseHandler());
	nodeHandlerMap.put("bind", new BindHandler());
}
複製程式碼
  • 靜態SQL

靜態SQL,就是不帶上面那些標籤的節點。它比較簡單,最後就是將SQL內容封裝到MixedSqlNode物件。MixedSqlNode物件裡面有個List,封裝的就是StaticTextSqlNode物件,而StaticTextSqlNode物件只有一個屬性text,即SQL內容。

protected MixedSqlNode parseDynamicTags(XNode node) {
	//SQL內容
	String data = child.getStringBody("");
	//生成TextSqlNode判斷是否為動態SQL
	TextSqlNode textSqlNode = new TextSqlNode(data);
	if (textSqlNode.isDynamic()) {
	  contents.add(textSqlNode);
	  isDynamic = true;
	} else {
	  contents.add(new StaticTextSqlNode(data));
	}
	return new MixedSqlNode(contents);
}
複製程式碼

如果是靜態SQL,將SQL語句中的#{}轉為?,返回StaticSqlSource物件 。

public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.configuration = configuration;
}
複製程式碼
  • 動態SQL

一個動態SQL會分為不同的子節點,我們以一個UPDATE語句為例,嘗試跟蹤下它的解析過程。比如下面的UPDATE節點會分為三個子節點。兩個靜態節點和一個SET動態節點,而SET節點又分為兩個IF動態節點。

<update id="updateUser" parameterType="user">
	update user 
	<set>
		<if test="username!=null">
			username = #{username},
		</if>
		<if test="password!=null">
			password = #{password},
		</if>
	</set>
	where uid = #{uid}
</update>

[
	[#text: update user ], 
	[set: null], 
	[#text: where uid = #{uid}]
]
複製程式碼

首先,獲得當前的節點的內容,即updateUser。呼叫parseDynamicTags

public class XMLScriptBuilder extends BaseBuilder {
	
	//引數node即為當前UDATE節點的內容
	protected MixedSqlNode parseDynamicTags(XNode node) {
		List<SqlNode> contents = new ArrayList<SqlNode>();
		//children分為3個子節點
		//2個靜態節點(update user和where id=#{id})
		//1個動態節點set
		NodeList children = node.getNode().getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
		  XNode child = node.newXNode(children.item(i));
		  //如果是靜態節點,將內容封裝成StaticTextSqlNode物件
		  if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
			String data = child.getStringBody("");
			TextSqlNode textSqlNode = new TextSqlNode(data);
			if (textSqlNode.isDynamic()) {
			  contents.add(textSqlNode);
			  isDynamic = true;
			} else {
			  contents.add(new StaticTextSqlNode(data));
			}
		  }
		  //動態節點
		  else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
			//獲取節點名稱 比如SET/IF
			String nodeName = child.getNode().getNodeName();
			//獲取節點標籤對應的處理類 比如SetHandler 
			NodeHandler handler = nodeHandlerMap.get(nodeName);
			handler.handleNode(child, contents);
			isDynamic = true;
		  }
		}
		return new MixedSqlNode(contents);
	}
}

複製程式碼

第一個是靜態節點update user。根據上面的原始碼,它將被封裝成StaticTextSqlNode物件,加入contents集合。

第二個是動態節點SET,他將呼叫到SetHandler.handleNode()。

private class SetHandler implements NodeHandler {
	//nodeToHandle為SET節點的內容,targetContents為已解析完成的Node集合
	public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
		//回撥parseDynamicTags
		MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
		//最終將SET節點封裝成SetSqlNode,放入Node集合
		SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
		targetContents.add(set);
	}
}
複製程式碼

在第二次回撥到parseDynamicTags方法時,這時候的引數為SET節點裡的2個IF子節點。同樣,它們會將當做動態節點解析,呼叫到IfHandler.handleNode()。

private class IfHandler implements NodeHandler {
	//nodeToHandle為IF節點的內容, targetContents為已解析完成的Node集合
	public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
		//回撥parseDynamicTags
		MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
		//獲取IF標籤的test屬性,即條件表示式
		String test = nodeToHandle.getStringAttribute("test");
		//將IF節點封裝成IfSqlNode物件,放入Node集合。
		IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
		targetContents.add(ifSqlNode);
    }
}
複製程式碼

就這樣,遞迴的呼叫parseDynamicTags方法,直到傳進來的引數Node為一個靜態節點,返回StaticTextSqlNode物件,並加入集合中。 第三個是靜態節點where id=#{id},封裝成StaticTextSqlNode物件,加入contents集合。

最後,contents集合就是UPDATE節點對應的各種sqlNode。

update節點

如果是動態SQL,返回DynamicSqlSource物件。

public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
	this.configuration = configuration;
	this.rootSqlNode = rootSqlNode;
}
複製程式碼

3、生成MappedStatement物件

mapper檔案中的每一個SELECT/INSERT/UPDATE/DELETE節點對應一個MappedStatement物件。

public MappedStatement addMappedStatement() {
	
	//全限定類名+方法名
    id = applyCurrentNamespace(id, false);
	//是否為查詢語句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
	
	//配置各種屬性
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);
	
	//引數型別
    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

	//將MappedStatement物件註冊到configuration
	//註冊其實就是往Map中新增。mappedStatements.put(ms.getId(), ms);
    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }
複製程式碼

六、Annotation的支援

1、註解式的SQL定義

除了在mapper檔案中配置SQL,Mybatis還支援註解方式的SQL。通過@Select,標註在Mapper介面的方法上。

public interface UserMapper {	
	@Select("select * from user ")
	List<User> AnnotationGetUserList();
}
複製程式碼

或者你想要的是動態SQL,那麼就加上<script>

public interface UserMapper {
	
	@Select("select * from user ")
	List<User> AnnotationGetUserList();
	
	@Select("<script>"
			+ "select * from user "
			+ "<if test='id!=null'>"
			+ "where id=#{id}"
			+ "</if>"
			+ "</script>")
	List<User> AnnotationGetUserById(@Param("id")String id);
}
複製程式碼

以上這兩種方式都不常用,如果你真的不想用mapper.xml檔案來定義SQL,那麼以下方式可能適合你。你可以通過@SelectProvider來宣告一個類的方法,此方法負責返回一個SQL的字串。

public interface UserMapper {
	@SelectProvider(type=SqlProvider.class,method="getUserById")
	List<User> AnnotationProviderGetUserById(String id);
}
複製程式碼

types指定了類的Class,method就是類的方法。其實這種方式也很不錯,動態SQL的生成不僅僅依靠Mybatis的動態標籤,在程式中可以隨便搞。

public class SqlProvider {
	public String getUserById(String id) {
		String sql = "select * from user ";
		if (id!=null) {
			sql += "	where id="+id;
		}
		return sql;
	}
}
複製程式碼

除了上面的@Select,當然還有對應其它幾種的註解。

sqlAnnotationTypes.add(Select.class);
sqlAnnotationTypes.add(Insert.class);
sqlAnnotationTypes.add(Update.class);
sqlAnnotationTypes.add(Delete.class);

sqlProviderAnnotationTypes.add(SelectProvider.class);
sqlProviderAnnotationTypes.add(InsertProvider.class);
sqlProviderAnnotationTypes.add(UpdateProvider.class);
sqlProviderAnnotationTypes.add(DeleteProvider.class);
複製程式碼

2、註解的掃描

上面我們看完了mapper檔案中SQL的解析,下面來看註解是在哪裡被掃描到的呢?

public class MapperRegistry {
	public <T> void addMapper(Class<T> type) {
		if (type.isInterface()) {  
			try {
				MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
				parser.parse();
			}
		}
	}
}
複製程式碼

type就是當前mapper介面的Class物件。獲取Class物件的所有Method[]。

Method[] methods = type.getMethods();
for (Method method : methods) {
	if (!method.isBridge()) {
		parseStatement(method);
	}
}
複製程式碼

parseStatement方法最終就生成MappedStatement物件,註冊到configuration中,這個流程是與XML的方式一樣,不會變的。

void parseStatement(Method method) {
	//判斷方法上是否包含那幾種註解
	//如果有,就根據註解的內容建立SqlSource物件。建立過程與XML建立過程一樣
	SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
	if (sqlSource != null) {
		//獲取各種屬性,過程略過
		......
		//建立MappedStatement物件,註冊到configuration
		assistant.addMappedStatement();
	}
}
複製程式碼

所以,我們看到。getSqlSourceFromAnnotations才是重點,拿到註解及註解上的值,建立SqlSource物件。

private SqlSource getSqlSourceFromAnnotations(Method method, 
			Class<?> parameterType, LanguageDriver languageDriver) {
	
	//註解就分為兩大類,sqlAnnotation和sqlProviderAnnotation
	//迴圈註解列表,判斷Method包含哪一種,就返回哪種型別註解的例項
	Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
	Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
	
	//不能兩種型別都配置哦
	if (sqlAnnotationType != null) {
		if (sqlProviderAnnotationType != null) {
			throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
		}
		Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
		final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
		return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
	}else if (sqlProviderAnnotationType != null) {
		Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
		return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
	}
	return null;
}
複製程式碼

3、建立SqlSource物件

掃描到註解後,就要根據註解型別的不同,建立SqlSource物件。

  • SELECT

我們以AnnotationGetUserList為例,它的註解是這樣:@Select("select * from user ")。最後建立SqlSource物件的過程與XML建立過程是一樣的。

public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {	
	//如果是帶script標籤的內容,最終呼叫到parseDynamicTags方法。
	//parseDynamicTags方法會遞迴呼叫,直到節點屬性為靜態節點。
	if (script.startsWith("<script>")) {
		XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
		return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
	}else {
		script = PropertyParser.parse(script, configuration.getVariables());
		TextSqlNode textSqlNode = new TextSqlNode(script);
		if (textSqlNode.isDynamic()) {
			return new DynamicSqlSource(configuration, textSqlNode);
		}else {
			//把#{}換成?,生成StaticSqlSource物件
			return new RawSqlSource(configuration, script, parameterType);
		}
	}
}
複製程式碼
  • SelectProvider

呼叫到ProviderSqlSource類的構造器,過程比較簡單,就是拿到SqlProvider類上的方法,將方法名、方法引數和引數型別設定一下。

public ProviderSqlSource(Configuration configuration, Object provider, Class<?> mapperType, Method mapperMethod) {
	String providerMethodName;
	this.configuration = configuration;
	this.sqlSourceParser = new SqlSourceBuilder(configuration);
	this.providerType = (Class<?>) provider.getClass().getMethod("type").invoke(provider);
	providerMethodName = (String) provider.getClass().getMethod("method").invoke(provider);
	for (Method m : this.providerType.getMethods()) {
		if (providerMethodName.equals(m.getName()) && CharSequence.class.isAssignableFrom(m.getReturnType())) {
			this.providerMethod = m;
			this.providerMethodArgumentNames = new ParamNameResolver(configuration, m).getNames();
			this.providerMethodParameterTypes = m.getParameterTypes();
		}
	}
}
複製程式碼

4、註冊

註冊過程也同XML方式一樣,生成MappedStatement物件,然後設定到configuration中。configuration.addMappedStatement(statement);

七、總結

本章節主要闡述了Mybatis的啟動過程之一,載入配置資訊,解析SQL。最後生成SqlSessionFactory物件。

1、配置資訊

Mybatis的配置資訊較多,但也並非都需要。常用的就是快取、型別轉換器、型別別名、外掛等。

2、解析SQL

生成SQL的方式大致有mapper.xml和Annotation兩種。Annotation又分為SqlAnnotation和SqlProviderAnnotation,如果真的想要註解式的SQL,還是比較推薦SqlProviderAnnotation。

相關文章