一、持久層框架設(shè)計實(shí)現(xiàn)及MyBatis源碼分析-自定義持久層框架(一)

首先薪寓,帶上先帶上問題進(jìn)行思考亡资,既然JDBC已經(jīng)能完成與數(shù)據(jù)庫的交互,已經(jīng)能夠完成對數(shù)據(jù)庫的CRUD操作向叉,為什么后期還會出現(xiàn)Mybatis呢锥腻?
是不是意味著JDBC在與數(shù)據(jù)庫進(jìn)行交互時,本身還是存在一些問題母谎,正是因?yàn)榇嬖趩栴}瘦黑,所以后期出現(xiàn)了很多持久層框架,將JDBC對數(shù)據(jù)庫的操作進(jìn)行了封裝奇唤,在封裝過程中幸斥,對JDBC存在的問題進(jìn)行規(guī)避和解決,而Mybatis只是眾多持久層框架其中之一
接下來新建一個簡單的maven項(xiàng)目咬扇,對JDBC代碼進(jìn)行一個簡單的回顧甲葬,并分析JDBC在與數(shù)據(jù)庫進(jìn)行交互時究竟存在哪些問題,然后給出對應(yīng)問題的解決思路懈贺,并在此基礎(chǔ)上完成一個自定義持久層框架(為什么需要完成這樣一個自定義這樣一個持久層框架呢经窖,其實(shí)此框架就是Mybatis的一個雛形,對后期翻閱Mybatis源碼會大有幫助)

一梭灿、JDBC代碼回顧及問題分析

新建一個maven項(xiàng)目画侣,項(xiàng)目中的pom.xml文件內(nèi)容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>study.lagou.com</groupId>
    <artifactId>jdbc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>

    </dependencies>
</project>

User實(shí)體對象

package study.lagou.com.jdbc.pojo;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-26 0:18
 */
public class User {
    /**
     * 主鍵信息
     */
    private Integer id;
    /**
     * 用戶名稱
     */
    private String username;
    /**
     * 用戶密碼
     */
    private String password;
    /**
     * 用戶昵稱
     */
    private String nickname;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }
}

JDBC連接數(shù)據(jù)操作類

package study.lagou.com.jdbc;
import study.lagou.com.jdbc.pojo.User;
import java.sql.*;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-25 23:32
 */
public class JDBCTest {
    public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //加載數(shù)據(jù)庫驅(qū)動
            Class.forName("com.mysql.cj.jdbc.Driver");
            //通過驅(qū)動管理類獲取到一個connection數(shù)據(jù)庫連接
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC",
                    "root","111111");
            //編寫對應(yīng)的SQL語句,其中?表示參數(shù)的占位符
            String sql = "select * from user where username = ?";
            //通過connection和sql獲取到數(shù)據(jù)庫預(yù)處理對象PreparedStatement
            preparedStatement = connection.prepareStatement(sql);
           //借助數(shù)據(jù)庫預(yù)處理對象設(shè)置參數(shù)堡妒,第一個參數(shù)為SQL語句中參數(shù)的序號(從1開始)配乱,第二個參數(shù)為設(shè)置的參數(shù)值
            preparedStatement.setString(1,"zhangsan");
            //向數(shù)據(jù)庫發(fā)出SQL執(zhí)行查詢,查詢出結(jié)果集
            resultSet = preparedStatement.executeQuery();
            //遍歷查詢的結(jié)果集
            while (resultSet.next()){
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String password = resultSet.getString("password");
                String nickname = resultSet.getString("nickname");
                // 封裝User
                User user = new User();
                user.setId(id);
                user.setUsername(username);
                user.setPassword(password);
                user.setNickname(nickname);
                System.out.println(user);
            }
        } catch (Exception e){
           e.printStackTrace();
        } finally {
            //釋放資源
            if(resultSet != null){
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(preparedStatement != null){
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(connection != null){
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

JDBC 代碼基本回顧完成,接下來我們對代碼進(jìn)行問題分析

//加載數(shù)據(jù)庫驅(qū)動
Class.forName("com.mysql.cj.jdbc.Driver");
//通過驅(qū)動管理類獲取到一個connection數(shù)據(jù)庫連接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC","root","111111");

首先分析此段代碼搬泥,我們將數(shù)據(jù)庫的連接驅(qū)動和數(shù)據(jù)庫連接信息編寫在了JAVA代碼當(dāng)中桑寨,存在硬編碼問題,如果后期連接的數(shù)據(jù)庫發(fā)生改變佑钾,我們將要在JAVA源代碼中修改數(shù)據(jù)庫連接驅(qū)動和數(shù)據(jù)庫連接配置信息西疤,修改完成后我們必須重新編譯JAVA源文件,并再次打包部署休溶,整個過程非常麻煩

其次,這句代碼如果是在實(shí)際項(xiàng)目中使用扰她,我們需要將此段代碼寫到持久層方法當(dāng)中兽掰,如果該持久層框架被請求多次,則意味著這段代碼也會被執(zhí)行多次徒役,而每一次執(zhí)行我們都獲取到一個新的數(shù)據(jù)庫連接孽尽,然后去執(zhí)行SQL,最終再釋放這個連接忧勿,如此反復(fù)杉女,每一次請求都開啟一個新的連接,造成了數(shù)據(jù)庫連接資源的浪費(fèi)(數(shù)據(jù)庫連接可以算做是一個非常寶貴的資源鸳吸,在我們獲取數(shù)據(jù)庫連接的時候熏挎,底層需要先去建立TCP連接,完成三次握手晌砾,整個過程比較耗費(fèi)資源坎拐,影響性能)

針對以上代碼,我們總結(jié)出兩個問題
1养匈、數(shù)據(jù)庫配置信息存在硬編碼問題
2哼勇、頻繁創(chuàng)建和釋放數(shù)據(jù)庫連接

            //編寫對應(yīng)的SQL語句,其中?表示參數(shù)的點(diǎn)位符
            String sql = "select * from user where username = ?";
            //通過connection獲取到數(shù)據(jù)庫預(yù)處理對象PreparedStatement
            preparedStatement = connection.prepareStatement(sql);
            //借助數(shù)據(jù)庫預(yù)處理對象設(shè)置參數(shù)呕乎,第一個參數(shù)為SQL語句中參數(shù)的序號(從1開始)积担,第二個參數(shù)為設(shè)置的參數(shù)值
            preparedStatement.setString(1,"zhangsan");
            //向數(shù)據(jù)庫發(fā)出SQL執(zhí)行查詢,查詢出結(jié)果集
            resultSet = preparedStatement.executeQuery();
            //遍歷查詢的結(jié)果集
            while (resultSet.next()){
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String password = resultSet.getString("password");
                String nickname = resultSet.getString("nickname");
                // 封裝User
                User user = new User();
                user.setId(id);
                user.setUsername(username);
                user.setPassword(password);
                user.setNickname(nickname);
                System.out.println(user);
            }

再分析上一段代碼猬仁,所存在的問題是
3帝璧、SQL語句、設(shè)置參數(shù)逐虚、獲取結(jié)果集參數(shù)均存在硬編碼問題

            //遍歷查詢的結(jié)果集
            while (resultSet.next()){
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String password = resultSet.getString("password");
                String nickname = resultSet.getString("nickname");
                // 封裝User
                User user = new User();
                user.setId(id);
                user.setUsername(username);
                user.setPassword(password);
                user.setNickname(nickname);
                System.out.println(user);
            }

最后看封裝返回結(jié)果集對象這段聋溜,現(xiàn)在看到的是User對象屬性值比較少的情況,但是如果試想叭爱,如果User對象存在幾十撮躁、上百個屬性值的時候,則在此處設(shè)置對象屬性值的過程將變得相當(dāng)繁瑣买雾,所以此段代碼我們分析出的問題就是
4把曼、需要手動封裝返回結(jié)果集杨帽,較為繁瑣

通過對JDBC連接代碼塊進(jìn)行分析,JDBC連接操作數(shù)據(jù)庫時主要存在以下問題:
1嗤军、 頻繁創(chuàng)建注盈、釋放數(shù)據(jù)庫連接,造成系統(tǒng)資源浪費(fèi)叙赚,從?影響系統(tǒng)性能老客。
2、 sql語句在代碼中硬編碼震叮,造成代碼不易維護(hù)胧砰,實(shí)際應(yīng)?中sql變化的可能較大,sql變動需要改變java代碼苇瓣。
3尉间、 使?preparedStatement向占位符號傳參數(shù)存在硬編碼,因?yàn)閟ql語句的where條件不?定击罪,可能多也可能少哲嘲,修改sql還要修改代碼,系統(tǒng)不易維護(hù)媳禁。
4眠副、 對結(jié)果集解析存在硬編碼(查詢列名),sql變化導(dǎo)致解析代碼變化损话,系統(tǒng)不易維護(hù)侦啸,如果能將數(shù)據(jù)庫記錄封裝成pojo對象解析比較方便

二、針對分析出的問題丧枪,給出問題解決思路

1光涂、數(shù)據(jù)庫配置信息存在硬編碼問題
看到硬編碼,我們?nèi)菀茁?lián)想到配置文件拧烦,此處給出的解決方案也是通過配置文件解決硬編碼問題

2忘闻、頻繁創(chuàng)建和釋放數(shù)據(jù)庫連接
此處問題我們可以通過數(shù)據(jù)庫連接池來進(jìn)行解決

3、SQL語句恋博、設(shè)置參數(shù)齐佳、獲取結(jié)果集參數(shù)均存在硬編碼問題
同樣通過配置文件來處理,但是此處配置文件建議和問題1中的配置文件分開來處理债沮,因?yàn)閱栴}1中的配置文件炼吴,保存的是數(shù)據(jù)庫連接的基本信息,相對來說內(nèi)容比較固定疫衩,不容易發(fā)生改變硅蹦,而問題3中所使用的配置文件,主要是用來存儲SQL語句、參數(shù)童芹、返回結(jié)果集等信息涮瞻,比較容易發(fā)生改變,所以建議和問題1中的配置文件分開

4假褪、需要手動封裝返回結(jié)果集署咽,較為繁瑣
針對這個問題,我們可以通過使用反射生音、內(nèi)省等技術(shù)去完成查詢出來的結(jié)果集與實(shí)體中屬性的自動轉(zhuǎn)換封裝

三宁否、自定義框架設(shè)計

前面進(jìn)行了JDBC代碼回顧以及問題分析,并給出了解決問題的基本思路缀遍,接下來我們設(shè)置一個自定義的持久層框架(注意:當(dāng)前我們自定義的持久層框架家淤,它的本質(zhì)是對JDBC代碼的一個封裝,只不過在封裝的過程當(dāng)中瑟由,我們需要對JDBC代碼存在的問題進(jìn)行規(guī)避或解決)

1、自定義持久層框架的設(shè)計思路

自定義持久層框架思路.png

自定義的框架分為使用端自定義持久層框架本身兩部分

1.1冤寿、使用端

使用端這個項(xiàng)目當(dāng)中我們將會使用自定義持久層框架來完成對數(shù)據(jù)庫的交互(對數(shù)據(jù)庫表的CRUD操作)歹苦,在使用端需要對自定義持久層框架進(jìn)行調(diào)用,那么使用端項(xiàng)目需要引入自定義持久層框架的jar包(自定義持久層框架也是一個項(xiàng)目督怜,使用端需要調(diào)用自定義持久層框架中的方法殴瘦,則需要引入對應(yīng)的jar包)

一段JDBC代碼,想要正常執(zhí)行号杠,那么必不可少的是兩部分信息蚪腋,一個是數(shù)據(jù)連接配置信息,一個是SQL配置信息姨蟋,所以使用端需要提供這兩部分配置信息屉凯,其中SQL配置信息包括SQL語句、參數(shù)類型以及返回值類型眼溶,因?yàn)槲覀円獙DBC硬編碼的問題進(jìn)行解決悠砚,所以我們通過配置文件來提供這兩部分信息
(1)、sqlMapConfig.xml:存放數(shù)據(jù)庫配置信息堂飞,存放mapper.xml的全路徑
(2)灌旧、mapper.xml:存放SQL配置信息

1.2、持久層框架本身

持久層框架本身也是一個項(xiàng)目绰筛,項(xiàng)目主要實(shí)現(xiàn)的功能就是對JDBC代碼進(jìn)行封裝枢泰,這就意味著當(dāng)使用端對自定義持久層框架進(jìn)行調(diào)用時,底層執(zhí)行的還是JDBC代碼铝噩,因?yàn)榕渲梦募嫒朐谑褂枚说呐渲梦募?dāng)中衡蚂,所以自定義持久層框架需要做的事情就是通過路徑讀取到對應(yīng)的配置文件,讀取到對應(yīng)配置文件的內(nèi)容,那么就可以調(diào)用底層的JDBC代碼進(jìn)行對數(shù)據(jù)庫的操作了(這里就是整個自定義持久層框架的一個基本思路)

有了自定義持久層框架的基本思路讳窟,那么接下來我們對具體的操作進(jìn)行細(xì)化
(1)让歼、加載配置文件(根據(jù)配置文件的路徑,將sqlMapConfig.xml和mapper.xml配置文件加載成字節(jié)輸入流丽啡,以流的形式存放在內(nèi)存當(dāng)中)
具體的做法就是創(chuàng)建一個Resources類谋右,在這個類中有一個方法,InputStream in = getResourceAsStream(String path)补箍,并且這個方法參見要傳遞一個參數(shù)path改执,這個path就是sqlMapConfig.xml和mapper.xml配置文件所存放的路徑(這里引發(fā)一個問題思考,我們在使用端有兩個配置文件坑雅,那么自定義持久層框架也要對配置文件加載2次呢辈挂?答案是可以加載2次,但是不建議加載2次裹粤,我們可以在sqlMapConfig.xml文件中引入mapper.xml文件的全路徑终蒂,在讀取sqlMapConfig.xml這個配置文件的時候,就可以將mapper.xml文件的全路徑也讀取出來遥诉,拿到文件路徑了拇泣,就方便對文件進(jìn)行操作了)

(2)、創(chuàng)建兩個javaBean(由于我們從配置文件加載出來的配置文件矮锈,是以流的形式存放到內(nèi)存當(dāng)中的霉翔,不方便進(jìn)行操作,所以我們需要創(chuàng)建兩個容器對象苞笨,將解析出來的配置文件的內(nèi)容存放到這兩個容器對象中债朵,方便操作)
Configuration:核心配置類:存放sqlMapConfig.xml解析出來的內(nèi)容
MappedStatement:映射配置類:存放mapper.xml解析出來的內(nèi)容

(3)、解析配置文件(使用dom4j來對配置文件進(jìn)行解析)
創(chuàng)建一個SqlSessionFactoryBuilder類瀑凝,該類中有一個build(Inputstream in)方法序芦,在build方法中主要完成兩件事
第一:使用dom4j解析配置文件,將解析出來的內(nèi)容封裝到容器對象中
第二:創(chuàng)建SqlSessionFactory工廠對象猜丹,主要用來生產(chǎn)sqlSession會話對象芝加,此處理用到工廠設(shè)計模式(工廠設(shè)計模式可以降低代碼間耦合度,并根據(jù)需求生產(chǎn)出不同狀態(tài)類型的對象)

(4)射窒、創(chuàng)建SqlSessionFactory接口及實(shí)現(xiàn)類DefaultSqlSessionFactory
接口中定義openSession()方法藏杖,該方法的主要作用是生產(chǎn)sqlSession

(5)、創(chuàng)建SqlSession接口及實(shí)現(xiàn)類DefaultSqlSession
定義對數(shù)據(jù)庫的CRUD操作脉顿,定義selectList()蝌麸、selectOne()、update()艾疟、delete()等方法来吩,在這些方法的實(shí)現(xiàn)當(dāng)中可以直接通過操作JDBC代碼來實(shí)現(xiàn)對數(shù)據(jù)庫的操作敢辩,但是由于存在大量的初始化JDBC配置的重復(fù)操作,所以衍生出后續(xù)一步的優(yōu)化

(6)弟疆、創(chuàng)建Executor接口及實(shí)現(xiàn)類SimpleExecutor實(shí)現(xiàn)類
定義一個query(Configuration configuration戚长,MappedStatement mappedStatement,Object... params)方法怠苔,該方法執(zhí)行的就是JDBC代碼同廉,由于執(zhí)行JDBC代碼需要對應(yīng)的配置文件,所以將兩個配置bean傳入進(jìn)來柑司,最后一個參數(shù)表示查詢時需要拼接的參數(shù)迫肖,由于不知道具體的參數(shù)個數(shù),所以直采用可變參數(shù)的寫法來處理

具體代碼對應(yīng)下載地址:https://gitee.com/happymima/mybatis.git

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末攒驰,一起剝皮案震驚了整個濱河市蟆湖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌玻粪,老刑警劉巖隅津,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異劲室,居然都是意外死亡饥瓷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門痹籍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人晦鞋,你說我怎么就攤上這事蹲缠。” “怎么了悠垛?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵线定,是天一觀的道長。 經(jīng)常有香客問我确买,道長斤讥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任湾趾,我火速辦了婚禮芭商,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘搀缠。我一直安慰自己铛楣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布艺普。 她就那樣靜靜地躺著簸州,像睡著了一般鉴竭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上岸浑,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天搏存,我揣著相機(jī)與錄音,去河邊找鬼矢洲。 笑死璧眠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的兵钮。 我是一名探鬼主播蛆橡,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼掘譬!你這毒婦竟也來了泰演?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤葱轩,失蹤者是張志新(化名)和其女友劉穎睦焕,沒想到半個月后汇在,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體劣光,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年嫡良,在試婚紗的時候發(fā)現(xiàn)自己被綠了袜炕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片本谜。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖偎窘,靈堂內(nèi)的尸體忽然破棺而出乌助,到底是詐尸還是另有隱情,我是刑警寧澤陌知,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布他托,位于F島的核電站,受9級特大地震影響仆葡,放射性物質(zhì)發(fā)生泄漏赏参。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一沿盅、第九天 我趴在偏房一處隱蔽的房頂上張望把篓。 院中可真熱鬧,春花似錦腰涧、人聲如沸纸俭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽揍很。三九已至郎楼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窒悔,已是汗流浹背呜袁。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留简珠,地道東北人阶界。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像聋庵,于是被迫代替她去往敵國和親膘融。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345