MyBatis 架構與原理深入解析,面試隨便問!

来源:https://www.cnblogs.com/javastack/archive/2023/09/07/17685017.html
-Advertisement-
Play Games

作者:七寸知架構 \ 鏈接:https://www.jianshu.com/p/ec40a82cae28 # 1 引言# 本文主要講解JDBC怎麼演變到Mybatis的漸變過程,**重點講解了為什麼要將JDBC封裝成Mybaits這樣一個持久層框架**。再而論述Mybatis作為一個數據持久層框架本 ...


作者:七寸知架構
鏈接:https://www.jianshu.com/p/ec40a82cae28

1 引言#

本文主要講解JDBC怎麼演變到Mybatis的漸變過程,重點講解了為什麼要將JDBC封裝成Mybaits這樣一個持久層框架。再而論述Mybatis作為一個數據持久層框架本身有待改進之處。

2 JDBC實現查詢分析#

我們先看看我們最熟悉也是最基礎的通過JDBC查詢資料庫數據,一般需要以下七個步驟:

  1. 載入JDBC驅動;
  2. 建立並獲取資料庫連接;
  3. 創建 JDBC Statements 對象;
  4. 設置SQL語句的傳入參數;
  5. 執行SQL語句並獲得查詢結果;
  6. 對查詢結果進行轉換處理並將處理結果返回;
  7. 釋放相關資源(關閉Connection,關閉Statement,關閉ResultSet);

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

以下是具體的實現代碼:

public static List<Map<String,Object>> queryForList(){  
    Connection connection = null;  
    ResultSet rs = null;  
    PreparedStatement stmt = null;  
    List<Map<String,Object>> resultList = new ArrayList<Map<String,Object>>();  
          
    try {  
        // 載入JDBC驅動  
        Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();  
        String url = "jdbc:oracle:thin:@localhost:1521:ORACLEDB";  
              
        String user = "trainer";   
        String password = "trainer";   
              
        // 獲取資料庫連接  
        connection = DriverManager.getConnection(url,user,password);   
              
        String sql = "select * from userinfo where user_id = ? ";  
        // 創建Statement對象(每一個Statement為一次資料庫執行請求)  
        stmt = connection.prepareStatement(sql);  
              
        // 設置傳入參數  
        stmt.setString(1, "zhangsan");  
              
        // 執行SQL語句  
        rs = stmt.executeQuery();  
              
        // 處理查詢結果(將查詢結果轉換成List<Map>格式)  
        ResultSetMetaData rsmd = rs.getMetaData();  
        int num = rsmd.getColumnCount();  
              
        while(rs.next()){  
            Map map = new HashMap();  
            for(int i = 0;i < num;i++){  
                String columnName = rsmd.getColumnName(i+1);  
                map.put(columnName,rs.getString(columnName));  
            }  
            resultList.add(map);  
        }  
              
    } catch (Exception e) {  
        e.printStackTrace();  
    } finally {  
        try {  
            // 關閉結果集  
            if (rs != null) {  
                rs.close();  
                rs = null;  
            }  
            // 關閉執行  
            if (stmt != null) {  
                stmt.close();  
                stmt = null;  
            }  
            if (connection != null) {  
                connection.close();  
                connection = null;  
            }  
        } catch (SQLException e) {  
            e.printStackTrace();  
        }  
    }        
    return resultList;  
}

3 JDBC演變到Mybatis過程#

上面我們看到了實現JDBC有七個步驟,哪些步驟是可以進一步封裝的,減少我們開發的代碼量。

3.1 第一步優化:連接獲取和釋放##

  1. 問題描述:

資料庫連接頻繁的開啟和關閉本身就造成了資源的浪費,影響系統的性能

解決問題:

資料庫連接的獲取和關閉我們可以使用資料庫連接池來解決資源浪費的問題。通過連接池就可以反覆利用已經建立的連接去訪問資料庫了。減少連接的開啟和關閉的時間。

  1. 問題描述:

但是現在連接池多種多樣,可能存在變化,有可能採用DBCP的連接池,也有可能採用容器本身的JNDI資料庫連接池。

解決問題:

我們可以通過DataSource進行隔離解耦,我們統一從DataSource裡面獲取資料庫連接,DataSource具體由DBCP實現還是由容器的JNDI實現都可以,所以我們將DataSource的具體實現通過讓用戶配置來應對變化。

3.2 第二步優化:SQL統一存取##

  1. 問題描述:

我們使用JDBC進行操作資料庫時,SQL語句基本都散落在各個JAVA類中,這樣有三個不足之處:

第一,可讀性很差,不利於維護以及做性能調優。

第二,改動Java代碼需要重新編譯、打包部署。

第三,不利於取出SQL在資料庫客戶端執行(取出後還得刪掉中間的Java代碼,編寫好的SQL語句寫好後還得通過+號在Java進行拼湊)。

解決問題:

我們可以考慮不把SQL語句寫到Java代碼中,那麼把SQL語句放到哪裡呢?首先需要有一個統一存放的地方,我們可以將這些SQL語句統一集中放到配置文件或者資料庫裡面(以key-value的格式存放)。然後通過SQL語句的key值去獲取對應的SQL語句。

既然我們將SQL語句都統一放在配置文件或者資料庫中,那麼這裡就涉及一個SQL語句的載入問題

3.3 第三步優化:傳入參數映射和動態SQL##

  1. 問題描述:

很多情況下,我們都可以通過在SQL語句中設置占位符來達到使用傳入參數的目的,這種方式本身就有一定局限性,它是按照一定順序傳入參數的,要與占位符一一匹配。但是,如果我們傳入的參數是不確定的(比如列表查詢,根據用戶填寫的查詢條件不同,傳入查詢的參數也是不同的,有時是一個參數、有時可能是三個參數),那麼我們就得在後臺代碼中自己根據請求的傳入參數去拼湊相應的SQL語句,這樣的話還是避免不了在Java代碼裡面寫SQL語句的命運。既然我們已經把SQL語句統一存放在配置文件或者資料庫中了,怎麼做到能夠根據前臺傳入參數的不同,動態生成對應的SQL語句呢?

解決問題:

第一,我們先解決這個動態問題,按照我們正常的程式員思維是,通過if和else這類的判斷來進行是最直觀的,這個時候我們想到了JSTL中的這樣的標簽,那麼,能不能將這類的標簽引入到SQL語句中呢?假設可以,那麼我們這裡就需要一個專門的SQL解析器來解析這樣的SQL語句,但是,if判斷的變數來自於哪裡呢?傳入的值本身是可變的,那麼我們得為這個值定義一個不變的變數名稱,而且這個變數名稱必須和對應的值要有對應關係,可以通過這個變數名稱找到對應的值,這個時候我們想到了key-value的Map。解析的時候根據變數名的具體值來判斷。

假如前面可以判斷沒有問題,那麼假如判斷的結果是true,那麼就需要輸出的標簽裡面的SQL片段,但是怎麼解決在標簽裡面使用變數名稱的問題呢?這裡我們需要使用一種有別於SQL的語法來嵌入變數(比如使用#變數名#)。這樣,SQL語句經過解析後就可以動態的生成符合上下文的SQL語句。

還有,怎麼區分開占位符變數和非占位變數?有時候我們單單使用占位符是滿足不了的,占位符只能為查詢條件占位,SQL語句其他地方使用不了。這裡我們可以使用#變數名#表示占位符變數,使用變數名錶示非占位符變數

3.4 第四步優化:結果映射和結果緩存##

  1. 問題描述:

執行SQL語句、獲取執行結果、對執行結果進行轉換處理、釋放相關資源是一整套下來的。假如是執行查詢語句,那麼執行SQL語句後,返回的是一個ResultSet結果集,這個時候我們就需要將ResultSet對象的數據取出來,不然等到釋放資源時就取不到這些結果信息了。我們從前面的優化來看,以及將獲取連接、設置傳入參數、執行SQL語句、釋放資源這些都封裝起來了,只剩下結果處理這塊還沒有進行封裝,如果能封裝起來,每個資料庫操作都不用自己寫那麼一大堆Java代碼,直接調用一個封裝的方法就可以搞定了。

解決問題:

我們分析一下,一般對執行結果的有哪些處理,有可能將結果不做任何處理就直接返回,也有可能將結果轉換成一個JavaBean對象返回、一個Map返回、一個List返回等`,結果處理可能是多種多樣的。從這裡看,我們必須告訴SQL處理器兩點:第一,需要返回什麼類型的對象;第二,需要返回的對象的數據結構怎麼跟執行的結果映射,這樣才能將具體的值copy到對應的數據結構上。

接下來,我們可以進而考慮對SQL執行結果的緩存來提升性能。緩存數據都是key-value的格式,那麼這個key怎麼來呢?怎麼保證唯一呢?即使同一條SQL語句幾次訪問的過程中由於傳入參數的不同,得到的執行SQL語句也是不同的。那麼緩存起來的時候是多對。但是SQL語句和傳入參數兩部分合起來可以作為數據緩存的key值

3.5 第五步優化:解決重覆SQL語句問題##

  1. 問題描述:

由於我們將所有SQL語句都放到配置文件中,這個時候會遇到一個SQL重覆的問題,幾個功能的SQL語句其實都差不多,有些可能是SELECT後面那段不同、有些可能是WHERE語句不同。有時候表結構改了,那麼我們就需要改多個地方,不利於維護。

解決問題:

當我們的代碼程式出現重覆代碼時怎麼辦?將重覆的代碼抽離出來成為獨立的一個類,然後在各個需要使用的地方進行引用。對於SQL重覆的問題,我們也可以採用這種方式,通過將SQL片段模塊化,將重覆的SQL片段獨立成一個SQL塊,然後在各個SQL語句引用重覆的SQL塊,這樣需要修改時只需要修改一處即可。

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

4 Mybaits有待改進之處#

  1. 問題描述:

Mybaits所有的資料庫操作都是基於SQL語句,導致什麼樣的資料庫操作都要寫SQL語句。一個應用系統要寫的SQL語句實在太多了。

改進方法:

我們對資料庫進行的操作大部分都是對錶數據的增刪改查,很多都是對單表的數據進行操作,由這點我們可以想到一個問題:單表操作可不可以不寫SQL語句,通過JavaBean的預設映射器生成對應的SQL語句,比如:一個類UserInfo對應於USER_INFO表, userId屬性對應於USER_ID欄位。這樣我們就可以通過反射可以獲取到對應的表結構了,拼湊成對應的SQL語句顯然不是問題

5 MyBatis框架整體設計#

MyBatis框架整體設計

5.1 介面層-和資料庫交互的方式#

MyBatis和資料庫的交互有兩種方式:

  1. 使用傳統的MyBatis提供的API;
  2. 使用Mapper介面;

5.1.1 使用傳統的MyBatis提供的API###

這是傳統的傳遞Statement Id 和查詢參數給 SqlSession 對象,使用 SqlSession對象完成和資料庫的交互;MyBatis提供了非常方便和簡單的API,供用戶實現對資料庫的增刪改查數據操作,以及對資料庫連接信息和MyBatis 自身配置信息的維護操作。

傳統的MyBatis工作模式

上述使用MyBatis 的方法,是創建一個和資料庫打交道的SqlSession對象,然後根據Statement Id 和參數來操作資料庫,這種方式固然很簡單和實用,但是它不符合面向對象語言的概念和麵向介面編程的編程習慣。由於面向介面的編程是面向對象的大趨勢,MyBatis 為了適應這一趨勢,增加了第二種使用MyBatis 支持介面(Interface)調用方式。

5.1.2 使用Mapper介面###

MyBatis 將配置文件中的每一個 節點抽象為一個 Mapper 介面:

這個介面中聲明的方法和 節點中的<select|update|delete|insert> 節點項對應,即<select|update|delete|insert> 節點的id值為Mapper 介面中的方法名稱,parameterType 值表示Mapper 對應方法的入參類型,而resultMap 值則對應了Mapper 介面表示的返回值類型或者返回結果集的元素類型

Mapper介面和Mapper.xml配置文件之間的對應關係

根據MyBatis 的配置規範配置好後,通過SqlSession.getMapper(XXXMapper.class)方法,MyBatis 會根據相應的介面聲明的方法信息,通過動態代理機制生成一個Mapper 實例,我們使用Mapper介面的某一個方法時,MyBatis會根據這個方法的方法名和參數類型,確定Statement Id,底層還是通過SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject); 等等來實現對資料庫的操作,MyBatis引用Mapper 介面這種調用方式,純粹是為了滿足面向介面編程的需要。(其實還有一個原因是在於,面向介面的編程,使得用戶在介面上可以使用註解來配置SQL語句,這樣就可以脫離XML配置文件,實現“0配置”)。

5.2 數據處理層##

數據處理層可以說是MyBatis的核心,從大的方面上講,它要完成兩個功能:

  1. 通過傳入參數構建動態SQL語句;
  2. SQL語句的執行以及封裝查詢結果集成List

5.2.1 參數映射和動態SQL語句生成###

動態語句生成可以說是MyBatis框架非常優雅的一個設計,MyBatis 通過傳入的參數值,使用 Ognl 來動態地構造SQL語句,使得MyBatis 有很強的靈活性和擴展性。

參數映射指的是對於java 數據類型和jdbc數據類型之間的轉換:這裡有包括兩個過程:查詢階段,我們要將java類型的數據,轉換成jdbc類型的數據,通過 preparedStatement.setXXX() 來設值;另一個就是對resultset查詢結果集的jdbcType 數據轉換成java 數據類型

5.2.2 SQL語句的執行以及封裝查詢結果集成List###

動態SQL語句生成之後,MyBatis 將執行SQL語句,並將可能返回的結果集轉換成List 列表。MyBatis 在對結果集的處理中,支持結果集關係一對多和多對一的轉換,並且有兩種支持方式,一種為嵌套查詢語句的查詢,還有一種是嵌套結果集的查詢

5.3 框架支撐層##

1、事務管理機制

事務管理機制對於ORM框架而言是不可缺少的一部分,事務管理機制的質量也是考量一個ORM框架是否優秀的一個標準。

2、連接池管理機制

由於創建一個資料庫連接所占用的資源比較大,對於數據吞吐量大和訪問量非常大的應用而言,連接池的設計就顯得非常重要

3、緩存機制

為了提高數據利用率和減小伺服器和資料庫的壓力,MyBatis 會對於一些查詢提供會話級別的數據緩存,會將對某一次查詢,放置到SqlSession 中,在允許的時間間隔內,對於完全相同的查詢,MyBatis會直接將緩存結果返回給用戶,而不用再到資料庫中查找。

4、SQL語句的配置方式

傳統的MyBatis 配置SQL語句方式就是使用XML文件進行配置的,但是這種方式不能很好地支持面向介面編程的理念,為了支持面向介面的編程,MyBatis 引入了Mapper介面的概念,面向介面的引入,對使用註解來配置SQL語句成為可能,用戶只需要在介面上添加必要的註解即可,不用再去配置XML文件了,但是,目前的MyBatis 只是對註解配置SQL語句提供了有限的支持,某些高級功能還是要依賴XML配置文件配置SQL 語句。

5.4 引導層##

引導層是配置和啟動MyBatis配置信息的方式。MyBatis 提供兩種方式來引導MyBatis :基於XML配置文件的方式和基於Java API 的方式

5.5 主要構件及其相互關係##

從MyBatis代碼實現的角度來看,MyBatis的主要的核心部件有以下幾個:

SqlSession:作為MyBatis工作的主要頂層API,表示和資料庫交互的會話,完成必要資料庫增刪改查功能;

Executor:MyBatis執行器,是MyBatis 調度的核心,負責SQL語句的生成和查詢緩存的維護;

StatementHandler:封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設置參數、將Statement結果集轉換成List集合。

ParameterHandler:負責對用戶傳遞的參數轉換成JDBC Statement 所需要的參數;

ResultSetHandler:負責將JDBC返回的ResultSet結果集對象轉換成List類型的集合;

TypeHandler:負責java數據類型和jdbc數據類型之間的映射和轉換;

MappedStatement:MappedStatement維護了一條<select|update|delete|insert>節點的封裝;

SqlSource:負責根據用戶傳遞的parameterObject,動態地生成SQL語句,將信息封裝到BoundSql對象中,並返回;

BoundSql:表示動態生成的SQL語句以及相應的參數信息;

Configuration:MyBatis所有的配置信息都維持在Configuration對象之中;

它們的關係如下圖所示:

MyBatis主要構件關係如圖

6 SqlSession工作過程分析#

  1. 開啟一個資料庫訪問會話---創建SqlSession對象
SqlSession sqlSession = factory.openSession(); 

MyBatis封裝了對資料庫的訪問,把對資料庫的會話和事務控制放到了SqlSession對象中

  1. 為SqlSession傳遞一個配置的Sql語句的Statement Id和參數,然後返回結果:
List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);

上述的"com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",是配置在EmployeesMapper.xml 的Statement ID,params是傳遞的查詢參數。

讓我們來看一下sqlSession.selectList()方法的定義:

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 {  
        //1.根據Statement Id,在mybatis 配置對象Configuration中查找和配置文件相對應的MappedStatement      
        MappedStatement ms = configuration.getMappedStatement(statement);  
        //2. 將查詢任務委托給MyBatis 的執行器 Executor  
        List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);  
        return result;  
    } catch (Exception e) {  
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);  
    } finally {  
        ErrorContext.instance().reset();  
    }  
} 

MyBatis在初始化的時候,會將MyBatis的配置信息全部載入到記憶體中,使用org.apache.ibatis.session.Configuration實例來維護。使用者可以使用sqlSession.getConfiguration()方法來獲取。MyBatis的配置文件中配置信息的組織格式和記憶體中對象的組織格式幾乎完全對應的

上述例子中的:

<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" >  
   select   
       EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY  
   from LOUIS.EMPLOYEES  
   <if test="min_salary != null">  
       where SALARY < #{min_salary,jdbcType=DECIMAL}  
   </if>  
</select>

載入到記憶體中會生成一個對應的MappedStatement對象,然後會以key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary" ,value為MappedStatement對象的形式維護到Configuration的一個Map中。當以後需要使用的時候,只需要通過Id值來獲取就可以了。

從上述的代碼中我們可以看到SqlSession的職能是:SqlSession根據Statement ID, 在mybatis配置對象Configuration中獲取到對應的MappedStatement對象,然後調用mybatis執行器來執行具體的操作

  1. MyBatis執行器Executor根據SqlSession傳遞的參數執行query()方法(由於代碼過長,讀者只需閱讀我註釋的地方即可):
/** 
   * BaseExecutor 類部分代碼 
   * 
   */  
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
      // 1. 根據具體傳入的參數,動態地生成需要執行的SQL語句,用BoundSql對象表示    
      BoundSql boundSql = ms.getBoundSql(parameter);  
      // 2. 為當前的查詢創建一個緩存Key  
      CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);  
      return query(ms, parameter, rowBounds, resultHandler, key, boundSql);  
}  
 
@SuppressWarnings("unchecked")  
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {  
       ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());  
       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 {  
               // 3.緩存中沒有值,直接從資料庫中讀取數據    
               list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);  
           }  
       } finally {  
           queryStack--;  
       }  
       if (queryStack == 0) {  
           for (DeferredLoad deferredLoad : deferredLoads) {  
               deferredLoad.load();  
           }  
           deferredLoads.clear(); // issue #601  
           if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {  
               clearLocalCache(); // issue #482  
           }  
       }  
       return list;  
}

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 {  
         
          //4. 執行查詢,返回List 結果,然後    將查詢的結果放入緩存之中  
          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;  
} 
/** 
   * 
   * SimpleExecutor類的doQuery()方法實現 
   * 
   */  
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();  
          //5. 根據既有的參數,創建StatementHandler對象來執行查詢操作  
          StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);  
          //6. 創建java.Sql.Statement對象,傳遞給StatementHandler對象  
          stmt = prepareStatement(handler, ms.getStatementLog());  
          //7. 調用StatementHandler.query()方法,返回List結果集  
          return handler.<E>query(stmt, resultHandler);  
       } finally {  
           closeStatement(stmt);  
       }  
}

上述的Executor.query()方法幾經轉折,最後會創建一個StatementHandler對象,然後將必要的參數傳遞給StatementHandler,使用StatementHandler來完成對資料庫的查詢,最終返回List結果集。

從上面的代碼中我們可以看出,Executor的功能和作用是:

  1. 根據傳遞的參數,完成SQL語句的動態解析,生成BoundSql對象,供StatementHandler使用;
  2. 為查詢創建緩存,以提高性能;
  3. 創建JDBC的Statement連接對象,傳遞給StatementHandler對象,返回List查詢結果;
  1. StatementHandler對象負責設置Statement對象中的查詢參數、處理JDBC返回的resultSet,將resultSet加工為List 集合返回:

接著上面的Executor第六步,看一下:prepareStatement() 方法的實現:

/** 
   * 
   * SimpleExecutor類的doQuery()方法實現 
   * 
   */  
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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); 
          // 1.準備Statement對象,並設置Statement對象的參數 
          stmt = prepareStatement(handler, ms.getStatementLog()); 
          // 2. StatementHandler執行query()方法,返回List結果 
          return handler.<E>query(stmt, resultHandler); 
      } finally {
          closeStatement(stmt); 
      } 
}  
 
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
      Statement stmt;  
      Connection connection = getConnection(statementLog);  
      stmt = handler.prepare(connection);  
      //對創建的Statement對象設置參數,即設置SQL 語句中 ? 設置為指定的參數  
      handler.parameterize(stmt);  
      return stmt;  
}

以上我們可以總結StatementHandler對象主要完成兩個工作:

  1. 對於JDBC的PreparedStatement類型的對象,創建的過程中,我們使用的是SQL語句字元串會包含 若幹個? 占位符,我們其後再對占位符進行設值。
    StatementHandler通過parameterize(statement)方法對Statement進行設值;
  2. StatementHandler通過List query(Statement statement, ResultHandler resultHandler)方法來完成執行Statement,和將Statement對象返回的resultSet封裝成List;
  1. StatementHandler 的parameterize(statement) 方法的實現:
/** 
   * StatementHandler 類的parameterize(statement) 方法實現  
   */  
public void parameterize(Statement statement) throws SQLException {  
      //使用ParameterHandler對象來完成對Statement的設值    
      parameterHandler.setParameters((PreparedStatement) statement);  
}  
/** 
   *  
   * ParameterHandler類的setParameters(PreparedStatement ps) 實現 
   * 對某一個Statement進行設置參數 
   */  
public void setParameters(PreparedStatement ps) throws SQLException {  
      ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());  
      List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();  
      if (parameterMappings != null) {  
          for (int i = 0; i < parameterMappings.size(); i++) {  
              ParameterMapping parameterMapping = parameterMappings.get(i);  
              if (parameterMapping.getMode() != ParameterMode.OUT) {  
                  Object value;  
                  String propertyName = parameterMapping.getProperty();  
                  if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params  
                      value = boundSql.getAdditionalParameter(propertyName);  
                  } else if (parameterObject == null) {  
                      value = null;  
                  } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {  
                      value = parameterObject;  
                  } else {  
                      MetaObject metaObject = configuration.newMetaObject(parameterObject);  
                      value = metaObject.getValue(propertyName);  
                  }  
         
                  // 每一個Mapping都有一個TypeHandler,根據TypeHandler來對preparedStatement進行設置參數  
                  TypeHandler typeHandler = parameterMapping.getTypeHandler();  
                  JdbcType jdbcType = parameterMapping.getJdbcType();  
                  if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();  
                  // 設置參數  
                  typeHandler.setParameter(ps, i + 1, value, jdbcType);  
              }  
          }  
      }  
}

從上述的代碼可以看到,StatementHandler的parameterize(Statement) 方法調用了 ParameterHandler的setParameters(statement) 方法,
ParameterHandler的setParameters(Statement)方法負責 根據我們輸入的參數,對statement對象的 ? 占位符處進行賦值。

  1. StatementHandler 的List query(Statement statement, ResultHandler resultHandler)方法的實現:
 /** 
    * PreParedStatement類的query方法實現 
    */  
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {  
      //1.調用preparedStatemnt。execute()方法,然後將resultSet交給ResultSetHandler處理    
      PreparedStatement ps = (PreparedStatement) statement;  
      ps.execute();  
      //2. 使用ResultHandler來處理ResultSet  
      return resultSetHandler.<E> handleResultSets(ps);  
}  

從上述代碼我們可以看出,StatementHandler 的List query(Statement statement, ResultHandler resultHandler)方法的實現,是調用了ResultSetHandler的handleResultSets(Statement) 方法。ResultSetHandler的handleResultSets(Statement) 方法會將Statement語句執行後生成的resultSet 結果集轉換成List 結果集

/**   
   * ResultSetHandler類的handleResultSets()方法實現 
   *  
   */  
public List<Object> handleResultSets(Statement stmt) throws SQLException {  
      final List<Object> multipleResults = new ArrayList<Object>();  
 
      int resultSetCount = 0;  
      ResultSetWrapper rsw = getFirstResultSet(stmt);  
 
      List<ResultMap> resultMaps = mappedStatement.getResultMaps();  
      int resultMapCount = resultMaps.size();  
      validateResultMapsCount(rsw, resultMapCount);  
     
      while (rsw != null && resultMapCount > resultSetCount) {  
          ResultMap resultMap = resultMaps.get(resultSetCount);  
       
          //將resultSet  
          handleResultSet(rsw, resultMap, multipleResults, null);  
          rsw = getNextResultSet(stmt);  
          cleanUpAfterHandlingResultSet();  
          resultSetCount++;  
      }
 
      String[] resultSets = mappedStatement.getResulSets();  
      if (resultSets != null) {  
          while (rsw != null && resultSetCount < resultSets.length) {  
              ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);  
              if (parentMapping != null) {  
                  String nestedResultMapId = parentMapping.getNestedResultMapId();  
                  ResultMap resultMap = configuration.getResultMap(nestedResultMapId);  
                  handleResultSet(rsw, resultMap, null, parentMapping);  
              }  
              rsw = getNextResultSet(stmt);  
              cleanUpAfterHandlingResultSet();  
              resultSetCount++;  
          }  
      }  
 
      return collapseSingleResultList(multipleResults);  
}  

7 MyBatis初始化機制#

7.1 MyBatis的初始化做了什麼##

任何框架的初始化,無非是載入自己運行時所需要的配置信息。MyBatis的配置信息,大概包含以下信息,其高層級結構如下:

MyBatis配置信息結構圖

MyBatis的上述配置信息會配置在XML配置文件中,那麼,這些信息被載入進入MyBatis內部,MyBatis是怎樣維護的呢?

MyBatis採用了一個非常直白和簡單的方式---使用 org.apache.ibatis.session.Configuration對象作為一個所有配置信息的容器,Configuration對象的組織結構和XML配置文件的組織結構幾乎完全一樣(當然,Configuration對象的功能並不限於此,它還負責創建一些MyBatis內部使用的對象,如Executor等,這將在後續的文章中討論)。如下圖所示:

Configuration對象的組織結構和XML配置文件的組織結構幾乎完全一樣

MyBatis根據初始化好Configuration信息,這時候用戶就可以使用MyBatis進行資料庫操作了。可以這麼說,MyBatis初始化的過程,就是創建 Configuration對象的過程

MyBatis的初始化可以有兩種方式:

基於XML配置文件:基於XML配置文件的方式是將MyBatis的所有配置信息放在XML文件中,MyBatis通過載入並XML配置文件,將配置文信息組裝成內部的Configuration對象。

基於Java API:這種方式不使用XML配置文件,需要MyBatis使用者在Java代碼中,手動創建Configuration對象,然後將配置參數set 進入Configuration對象中。

接下來我們將通過 基於XML配置文件方式的MyBatis初始化,深入探討MyBatis是如何通過配置文件構建Configuration對象,並使用它。

7.2 基於XML配置文件創建Configuration對象##

現在就從使用MyBatis的簡單例子入手,深入分析一下MyBatis是怎樣完成初始化的,都初始化了什麼。看以下代碼:

String resource = "mybatis-config.xml";  
InputStream inputStream = Resources.getResourceAsStream(resource);  
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);  
SqlSession sqlSession = sqlSessionFactory.openSession();  
List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");

有過MyBatis使用經驗的讀者會知道,上述語句的作用是執行com.foo.bean.BlogMapper.queryAllBlogInfo 定義的SQL語句,返回一個List結果集。總的來說,上述代碼經歷了mybatis初始化 -->創建SqlSession -->執行SQL語句返回結果三個過程。

上述代碼的功能是根據配置文件mybatis-config.xml 配置文件,創建SqlSessionFactory對象,然後產生SqlSession,執行SQL語句。而mybatis的初始化就發生在第三句:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 現在就讓我們看看第三句到底發生了什麼。

  1. MyBatis初始化基本過程:

SqlSessionFactoryBuilder根據傳入的數據流生成Configuration對象,然後根據Configuration對象創建預設的SqlSessionFactory實例。

初始化的基本過程如下序列圖所示:

MyBatis初始化序列圖

由上圖所示,mybatis初始化要經過簡單的以下幾步:

  1. 調用SqlSessionFactoryBuilder對象的build(inputStream)方法;
  2. SqlSessionFactoryBuilder會根據輸入流inputStream等信息創建XMLConfigBuilder對象;
  3. SqlSessionFactoryBuilder調用XMLConfigBuilder對象的parse()方法;
  4. XMLConfigBuilder對象返回Configuration對象;
  5. SqlSessionFactoryBuilder根據Configuration對象創建一個DefaultSessionFactory對象;
  6. SqlSessionFactoryBuilder返回 DefaultSessionFactory對象給Client,供Client使用。

SqlSessionFactoryBuilder相關的代碼如下所示:

public SqlSessionFactory build(InputStream inputStream)  {  
      return build(inputStream, null, null);  
}  

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties)  {  
      try  {  
          //2. 創建XMLConfigBuilder對象用來解析XML配置文件,生成Configuration對象  
          XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);  
          //3. 將XML配置文件內的信息解析成Java對象Configuration對象  
          Configuration config = parser.parse();  
          //4. 根據Configuration對象創建出SqlSessionFactory對象  
          return build(config);  
      } catch (Exception e) {  
          throw ExceptionFactory.wrapException("Error building SqlSession.", e);  
      } finally {  
          ErrorContext.instance().reset();  
          try {  
              inputStream.close();  
          } catch (IOException e) {  
              // Intentionally ignore. Prefer previous error.  
          }  
      }
}

// 從此處可以看出,MyBatis內部通過Configuration對象來創建SqlSessionFactory,用戶也可以自己通過API構造好Configuration對象,調用此方法創SqlSessionFactory  
public SqlSessionFactory build(Configuration config) {  
      return new DefaultSqlSessionFactory(config);  
}  

上述的初始化過程中,涉及到了以下幾個對象:

SqlSessionFactoryBuilder :SqlSessionFactory的構造器,用於創建SqlSessionFactory,採用了Builder設計模式

Configuration :該對象是mybatis-config.xml文件中所有mybatis配置信息

SqlSessionFactory:SqlSession工廠類,以工廠形式創建SqlSession對象,採用了Factory工廠設計模式

XMLConfigBuilder :負責將mybatis-config.xml配置文件解析成Configuration對象,共SqlSessonFactoryBuilder使用,創建SqlSessionFactory

  1. 創建Configuration對象的過程:
    接著上述的 MyBatis初始化基本過程討論,當SqlSessionFactoryBuilder執行build()方法,調用了XMLConfigBuilder的parse()方法,然後返回了Configuration對象。那麼parse()方法是如何處理XML文件,生成Configuration對象的呢?
  • (1)XMLConfigBuilder會將XML配置文件的信息轉換為Document對象,而XML配置定義文件DTD轉換成XMLMapperEntityResolver對象,然後將二者封裝到XpathParser對象中,XpathParser的作用是提供根據Xpath表達式獲取基本的DOM節點Node信息的操作

如下圖所示:

XpathParser組成結構圖和生成圖

  • (2)之後XMLConfigBuilder調用parse()方法:會從XPathParser中取出 節點對應的Node對象,然後解析此Node節點的子Node:properties, settings, typeAliases,typeHandlers, objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers:

    public Configuration parse() {  
         if (parsed) {  
             throw new BuilderException("Each XMLConfigBuilder can only be used once.");  
         }  
         parsed = true;  
         //源碼中沒有這一句,只有parseConfiguration(parser.evalNode("/configuration"));  
         //為了讓讀者看得更明晰,源碼拆分為以下兩句  
         XNode configurationNode = parser.evalNode("/configuration");  
         parseConfiguration(configurationNode);  
         return configuration;
    }
    /** 
      * 解析 "/configuration"節點下的子節點信息,然後將解析的結果設置到Configuration對象中 
      */  
    private void parseConfiguration(XNode root) {  
         try {  
             //1.首先處理properties 節點     
             propertiesElement(root.evalNode("properties")); //issue #117 read properties first  
             //2.處理typeAliases  
             typeAliasesElement(root.evalNode("typeAliases"));  
             //3.處理插件  
             pluginElement(root.evalNode("plugins"));  
             //4.處理objectFactory  
             objectFactoryElement(root.evalNode("objectFactory"));  
             //5.objectWrapperFactory  
             objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));  
             //6.settings  
             settingsElement(root.evalNode("settings"));  
             //7.處理environments  
             environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631  
             //8.database  
             databaseIdProviderElement(root.evalNode("databaseIdProvider"));  
             //9.typeHandlers  
             typeHandlerElement(root.evalNode("typeHandlers"));  
             //10.mappers  
             mapperElement(root.evalNode("mappers"));  
         } catch (Exception e) {  
             throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);  
         }  
     }  
    

    註意:在上述代碼中,還有一個非常重要的地方,就是解析XML配置文件子節點的方法mapperElements(root.evalNode("mappers")), 它將解析我們配置的Mapper.xml配置文件,Mapper配置文件可以說是MyBatis的核心,MyBatis的特性和理念都體現在此Mapper的配置和設計上。

  • (3)然後將這些值解析出來設置到Configuration對象中:

    解析子節點的過程這裡就不一一介紹了,用戶可以參照MyBatis源碼仔細揣摩,我們就看上述的environmentsElement(root.evalNode("environments")); 方法是如何將environments的信息解析出來,設置到Configuration對象中的:

    /** 
      * 解析environments節點,並將結果設置到Configuration對象中 
      * 註意:創建envronment時,如果SqlSessionFactoryBuilder指定了特定的環境(即數據源); 
      *      則返回指定環境(數據源)的Environment對象,否則返回預設的Environment對象; 
      *      這種方式實現了MyBatis可以連接多數據源 
      */  
    private void environmentsElement(XNode context) throws Exception {  
        if (context != null)  
        {  
             if (environment == null)  
             {  
                 environment = context.getStringAttribute("default");  
             }  
             for (XNode child : context.getChildren())  
             {  
                  String id = child.getStringAttribute("id");  
                  if (isSpecifiedEnvironment(id))  
                  {  
                      //1.創建事務工廠 TransactionFactory  
                      TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));  
                      DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));  
                      //2.創建數據源DataSource  
                      DataSource dataSource = dsFactory.getDataSource();  
                      //3. 構造Environment對象  
                      Environment.Builder environmentBuilder = new Environment.Builder(id)  
                 .transactionFactory(txFactory)  
                 .dataSource(dataSource);  
                      //4. 將創建的Envronment對象設置到configuration 對象中  
                      configuration.setEnvironment(environmentBuilder.build());  
                 }  
             }  
        }  
    }
    
    private boolean isSpecifiedEnvironment(String id)  
    {  
          if (environment == null)  
          {  
               throw new BuilderException("No environment specified.");  
          }  
          else if (id == null)  
          {  
               throw new BuilderException("Environment requires an id attribute.");  
          }  
          else if (environment.equals(id))  
          {  
              return true;  
          }  
          return false;  
     }  
    
  • (4)返回Configuration對象:

    將上述的MyBatis初始化基本過程的序列圖細化:

    基於XML配置創建Configuration對象的過程

7.3 基於Java API手動載入XML配置文件創建Configuration對象,並使用SqlSessionFactory對象##

我們可以使用XMLConfigBuilder手動解析XML配置文件來創建Configuration對象,代碼如下:

String resource = "mybatis-config.xml";  
InputStream inputStream = Resources.getResourceAsStream(resource);  
// 手動創建XMLConfigBuilder,並解析創建Configuration對象  
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null,null);  
Configuration configuration=parse();  
// 使用Configuration對象創建SqlSessionFactory  
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);  
// 使用MyBatis  
SqlSession sqlSession = sqlSessionFactory.openSession();  
List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");  

7.4 涉及到的設計模式##

初始化的過程涉及到創建各種對象,所以會使用一些創建型的設計模式。在初始化的過程中,Builder模式運用的比較多

7.4.1 Builder模式應用1: SqlSessionFactory的創建###

對於創建SqlSessionFactory時,會根據情況提供不同的參數,其參數組合可以有以下幾種

根據情況提供不同的參數,創建SqlSessionFactory

由於構造時參數不定,可以為其創建一個構造器Builder,將SqlSessionFactory的構建過程和表示分開

MyBatis將SqlSessionFactoryBuilder和SqlSessionFactory相互獨立

7.4.2 Builder模式應用2: 資料庫連接環境Environment對象的創建###

在構建Configuration對象的過程中,XMLConfigBuilder解析 mybatis XML配置文件節點節點時,會有以下相應的代碼:

private void environmentsElement(XNode context) throws Exception {  
    if (context != null) {  
        if (environment == null) {  
            environment = context.getStringAttribute("default");  
        }  
        for (XNode child : context.getChildren()) {  
            String id = child.getStringAttribute("id");  
            //是和預設的環境相同時,解析之  
            if (isSpecifiedEnvironment(id)) {  
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));  
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));  
                DataSource dataSource = dsFactory.getDataSource();  
  
                //使用了Environment內置的構造器Builder,傳遞id 事務工廠和數據源  
                Environment.Builder environmentBuilder = new Environment.Builder(id)  
                .transactionFactory(txFactory)  
                .dataSource(dataSource);  
                configuration.setEnvironment(environmentBuilder.build());  
            }  
        }  
    }  
}  

在Environment內部,定義了靜態內部Builder類:

public final class Environment {  
    private final String id;  
    private final TransactionFactory transactionFactory;  
    private final DataSource dataSource;  
  
    public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {  
        if (id == null) {  
            throw new IllegalArgumentException("Parameter 'id' must not be null");  
        }  
        if (transactionFactory == null) {  
            throw new IllegalArgumentException("Parameter 'transactionFactory' must not be null");  
        }  
        this.id = id;  
        if (dataSource == null) {  
            throw new IllegalArgumentException("Parameter 'dataSource' must not be null");  
        }  
        this.transactionFactory = transactionFactory;  
        this.dataSource = dataSource;  
    }  
  
    public static class Builder {  
        private String id;  
        private TransactionFactory transactionFactory;  
        private DataSource dataSource;  
  
        public Builder(String id) {  
            this.id = id;  
        }  
  
        public Builder transactionFactory(TransactionFactory transactionFactory) {  
            this.transactionFactory = transactionFactory;  
            return this;  
        }  
  
        public Builder dataSource(DataSource dataSource) {  
            this.dataSource = dataSource;  
            return this;  
        }  
  
        public String id() {  
            return this.id;  
        }  
  
        public Environment build() {  
            return new Environment(this.id, this.transactionFactory, this.dataSource);  
        }  
    }  
  
    public String getId() {  
        return this.id;  
    }  
  
    public TransactionFactory getTransactionFactory() {  
        return this.transactionFactory;  
    }  
  
    public DataSource getDataSource() {  
        return this.dataSource;  
    }
}

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 隨著金融科技的發展,越來越多的人選擇線上銀行或移動銀行上進行日常交易。在進行這些交易之前,通常需要進行身份驗證以確保賬戶的安全性。其中,銀行卡二要素驗證是一種常見的身份驗證方式。本文將為大家介紹如何使用銀行卡二要素驗證API介面,具體實現方法如下。 一、API介面介紹 銀行卡二要素驗證API介面是一 ...
  • JDK 代理和 CGLib 有什麼區別? 動態代理是一種機制,程式通過該機制在運行時動態生成代理對象並調用代理方法。動態代理主要有兩種實現機制,一種是基於反射動態代理的JDK,另一種是基於ASM動態代理機制的CGLib實現。現在讓我們談談兩種實現之間的區別以及如何實現它們 JDK 代理和 CGLib ...
  • ### 前言 上篇文章[10分鐘從源碼級別搞懂AQS(AbstractQueuedSynchronizer)](https://juejin.cn/post/7273506068104478760)說到JUC併發包中的同步組件大多使用AQS來實現 本篇文章通過AQS自己來實現一個同步組件,並從源碼級 ...
  • 函數是一組語句,可以在程式中重覆使用。函數不會在頁面載入時自動執行。函數將通過調用函數來執行。 ### 創建函數 要創建(通常稱為聲明)一個函數,請執行以下操作: - 使用 `func` 關鍵字。 - 指定函數的名稱,後跟括弧 `()`。 - 最後,在花括弧 `{}` 內添加定義函數應執行的代碼。 ...
  • 1.雙擊圖標 2.彈出如下對話框: 3、單擊按鈕Next,彈出如下對話框: 4、單擊按鈕I Agree,彈出如下對話框: 5、單擊按鈕Next,彈出如下對話框: 6、單擊Browse按鈕,可以重新設置安裝路徑 7、路徑重新設置後,單擊確定按鈕彈出如下對話框(註意,此時路徑已更改): 註意:如果想要設 ...
  • # 集合總結 ## 一、概述 1. 作用:存儲對象的容器,代替數組的,使用更加的便捷 2. 所處的位置:java.util 3. 體繫結構 ![image](https://img2023.cnblogs.com/blog/3245131/202309/3245131-202309071934421 ...
  • Vue 3 的Composition API + ``` ``` 這就把清單功能獨立出來,可在任意需要的地方復用。 基於組件去搭建應用,可實現對業務邏輯的復用。如有其他頁面也需要用到這功能,直接復用。 然後,就可基於新語法實現清單應用。 把之前的代碼移植過來後,使用ref包裹的響應式數據。修改tit ...
  • # Python名稱空間和作用域,閉包函數 - 名稱的查詢順序 - 名稱空間的作用域 - global和nonlocal關鍵字的使用 - 函數對象(函數名) - 函數的嵌套調用 - 函數的嵌套定義 - 閉包函數 ## 名稱空間 ### 定義 ```python # 什麼是名稱空間? 名稱空間即存放名 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...