MyBatis簡單介紹
【1】MyBatis是一個持久層的ORM框架【Object Relational Mapping,物件關系映射】,使用簡單,學習成本較低,可以執行自己手寫的SQL陳述句,比較靈活,但是MyBatis的自動化程度不高,移植性也不高,有時從一個資料庫遷移到另外一個資料庫的時候需要自己修改配置,所以稱只為半自動ORM框架,
傳統JDBC介紹
【1】簡單使用
@Test public void test() throws SQLException { Connection conn=null; PreparedStatement pstmt=null; try { // 1.加載驅動,其實這一步可以不加因為DriverManager里面會有自動加載驅動的一步 Class.forName("com.mysql.jdbc.Driver"); // 2.創建連接 conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis_example", "root", "123456"); //開啟事務 conn.setAutoCommit(false); // SQL陳述句 String sql="select id,user_name,create_time from t_user where id=? "; // 獲得sql執行者 pstmt=conn.prepareStatement(sql); pstmt.setInt(1,1); // 執行查詢 //ResultSet rs= pstmt.executeQuery(); pstmt.execute(); ResultSet rs= pstmt.getResultSet(); rs.next(); User user =new User(); user.setId(rs.getLong("id")); user.setUserName(rs.getString("user_name")); user.setCreateTime(rs.getDate("create_time")); System.out.println(user.toString()); } catch (Exception e) { e.printStackTrace(); } finally{ // 關閉資源 try { if(conn!=null){ conn.close(); } if(pstmt!=null){ pstmt.close(); } } catch (SQLException e) { e.printStackTrace(); } } }
【1.1】DriverManager如何自動加載驅動【利用靜態代碼塊呼叫初始化方法,至于為什么要使用SPI機制,主要是為了實作解耦和可插拔,因為驅動有多種】
注:驅動展示【位于mysql-connector-java-5.1.22.jar/META-INF/services/java.sql.Driver 內容為:com.mysql.jdbc.Driver】
static { // 啟動類加載器加載DriverManager類,觸發靜態方法執行 loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // 加載java.sql.Driver驅動的實作 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // 1、創建一個 ServiceLoader物件,【這里就將背景關系類加載器設定到ServiceLoader物件的變數上了】 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 2、創建一個迭代器物件 Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ // 3、這里呼叫driversIterator.hasNext()的時候,觸發將 META-INF/services 下的組態檔中的資料讀取進來,方便下面的next方法使用 while(driversIterator.hasNext()) { // 4、【關鍵】:觸發上面創建的迭代器物件的方法呼叫,這里才是具體加載的實作邏輯,非常不好找 driversIterator.next(); } } catch(Throwable t) {} return null; } }); //判斷有沒有加載到 if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) {...} } } //ServiceLoader類#hasNext方法 public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // 1、拼湊要讀取的檔案的全名 // final String PREFIX = "META-INF/services/"; String fullName = PREFIX + service.getName(); // 2、根據 fullName 去到META-INF/services/目錄下尋找組態檔 // 如果類加載器為空,則使用系統類加載器,如果不為空則使用指定的類加載器 if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } // 3、使用parse方法決議組態檔中的每一行資料 pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; }
【2】當然正常情況一般是封裝成類來使用的,如
//資料訪問基類 public class BaseDao { // 驅動 private static String DRIVER = null; // 鏈接字串 private static String URL = null; // 用戶名 private static String USERNAME = null; // 密碼 private static String PASSWORD = null; //初始化 static { init(); } // 初始化 private static void init() { try { // 使用Properties物件讀取資源檔案屬性 Properties pro = new Properties(); // 獲得資源檔案輸入流 InputStream inStream = BaseDao.class.getClassLoader().getResourceAsStream("db.properties"); // 加載輸入流 pro.load(inStream); DRIVER = pro.getProperty("mysql.driverClass"); URL = pro.getProperty("mysql.jdbcUrl"); USERNAME = pro.getProperty("mysql.user"); PASSWORD = pro.getProperty("mysql.password"); Class.forName(DRIVER); } catch (Exception e) { e.printStackTrace(); } } //獲取資料庫連接物件 protected Connection getConnection() { Connection conn = null; try { conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return conn; } /** * 關閉所有鏈接 * @param conn * @param stmt * @param rs */ protected void CloseAll(Connection conn, Statement stmt, ResultSet rs) { try { if (conn != null) { conn.close(); } if (stmt != null) { stmt.close(); } if (rs != null) { rs.close(); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 執行 增、刪、 改的公共方法 * @param sql SQL陳述句 * @param prams 引數 * @return 受影響的行數 */ protected int executeUpdate(String sql, Object... prams) { // 獲得資料庫鏈接物件 Connection conn = getConnection(); // 宣告SQL執行者 PreparedStatement pstmt = null; try { // 獲得SQL執行者 pstmt = conn.prepareStatement(sql); // 回圈加載引數 for (int i = 0; i < prams.length; i++) { pstmt.setObject(i + 1, prams[i]); } // 執行executeUpdate 回傳受影響行數 return pstmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { // 關閉所有需要關閉的物件 CloseAll(conn, pstmt, null); } return 0; } /** * 執行查詢 回傳單個值 * @param sql SQL陳述句 * @param prams 引數 * @return OBJECT */ protected Object executeScaler(String sql, Object... prams) { // 獲得資料庫鏈接物件 Connection conn = getConnection(); // 宣告SQL執行者 PreparedStatement pstmt = null; // 宣告查詢結果集 ResultSet rs = null; // 接收單個值 Object value = https://www.cnblogs.com/chafry/p/null; try { // 獲得SQL執行者 pstmt = conn.prepareStatement(sql); // 回圈加載引數 for (int i = 0; i < prams.length; i++) { pstmt.setObject(i + 1, prams[i]); } // 執行executeUpdate 回傳受影響行數 rs = pstmt.executeQuery(); if (rs.next()) { value = rs.getObject(1); } } catch (SQLException e) { e.printStackTrace(); } finally { CloseAll(conn, pstmt, rs); } return value; } /** * 執行查詢回傳list * * @param sql SQL陳述句 * @param clazz 類的型別 * @return List */ public <T> List<T> executeList(String sql, Class<T> clazz, Object... prams) { // 資料集合 List<T> list = new ArrayList<T>(); // 獲得資料庫連接 Connection conn = getConnection(); // 宣告SQL執行者 PreparedStatement pstmt = null; // 宣告查詢結果集 ResultSet rs = null; try { // 3. 通過鏈接創建一個SQL執行者 pstmt = conn.prepareStatement(sql); // 回圈加載引數 for (int i = 0; i < prams.length; i++) { //內部會通過instance of判斷這個引數到底是哪個型別的具體物件 pstmt.setObject(i + 1, prams[i]); } // 4 執行查詢SQL 回傳查詢結果 rs = pstmt.executeQuery(); // 獲得結果集的列資訊物件 ResultSetMetaData rsmd = rs.getMetaData(); while (rs.next()) { // 通過類反射實體化 T obj = clazz.newInstance(); // 回圈所有的列 for (int i = 1; i <= rsmd.getColumnCount(); i++) { /* 通過屬性名稱使用反射給泛型實體賦值 Begin */ // 獲得每一列的列名 String cloName = rsmd.getColumnName(i); // 根據列名反射到類的欄位 Field filed = clazz.getDeclaredField(cloName); // 設定私有屬性的訪問權限 filed.setAccessible(true); // 給泛型實體的某一個屬性賦值 filed.set(obj, rs.getObject(cloName)); /* 通過屬性名稱使用反射給泛型實體賦值 End */ } // 將泛型實體添加到 泛型集合中 list.add(obj); } } catch (Exception e) { e.printStackTrace(); } return list; } /** * 執行查詢回傳JavaBean * * @param sql SQL陳述句 * @param clazz 類的型別 * @return JavaBean */ public <T> T executeJavaBean(String sql, Class<T> clazz, Object... prams) { // 宣告資料物件 T obj=null; // 獲得資料庫連接 Connection conn = getConnection(); // 宣告SQL執行者 PreparedStatement pstmt = null; // 宣告查詢結果集 ResultSet rs = null; try { // 3. 通過鏈接創建一個SQL執行者 pstmt = conn.prepareStatement(sql); // 回圈加載引數 for (int i = 0; i < prams.length; i++) { pstmt.setObject(i + 1, prams[i]); } // 4 執行查詢SQL 回傳查詢結果 rs = pstmt.executeQuery(); // 獲得結果集的列資訊物件 ResultSetMetaData rsmd = rs.getMetaData(); if (rs.next()) { // 通過類反射實體化 obj = clazz.newInstance(); // 回圈所有的列 for (int i = 1; i <= rsmd.getColumnCount(); i++) { /* 通過屬性名稱使用反射給泛型實體賦值 Begin */ // 獲得每一列的列名 String cloName = rsmd.getColumnName(i); // 根據列名反射到類的欄位 Field filed = clazz.getDeclaredField(cloName); // 設定私有屬性的訪問權限 filed.setAccessible(true); // 給泛型實體的某一個屬性賦值 filed.set(obj, rs.getObject(cloName)); /* 通過屬性名稱使用反射給泛型實體賦值 End */ } } } catch (Exception e) { e.printStackTrace(); } return obj; } }
【3】總結JDBC的四大核心物件
1)DriverManager(驅動管理物件):獲取資料庫連接;
2)Connection(資料庫連接物件):獲取執行sql物件;管理事務;
3)Statement(執行sql物件):executeUpdate執行DML陳述句(增刪改)DDL陳述句;executeQuery 執行DQL陳述句;
4)ResultSet(結果集物件):
【4】傳統JDBC的問題
1)資料庫連接創建,釋放頻繁造成西戎資源的浪費,從而影響系統性能,使用資料庫連接池可以解決問題,
2)sql陳述句在代碼中硬編碼,造成代碼的不已維護,實際應用中sql的變化可能較大,sql代碼和java代碼沒有分離開來維護不方便,
3)使用preparedStatement向有占位符傳遞引數存在硬編碼問題因為sql中的where子句的條件不確定,同樣是修改不方便,
4)對結果集中決議存在硬編碼問題,sql的變化導致決議代碼的變化,系統維護不方便,
【5】針對問題的優化
1、資料庫連接創建、釋放頻繁造成系統資源浪費從而影響系統性能,如果使用資料庫連接池可解決此問題,
優化部分,如mybatis:在SqlMapConfig.xml中配置資料連接池,使用連接池管理資料庫鏈接,
2、Sql陳述句寫在代碼中造成代碼不易維護,實際應用sql變化的可能較大,sql變動需要改變java代碼,
優化部分,如mybatis:將Sql陳述句配置在XXXXmapper.xml檔案中與java代碼分離,
3、向sql陳述句傳引數麻煩,因為sql陳述句的where條件不一定,可能多也可能少,占位符需要和引數一一對應,
優化部分,如mybatis:自動將java物件映射至sql陳述句,通過statement中的parameterType定義輸入引數的型別,
4、對結果集決議麻煩,sql變化導致決議代碼變化,且決議前需要遍歷,如果能將資料庫記錄封裝成pojo物件決議比較方便,
優化部分,如mybatis:自動將sql執行結果映射至java物件,通過statement中的resultType定義輸出結果的型別,
Mybaits整體體系圖
【1】圖示

【2】分析
把Mybatis的功能架構分為三層:
1)基礎支撐層:負責最基礎的功能支撐,包括連接管理、事務管理、配置加載和快取處理,這些都是共用的東西,將他們抽取出來作為最基礎的組件,為上層的資料處理層提供最基礎的支撐,
對應示例中的部分為: SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
2)API介面層:提供給外部使用的介面API,開發人員通過這些本地API來操縱資料庫,介面層一接收到呼叫請求就會呼叫資料處理層來完成具體的資料處理,
對應示例中的部分為: SqlSession session = sqlMapper.openSession();
3)資料處理層:負責具體的SQL查找、SQL決議、SQL執行和執行結果映射處理等,它主要的目的是根據呼叫的請求完成一次資料庫操作,
對應示例中的部分為:
User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1);
或者
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1L);
【3】簡單示例
public static void main(String[] args) { String resource = "mybatis-config.xml"; Reader reader; try { //將XML組態檔構建為Configuration配置類 reader = Resources.getResourceAsReader(resource); // 通過加載組態檔流構建一個SqlSessionFactory DefaultSqlSessionFactory SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); // 資料源 執行器 DefaultSqlSession SqlSession session = sqlMapper.openSession(); try { // 執行查詢 底層執行jdbc //User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1); UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.selectById(1L); session.commit(); System.out.println(user.getUserName()); } catch (Exception e) { e.printStackTrace(); }finally { session.close(); } } catch (IOException e) { e.printStackTrace(); } }
Mybaits插件的分析
【1】插件主要作用于四?組件物件
1)執?器 Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 等?法)
2)引數處理器 ParameterHandler (getParameterObject, setParameters 等?法)
3)結果集處理器 ResultSetHandler (handleResultSets, handleOutputParameters 等?法)
4)SQL語法構建處理器 StatementHandler (prepare, parameterize, batch, update, query 等?法)
原始碼分析部分
MyBatis決議全域組態檔的原始碼分析
【1】分析怎么通過加載組態檔流構建一個SqlSessionFactory
public SqlSessionFactory build(Reader reader) { return build(reader, null, null); } public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } // 到這里組態檔已經決議成了Configuration public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
【1.1】分析XMLConfigBuilder類怎么將xml的資源檔案流決議成Configuration:
【1.1.1】新建XMLConfigBuilder程序【所以說XMLConfigBuilder并不負責決議,決議的是它里面的XPathParser類,】
/** * 創建一個用于決議xml配置的構建器物件 * @param inputStream 傳入進來的xml的配置 * @param environment 我們的環境變數 * @param props:用于保存我們從xml中決議出來的屬性 */ public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { /** * 該方法做了二個事情 * 第一件事情:創建XPathParser 決議器物件,在這里會把我們的 * 把我們的mybatis-config.xml決議出一個Document物件 * 第二節事情:呼叫重寫的建構式來構建我XMLConfigBuilder */ this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); } private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { //呼叫父類的BaseBuilder的構造方法:給configuration賦值,typeAliasRegistry別名注冊器賦值,TypeHandlerRegistry賦值 super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); //把props系結到configuration的props屬性上 this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; } //又因為class XMLConfigBuilder extends BaseBuilder,所以看一下他的父類 public abstract class BaseBuilder { // mybatis的全域組態檔 protected final Configuration configuration; // 用于保存我們的Entity的別名 protected final TypeAliasRegistry typeAliasRegistry; // 用戶保存我們java型別和jdbc資料庫型別的 protected final TypeHandlerRegistry typeHandlerRegistry; public BaseBuilder(Configuration configuration) { this.configuration = configuration; this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); } .... }
【1.1.2】parser.parse()決議程序【本質上就是從根結點開始決議】
public Configuration parse() { //若已經決議過了 就拋出例外 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } //設定決議標志位,保證只決議一次 parsed = true; /** * 決議我們的mybatis-config.xml的節點 * <configuration> </configuration> */ parseConfiguration(parser.evalNode("/configuration")); return configuration; } //方法實作說明:決議我們mybatis-config.xml的 configuration節點 private void parseConfiguration(XNode root) { try { // 決議 properties節點 // 如:<properties resource="mybatis/db.properties" /> propertiesElement(root.evalNode("properties")); // 決議我們的mybatis-config.xml中的settings節點 // 如:<setting name="mapUnderscoreToCamelCase" value="https://www.cnblogs.com/chafry/p/false"/> Properties settings = settingsAsProperties(root.evalNode("settings")); // 基本沒有用過該屬性 // VFS含義是虛擬檔案系統;主要是通程序式能夠方便讀取本地檔案系統、FTP檔案系統等系統中的檔案資源, // Mybatis中提供了VFS這個配置,主要是通過該配置可以加載自定義的虛擬檔案系統應用程式 loadCustomVfs(settings); // 指定 MyBatis 所用日志的具體實作,未指定時將自動查找, // SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING // 決議到org.apache.ibatis.session.Configuration#logImpl loadCustomLogImpl(settings); // 決議別名typeAliases節點 typeAliasesElement(root.evalNode("typeAliases")); // 決議插件節點(比如分頁插件),決議到 interceptorChain.interceptors pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); // 設定settings 到 configuration 里面 settingsElement(settings); // 決議mybatis環境,決議到:org.apache.ibatis.session.Configuration#environment // 在集成spring情況下由 spring-mybatis提供資料源 和事務工廠 environmentsElement(root.evalNode("environments")); // 決議資料庫廠商 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 決議型別處理器節點 typeHandlerElement(root.evalNode("typeHandlers")); // 決議mapper節點(這個是最重要的) mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException(...); } }
【1.1.2.1】展示xml組態檔與Mapper檔案
<?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 掃描屬性檔案.properties --> <properties resource="db.properties"></properties> <!-- 具體可以配置哪些屬性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings --> <settings> <!-- 把資料庫欄位映射為駝峰式的欄位 --> <setting name="mapUnderscoreToCamelCase" value=https://www.cnblogs.com/chafry/p/"true"/> <!-- 內部的屬性可以配置日志,常用的日志是:LOG4J 、 STDOUT_LOGGING,用于列印執行的sql陳述句長什么樣 --> <setting name="logImpl" value=https://www.cnblogs.com/chafry/p/"LOG4J"/> <!-- <setting name="cacheEnabled" value=https://www.cnblogs.com/chafry/p/"true"/>--> <!-- <setting name="lazyLoadingEnabled" value=https://www.cnblogs.com/chafry/p/"true"/>--> <!-- <setting name="localCacheScope" value=https://www.cnblogs.com/chafry/p/"SESSION"/>--> <!-- <setting name="jdbcTypeForNull" value=https://www.cnblogs.com/chafry/p/"OTHER"/>--> </settings> <!-- 別名設定,常有單個設定【如com.project.entity.User,就是類的全路徑】,或者按路徑設定,本質上就是將其設定于 Configuration 或者 BaseBuilder中,可以用于查詢回傳結果的映射 --> <!-- <typeAliases>--> <!-- <typeAlias alias="User" type="com.project.entity.User"/>--> <!-- </typeAliases>--> <!-- <typeAliases>--> <!-- <package name="com.project.entity"/>--> <!-- </typeAliases>--> <!-- 插件設定 --> <!-- <plugins> <plugin interceptor="com.project.plugins.ExamplePlugin" ></plugin> </plugins>--> <!-- 型別處理器設定 --> <typeHandlers> <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/> </typeHandlers> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <!--// mybatis內置了JNDI、POOLED、UNPOOLED三種型別的資料源,其中POOLED對應的實作為org.apache.ibatis.datasource.pooled.PooledDataSource,它是mybatis自帶實作的一個同步、執行緒安全的資料庫連接池 一般在生產中,我們會使用c3p0或者druid連接池--> <dataSource type="POOLED"> <property name="driver" value=https://www.cnblogs.com/chafry/p/"${mysql.driverClass}"/> <property name="url" value=https://www.cnblogs.com/chafry/p/"${mysql.jdbcUrl}"/> <property name="username" value=https://www.cnblogs.com/chafry/p/"${mysql.user}"/> <property name="password" value=https://www.cnblogs.com/chafry/p/"${mysql.password}"/> </dataSource> </environment> </environments> <mappers> <!-- resource:注冊class類路徑下的--> <mapper resource="mybatis/mapper/EmployeeMapper.xml"/> <mapper class="com.project.mapper.DeptMapper"></mapper> <package name="com.mapper"/> </mappers> </configuration>
【1.1.2.2】部分內容分析:
【1.1.2.2.1】決議 properties節點
// 本質上這個分離方式便是為了解耦 // 大體上看來加載properties有兩種,一種是本地檔案,一種是遠程檔案 // 而且加載出來的資料其實是會放置于configuration屬性里面的,也就是XMLConfigBuilder類里面 private void propertiesElement(XNode context) throws Exception { if (context != null) { Properties defaults = context.getChildrenAsProperties(); String resource = context.getStringAttribute("resource"); String url = context.getStringAttribute("url"); if (resource != null && url != null) { throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); } if (resource != null) { defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { defaults.putAll(Resources.getUrlAsProperties(url)); } Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } parser.setVariables(defaults); configuration.setVariables(defaults); } } //下面便展示一下db.properties的常用內容 mysql.driverClass=com.mysql.jdbc.Driver mysql.jdbcUrl=jdbc:mysql://localhost:3306/mybatis_example?characterEncoding=utf8 mysql.user= root mysql.password= 123456
【1.1.2.2.2】決議我們的mybatis-config.xml中的settings節點
private Properties settingsAsProperties(XNode context) { if (context == null) { return new Properties(); } //分析配置的節點 Properties props = context.getChildrenAsProperties(); // 其實就是去configuration類里面拿到所有setter方法,看看有沒有當前的配置項 MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); for (Object key : props.keySet()) { if (!metaConfig.hasSetter(String.valueOf(key))) { throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); } } return props; }
【1.1.2.2.3】決議別名typeAliases節點
// 別名設定,別名設定最常用的就是對于entity【也就是物體類物件】 // 常常作用域Mapper的resultMap屬性,也就是查詢結果的映射,映射到對于的物體類上 // 由下面分析可以看出來是具備單個或者對包類路徑的掃描 private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { String typeAliasPackage = child.getStringAttribute("name"); configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else { String alias = child.getStringAttribute("alias"); String type = child.getStringAttribute("type"); try { Class<?> clazz = Resources.classForName(type); if (alias == null) { typeAliasRegistry.registerAlias(clazz); } else { typeAliasRegistry.registerAlias(alias, clazz); } } catch (ClassNotFoundException e) { throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); } } } } }
【1.1.2.2.4】決議插件節點(比較重要)
// 對于插件的決議,很明顯的有用到裝飾器模式的概念,其實都是封裝成了interceptor // 然后統一存放于configuration里面 private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } } } //實際上是封裝成了這個類,因為Interceptor是介面,是不具備實體化能力的 @Intercepts({}) public class ExamplePlugin implements Interceptor { private Properties properties; @Override public Object intercept(Invocation invocation) throws Throwable { return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { this.properties = properties; } public Properties getProperties() { return properties; } }
【1.1.2.2.5】決議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)) { TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } } //展示一下一般可以在xml里面怎么配置 <environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value=https://www.cnblogs.com/chafry/p/"${jdbc.driver}"/> <property name="url" value=https://www.cnblogs.com/chafry/p/"${jdbc.url}"/> <property name="username" value=https://www.cnblogs.com/chafry/p/"root"/> <property name="password" value=https://www.cnblogs.com/chafry/p/"Zw726515"/> </dataSource> </environment> <environment id="test"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value=https://www.cnblogs.com/chafry/p/"${jdbc.driver}"/> <property name="url" value=https://www.cnblogs.com/chafry/p/"${jdbc.url}"/> <property name="username" value=https://www.cnblogs.com/chafry/p/"root"/> <property name="password" value=https://www.cnblogs.com/chafry/p/"123456"/> </dataSource> </environment> </environments> //其實里面的JDBC和POOLED是怎么來的呢,都是源于別名那部分的,在Configuration類建構式里面,他就會為別名里面注冊一部分常用資料 public Configuration() { typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); typeAliasRegistry.registerAlias("FIFO", FifoCache.class); typeAliasRegistry.registerAlias("LRU", LruCache.class); typeAliasRegistry.registerAlias("SOFT", SoftCache.class); typeAliasRegistry.registerAlias("WEAK", WeakCache.class); typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class); typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); languageRegistry.register(RawLanguageDriver.class); }
【1.1.2.2.6】型別處理器決議
private void typeHandlerElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { // 根據包類路徑注冊 if ("package".equals(child.getName())) { String typeHandlerPackage = child.getStringAttribute("name"); typeHandlerRegistry.register(typeHandlerPackage); } else { //根據單個進行注冊,屬性有 handler(處理器) ,jdbcType(對應資料庫型別) ,javaType(對應java型別) String javaTypeName = child.getStringAttribute("javaType"); String jdbcTypeName = child.getStringAttribute("jdbcType"); String handlerTypeName = child.getStringAttribute("handler"); Class<?> javaTypeClass = resolveClass(javaTypeName); JdbcType jdbcType = resolveJdbcType(jdbcTypeName); Class<?> typeHandlerClass = resolveClass(handlerTypeName); if (javaTypeClass != null) { if (jdbcType == null) { typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { typeHandlerRegistry.register(typeHandlerClass); } } } } }
【1.1.2.2.7】最重要的mapper決議,明顯是有四種型別:
private void mapperElement(XNode parent) throws Exception { if (parent != null) { // 獲取我們mappers節點下的一個一個的mapper節點 for (XNode child : parent.getChildren()) { // 判斷我們mapper是不是通過批量注冊的,如<package name="com.project.mapper"></package> if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { // 判斷從classpath下讀取我們的mapper,如<mapper resource="mybatis/mapper/EmployeeMapper.xml"/> String resource = child.getStringAttribute("resource"); // 判斷是不是從我們的網路資源讀取(或者本地磁盤得),如<mapper url="D:/mapper/EmployeeMapper.xml"/> String url = child.getStringAttribute("url"); // 決議這種型別(要求介面和xml在同一個包下),如<mapper ></mapper> String mapperClass = child.getStringAttribute("class"); // 拿<mapper resource="mybatis/mapper/EmployeeMapper.xml"/>這種進行分析 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); // 把我們的檔案讀取出一個流 InputStream inputStream = Resources.getResourceAsStream(resource); // 創建讀取XmlMapper構建器物件,用于來決議我們的mapper.xml檔案 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // 真正的決議我們的mapper.xml組態檔(說白了就是來決議我們的sql) mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { // 這種從網路資源讀取,與上面的大體一致就不解釋了 ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { // 都不是則拋出例外 throw new BuilderException(...); } } } } }
【1.1.2.2.7.1】分析addMapper方法最后是怎么決議的
// MapperRegistry類的兩個屬性值 // private final Configuration config; // private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); //Configuration類#addMapper方法 // 傳入包路徑的方法 public void addMappers(String packageName) { mapperRegistry.addMappers(packageName); } public void addMappers(String packageName) { addMappers(packageName, Object.class); } //說白了也就是從包路徑下面拿到所有的類,然后呼叫傳入類的方法 public void addMappers(String packageName, Class<?> superType) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses(); for (Class<?> mapperClass : mapperSet) { // 呼叫的也是核心方法1 addMapper(mapperClass); } } // 傳入類的方法 public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); } // 核心方法1 // 方法實作說明:把我們的Mapper class保存到我們的knownMappers map 中 public <T> void addMapper(Class<T> type) { // 判斷我們傳入進來的type型別是不是介面 if (type.isInterface()) { // 判斷我們的快取中有沒有該型別 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // 創建一個MapperProxyFactory 把我們的Mapper介面保存到工廠類中 knownMappers.put(type, new MapperProxyFactory<>(type)); // mapper注解構造器 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 進行決議 parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
【1.1.2.2.7.2】分析parse()方法怎么決議,其實有兩種方式,一種是 XMLMapperBuilder類#parse方法,另一種是 MapperAnnotationBuilder類#parse方法
【1.1.2.2.7.2.1】分析XMLMapperBuilder類#parse方法
// XMLMapperBuilder類#parse方法 public void parse() { // 判斷當前的Mapper是否被加載過 if (!configuration.isResourceLoaded(resource)) { // 真正的決議我們的 <mapper namespace="com.project.mapper.EmployeeMapper"> configurationElement(parser.evalNode("/mapper")); // 把資源保存到我們Configuration中 configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } // 方法實作說明:決議我們的<mapper></mapper>節點 private void configurationElement(XNode context) { try { // 決議我們的namespace屬性 <mapper namespace="com.project.mapper.EmployeeMapper"> String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } // 保存我們當前的namespace 并且判斷介面完全類名==namespace builderAssistant.setCurrentNamespace(namespace); // 決議快取參考 // 示例說明:說明當前的快取參考和DeptMapper的快取參考一致,<cache-ref namespace="com.project.mapper.DeptMapper"></cache-ref> // 決議到org.apache.ibatis.session.Configuration#cacheRefMap<當前namespace,ref-namespace> // 例外下(參考快取未使用快取):org.apache.ibatis.session.Configuration#incompleteCacheRefs cacheRefElement(context.evalNode("cache-ref")); // 決議cache節點,如<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache> // 決議到:org.apache.ibatis.session.Configuration#caches // org.apache.ibatis.builder.MapperBuilderAssistant#currentCache cacheElement(context.evalNode("cache")); // 決議paramterMap節點(該節點mybaits3.5貌似不推薦使用了) parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 決議resultMap節點 // 決議到:org.apache.ibatis.session.Configuration#resultMaps // 例外 org.apache.ibatis.session.Configuration#incompleteResultMaps resultMapElements(context.evalNodes("/mapper/resultMap")); // 決議sql節點 // 決議到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments,其實等于 org.apache.ibatis.session.Configuration#sqlFragments,因為他們是同一參考,在構建XMLMapperBuilder 時把Configuration.getSqlFragments傳進去了 sqlElement(context.evalNodes("/mapper/sql")); // 決議我們的select | insert |update |delete節點,決議到org.apache.ibatis.session.Configuration#mappedStatements // 實際上便是將每個陳述句都構建成mappedStatement物件 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); } }
【1.1.2.2.7.2.1.1】MappedStatement類結構展示
public final class MappedStatement { private String resource;//mapper組態檔名,如:UserMapper.xml private Configuration configuration;//全域配置 private String id;//節點的id屬性加命名空間,如:com.lucky.mybatis.dao.UserMapper.selectByExample private Integer fetchSize; private Integer timeout;//超時時間 private StatementType statementType;//操作SQL的物件的型別 private ResultSetType resultSetType;//結果型別 private SqlSource sqlSource;//sql陳述句 private Cache cache;//快取 private ParameterMap parameterMap; private List<ResultMap> resultMaps; private boolean flushCacheRequired; private boolean useCache;//是否使用快取,默認為true private boolean resultOrdered;//結果是否排序 private SqlCommandType sqlCommandType;//sql陳述句的型別,如select、update、delete、insert private KeyGenerator keyGenerator; private String[] keyProperties; private String[] keyColumns; private boolean hasNestedResultMaps; private String databaseId;//資料庫ID private Log statementLog; private LanguageDriver lang; private String[] resultSets; }
【1.1.2.2.7.2.1.2】決議cache節點部分cacheElement方法決議
//這里面便是二級快取的產生 private void cacheElement(XNode context) { if (context != null) { //決議cache節點的type屬性 String type = context.getStringAttribute("type", "PERPETUAL"); //根據type的String獲取class型別 Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); // 獲取快取過期策略:默認是LRU, // LRU - 最近最少使用:移除最長時間不被使用的物件(默認) // FIFO - 先進先出:按物件進入快取的順序來移除他們 // SOFT - 軟參考:基于垃圾回收器狀態和軟參考規則移除物件 // WEAK - 弱參考:更積極的基于垃圾收集器狀態和弱參考規則移除物件 String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); //flushInterval(重繪間隔)屬性可以被設定為任意的正整數,設定的值應該是一個以毫秒為單位的合理時間量, 默認情況是不設定,也就是沒有重繪間隔,快取僅僅會在呼叫陳述句時重繪, Long flushInterval = context.getLongAttribute("flushInterval"); //size(參考數目)屬性可以被設定為任意正整數,要注意欲快取物件的大小和運行環境中可用的記憶體資源,默認值是 1024, Integer size = context.getIntAttribute("size"); //只讀)屬性可以被設定為 true 或 false,只讀的快取會給所有呼叫者回傳快取物件的相同實體, 因此這些物件不能被修改,這就提供了可觀的性能提升,而可讀寫的快取會(通過序列化)回傳快取物件的拷貝, 速度上會慢一些,但是更安全,因此默認值是 false boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); //把快取節點加入到Configuration中 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }
【1.1.2.2.7.2.1.2.0】分析快取是如何封裝的
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); configuration.addCache(cache); currentCache = cache; return cache; } public Cache build() { setDefaultImplementations(); //這里便是設定了PerpetualCache為最基礎層的cache Cache cache = newBaseCacheInstance(implementation, id); setCacheProperties(cache); //在這里利用decorators作為包裝器進行包裝 if (PerpetualCache.class.equals(cache.getClass())) { //便會存在LruCache【 delegate屬性-》PerpetualCache】,利用了反射 for (Class<? extends Cache> decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } //這里又會包裝別的 cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { cache = new LoggingCache(cache); } return cache; } private Cache setStandardDecorators(Cache cache) { try { MetaObject metaCache = SystemMetaObject.forObject(cache); if (size != null && metaCache.hasSetter("size")) { metaCache.setValue("size", size); } if (clearInterval != null) { cache = new ScheduledCache(cache);//ScheduledCache:調度快取,負責定時清空快取 ((ScheduledCache) cache).setClearInterval(clearInterval); } if (readWrite) { // 將LRU 裝飾到Serialized cache = new SerializedCache(cache); //SerializedCache:快取序列化和反序列化存盤 } cache = new LoggingCache(cache); //日志記錄層包裝 cache = new SynchronizedCache(cache); //執行緒同步層包裝 if (blocking) { //判斷是否有進行防穿透設定 cache = new BlockingCache(cache); } return cache; } catch (Exception e) { throw new CacheException("Error building standard cache decorators. Cause: " + e, e); } }
【1.1.2.2.7.2.1.2.1】對cache的分析:
//Cache本身只是一個介面,用于定義快取 public interface Cache { String getId(); void putObject(Object key, Object value); Object getObject(Object key); Object removeObject(Object key); void clear(); int getSize(); default ReadWriteLock getReadWriteLock() { return null; } } //重點在于它的實作類 //四種具備淘汰機制的cache LruCache:(最近最少使用)防溢位快取區 FifoCache:(先進先出) WeakCache:基于弱參考實作的快取管理策略 SoftCache:基于軟參考實作的快取管理策略 //底層的cache PerpetualCache:真正存盤快取的地方 //額外輔助功能的cache ScheduledCache:過期清理快取區 SynchronizedCache:執行緒同步快取區 LoggingCache:統計命中率以及列印日志 SerializedCache:快取序列化和反序列化存盤 BlockingCache:防穿透
【1.1.2.2.7.2.1.2.1.1】對cache的最常用的兩種淘汰策略分析:
【1.1.2.2.7.2.1.2.1.1.1】對LRU分析:【這個本質上就是利用LinkedHashMap】
public class LruCache implements Cache { private final Cache delegate; private Map<Object, Object> keyMap; private Object eldestKey; public LruCache(Cache delegate) { this.delegate = delegate; setSize(1024); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } public void setSize(final int size) { keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) { private static final long serialVersionUID = 4267176411845948333L; //當put進新值方法但會true時,便移除該map中最老的鍵和值 @Override protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) { boolean tooBig = size() > size; if (tooBig) { eldestKey = eldest.getKey(); } return tooBig; } }; } @Override public void putObject(Object key, Object value) { delegate.putObject(key, value); cycleKeyList(key); } @Override public Object getObject(Object key) { keyMap.get(key); //touch return delegate.getObject(key); } @Override public Object removeObject(Object key) { return delegate.removeObject(key); } @Override public void clear() { delegate.clear(); keyMap.clear(); } private void cycleKeyList(Object key) { keyMap.put(key, key); if (eldestKey != null) { delegate.removeObject(eldestKey); eldestKey = null; } } }
【1.1.2.2.7.2.1.2.1.1.2】對FIFO分析:【這個本質上就是利用LinkedList】
public class FifoCache implements Cache { private final Cache delegate; private final Deque<Object> keyList; private int size; public FifoCache(Cache delegate) { this.delegate = delegate; this.keyList = new LinkedList<>(); this.size = 1024; } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } public void setSize(int size) { this.size = size; } //可以看出都是移除頭部節點,將新的塞入尾結點 @Override public void putObject(Object key, Object value) { cycleKeyList(key); delegate.putObject(key, value); } @Override public Object getObject(Object key) { return delegate.getObject(key); } @Override public Object removeObject(Object key) { return delegate.removeObject(key); } @Override public void clear() { delegate.clear(); keyList.clear(); } //加到最后,移除最前 private void cycleKeyList(Object key) { keyList.addLast(key); if (keyList.size() > size) { Object oldestKey = keyList.removeFirst(); delegate.removeObject(oldestKey); } } }
【1.1.2.2.7.2.1.3】決議sql節點部分sqlElement方法決議:
private void buildStatementFromContext(List<XNode> list) { // 判斷有沒有配置資料庫廠商ID if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); } /** * 方法實作說明:決議select|update|delte|insert節點然后創建mapperStatment物件 * @param list:所有的select|update|delte|insert節點 * @param requiredDatabaseId:判斷有沒有資料庫廠商Id */ private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { // 回圈select|delte|insert|update節點 for (XNode context : list) { // 創建一個xmlStatement的構建器物件 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
【1.1.2.2.7.2.1.3.1】深入分析是怎么決議的:
public void parseStatementNode() { // insert|delte|update|select 陳述句的sqlId String id = context.getStringAttribute("id"); // 判斷insert|delte|update|select 節點是否配置了資料庫廠商標注 String databaseId = context.getStringAttribute("databaseId"); // 匹配當前的資料庫廠商id是否匹配當前資料源的廠商id if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } // 獲得節點名稱:select|insert|update|delete String nodeName = context.getNode().getNodeName(); // 根據nodeName 獲得 SqlCommandType列舉 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); // 判斷是不是select陳述句節點 boolean isSelect = sqlCommandType == SqlCommandType.SELECT; // 獲取flushCache屬性,默認值為isSelect的反值:查詢:默認flushCache=false,增刪改:默認flushCache=true boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); // 獲取useCache屬性,默認值為isSelect:查詢:默認useCache=true,增刪改:默認useCache=false boolean useCache = context.getBooleanAttribute("useCache", isSelect); // resultOrdered: 是否需要處理嵌套查詢結果 group by (使用極少),可以將比如 30條資料的三組資料 組成一個嵌套的查詢結果 boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // 決議sql公用片段,如 //<select id="qryEmployeeById" resultType="Employee" parameterType="int"> // <include refid="selectInfo"></include> // employee where id=#{id} //</select> //將 <include refid="selectInfo"></include> 決議成sql陳述句 放在<select>Node的子節點中 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // 決議我們sql節點的引數型別 String parameterType = context.getStringAttribute("parameterType"); // 把引數型別字串轉化為class Class<?> parameterTypeClass = resolveClass(parameterType); // 查看sql是否支撐自定義語言,如 // <delete id="delEmployeeById" parameterType="int" lang="tulingLang"> // <settings> // setting name="defaultScriptingLanguage" value="https://www.cnblogs.com/chafry/p/tulingLang"/> // </settings> String lang = context.getStringAttribute("lang"); // 獲取自定義sql腳本語言驅動 默認:class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver LanguageDriver langDriver = getLanguageDriver(lang); // Parse selectKey after includes and remove them. // 決議<insert 陳述句的的selectKey節點, 一般在oracle里面設定自增id processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) // insert陳述句 用于主鍵生成組件 KeyGenerator keyGenerator; /** * selectById!selectKey * id+!selectKey */ String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; //把命名空間拼接到keyStatementId中 , com.tuling.mapper.Employee.saveEmployee!selectKey keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); /** *<insert id="saveEmployee" parameterType="com.tuling.entity.Employee" useGeneratedKeys="true" keyProperty="id"> *判斷全域的配置類configuration中是否包含以及決議過的組件生成器物件 */ if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { /** * 若配置了useGeneratedKeys 那么就去除useGeneratedKeys的配置值, * 否者就看mybatis-config.xml組態檔中是配置了 * <setting name="useGeneratedKeys" value="https://www.cnblogs.com/chafry/p/true"></setting> 默認是false * 并且判斷sql操作型別是否為insert * 若是的話,那么使用的生成策略就是Jdbc3KeyGenerator.INSTANCE * 否則就是NoKeyGenerator.INSTANCE */ keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } // 通過class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver來決議我們的sql腳本物件. // 決議SqlNode. 注意:只是決議成一個個的SqlNode,并不會完全決議sql,因為這個時候引數都沒確定,動態sql無法決議 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); // STATEMENT,PREPARED 或 CALLABLE 中的一個, // 這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,默認值:PREPARED StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); // 這是一個給驅動的提示,嘗試讓驅動程式每次批量回傳的結果行數和這個設定值相等, 默認值為未設定(unset)(依賴驅動) Integer fetchSize = context.getIntAttribute("fetchSize"); // 這個設定是在拋出例外之前,驅動程式等待資料庫回傳請求結果的秒數,默認值為未設定(unset)(依賴驅動), Integer timeout = context.getIntAttribute("timeout"); // 將會傳入這條陳述句的引數類的完全限定名或別名,這個屬性是可選的,因為 MyBatis 可以通過型別處理器(TypeHandler) 推斷出具體傳入陳述句的引數,默認值為未設定 String parameterMap = context.getStringAttribute("parameterMap"); // 從這條陳述句中回傳的期望型別的類的完全限定名或別名, 注意如果回傳的是集合,那應該設定為集合包含的型別,而不是集合本身, // 可以使用 resultType 或 resultMap,但不能同時使用 String resultType = context.getStringAttribute("resultType"); // 決議我們查詢結果集回傳的型別 Class<?> resultTypeClass = resolveClass(resultType); // 外部 resultMap 的命名參考,結果集的映射是 MyBatis 最強大的特性,如果你對其理解透徹,許多復雜映射的情形都能迎刃而解, // 可以使用 resultMap 或 resultType,但不能同時使用, String resultMap = context.getStringAttribute("resultMap"); String resultSetType = context.getStringAttribute("resultSetType"); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); if (resultSetTypeEnum == null) { resultSetTypeEnum = configuration.getDefaultResultSetType(); } // 決議 keyProperty keyColumn 僅適用于 insert 和 update String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); String resultSets = context.getStringAttribute("resultSets"); // 為我們的insert|delete|update|select節點構建成我們的mappedStatment物件 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }
【1.1.2.2.7.2.1.3.1.1】深入分析SqlNode的生成:
@Override //方法實作說明:創建sqlSource物件 public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); } public SqlSource parseScriptNode() { MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource; if (isDynamic) { // 動態Sql源(需要引數才能確定的sql陳述句) // 如:select id,user_name,create_time from t_user where id=${param1} ,這種是拼接的 sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { // 靜態Sql源(不需要通過引數就能確定的sql陳述句),它會在這里決議 // 如:select id,user_name,create_time from t_user where id=#{param1} ,因為是會把 #{param1} 這部分用?替換 sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; } // 決議${} 和 動態節點 /** * 遞回決議 selectById這個sql元素會決議成 * 1層 MixedSqlNode <SELECT> * 2層 WhereSqlNode <WHERE> * 2層 IfSqlNode <IF> * test="條件運算式" * * contexts= sql陳述句分: 1.TextSqlNode 帶${} 2.StaticTextSqlNode */ protected MixedSqlNode parseDynamicTags(XNode node) { List<SqlNode> contents = new ArrayList<>(); NodeList children = node.getNode().getChildNodes(); //獲得<select>的子節點 for (int i = 0; i < children.getLength(); i++) { 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(""); // 獲得sql文本 TextSqlNode textSqlNode = new TextSqlNode(data); if (textSqlNode.isDynamic()) { // 怎樣算Dynamic? 其實就是判斷sql文本中有${} 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(); // 判斷當前節點是不是動態sql節點{@link XMLScriptBuilder#initNodeHandlerMap()} NodeHandler handler = nodeHandlerMap.get(nodeName); if (handler == null) { throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); } handler.handleNode(child, contents); // 不同動態節點有不用的實作 isDynamic = true; // 怎樣算Dynamic? 其實就是判斷sql文本動態sql節點 } } return new MixedSqlNode(contents); } // SqlNode的型別 // ChooseSqlNode // ForEachSqlNode // IfSqlNode // StaticTextSqlNode // TextSqlNode // MixedSqlNode:如果某階段還包含其他SqlNode節點,用這個進行包裝 // SetSqlNode // WhereSqlNode // TrimSqlNode //如示例: <select id="selectById" resultMap="result" > select id,user_name,create_time from t_user <where> <if test="id>0"> and id=#{id} </if> </where> </select> //劃分情況: MixedSqlNode StaticTextSqlNode WhereSqlNode MixedSqlNode IfSqlNode MixedSqlNode StaticTextSqlNode
【1.1.2.2.7.2.2】分析MapperAnnotationBuilder類#parse方法
// MapperAnnotationBuilder類#parse方法 public void parse() { String resource = type.toString(); // 是否已經決議mapper介面對應的xml if (!configuration.isResourceLoaded(resource)) { // 根據mapper介面名獲取 xml檔案并決議,決議<mapper></mapper>里面所有東西放到configuration loadXmlResource(); // 添加已決議的標記 configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); // 獲取所有方法 看是不是用了注解 Method[] methods = type.getMethods(); for (Method method : methods) { try { if (!method.isBridge()) { // 是不是用了注解 用了注解會將注解決議成MappedStatement parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }
引數
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/538506.html
標籤:Java
