Mybatis原始碼解析:sql引數處理,原來可以這麼簡單
技術標籤:JavamybatisMySQLjavamybatissql
在這個章節中我們討論當sql帶有引數時,Mybatis是如何處理的。使用的還是User類。
//省略get set方法
public class User {
private int id;
private String name;
private String phone;
}
例1 帶有全域性變數的sql
//UserMapper中的dao介面
List<User> getByglobal();
<select id="getByglobal" resultType="com.entity.User"> select * from user where id = ${globalId} </select>
<!--mybatis.xml中的部分配置-->
<properties>
<property name="globalId" value="1"/>
</properties>
注意我是用的符號為$。在這個例子中globalId是在mybatis.xml檔案中的property配置的。介面不傳引數。
在學習第二個章節時,我們知道每一個查詢語句都會被包裝成一個MappedStatement,這個物件用來存放我們的sql語句,返回型別,id等等。讓我們回到之前的程式碼。
//XMLMapperBuilder.configurationElement private void configurationElement(XNode context) { try { //mapper的快取資訊,名稱空間等會被臨時儲存到MapperBuilderAssistant中,最後把這些公用的資訊在存到MappedStatement中 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")); //在解析增刪改查節點時,每個節點都會生成一個mapperStatement物件並儲存到配置檔案類中. //mapperStatement儲存這這個節點的全部資訊,如id,fetchSize,timeout buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } }
這部分程式碼應該不陌生,第二章分析了buildStatementFromContext()方法,現在我們從context.evalNodes("select|insert|update|delete")開始。該方法開始解析增刪改查的節點
//XNode.evalNodes
public List<XNode> evalNodes(String expression) {
return xpathParser.evalNodes(node, expression);
}
public List<XNode> evalNodes(Object root, String expression) { List<XNode> xnodes = new ArrayList<>(); NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET); for (int i = 0; i < nodes.getLength(); i++) { //這裡建立的新的節點並新增到結合中,解析也是在建立節點的時候開始的。 xnodes.add(new XNode(this, nodes.item(i), variables)); } return xnodes; }
XNode構造方法,在建立新節點的時候,會在構造器中先進性解析,也就是呼叫parseBody方法.
public XNode(XPathParser xpathParser, Node node, Properties variables) {
this.xpathParser = xpathParser;
this.node = node;
this.name = node.getNodeName();
this.variables = variables;
//獲取節點的屬性
//例如這個select就有id和resultType兩個屬性
this.attributes = parseAttributes(node);
//解析節點裡面的內容,也就是sql了
this.body = parseBody(node);
}
variables傳入配置中的全域性變數
//XNode.parseBody
private String parseBody(Node node) {
//獲取當前節點的資訊
//例子中這裡返回空
String data = getBodyData(node);
if (data == null) {
//獲取孩子節點的資訊
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
//獲取當前節點的資訊
data = getBodyData(child);
if (data != null) {
break;
}
}
}
return data;
}
為什麼select節點getBodyData會返回空呢,從它的方法體中可以看出,首先它會判斷節點的型別,select這個節點是ELEMENT_NODE型別,不屬於它要求的文字型別或者部分節點型別。那麼就直接返回空了。而當select的孩子節點,也就是sql語句select * from user where id = ${globalId}這個節點呼叫getBodyData方法時,sql語句是文字型別的,滿足條件,才會使用解析器開始解析。
//XNode.getBodyData
private String getBodyData(Node child) {
//判斷節點的型別
if (child.getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNodeType() == Node.TEXT_NODE) {
String data = ((CharacterData) child).getData();
data = PropertyParser.parse(data, variables);
return data;
}
return null;
}
//PropertyParser.parse
public static String parse(String string, Properties variables) {
//先建立一個處理器
VariableTokenHandler handler = new VariableTokenHandler(variables);
//建立解析器
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
//進行解析
return parser.parse(string);
}
這裡出現了很多陌生的類。首先是GenericTokenParser通用型別的解析器,他能根據傳入的引數做出相應。如果引數滿足條件,就會呼叫handler處理器來處理引數。每個handler都要實現handleToken方法,該方法就是用來處理引數的。
例如這裡傳入的是以${作為開頭,}作為結尾。如果傳入的字串包含一個或者多個這樣的格式,就會呼叫VariableTokenHandler.handleToken,該方法會試圖從全域性中找到該變數,並修改成具體的值。
VariableTokenHandler.handleToken 傳入String變數globalId,將其替換成1並返回。
public String handleToken(String content) {
//variables裡面存放全域性的變數,為空直接return
if (variables != null) {
String key = content;
//是否存在預設值,預設是false
if (enableDefaultValue) {
final int separatorIndex = content.indexOf(defaultValueSeparator);
String defaultValue = null;
if (separatorIndex >= 0) {
key = content.substring(0, separatorIndex);
defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
}
if (defaultValue != null) {
return variables.getProperty(key, defaultValue);
}
}
//variables是用來存放全域性變數的容器。
//這裡會從全域性變數中找到我們定義的globalId,然後將對應的值返回,這樣我們的sql就拼接完成了
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
return "${" + content + "}";
}
}
解析器程式碼,根據傳入的標記開始解析,這裡傳入開始標記${和結束標記$}。在這之後還會用來解析#{}。程式碼比較長,最好打個斷點進去看。
//GenericTokenParser.parse
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
//查詢開始標記,如果不存在返回-1 ,存在返回偏移量
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
//這個變數用來存放中間的字元,如${id}中的id
StringBuilder expression = null;
//如果存在開始標誌
while (start > -1) {
//這裡將從offset開始,一直到start的字元先放入builder中
//例如select * from user where id =
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
//更新偏移量
offset = start + openToken.length();
//找到與開始標誌對應的結束標誌
int end = text.indexOf(closeToken, offset);
//取到中間字元globalId
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//這裡根據不同的處理器會有不同的操作,剛才傳入的是VariableTokenHandler
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
到這裡全域性變數就解析完成了,那麼如果在全域性變數中沒有找到對應的值該怎麼辦呢?例如我這裡使用的sql是select * from user where id = ${id},而不是${globalId},那麼根據VariableTokenHandler處理器,它會原封不動的進行返回,等待後文的解析。
順便一提,這一部分的解析實在解析我們的配置檔案的時候就發生了,方法入口為context.evalNodes("select|insert|update|delete"),在解析配置的時候,其他節點也大量使用了context.evalNodes()方法去解,所以只要當配置mybatis.xml檔案中的properties節點解析完成之後,裡面的變數就是能全域性使用了,這也是為什麼properties節點要放在第一個解析。
又由於這個通用解析器只解析${XXX}格式的變數,所以全域性的變數不能寫成#{xxx}.
入參${}的解析
List<User> get(Integer id);
<select id="get" resultType="com.entity.User">
select * from user where id = ${id}
</select>
這個例子,我們沒有在全域性變數中定義id,而是在方法中傳入這個值。根據上文中的VariableTokenHandler.handleToken方法就會返回${id},表示這個引數全域性變數中沒有,是待解析的引數。
這是解析buildStatementFromContext(context.evalNodes("select|insert|update|delete"));的後續程式碼,用來解析標籤,並建立mappedStaement,在第二章中也分析過,這裡直接copy過來.
//XMLStatementBuilder.parseStatementNode
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
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);
//是否需要處理巢狀查詢結果 group by
// 三組資料 分成一個巢狀的查詢結果
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
//替換Includes標籤為對應的sql標籤裡面的值
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
//解析配置的自定義指令碼語言驅動 mybatis plus
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
//解析selectKey
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//設定主鍵自增規則
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))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//解析Sql 根據sql文字來判斷是否需要動態解析 如果沒有動態sql語句且 只有#{}的時候 直接靜態解析使用?佔位 當有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
//暗示驅動程式每次批量返回的結果行數
Integer fetchSize = context.getIntAttribute("fetchSize");
//超時時間
Integer timeout = context.getIntAttribute("timeout");
//引用外部 parameterMap,已廢棄
String parameterMap = context.getStringAttribute("parameterMap");
//結果型別
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
//引用外部的 resultMap
String resultMap = context.getStringAttribute("resultMap");
//結果集型別,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一種
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
//(僅對 insert 有用) 標記一個屬性, MyBatis 會通過 getGeneratedKeys 或者通過 insert 語句的 selectKey 子元素設定它的值
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
找到解析sql的部分具體來分析,一層一層往下。
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
RawLanguageDriver.createSqlSource 該類是XMLLanguageDriver的子類
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
SqlSource source = super.createSqlSource(configuration, script, parameterType);
checkIsNotDynamic(source);
return source;
}
XMLLanguageDriver.createSqlSource
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
XMLScriptBuilder.parseScriptNode
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
//判斷節點是否是動態的,包含是否包含if、where 、choose、trim、foreach、bind、sql標籤,這個例子中我們進入else
if (isDynamic) {
//不解析
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//用佔位符方式來解析
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
這裡進行判斷isDynamic的值,這個方法我們只需要關注textSqlNode.isDynamic()就行了。程式碼與之前解析node有些類似。
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
//注意!!這裡又new了一個XNode,也就是說,這個節點中的sql語句又被解析了一次,解析方式和上文從同全域性獲取變數一樣。
//與上文不同的是,這裡傳入的是子節點,也就是sql文字語句,而上文解析的是整個select元素
//這個child是臨時變數,節點解析的結果不做儲存
XNode child = node.newXNode(children.item(i));
//判斷節點型別
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
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
TextSqlNode.isDynamic
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
//這裡建立一個解析器進行解析sql語句,這裡解析的是仍然是${}
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
熟悉的程式碼,還是同樣的解析器,用來處理${,和},不過這次的hander不同,為DynamicCheckerTokenParser
//DynamicCheckerTokenParser.handleToken
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
}
這次的處理方式是將直接返回空,也就是說,sql會變成 select * from user where id = null。但是返回的結果並沒有被儲存,parser.parse(text)並沒有引數來接受它的返回值,所以這裡只是用來更新isDynamic引數。
回到XMLScriptBuilder.parseScriptNode方法,這裡根據isDynamic的布林值,會有兩種SqlSource.DynamicSqlSource和RawSqlSource。到這裡配置檔案就解析完成了,後續sql中的引數都是從方法中獲取的,所以只能在執行的時候動態進行替換。
來到query查詢方法,方法在第三章執行sql的時候簡單說過。ms.getBoundSql會獲取繫結的封裝sql.
//CachingExecutor.query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
MappedStatement.getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
//獲取繫結的sql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//獲取sql中對應的引數
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
//DynamicSqlSource.getBoundSql。
public BoundSql getBoundSql(Object parameterObject) {
//parameterObject中有我們方法傳入的引數
DynamicContext context = new DynamicContext(configuration, parameterObject);
//這裡解析${}
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
為什麼是DynamicSqlSource而不是RawSqlSource,這個前文分析過,在替換完全域性變數後,語句中如果還包含${},使用的就是DynamicSqlSource。
//MixedSqlNode.apply
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
TextSqlNode.apply
@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
這裡再次建立了${}的解析器,這次的handler是BindingTokenParser
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
BindingTokenParser.handleToken,如果sql中存在${},就會將其替換成具體的引數,語句就變成 select * from user where id = 1,就能直接執行了
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}
入參#{}的解析
那麼如果是#{}該怎麼處理呢?
<select id="get" resultType="com.entity.User">
select * from user where id = #{id}
</select>
List<User> get(Integer id);
由上文得知,由於沒有${},那麼SqlSource就會變成RawSqlSource。在建立RawSqlSource的時候,在構造方法中就會對#{}解析。
RawSqlSource的構造方法。
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
SqlSourceBuilder.parse
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
這裡用的hander是ParameterMappingTokenHandler,它的作用是將#{XXX}替換成?
ParameterMappingTokenHandler.handleToken
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
這時sql就變成了select * from user where id = ?,到這裡還只是解析配置檔案。在具體執行方法時也要呼叫getBoundSql方法將引數進行賦值
//RawSqlSource.getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
StaticSqlSource.getBoundSql,最後呼叫BoundSql的構造方法,將sql語句,入參等傳入
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
之後就要建立資料庫連線,進行查詢了。回到這個方法SimpleExecutor.prepareStatement。回顧一下,這是建立StatementHandler後做的一些連線資料庫的準備操作。
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//獲取jdbc資料庫連線
Connection connection = getConnection(statementLog);
//一些準備工作,初始化Statement連線
stmt = handler.prepare(connection, transaction.getTimeout());
//使用ParameterHandler處理入參
handler.parameterize(stmt);
return stmt;
}
我們先進入這個方法PreparedStatementHandler.parameterize。
為什麼是PreparedStatementHandler之前也說過,因為語句的預設型別是PREPARED, 還有其他的型別如果是CALLABLE,對應CallableStatementHandler,STATEMENT對應SimpleStatementHandler。可以用引數statementType進行設定。
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
DefaultParameterHandler.setParameters.
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//boundSql用來解析我們的sql語句,parameterMappings是我們傳入的引數
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
//這裡第一個引數就是id
ParameterMapping parameterMapping = parameterMappings.get(i);
//mode屬性允許能指定IN,OUT或INOUT引數。如果引數的 mode 為 OUT 或 INOUT,將會修改引數物件的屬性值,以便作為輸出引數返回。
//#{id}預設mode為OUT
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
//這裡是boundsql中的額外引數,可以使用攔截器新增,例子放在下文
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
//如果型別處理器中有這個型別,那麼直接賦值就行了,例如這裡是Integer型別,型別處理器是有的
//那麼直接賦值
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
//如果不是的會轉化為元資料進行處理,metaObject元資料可以理解為用來反射的工具類,可以處理引數的get,set
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//獲取型別處理器
TypeHandler typeHandler = parameterMapping.getTypeHandler();
//獲取資料庫型別
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//使用不同的型別處理器向jdbc中的PreparedStatement設定引數
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
value如果是空的那麼就直接設定為jdbc的空型別,不為空呼叫具體的型別處理器。
BaseTypeHandler.setParameter。該類是所有typeHandler的父類.如果不為空呼叫setNonNullParameter,該方法時抽象的,由具體的子類實現。這裡使用的是一個相當於路由的的子類UnknownTypeHandler,這個子類可以根據傳入的型別,再去找到具體的型別處理器,例如IntegerTypeHander.
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
+ "Cause: " + e, e);
}
} else {
try {
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different configuration property. "
+ "Cause: " + e, e);
}
}
}
UnknownTypeHandler.setNonNullParameter
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
handler.setParameter(ps, i, parameter, jdbcType);
}
UnknownTypeHandler.resolveTypeHandler這個方法根據傳入的引數型別,找到具體的TypeHandler
private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
TypeHandler<?> handler;
if (parameter == null) {
handler = OBJECT_TYPE_HANDLER;
} else {
handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
// check if handler is null (issue #270)
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
}
return handler;
}
例如如果這個引數是id,Integer型別,那麼就會找到IntegerTypeHandler
//IntegerTypeHandler
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter);
}
最後還是使用jdbc的PreparedStatement處理引數。
附:自定義的攔截器用來加入引數。
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})
})
public class MyInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler bs = (StatementHandler) invocation.getTarget();
BoundSql boundSql = bs.getBoundSql();
boundSql.setAdditionalParameter("id","1");
return invocation.proceed();
}
}
例4 ${}和#{}都存在的情況
如果是都存在的情況呢?
<select id="findUserByIdAndName" resultType="com.entity.User">
select * from user where id = ${id} AND name = #{name}
</select>
List<User> findUserByIdAndName(@Param("id") Integer id, @Param("name") String name);
結合上文的分析,由於存在${},所以選擇的DynamicSqlSource。
DynamicSqlSource.getBoundSql。這個方法上文分析到了rootSqlNode.apply(context);會將${}替換成具體引數。我們接著分析。
public BoundSql getBoundSql(Object parameterObject) {
//parameterObject中有我們方法傳入的引數
DynamicContext context = new DynamicContext(configuration, parameterObject);
//解析${}並替換成具體的值
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
//這裡又進行了一次解析
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
解析#{},並將其替換成?
//RawSqlSource.parse
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
這樣我們的語句就變成了select * from user where id = 1 AND name = ?,然後呼叫sqlSource.getBoundSql
//StaticSqlSource.getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
最後的處理方式與例3相同,使用jdbc自帶的PreparedStatement進行引數處理。
小結
當我們在解析mapper.xml檔案時,就會將sql進行第一遍的解析,將其中的全域性變數替換成具體的值。
接著進行第二遍的解析,選擇不同的SqlSource。這一邊的解析不改變語句中的sql內容。
如果語句中包含${},就選擇DynamicSqlSource,等待具體執行sql的時候再做處理.如果僅包含#{}型別的,就選擇RawSqlSource。RawSqlSource在建立的時候就會有進行一輪的解析,將語句中的#{XXX}替換為 ?(問號)
之後在執行具體的語句才動態的替換,如果之前選擇的是DynamicSqlSource,那麼進行兩次的解析,第一次將${}替換成具體值,第二次解析#{},使用jdbc的PreparedStatement處理。如果選擇的是RawSqlSource,那麼這條語句就只有#{},直接用PreparedStatement處理。
可以發現,無論什麼型別的sql都會被解析了4次。