上一篇咱們分析了Mapper接口代理類的生成,本篇接着分析是如何調用到XML中的SQLjava
咱們回顧一下MapperMethod 的execute方法正則表達式
public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 根據 SQL 類型執行相應的數據庫操做 switch (command.getType()) { case INSERT: { // 對用戶傳入的參數進行轉換,下同 Object param = method.convertArgsToSqlCommandParam(args); // 執行插入操做,rowCountResult 方法用於處理返回值 result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); // 執行更新操做 result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); // 執行刪除操做 result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: // 根據目標方法的返回類型進行相應的查詢操做 if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { // 執行查詢操做,並返回多個結果 result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { // 執行查詢操做,並將結果封裝在 Map 中返回 result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { // 執行查詢操做,並返回一個 Cursor 對象 result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); // 執行查詢操做,並返回一個結果 result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: // 執行刷新操做 result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } return result; }
本節選擇分析 selectOne 方法,主要是由於 selectOne 在內部會調用 selectList 方法。同時分析 selectOne 方法等同於分析 selectList 方法。代碼以下sql
// 執行查詢操做,並返回一個結果 result = sqlSession.selectOne(command.getName(), param);
咱們看到是經過sqlSession來執行查詢的,而且傳入的參數爲command.getName()和param,也就是namespace.methodName(mapper.EmployeeMapper.getAll)和方法的運行參數。咱們知道了,全部的數據庫操做都是交給sqlSession來執行的,那咱們就來看看sqlSession的方法數據庫
DefaultSqlSessionexpress
public <T> T selectOne(String statement, Object parameter) { // 調用 selectList 獲取結果 List<T> list = this.<T>selectList(statement, parameter); if (list.size() == 1) { // 返回結果 return list.get(0); } else if (list.size() > 1) { // 若是查詢結果大於1則拋出異常 throw new TooManyResultsException( "Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else { return null; } }
如上,selectOne 方法在內部調用 selectList 了方法,並取 selectList 返回值的第1個元素做爲本身的返回值。若是 selectList 返回的列表元素大於1,則拋出異常。下面咱們來看看 selectList 方法的實現。緩存
DefaultSqlSessionapp
private final Executor executor; public <E> List<E> selectList(String statement, Object parameter) { // 調用重載方法 return this.selectList(statement, parameter, RowBounds.DEFAULT); } public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { // 經過MappedStatement的Id獲取 MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); // 調用 Executor 實現類中的 query 方法 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
咱們以前建立DefaultSqlSession的時候,是建立了一個Executor的實例做爲其屬性的,咱們看到經過MappedStatement的Id獲取 MappedStatement後,就交由Executor去執行了ide
咱們回顧一下前面的文章,Executor的建立過程,代碼以下ui
//建立一個執行器,默認是SIMPLE public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; //根據executorType來建立相應的執行器,Configuration默認是SIMPLE if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { //建立SimpleExecutor實例,而且包含Configuration和transaction屬性 executor = new SimpleExecutor(this, transaction); } //若是要求緩存,生成另外一種CachingExecutor,裝飾者模式,默認都是返回CachingExecutor /** * 二級緩存開關配置示例 * <settings> * <setting name="cacheEnabled" value="true"/> * </settings> */ if (cacheEnabled) { //CachingExecutor使用裝飾器模式,將executor的功能添加上了二級緩存的功能,二級緩存會單獨文章來說 executor = new CachingExecutor(executor); } //此處調用插件,經過插件能夠改變Executor行爲,此處咱們後面單獨文章講 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
executor包含了Configuration和Transaction,默認的執行器爲SimpleExecutor,若是開啓了二級緩存(默認開啓),則CachingExecutor會包裝SimpleExecutor,那麼咱們該看CachingExecutor的query方法了this
CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 獲取 BoundSql BoundSql boundSql = ms.getBoundSql(parameterObject); // 建立 CacheKey CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // 調用重載方法 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
上面的代碼用於獲取 BoundSql 對象,建立 CacheKey 對象,而後再將這兩個對象傳給重載方法。CacheKey 以及接下來即將出現的一二級緩存將會獨立成文進行分析。
咱們先來看看獲取BoundSql
// 獲取 BoundSql BoundSql boundSql = ms.getBoundSql(parameterObject);
調用了MappedStatement的getBoundSql方法,並將運行時參數傳入其中,咱們大概的猜一下,這裏是否是拼接SQL語句呢,並將運行時參數設置到SQL語句中?
咱們都知道 SQL 是配置在映射文件中的,但因爲映射文件中的 SQL 可能會包含佔位符 #{},以及動態 SQL 標籤,好比 <if>、<where> 等。所以,咱們並不能直接使用映射文件中配置的 SQL。MyBatis 會將映射文件中的 SQL 解析成一組 SQL 片斷。咱們須要對這一組片斷進行解析,從每一個片斷對象中獲取相應的內容。而後將這些內容組合起來便可獲得一個完成的 SQL 語句,這個完整的 SQL 以及其餘的一些信息最終會存儲在 BoundSql 對象中。下面咱們來看一下 BoundSql 類的成員變量信息,以下:
private final String sql; private final List<ParameterMapping> parameterMappings; private final Object parameterObject; private final Map<String, Object> additionalParameters; private final MetaObject metaParameters;
下面用一個表格列舉各個成員變量的含義。
變量名 | 類型 | 用途 |
---|---|---|
sql | String | 一個完整的 SQL 語句,可能會包含問號 ? 佔位符 |
parameterMappings | List | 參數映射列表,SQL 中的每一個 #{xxx} 佔位符都會被解析成相應的 ParameterMapping 對象 |
parameterObject | Object | 運行時參數,即用戶傳入的參數,好比 Article 對象,或是其餘的參數 |
additionalParameters | Map | 附加參數集合,用於存儲一些額外的信息,好比 datebaseId 等 |
metaParameters | MetaObject | additionalParameters 的元信息對象 |
接下來咱們接着MappedStatement 的 getBoundSql 方法,代碼以下:
public BoundSql getBoundSql(Object parameterObject) { // 調用 sqlSource 的 getBoundSql 獲取 BoundSql,把method運行時參數傳進去 BoundSql boundSql = sqlSource.getBoundSql(parameterObject);return boundSql; }
MappedStatement 的 getBoundSql 在內部調用了 SqlSource 實現類的 getBoundSql 方法,並把method運行時參數傳進去,SqlSource 是一個接口,它有以下幾個實現類:
當 SQL 配置中包含 ${}
(不是 #{})佔位符,或者包含 <if>、<where> 等標籤時,會被認爲是動態 SQL,此時使用 DynamicSqlSource 存儲 SQL 片斷。不然,使用 RawSqlSource 存儲 SQL 配置信息。咱們來看看DynamicSqlSource的getBoundSql
DynamicSqlSource
public BoundSql getBoundSql(Object parameterObject) { // 建立 DynamicContext DynamicContext context = new DynamicContext(configuration, parameterObject); // 解析 SQL 片斷,並將解析結果存儲到 DynamicContext 中,這裏會將${}替換成method對應的運行時參數,也會解析<if><where>等SqlNode rootSqlNode.apply(context); SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); /* * 構建 StaticSqlSource,在此過程當中將 sql 語句中的佔位符 #{} 替換爲問號 ?, * 併爲每一個佔位符構建相應的 ParameterMapping */ SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); // 調用 StaticSqlSource 的 getBoundSql 獲取 BoundSql BoundSql boundSql = sqlSource.getBoundSql(parameterObject); // 將 DynamicContext 的 ContextMap 中的內容拷貝到 BoundSql 中 for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) { boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } return boundSql; }
該方法由數個步驟組成,這裏總結一下:
DynamicContext
DynamicContext 是 SQL 語句構建的上下文,每一個 SQL 片斷解析完成後,都會將解析結果存入 DynamicContext 中。待全部的 SQL 片斷解析完畢後,一條完整的 SQL 語句就會出如今 DynamicContext 對象中。
public class DynamicContext { public static final String PARAMETER_OBJECT_KEY = "_parameter"; public static final String DATABASE_ID_KEY = "_databaseId"; //bindings 則用於存儲一些額外的信息,好比運行時參數 private final ContextMap bindings; //sqlBuilder 變量用於存放 SQL 片斷的解析結果 private final StringBuilder sqlBuilder = new StringBuilder(); public DynamicContext(Configuration configuration, Object parameterObject) { // 建立 ContextMap,並將運行時參數放入ContextMap中 if (parameterObject != null && !(parameterObject instanceof Map)) { MetaObject metaObject = configuration.newMetaObject(parameterObject); bindings = new ContextMap(metaObject); } else { bindings = new ContextMap(null); } // 存放運行時參數 parameterObject 以及 databaseId bindings.put(PARAMETER_OBJECT_KEY, parameterObject); bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId()); } public void bind(String name, Object value) { this.bindings.put(name, value); } //拼接Sql片斷 public void appendSql(String sql) { this.sqlBuilder.append(sql); this.sqlBuilder.append(" "); } //獲得sql字符串 public String getSql() { return this.sqlBuilder.toString().trim(); } //繼承HashMap static class ContextMap extends HashMap<String, Object> { private MetaObject parameterMetaObject; public ContextMap(MetaObject parameterMetaObject) { this.parameterMetaObject = parameterMetaObject; } @Override public Object get(Object key) { String strKey = (String) key; // 檢查是否包含 strKey,若包含則直接返回 if (super.containsKey(strKey)) { return super.get(strKey); } if (parameterMetaObject != null) { // 從運行時參數中查找結果,這裏會在${name}解析時,經過name獲取運行時參數值,替換掉${name}字符串 return parameterMetaObject.getValue(strKey); } return null; } } // 省略部分代碼 }
解析 SQL 片斷
接着咱們來看看解析SQL片斷的邏輯
rootSqlNode.apply(context);
對於一個包含了 ${} 佔位符,或 <if>、<where> 等標籤的 SQL,在解析的過程當中,會被分解成多個片斷。每一個片斷都有對應的類型,每種類型的片斷都有不一樣的解析邏輯。在源碼中,片斷這個概念等價於 sql 節點,即 SqlNode。
StaticTextSqlNode 用於存儲靜態文本,TextSqlNode 用於存儲帶有 ${} 佔位符的文本,IfSqlNode 則用於存儲 <if> 節點的內容。MixedSqlNode 內部維護了一個 SqlNode 集合,用於存儲各類各樣的 SqlNode。接下來,我將會對 MixedSqlNode 、StaticTextSqlNode、TextSqlNode、IfSqlNode、WhereSqlNode 以及 TrimSqlNode 等進行分析
public class MixedSqlNode implements SqlNode { private final List<SqlNode> contents; public MixedSqlNode(List<SqlNode> contents) { this.contents = contents; } @Override public boolean apply(DynamicContext context) { // 遍歷 SqlNode 集合 for (SqlNode sqlNode : contents) { // 調用 salNode 對象自己的 apply 方法解析 sql sqlNode.apply(context); } return true; } }
MixedSqlNode 能夠看作是 SqlNode 實現類對象的容器,凡是實現了 SqlNode 接口的類均可以存儲到 MixedSqlNode 中,包括它本身。MixedSqlNode 解析方法 apply 邏輯比較簡單,即遍歷 SqlNode 集合,並調用其餘 SqlNode實現類對象的 apply 方法解析 sql。
StaticTextSqlNode
public class StaticTextSqlNode implements SqlNode { private final String text; public StaticTextSqlNode(String text) { this.text = text; } @Override public boolean apply(DynamicContext context) { //直接拼接當前sql片斷的文本到DynamicContext的sqlBuilder中 context.appendSql(text); return true; } }
StaticTextSqlNode 用於存儲靜態文本,直接將其存儲的 SQL 的文本值拼接到 DynamicContext 的sqlBuilder中便可。下面分析一下 TextSqlNode。
TextSqlNode
public class TextSqlNode implements SqlNode { private final String text; private final Pattern injectionFilter; @Override public boolean apply(DynamicContext context) { // 建立 ${} 佔位符解析器 GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); // 解析 ${} 佔位符,經過ONGL 從用戶傳入的參數中獲取結果,替換text中的${} 佔位符 // 並將解析結果的文本拼接到DynamicContext的sqlBuilder中 context.appendSql(parser.parse(text)); return true; } private GenericTokenParser createParser(TokenHandler handler) { // 建立佔位符解析器 return new GenericTokenParser("${", "}", handler); } private static class BindingTokenParser implements TokenHandler { private DynamicContext context; private Pattern injectionFilter; public BindingTokenParser(DynamicContext context, Pattern injectionFilter) { this.context = context; this.injectionFilter = injectionFilter; } @Override 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); } // 經過 ONGL 從用戶傳入的參數中獲取結果 Object value = OgnlCache.getValue(content, context.getBindings()); String srtValue = (value == null ? "" : String.valueOf(value)); // 經過正則表達式檢測 srtValue 有效性 checkInjection(srtValue); return srtValue; } } }
GenericTokenParser 是一個通用的標記解析器,用於解析形如 ${name},#{id} 等標記。此時是解析 ${name}的形式,從運行時參數的Map中獲取到key爲name的值,直接用運行時參數替換掉 ${name}字符串,將替換後的text字符串拼接到DynamicContext的sqlBuilder中
舉個例子吧,比喻咱們有以下SQL
SELECT * FROM user WHERE name = '${name}' and id= ${id}
假如咱們傳的參數 Map中name值爲 chenhao,id爲1,那麼該 SQL 最終會被解析成以下的結果:
SELECT * FROM user WHERE name = 'chenhao' and id= 1
很明顯這種直接拼接值很容易形成SQL注入,假如咱們傳入的參數爲name值爲 chenhao'; DROP TABLE user;# ,解析獲得的結果爲
SELECT * FROM user WHERE name = 'chenhao'; DROP TABLE user;#'
因爲傳入的參數沒有通過轉義,最終致使了一條 SQL 被惡意參數拼接成了兩條 SQL。這就是爲何咱們不該該在 SQL 語句中是用 ${} 佔位符,風險太大。接着咱們來看看IfSqlNode
IfSqlNode
public class IfSqlNode implements SqlNode { private final ExpressionEvaluator evaluator; private final String test; private final SqlNode contents; public IfSqlNode(SqlNode contents, String test) { this.test = test; this.contents = contents; this.evaluator = new ExpressionEvaluator(); } @Override public boolean apply(DynamicContext context) { // 經過 ONGL 評估 test 表達式的結果 if (evaluator.evaluateBoolean(test, context.getBindings())) { // 若 test 表達式中的條件成立,則調用其子節點節點的 apply 方法進行解析 // 若是是靜態SQL節點,則會直接拼接到DynamicContext中 contents.apply(context); return true; } return false; } }
IfSqlNode 對應的是 <if test='xxx'> 節點,首先是經過 ONGL 檢測 test 表達式是否爲 true,若是爲 true,則調用其子節點的 apply 方法繼續進行解析。若是子節點是靜態SQL節點,則子節點的文本值會直接拼接到DynamicContext中
好了,其餘的SqlNode我就不一一分析了,你們有興趣的能夠去看看
解析 #{} 佔位符
通過前面的解析,咱們已經能從 DynamicContext 獲取到完整的 SQL 語句了。但這並不意味着解析過程就結束了,由於當前的 SQL 語句中還有一種佔位符沒有處理,即 #{}。與 ${} 佔位符的處理方式不一樣,MyBatis 並不會直接將 #{} 佔位符替換爲相應的參數值,而是將其替換成?。其解析是在以下代碼中實現的
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
咱們看到將前面解析過的sql字符串和運行時參數的Map做爲參數,咱們來看看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); // 封裝解析結果到 StaticSqlSource 中,並返回,由於全部的動態參數都已經解析了,能夠封裝成一個靜態的SqlSource return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); } public String handleToken(String content) { // 獲取 content 的對應的 ParameterMapping parameterMappings.add(buildParameterMapping(content)); // 返回 ? return "?"; }
咱們看到將Sql中的 #{} 佔位符替換成"?",而且將對應的參數轉化成ParameterMapping 對象,經過buildParameterMapping 完成,最後建立一個StaticSqlSource,將sql字符串和ParameterMappings爲參數傳入,返回這個StaticSqlSource
private ParameterMapping buildParameterMapping(String content) { /* * 將#{xxx} 佔位符中的內容解析成 Map。 * #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler} * 上面佔位符中的內容最終會被解析成以下的結果: * { * "property": "age", * "typeHandler": "MyTypeHandler", * "jdbcType": "NUMERIC", * "javaType": "int" * } */ Map<String, String> propertiesMap = parseParameterMapping(content); String property = propertiesMap.get("property"); Class<?> propertyType; // metaParameters 爲 DynamicContext 成員變量 bindings 的元信息對象 if (metaParameters.hasGetter(property)) { propertyType = metaParameters.getGetterType(property); /* * parameterType 是運行時參數的類型。若是用戶傳入的是單個參數,好比 Employe 對象,此時 * parameterType 爲 Employe.class。若是用戶傳入的多個參數,好比 [id = 1, author = "chenhao"], * MyBatis 會使用 ParamMap 封裝這些參數,此時 parameterType 爲 ParamMap.class。 */ } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { propertyType = parameterType; } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { propertyType = java.sql.ResultSet.class; } else if (property == null || Map.class.isAssignableFrom(parameterType)) { propertyType = Object.class; } else { /* * 代碼邏輯走到此分支中,代表 parameterType 是一個自定義的類, * 好比 Employe,此時爲該類建立一個元信息對象 */ MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); // 檢測參數對象有沒有與 property 想對應的 getter 方法 if (metaClass.hasGetter(property)) { // 獲取成員變量的類型 propertyType = metaClass.getGetterType(property); } else { propertyType = Object.class; } } ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); // 將 propertyType 賦值給 javaType Class<?> javaType = propertyType; String typeHandlerAlias = null; // 遍歷 propertiesMap for (Map.Entry<String, String> entry : propertiesMap.entrySet()) { String name = entry.getKey(); String value = entry.getValue(); if ("javaType".equals(name)) { // 若是用戶明確配置了 javaType,則以用戶的配置爲準 javaType = resolveClass(value); builder.javaType(javaType); } else if ("jdbcType".equals(name)) { // 解析 jdbcType builder.jdbcType(resolveJdbcType(value)); } else if ("mode".equals(name)) {...} else if ("numericScale".equals(name)) {...} else if ("resultMap".equals(name)) {...} else if ("typeHandler".equals(name)) { typeHandlerAlias = value; } else if ("jdbcTypeName".equals(name)) {...} else if ("property".equals(name)) {...} else if ("expression".equals(name)) { throw new BuilderException("Expression based parameters are not supported yet"); } else { throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + parameterProperties); } } if (typeHandlerAlias != null) { builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); } // 構建 ParameterMapping 對象 return builder.build(); }
SQL 中的 #{name, ...} 佔位符被替換成了問號 ?。#{name, ...} 也被解析成了一個 ParameterMapping 對象。咱們再來看一下 StaticSqlSource 的建立過程。以下:
public class StaticSqlSource implements SqlSource { private final String sql; private final List<ParameterMapping> parameterMappings; private final Configuration configuration; public StaticSqlSource(Configuration configuration, String sql) { this(configuration, sql, null); } public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) { this.sql = sql; this.parameterMappings = parameterMappings; this.configuration = configuration; } @Override public BoundSql getBoundSql(Object parameterObject) { // 建立 BoundSql 對象 return new BoundSql(configuration, sql, parameterMappings, parameterObject); } }
最後咱們經過建立的StaticSqlSource就能夠獲取BoundSql對象了,並傳入運行時參數
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
也就是調用上面建立的StaticSqlSource 中的getBoundSql方法,這是簡單的 return new BoundSql(configuration, sql, parameterMappings, parameterObject); ,接着看看BoundSql
public class BoundSql { private String sql; private List<ParameterMapping> parameterMappings; private Object parameterObject; private Map<String, Object> additionalParameters; private MetaObject metaParameters; public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) { this.sql = sql; this.parameterMappings = parameterMappings; this.parameterObject = parameterObject; this.additionalParameters = new HashMap(); this.metaParameters = configuration.newMetaObject(this.additionalParameters); } public String getSql() { return this.sql; } //略 }
咱們看到只是作簡單的賦值。BoundSql中包含了sql,#{}解析成的parameterMappings,還有運行時參數parameterObject。好了,SQL解析咱們就介紹這麼多。咱們先回顧一下咱們代碼是從哪裏開始的
CachingExecutor
1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { 2 // 獲取 BoundSql 3 BoundSql boundSql = ms.getBoundSql(parameterObject); 4 // 建立 CacheKey 5 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); 6 // 調用重載方法 7 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); 8 }
如上,咱們剛纔都是分析的第三行代碼,獲取到了BoundSql,CacheKey 和二級緩存有關,咱們留在下一篇文章單獨來說,接着咱們看第七行重載方法 query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 從 MappedStatement 中獲取緩存 Cache cache = ms.getCache(); // 若映射文件中未配置緩存或參照緩存,此時 cache = null if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { // 若緩存未命中,則調用被裝飾類的 query 方法,也就是SimpleExecutor的query方法 list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } // 調用被裝飾類的 query 方法,這裏的delegate咱們知道應該是SimpleExecutor return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
上面的代碼涉及到了二級緩存,若二級緩存爲空,或未命中,則調用被裝飾類的 query 方法。被裝飾類爲SimpleExecutor,而SimpleExecutor繼承BaseExecutor,那咱們來看看 BaseExecutor 的query方法
BaseExecutor
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; // 從一級緩存中獲取緩存項,一級緩存咱們也下一篇文章單獨講 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 一級緩存未命中,則從數據庫中查詢 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); } } return list; }
從一級緩存中查找查詢結果。若緩存未命中,再向數據庫進行查詢。至此咱們明白了一級二級緩存的大概思路,先從二級緩存中查找,若未命中二級緩存,再從一級緩存中查找,若未命中一級緩存,再從數據庫查詢數據,那咱們來看看是怎麼從數據庫查詢的
BaseExecutor
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; // 向緩存中存儲一個佔位符 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 調用 doQuery 進行查詢 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { // 移除佔位符 localCache.removeObject(key); } // 緩存查詢結果 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
調用了doQuery方法進行查詢,最後將查詢結果放入一級緩存,咱們來看看doQuery,在SimpleExecutor中
SimpleExecutor
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // 建立 StatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 建立 Statement stmt = prepareStatement(handler, ms.getStatementLog()); // 執行查詢操做 return handler.<E>query(stmt, resultHandler); } finally { // 關閉 Statement closeStatement(stmt); } }
咱們先來看看第一步建立StatementHandler
StatementHandler有什麼做用呢?經過這個對象獲取Statement對象,而後填充運行時參數,最後調用query完成查詢。咱們來看看其建立過程
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 建立具備路由功能的 StatementHandler StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 應用插件到 StatementHandler 上 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
咱們看看RoutingStatementHandler的構造方法
public class RoutingStatementHandler implements StatementHandler { private final StatementHandler delegate; public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 根據 StatementType 建立不一樣的 StatementHandler switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case PREPARED: delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case CALLABLE: delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } } }
RoutingStatementHandler 的構造方法會根據 MappedStatement 中的 statementType 變量建立不一樣的 StatementHandler 實現類。那statementType 是什麼呢?咱們還要回顧一下MappedStatement 的建立過程
咱們看到statementType 的默認類型爲PREPARED,這裏將會建立PreparedStatementHandler。
接着咱們看下面一行代碼prepareStatement,
建立 Statement 在 stmt = prepareStatement(handler, ms.getStatementLog()); 這句代碼,那咱們跟進去看看
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; // 獲取數據庫鏈接 Connection connection = getConnection(statementLog); // 建立 Statement, stmt = handler.prepare(connection, transaction.getTimeout()); // 爲 Statement 設置參數 handler.parameterize(stmt); return stmt; }
在上面的代碼中咱們終於看到了和jdbc相關的內容了,建立完Statement,最後就能夠執行查詢操做了。因爲篇幅的緣由,咱們留在下一篇文章再來詳細講解