一文全瞭解Mybatis

来源:https://www.cnblogs.com/jiuxialb/archive/2023/05/22/17420775.html
-Advertisement-
Play Games

## 初步瞭解 ### 總體架構設計 Mybatis 整體框架如下: ![img](https://zhangjiahao-blog.oss-cn-beijing.aliyuncs.com/picgo/202305161021323.png) ##### 介面層 MyBatis 和資料庫的交互有兩種 ...


初步瞭解

總體架構設計

Mybatis 整體框架如下:

img

介面層

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

  • 使用傳統的 MyBatis 提供的 API;
  • 使用 Mapper 介面;
使用傳統的 MyBatis 提供的 API

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

img

使用 Mapper 介面

MyBatis 將配置文件中的每一個<mapper> 節點抽象為一個 Mapper 介面,而這個介面中聲明的方法和跟<mapper> 節點中的<select|update|delete|insert> 節點項對應,即<select|update|delete|insert> 節點的 id 值為 Mapper 介面中的方法名稱,parameterType 值表示 Mapper 對應方法的入參類型,而 resultMap 值則對應了 Mapper 介面表示的返回值類型或者返回結果集的元素類型。

img

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

數據處理層

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

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

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

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

SQL 語句的執行以及封裝查詢結果集成List<E>

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

框架支撐層
  • 事務管理機制

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

  • 連接池管理機制

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

  • 緩存機制

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

  • SQL 語句的配置方式

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

引導層

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

主要構件及其相互關係

img

主要的核心部件解釋如下:

  • 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 對象之中。

流程簡解

準備

  1. /src/main/resources/mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 根標簽 -->
<configuration>
    <properties>
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url"
                  value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF8&amp;useSSL=false&amp;autoReconnect=true"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </properties>

    <!-- 環境,可以配置多個,default:指定採用哪個環境 -->
    <environments default="test">
        <environment id="test">
            <!-- 事務管理器,JDBC類型的事務管理器 -->
            <transactionManager type="JDBC"/>
            <!-- 數據源,池類型的數據源 -->
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/> <!-- 配置了properties,所以可以直接引用 -->
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="TeacherMapper.xml"/>
    </mappers>
</configuration>

  1. /src/main/resources/TeacherMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!-- mapper:根標簽,namespace:命名空間,隨便寫,一般保證命名空間唯一 -->
    <mapper namespace="TeacherMapper">
        <!-- statement,內容:sql語句。id:唯一標識,隨便寫,在同一個命名空間下保持唯一
           resultType:sql語句查詢結果集的封裝類型,tb_user即為資料庫中的表
         -->
        <select id="selectTest" resultType="org.apache.ibatis.test.Teacher">
            select *
            from teacher
            where id = #{id}
        </select>
    </mapper>
    
    
  2. /src/main/resources/log4j.properties

    log4j.rootLogger=DEBUG, stdout
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
    
  3. /src/main/java/org/apache/ibatis/test/Teacher.java

    package org.apache.ibatis.test;
    
    
    public class Teacher {
        private Long id;
        private String name;
    
        public Long getId() {
            return id;
        }
     
        public void setId(Long id) {
            this.id = id;
        }
     
        public String getName() {
            return name;
        }
     
        public void setName(String name) {
            this.name = name;
        }
     
        @Override
        public String toString() {
            return "Teacher{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    
    
  4. /src/main/java/org/apache/ibatis/test/Test.java

    package org.apache.ibatis.test;
    
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    public class Test {
        public static void main(String[] args) throws IOException {
            // 指定全局配置文件
            String resource = "mybatis-config.xml";
            // 讀取配置文件
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 構建sqlSessionFactory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 獲取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession();
            // 操作CRUD,第一個參數:指定statement,規則:命名空間+“.”+statementId
            // 第二個參數:指定傳入sql的參數:這裡是用戶id
            Teacher test = sqlSession.selectOne("TeacherMapper.selectTest", 1);
            System.out.println(test.getName());
        }
    }
    

整體流程

  1. 通過 mybatis-config.xml 進行初始化創建出 SqlSessionFactory。其內部是通過創建XMLConfigBuilder對象,然後自己進行解析 XML文件,把文件內容解析封裝為Configuration對象,最後由SqlSessionFactory進行封裝為SqlSessionFactory對象來完成SqlSessionFactory的創建。
  2. 通過SqlSessionFactory對象進行開啟一個SqlSession。其內部是通過獲取ConfigurationEnvironment數據進行構建TransactionFactory–>Executor,最後封裝為SqlSession返回進行使用
  3. 通過SqlSession執行 XML 中對應的方法。其中是先通過statementXML 指定的類#方法名稱獲取MappedStatement對象,然後提交給Executor進行執行。

目錄詳解

exception

下圖是Mybatis中異常的關係圖:

image-20230522142059506

背景知識

  • 工廠模式
  • 異常的封裝

講解

工廠模式

public class ExceptionFactory {

    private ExceptionFactory() {
        // Prevent Instantiation
    }

    public static RuntimeException wrapException(String message, Exception e) {
        return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
    }

}
  • 私有構造函數:導致該工廠無法創建出對應的對象
  • 靜態wrapException方法,用於通過異常信息和異常類型進行封裝異常為Mybatis中的異常類型。全局通過ExceptionFactory.wrapException()進行生產出對應的異常對象

異常類型

  • IbatisException:Mybatis中最高的異常,但是直接繼承該類的子類只有PersistenceException,而且該類也添加了@Deprecated說明以後可能去除。

  • PersistenceException: 譯為持久化異常。Mybatis對應是持久化框架,後期可能該異常類型為Mybatis所有異常的父類。

  • TooManyResultsException:譯為多條返回結果異常。用處為selectOne卻返回多條記錄時所拋出的異常。

  • TypeException: 譯為類型異常。當 Mybatis 中需要類型轉化時,若轉換失敗則會拋出該異常。

  • CacheException: 譯為緩存異常。當Mybatis讀取緩存中數據出現問題時則會拋出該異常。

  • ParsingException: 譯為解析異常。當前代碼未看到使用。

  • ScriptingException: 譯為腳本異常。

  • ResultMapException: 譯為結果映射異常。在結果轉換為對應類型的對象時,若轉換失敗則會拋出異常。

  • DataSourceException: 譯為數據源異常。在初始化數據源時若出現錯誤則會拋出該異常。

  • TransactionException: 譯為事務異常。在給connection開啟事務時若失敗則會拋出該異常。

  • BuilderException: 譯為建造異常。在建造對象失敗時會拋出該異常。

  • SqlSessionException: 譯為SqlSession的異常。基本只會在SqlSessionManager中使用,主要是SqlSession使用過程中的異常。

  • ReflectionException: 譯為反射異常。基本只會在反射使用時會拋出該異常。

  • ExecutorException: 譯為執行器異常。會線上程操作資料庫的時候拋出該異常。

  • BatchExecutorException:譯為批量執行器異常。會線上程批量操作資料庫的時候拋出該異常。

  • BindingException: 譯為綁定異常。主要是 mapper映射的時候會拋出該異常。

  • LogException: 譯為日誌異常。目前只在LogFactory構建日誌相關的時候才會拋出該異常。

  • PluginException: 譯為插件異常。目前只在Plugin中使用,在獲取插件信息時候會拋出該異常。

Mybatis類型主要是根據業務相關包放在一起,所以命名絕大多數都能夠直觀的看到原因所在。

jdbc

背景知識

  • 模版模式
  • 易用性

講解

模版模式

某些類通用的一些處理方法一致,但是處理對象可能存在不同,此時可以使用模版方法,抽取父類編寫通用處理方法,子類只需實現獲取對象的方法即可。

public class SQL extends AbstractSQL<SQL> {

    @Override
    public SQL getSelf() {
        return this;
    }

}
public abstract class AbstractSQL<T> {

    private static final String AND = ") \nAND (";
    private static final String OR = ") \nOR (";

    private final SQLStatement sql = new SQLStatement();

    public abstract T getSelf();

    public T UPDATE(String table) {
        sql().statementType = SQLStatement.StatementType.UPDATE;
        sql().tables.add(table);
        return getSelf();
    }

    public T SET(String sets) {
        sql().sets.add(sets);
        return getSelf();
    }
		......
}

以上代碼可知:

  1. 若用戶需要自定SQLExplainSQL,從而進行性能調優,此時只需要繼承 AbstractSQL即可,而無需編寫原方法。
易用性

為了用戶使用方便和構建 SQL的直觀性,AbstractSQL命名採用了全大寫的模式,以此讓用戶更加易用。

image-20230522152141832

SqlRunner
    public int insert(String sql, Object... args) throws SQLException {
        PreparedStatement ps;
        if (useGeneratedKeySupport) {
            ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        } else {
            ps = connection.prepareStatement(sql);
        }

        try {
            setParameters(ps, args);
            ps.executeUpdate();
            if (useGeneratedKeySupport) {
                List<Map<String, Object>> keys = getResults(ps.getGeneratedKeys());
                if (keys.size() == 1) {
                    Map<String, Object> key = keys.get(0);
                    Iterator<Object> i = key.values().iterator();
                    if (i.hasNext()) {
                        Object genkey = i.next();
                        if (genkey != null) {
                            try {
                                return Integer.parseInt(genkey.toString());
                            } catch (NumberFormatException e) {
                                //ignore, no numeric key support
                            }
                        }
                    }
                }
            }
            return NO_GENERATED_KEY;
        } finally {
            try {
                ps.close();
            } catch (SQLException e) {
                //ignore
            }
        }
    }

    public int update(String sql, Object... args) throws SQLException {
        PreparedStatement ps = connection.prepareStatement(sql);
        try {
            setParameters(ps, args);
            return ps.executeUpdate();
        } finally {
            try {
                ps.close();
            } catch (SQLException e) {
                //ignore
            }
        }
    }

此類在Mybatis中沒有任何的使用,此類應該只是為了提供給用戶,讓用戶可以自定義執行相關 SQL,分析其方法本質為原始JDBC相關操作流程。

ScriptRunner
   private void executeFullScript(Reader reader) {
        StringBuilder script = new StringBuilder();
        try {
            BufferedReader lineReader = new BufferedReader(reader);
            String line;
            while ((line = lineReader.readLine()) != null) {
                script.append(line);
                script.append(LINE_SEPARATOR);
            }
            String command = script.toString();
            println(command);
            executeStatement(command);
            commitConnection();
        } catch (Exception e) {
            String message = "Error executing: " + script + ".  Cause: " + e;
            printlnError(message);
            throw new RuntimeSqlException(message, e);
        }
    }

    private void executeLineByLine(Reader reader) {
        StringBuilder command = new StringBuilder();
        try {
            BufferedReader lineReader = new BufferedReader(reader);
            String line;
            while ((line = lineReader.readLine()) != null) {
                handleLine(command, line);
            }
            commitConnection();
            checkForMissingLineTerminator(command);
        } catch (Exception e) {
            String message = "Error executing: " + command + ".  Cause: " + e;
            printlnError(message);
            throw new RuntimeSqlException(message, e);
        }
    }
    private void handleLine(StringBuilder command, String line) throws SQLException {
        String trimmedLine = line.trim();
        if (lineIsComment(trimmedLine)) {
            Matcher matcher = DELIMITER_PATTERN.matcher(trimmedLine);
            if (matcher.find()) {
                delimiter = matcher.group(5);
            }
            println(trimmedLine);
        } else if (commandReadyToExecute(trimmedLine)) {
            command.append(line.substring(0, line.lastIndexOf(delimiter)));
            command.append(LINE_SEPARATOR);
            println(command);
            executeStatement(command.toString());
            command.setLength(0);
        } else if (trimmedLine.length() > 0) {
            command.append(line);
            command.append(LINE_SEPARATOR);
        }
    }

此類的核心方法如上,可看出其本質和SqlRunner類一致。

datasource

背景知識

講解

transaction

背景知識

講解

cursor

背景知識

講解

io

背景知識

講解

reflection

背景知識

講解

logging

背景知識

講解

scripting

背景知識

講解

type

背景知識

講解

mapping

背景知識

講解

builder

背景知識

講解

parsing

背景知識

講解

binding

背景知識

講解

annotationslang

背景知識

講解

cache

背景知識

講解

plugin

背景知識

講解

session

背景知識

講解

流程詳解

總結

生活在失去所有的希望和期盼,才能一直做出納什均衡解
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # 前言 Java 中的三元運算,平時也叫做三目運算,大家瞭解嗎?下麵就詳細介紹一下,以後在項目編程中用得到。 # 一、Java運算符 在最底層,Java 中的數據是通過使用運算符來操作的。運算符是一種特殊的符號,用來表示數據的運算、賦值和比較等等。每一種編程語言都有運算符,在 Java 中運算符可 ...
  • 視頻地址 # 1 Stream流概述 - 目的:簡化集合和數組操作的API,結合了Lambda表達式。 - Stream流式思想的核心: 1. 先得到集合或者數組的Stream流(就是一根傳送帶) 2. 把元素放上去 3. 用這個Stream流簡化的API來方便的操作元素 # 2 Stream流獲取 ...
  • ### 子類調用父類的方法的三種方式: - 父類名.方法名(self) - super(子類名,self).父類方法名() - super().父類方法名 註意:super()通過子類調用當前父類的方法,super預設會調用第一個父類的方法(適用於單繼承的多層繼承 如下代碼: ```python # ...
  • 話說兄弟們,女朋友生氣了都是怎麼哄的? 不會吧不會吧,不會有人還是單身狗吧! 算了,還是回到正題吧,再說我要挨打了~ 今天咱們來交流一下程式員是怎麼哄女朋友的,話不多說直接開始! 準備工作 1、環境 首先我們準備好環境和編輯器,我使用的是: Python 3.8 解釋器 Pycharm 編輯器 2、 ...
  • 雖然PDF文件適合用於列印和發佈,但不適合所有類型的文檔。例如,包含複雜圖表和圖形的文檔可能無法在PDF中呈現得很好。但是HTML文件可以在任何可運行瀏覽器的電腦上進行閱讀並顯示。並且HTML還具有占用伺服器資源較小,便於搜索引擎收錄的特點。那麼今天這篇文章就將展示如何通過Java應用程式將PDF ...
  • 你好!我是[@馬哥python說](https://www.zhihu.com/people/13273183132),一枚10年程式猿👨🏻‍💻,正在試錯用pyecharts開發可視化大屏的非常規排版。 以下,我用8種ThemeType展示的同一個可視化數據大屏。 **1、SHINE主題** ...
  • 前文我們說過了BIO,今天我們聊聊NIO。NIO 是什麼?NIO官方解釋它為New lO,由於其特性我們也稱之為,Non-Blocking IO。這是jdk1.4之後新增的一套IO標準。為什麼要用NIO呢?我們再簡單回顧下BIO:阻塞式IO,原理很簡單,其實就是多個端點與服務端進行通信時,每個客戶端 ...
  • # 前置需求 一臺電腦(我用的是Windows),有網 # 第一步:下載並安裝 去java官網下載開發版java(考慮到可能有小白,我暫且這麼說) java官網下載鏈接:https://www.oracle.com/java/technologies/downloads/ 寫隨筆時間為2023、05 ...
一周排行
    -Advertisement-
    Play Games
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...
  • 1. JUnit 最佳實踐指南 原文: https://howtodoinjava.com/best-practices/unit-testing-best-practices-junit-reference-guide/ 我假設您瞭解 JUnit 的基礎知識。 如果您沒有基礎知識,請首先閱讀(已針 ...