<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      由 Mybatis 源碼暢談軟件設計(四):動態 SQL 執行流程

      本節我們探究動態 SQL 的執行流程,由于在前一節我們已經對各個組件進行了詳細介紹,所以本節不再贅述相關內容,在本節中主要強調靜態 SQL 和動態 SQL 執行的不同之處。在這個過程中,SqlNode 相關實現值得關注,它為動態 SQL 標簽都定義了專用實現類,遵循單一職責的原則,并且應用了 裝飾器模式。最后,我們還會討論動態 SQL 避免注入的解決方案,它是在 Mybatis 中不可略過的一環。

      動態 SQL 執行流程

      以單測 org.apache.ibatis.session.SqlSessionTest#dynamicSqlParse 為例,動態 SQL 執行查詢時,第一個需要注意點是獲取 BoundSql 對象:

      public final class MappedStatement {
      
          // sqlSource 存儲 SQL 語句,區分靜態、動態SQL
          private SqlSource sqlSource;
          
          public BoundSql getBoundSql(Object parameterObject) {
              BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
              // ...
          }
      
          // ...
      }
      

      在講解 MappedStatement 時,我們提到了包含動態標簽和 $ 符號的 SQL 會被解析成 DynamicSqlSource,所以它在獲取 BoundSql 時會執行如下邏輯:

      public class DynamicSqlSource implements SqlSource {
      
          private final Configuration configuration;
          private final SqlNode rootSqlNode;
      
          public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
              this.configuration = configuration;
              this.rootSqlNode = rootSqlNode;
          }
          
          public BoundSql getBoundSql(Object parameterObject) {
              // 創建動態 SQL 的上下文信息
              DynamicContext context = new DynamicContext(configuration, parameterObject);
              // 根據上下文信息拼接 SQL,處理 SQL 中的動態標簽
              // 處理完成后 SQL 為不包含任何動態標簽,但可能包含 #{} 占位符的 SQL 信息,SQL 會被封裝到上下文的 sqlBuilder 對象中
              rootSqlNode.apply(context);
      
              // 處理拼接完成后 SQL 中的 #{} 占位符,將占位符替換為 ?
              SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
              Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
              // 解析完成后的 SqlSource 均為 StaticSqlSource 類型,其中記錄解析完成后的完整 SQL
              SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
              // StaticSqlSource 獲取 BoundSql SQL 的方法就非常簡單了:將 SQL 和參數信息記錄下來
              BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
              // 在 BoundSql 對象中 additionalParameters Map 中添加 key 為 _parameter,value 為入參 的附加參數信息
              context.getBindings().forEach(boundSql::setAdditionalParameter);
              return boundSql;
          }
      }
      

      首先它會創建動態 SQL 上下文信息 DynamicContext,這里并不復雜,所以不再追溯源碼信息。rootSqlNode 對象在講解映射配置時我們提到過,它會被解析成 MixedSqlNode 類型,其中包含著各個節點的信息,如下所示:

      sqlNode2.png

      MixedSqlNode 會根據上下文信息完成 apply 操作,如注釋信息所述,最終會將帶有動態標簽的多個節點的 SQL 解析成一條 SQL 字符串記錄在上下文中。下面我們重點看一下 動態標簽 的處理邏輯,它使用到了 裝飾器模式靜態代理模式WhereSqlNode 實現了 TrimSqlNode,但是它幾乎并沒有承載任何功能,只是定義了 SQL 連接符信息,這個實現類起到更多的作用是增強代碼可讀性和遵守單一職責的原則:

      public class WhereSqlNode extends TrimSqlNode {
      
          private static final List<String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t",
                  "OR\t");
      
          public WhereSqlNode(Configuration configuration, SqlNode contents) {
              super(configuration, contents, "WHERE", prefixList, null, null);
          }
      
      }
      

      處理邏輯均在 TrimSqlNode 中實現,它在其中定義了 SqlNode contents,其中最重要的是 apply 方法,裝飾器模式便體現在這里:它對組合進來的其他 SqlNodeapply 方法進行增強,添加處理前綴和后綴標識符信息的邏輯,如下所示:

      public class TrimSqlNode implements SqlNode {
      
          private final SqlNode contents;
      
          @Override
          public boolean apply(DynamicContext context) {
              FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
              boolean result = contents.apply(filteredDynamicContext);
              // 處理前綴和后綴標識符信息
              filteredDynamicContext.applyAll();
              return result;
          }
      
          private class FilteredDynamicContext extends DynamicContext {
              // ...
          }
      }
      

      WhereSqlNode.drawio.png

      實現處理前綴和后綴表示邏輯的 FilteredDynamicContext 是定義在 TrimSqlNode 中的內部類,它使用到了靜態代理模式,在 Mybatis 框架中,出現 delegate 字段命名時,便需要對代理模式多留意了,而且這種命名也提醒我們,未來在使用到代理模式時,可以將被代理對象命名為 delegate

      DynamicContext delegate 對象被代理,由代理對象 FilteredDynamicContext 完成前后綴處理,最后將處理完的 SQL 拼接到原上下文中:

      public class TrimSqlNode implements SqlNode {
          // ...
      
          private class FilteredDynamicContext extends DynamicContext {
              private final DynamicContext delegate;
              private boolean prefixApplied;
              private boolean suffixApplied;
              private StringBuilder sqlBuffer;
      
              public void applyAll() {
                  sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
                  String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
                  if (trimmedUppercaseSql.length() > 0) {
                      // 處理前綴標識符比如,WHERE,SET
                      applyPrefix(sqlBuffer, trimmedUppercaseSql);
                      // 處理后綴標識符,一般用于自定義 TrimSqlNode
                      applySuffix(sqlBuffer, trimmedUppercaseSql);
                  }
                  delegate.appendSql(sqlBuffer.toString());
              }
          }
          
      }
      

      這段邏輯并不復雜,除此之外我們需要再關注下 IfSqlNode 的邏輯,探究 IF 標簽 中的內容是如何被拼接到 SQL 中的:

      public class IfSqlNode implements SqlNode {
          private final ExpressionEvaluator evaluator;
          private final String test;
          private final SqlNode contents;
      
          @Override
          public boolean apply(DynamicContext context) {
              // 判斷表達式,如果 if 標簽中 test 判斷為 true 則將對應的 SQL 片段拼接到 SQL 上
              if (evaluator.evaluateBoolean(test, context.getBindings())) {
                  contents.apply(context);
                  return true;
              }
              return false;
          }
      
      }
      

      IfSqlNode.png

      它會借助 OGNL 完成 test 表達式內容的判斷,為 True 則會追加對應 SQL 信息。

      接下來繼續回到 DynamicSqlSource#getBoundSql 方法,將 #{} 占位符替換為 ? 的邏輯在講解映射配置時已講過,不清楚的小伙伴可以再去了解一下,這部分內容沒有特別需要關注的,了解下該方法的作用即可:

      public class DynamicSqlSource implements SqlSource {
          // ...
          
          @Override
          public BoundSql getBoundSql(Object parameterObject) {
              // ...
      
              // 處理拼接完成后 SQL 中的 #{} 占位符,將占位符替換為 ?
              SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
              Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
              // 解析完成后的 SqlSource 均為 StaticSqlSource 類型,其中記錄解析完成后的完整 SQL
              SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
              // StaticSqlSource 獲取 BoundSql SQL 的方法就非常簡單了:將 SQL 和參數信息記錄下來
              BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
              // 在 BoundSql 對象中 additionalParameters Map 中添加 key 為 _parameter,value 為入參 的附加參數信息
              context.getBindings().forEach(boundSql::setAdditionalParameter);
              return boundSql;
          }
      }
      

      到這里,帶有動態標簽的 SQL 已被處理成可能帶有 ? 占位符的 SQL 字符串了,后續邏輯與上一節中介紹 SQL 的執行流程沒有區別,便不再贅述了。接下來我們討論下 #{} 占位符是如何避免 SQL 注入的問題。

      #{} 是如何解決 SQL 注入的?

      我們已經了解到 #{} 占位符會被解析成 ?,在 SQL 被執行時,由 JDBC 的 PreparedStatement 將對應的參數會綁定到對應的位置上,它并 不是直接將內容拼接到 SQL 上,注入的 SQL 內容將會 被看作字符串處理,它便是通過這種方式來避免 SQL 注入的。

      org.apache.ibatis.session.SqlSessionTest#dynamicTableName 單測為例:

      class SqlSessionTest extends BaseDataTest {
          @Test
          void dynamicTableName() {
              try (SqlSession session = sqlMapper.openSession()) {
                  AuthorMapper mapper = session.getMapper(AuthorMapper.class);
                  List<Author> author = mapper.selectDynamicTableName("author");
                  assertEquals(2, author.size());
              }
          }
      }
      
          <select id="selectDynamicTableName" parameterType="string" resultMap="selectAuthor">
              select id, username, password, email, bio, favourite_section
              from #{tableName}
          </select>
      

      我們想使用 #{} 占位符動態替換表名,試驗下能不能成功,結果控制臺打印以下內容:

      ### SQL: select id, username, password, email, bio, favourite_section from ?
      ### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''author'' at line 2
      

      發現它將表名參數作為字符串處理,實際執行的 SQL 為:

      select id, username, password, email, bio, favourite_section from 'author'
      

      所以任何要注入的 SQL 內容是不能影響到 SQL 語句的,保證了安全性。那么 $ 占位符是如何實現動態 SQL 拼接的呢?我們將 SQL 修改一下:

          <select id="selectDynamicTableName" parameterType="string" resultMap="selectAuthor">
              select id, username, password, email, bio, favourite_section
              from ${tableName}
          </select>
      

      先前我們提到過,包含 $ 占位符的 SQL 也會被識別為動態 SQL(SqlSource 類型為 DynamicSqlSource),同樣我們需要看一下它獲取 BoundSql 的邏輯 org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql。在執行該方法時,可以發現整條 SQL 語句被解析為字符串保存在 TextSqlNode 中:

      $占位符的解析.png

      我們繼續看一下 apply 方法的邏輯,發現它會創建一個專門替換 ${} 占位符 GenericTokenParser 解析器:

      public class TextSqlNode implements SqlNode {
          // eg: select id, username, password, email, bio, favourite_section from ${tableName}
          private final String text;
          
          @Override
          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);
          }
      
      }
      

      這樣它在執行 GenericTokenParser#parser 方法時,便會根據上下文信息 ${} 替換成參數直接拼接到 SQL 上,最終 SQL 為:

      select id, username, password, email, bio, favourite_section from author
      

      它會直接在原 SQL 上進行拼接,所以會有 SQL 注入的風險,而且我們也能理解包含 ${} 的 SQL 節點被命名為 TextSqlNode 的原因了,Text 便表示 SQL 會被解析為一段 SQL 的文本表達式。

      巨人的肩膀

      posted @ 2025-10-29 20:24  京東云開發者  閱讀(7)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 久久这里只精品热免费99| 久99久热只有精品国产99| 欧美成人影院亚洲综合图 | 日日噜噜夜夜爽爽| 亚洲人成网站在小说| 国产精品深夜福利在线观看| 日本一区二区国产在线| jizz视频在线观看| 国产粉嫩一区二区三区av| 黄色A级国产免费大片视频| 国产视频一区二区三区麻豆| 深夜免费av在线观看| 777米奇影视第四色| 午夜成年男人免费网站| 亚洲精品色无码AV试看| 中文字幕av日韩有码| 精品一区二区三区蜜桃麻豆| 亚洲AV永久中文无码精品综合| 亚洲欧美色综合影院| 久久精品国产成人午夜福利| 女人香蕉久久毛毛片精品| 99久久久无码国产麻豆| 久久人人爽人人爽人人av| 中国大陆高清aⅴ毛片| 熟妇的奶头又大又长奶水视频| 亚洲精品网站在线观看不卡无广告| AV人摸人人人澡人人超碰| 蜜臀91精品高清国产福利| caoporn免费视频公开| 麻豆精产国品一二三区区| 377P欧洲日本亚洲大胆| 一本精品99久久精品77| 中文字幕第一页国产| 亚洲 成人 无码 在线观看| 亚洲色拍拍噜噜噜最新网站| 亚洲熟妇色自偷自拍另类| 国产一区二区三区亚洲精品| 午夜男女爽爽影院免费视频下载| 亚洲欧洲精品一区二区| 亚洲性一交一乱一伦视频| 好紧好爽午夜视频|