1. 程式人生 > 其它 >原始碼學習之路----mybatis

原始碼學習之路----mybatis

在開始本文之前呢,我們首先需要了解一下傳統的jdbc存在的問題

傳統的jdbc的程式碼是這樣的:

程式碼 1

public class Test {

    public static final String URL = "jdbc:mysql://localhost:3306/test";
    public static final String USER = "root";
    public static final String PASSWORD = "root";

    public static void main(String[] args) throws Exception {
        
//1.載入驅動程式 Class.forName("com.mysql.jdbc.Driver"); //2. 獲得資料庫連線 Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); //3.操作資料庫,實現增刪改查 Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT user_name, age FROM imooc_goddess");
//如果有資料,rs.next()返回true while(rs.next()){ System.out.println(rs.getString("user_name")+" 年齡:"+rs.getInt("age")); } }

觀察如上程式碼發現 ,傳統的jdbc存在如下問題:

1.資料庫配置資訊存在硬編碼。 2.每次執行都要開啟資料庫連線,頻繁建立釋放資料庫連線。 3.sql語句存在硬編碼。 4.需要手動封裝返回結果集,較為繁瑣。 那麼就上述問題,我們給出如下的解決方案:
1)資料庫配置資訊,以及sql語句的編寫,我們都放入配置檔案中,由此以來,方便維護。 2)對於每次都要建立釋放資料庫連線,我們使用池技術來避免頻繁建立釋放資料庫連線帶來的資源浪費。 3)返回結果我們可以使用反射,內省的方式進行封裝。 接下來,我們開始手動封裝一個框架來解決傳統的jdbc問題 對於使用者來說,我們需要提供資料庫的配置資訊 和 sql配置資訊,所以,需要提供兩個檔案(使用xml來進行存放),sqlMapConfig.xml,mapper.xml來存放資料庫的配置資訊以及sql的配置資訊。 程式碼 2
sqlMapConfig.xml
<configuration>

    <!--資料庫配置資訊-->
    <dataSource>
        <property name="dirverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF-8"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </dataSource>

<!--    為了一次性將所有的資源全部讀入,此處我們用來存放mapper.xml全路徑-->
    <mapper resource="UserMapper.xml"></mapper>
</configuration>

程式碼 3

mapper.xml

說明:

  為了防止我們在呼叫方法時,不同類中方法名重複,我們設定 sql的唯一標識namespace.id =》statementId 來避免這個問題,
比如 UserMapper.class中與Order.class中都存在findAll方法,我們就要在userMapper.xml和OrderMapper.xml中都設定一下namespace屬性,
這樣 mapper標籤的namespace屬性加上select標籤的id屬性就可以唯一的確定一個sql語句了。
<mapper namespace="user">
<!--   為了防止我們在呼叫方法時,不同類中方法名重複,我們設定 sql的唯一標識namespace.id =》statementId 來避免這個問題 
比如 UserMapper.class中與Order.class中都存在findAll方法,我們就要在userMapper.xml和OrderMapper.xml中都設定一下namespace屬性,
這樣 mapper標籤的namespace屬性加上select標籤的id屬性就可以唯一的確定一個sql語句了
--> <select id="findAll" resultType="com.hg.pojo.User"> select * from user </select> <select id="findByCondition" resultType="com.hg.pojo.User" paramterType="com.hg.pojo.User"> select * from user where id = #{id} and username = #{username} </select> </mapper>

這樣,配置檔案就寫好了。

接下來,我們分析下框架中需要的類,以及實現方法:

首先,我們需要載入使用者的配置檔案:根據配置檔案的路徑載入配置檔案成位元組輸入流,儲存在記憶體中。

於是,我們建立一個Resource類來實現這個功能

我們建立一個maven工程,建立一個包com.xx.io來存放我們的Resource類,該類只有一個方法,就是讀取配置資訊成位元組流到記憶體中。

引入依賴

 <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.17</version>
        </dependency>
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>

程式碼 4

public class Resource {
    //根據配置檔案路徑,將配置檔案ji載入成位元組流,儲存在記憶體中
    public static InputStream getResourceAsStream(String path){
        InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }
}

然後我們需要建立兩個Bean來存放解析出來的資料庫配置資訊和sql配置資訊:

  Configuration:核心配置類:存放sqlMapConfig.xml解析出來的內容   MappedStatement:對映配置類:存放mapper.xml解析出來的內容 建立一個包com.xx.pojo來存放我們的Configuration類以及MappedStatement類 程式碼 5

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

public
class Configuration { private DataSource dataSource; public DataSource getDataSource() { return dataSource; } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Map<String, MappedStatement> getMappedStatementMap() { return mappedStatementMap; } public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) { this.mappedStatementMap = mappedStatementMap; } //key statementId value:封裝好的 mappedStatement Map<String,MappedStatement> mappedStatementMap = new HashMap<>(); }

public class   MappedStatement {
    //id標識
    private String id;
    // 返回值型別
    private String resultType;
    // 引數值型別
    private String paramterType;
    // sql語句
    private String sql;
public String getId() {
        return id;
    }

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

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }

    public String getParamterType() {
        return paramterType;
    }

    public void setParamterType(String paramterType) {
        this.paramterType = paramterType;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }

}

MappedStatement類中,我們定義了四個屬性,分別對應我們將來解析出來的mapper.xml檔案中的id、resultType、paramterType和sql。

Configuration類中,我們設定兩個屬性,分別是dataSource和mappedStatementMap ,dataSource屬性存放的是資料庫的配置資訊,mappedStatementMap存放的是sql的資訊,這裡可以看到,我們的mappedStatementMap是一個map型別,因為我們每個mapper.xml中都會存在多個sql語句,所以,我們在解析時,將會把我們前面提到的用來唯一定位一條sql語句的statementid來當做map的key,而解析出來的MappedStatement就是map的value。

至此,我們就把兩個配置檔案解析好後存放配置資訊的類都建立好了,接下來,我們來看一下,這個配置檔案是如何解析的呢?

我們需要建立類:SQLSessionFactoryBuilder類來解析Resource類讀取來的位元組流中的資訊 首先,我們在com.xx.sqlsession包下建立一個SqlSession介面,和一個SqlSessionFactory介面,在SqlSessionFactory介面中我們建立一個openSession方法用來建立SqlSession 程式碼 6
public interface SqlSession {
//    查詢所有
    public <E> List<E> selectList(String statementId,Object... params) throws Exception;
    // 根據條件查詢一條
    public <T> T selectOne(String statementId,Object... params) throws Exception;

    public int insert(String statementId,Object... params) throws Exception;

    public int delete(String statementId,Object... params) throws Exception;

    public int update(String statementId,Object... params) throws Exception;

    //為dao介面生成代理實現類
    public <T> T getMapper(Class<?> mapperClass);
}
public interface SqlSessionFactory {
    public SqlSession openSession();
}
接下來我們在com.xx.sqlsession包下建立SQLSessionFactoryBuilder類用來構建一個SqlSessionFactory: 程式碼 7
import com.hg.config.XMLConfigBuilder;
import com.hg.pojo.Configuration;
import org.dom4j.DocumentException;

import java.beans.PropertyVetoException;
import java.io.InputStream;

public class SqlSessionFactoryBuilder {
    public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
//        第一:使用dom4j解析配置檔案,將解析出來的內容封裝到Configuration中
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(in);


//        第二:建立sqlSessionFactory物件: 工廠類:生產sqlSession:會話物件
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);

        return defaultSqlSessionFactory;
    }
}
這個類主要有兩大作用: 第一:我們將使用dom4j解析配置檔案,將解析出來的內容封裝到容器物件中(Configuration,MappedStatement) 第二:建立SqlSessionFactory物件;生產sqlSession:會話物件(此處使用的是工廠模式) 接下來我們詳細介紹SqlSessionFactoryBuilder的具體實現 首先我們建立一個XMLConfigBuilder類來對sqlMapConfig.xml進行解析


程式碼 8


import com.hg.io.Resource;
import com.hg.pojo.Configuration;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;

public class XMLConfigBuilder {
    private Configuration configuration;

    public XMLConfigBuilder() {
        this.configuration = new Configuration();
    }

    //該方法是使用dom4j將配置檔案踐行解析,封裝Configuration
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(inputStream);
        //拿到 <configuration>
        Element rootElement = document.getRootElement();
        List<Element> list = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        for (Element element : list) {
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.setProperty(name,value);
        }

        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("dirverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));

        configuration.setDataSource(comboPooledDataSource);

//        mapper.xml解析  拿到路徑 獲取位元組輸入流 進行解析
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream resourceAsStream = Resource.getResourceAsStream(mapperPath);
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parse(resourceAsStream);
        }

        return configuration;
    }
}

這邊我們詳細講解一下這個類:

首先我們建立一個午餐的構造方法來初始化一個Configuration類用來儲存接下來解析的所有配置資訊。

然後是建立一個parseConfig方法來進行sqlMapConfig.xml的內容解析,這裡傳入的引數就是我們Resource類中獲取到的位元組流,然後通過dom4j的SAXReader開始對內容進行解析。 程式碼中的document就是獲取到的配置檔案內容,通過呼叫document.getRootElement()獲取到<configuration>中的所有內容,然後通過rootElement.selectNodes("//property");獲取到所有property標籤下的內容,我們建立一個Properties來儲存我們接下來解析出來的資料庫配置資訊,簡單的來講此處其實也可以用map來操作; 程式碼中迴圈遍歷property並通過element.attributeValue("name")和element.attributeValue("value")將property標籤中的內容取出存放到Properties中,這樣資料庫的配置資訊就全部都解析完成了。 接下來我們建立一個ComboPooledDataSource連線池,並將解析出來的資料庫配置資訊配置到連線池中,然後將連線池配置到我們的Configuration類的DataSource中。

上面我們提到為了一次性將所有的資源全部讀入我們將mapper.xml檔案的的全路徑也配置到了sqlMapConfig.xml中,接下來我們將進行mapper.xml 的解析

我們通過rootElement.selectNodes("//mapper");來獲取到配置資訊中的所有mapper標籤,上文程式碼中層提到,裡面包含著所有的mapper.xml 的路徑資訊(因為我們可能會有多個mapper檔案)。

接下來我們通過迴圈遍歷element.attributeValue("resource")獲取到每一個mapper.xml的路徑,並使用Resource類進行位元組流的讀取。

然後我們需要建立一個XMLMapperBuilder類來對每個mapper.xml進行處理。我們將Configuration作為引數傳遞進去是為了將解析後的mapper.xml資訊儲存到Configuration裡面的mappedStatementMap集合中。

然後我們呼叫XMLMapperBuilder的parse方法來處理讀取到的mapper.xml檔案位元組流。

好了,接下來我們開始研究下如何將mapper.xml檔案的資訊解析出來。

我們先建立XMLMapperBuilder類

程式碼 9

package com.hg.config;
import com.hg.pojo.Configuration;
import com.hg.pojo.MappedStatement;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.List;

public class XMLMapperBuilder {
    private Configuration configuration;
    public XMLMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }
    public void parse(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        List<Element> list = rootElement.selectNodes("//select");
        for (Element element : list) {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String paramterType = element.attributeValue("paramterType");
            String sqlText = element.getTextTrim();
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setParamterType(paramterType);
            mappedStatement.setResultType(resultType);
            mappedStatement.setSql(sqlText);
            String key = namespace+"."+id;
            configuration.getMappedStatementMap().put(key,mappedStatement);
        }
    }
}

上面解析sqlMapConfig.xml 的程式碼中我們曾提到,將 Configuration 作為引數傳遞給 XMLMapperBuilder ,那麼XMLMapperBuilder程式碼中,我們建立了一個有參的構造方法來進行引數的傳遞,接下來我們建立一個parse方法來進行mapper.xml檔案的解析,這裡我們接收到的引數是 XMLConfigBuilder 中傳遞過來的mapper.xml位元組流,同樣的使用dom4j來進行解析,重複的程式碼我不再贅述,這邊解析的是mapper.xml中所有select(insert,update,delete)標籤下的所有屬性,包括id,resultType,paramterType以及sql(後面我們還要在MappedStatement中加入一個標記屬性,來判斷我們這個MappedStatement是增,刪,改,查中的哪種);

上文我們提到要想準確的定位一個sql需要通過statementid(namespace.id)來進行唯一標示,所以這邊使用statementid作為mappedStatementMap的key,而獲取到的select標籤下的所有資訊封裝成MappedStatement作為mappedStatementMap的value,這樣這個parse方法就把mapper.xml中的配置資訊傳遞到了Configuration中的mappedStatementMap裡。最終XMLConfigBuilder中的parseConfig方法將所有配置資訊都讀取到了Configuration中並將Configuration返回。這樣上文中的 程式碼7SQLSessionFactoryBuilder類的第一部分就完成了,有了配置資訊,接下來,我們繼續探討如何來進行SQLSession的建立以及如何進行查詢,對查詢結果的解析以及封裝。

我們建立一個DefaultSqlSessionFactory來實現SqlSessionFactory介面

程式碼 10

public class DefaultSqlSessionFactory implements SqlSessionFactory{
    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }
}

此時,我們要建立一個建構函式來傳遞我們剛才解析出來的Configuration,然後我們再建立一個DefaultSqlSession類來實現SqlSession介面的方法,同使這個DefaultSqlSession類也要建立一個有參的構造方法用來傳遞我們的Configuration引數(上面我們分析過,這個Configuration引數很重要,資料庫的配置資訊以及sql的資訊全都儲存在這個Configuration中),然後我們在DefaultSqlSessionFactory類的openSession方法中建立這個DefaultSqlSession例項物件。

接下來我們一起研究一下這個DefaultSqlSession都完成了什麼功能:

程式碼 11

public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;

    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public <E> List<E> selectList(String statementId, Object... params) throws Exception {
//        將要完成對simpleExcutor裡的query方法
        SimpleExecutor simpleExecutor = new SimpleExecutor();
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        List<Object> list = simpleExecutor.query(configuration, mappedStatement, params);
        return (List<E>) list;
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws Exception {
        List<Object> objects = selectList(statementId, params);
        if(objects.size()==1){
            return (T) objects.get(0);
        }else{
            throw new RuntimeException("查詢結果為空或返回結果過多");
        }
    }

    @Override
    public int insert(String statementId, Object... params) throws Exception {
        return this.update(statementId,params);
    }

    @Override
    public int delete(String statementId, Object... params) throws Exception {
        return this.update(statementId,params);
    }


    @Override
    public int update(String statementId, Object... params) throws Exception {
        SimpleExecutor simpleExecutor = new SimpleExecutor();
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        int rows = simpleExecutor.update(configuration, mappedStatement, params);
        return rows;
    }



    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        //使用動態代理為DAO介面生成代理物件;並返回
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, (proxy, method, args) -> {
//                底層就是執行jdbc  根據不同情況,呼叫  selectList和selectOne
//                準備引數  1statementid  sql的唯一標識  namespace.id=介面許可權定名.方法名
            System.out.println(DefaultSqlSession.class.getClassLoader());
            System.out.println(mapperClass.getClassLoader());
            String methodName = method.getName();
            String className = method.getDeclaringClass().getName();
            String statementId = className+"."+methodName;
            MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
            SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
            switch (sqlCommandType){
                case SELECT:
                    //準備引數2:params:args
                    //獲取被呼叫方法的返回值型別
                    Type genericReturnType = method.getGenericReturnType();
                    //判斷是否進行了泛型型別引數化
                    if(genericReturnType instanceof ParameterizedType){
                        List<Object> objects = selectList(statementId, args);
                        return objects;
                    }
                    return selectOne(statementId,args);
                case DELETE:
                    return delete(statementId,args);
                case INSERT:
                    return insert(statementId,args);
                case UPDATE:
                    return update(statementId,args);
                default:
                    return null;
            }

        });
        return (T) proxyInstance;
    }
}

我們實現了SqlSession介面中的所有方法,接下來我們逐步介紹這個類:

可以看到,我們的增刪改查方法中都用到一個SimpleExecutor類,這個類就是用來實現我們的核心功能:

1.資料庫的連線

2.獲取sql語句並進行轉換(把我們寫的sql語句中的#{}替換成?)

3.獲取預處理物件

4.設定引數(將我們的傳值設定到預處理物件中)

5.執行sql

6.封裝返回結果(將查詢到的資料庫資料封裝成我們的pojo物件)

接下來我們將一步一步完成這些功能。

我們建立一個執行器介面Executor,這裡我們提供兩個方法,query和update,query我們用來查資料,update我們用來 增 刪 改 資料。

程式碼 12

public interface Executor {
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;

    public int update(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;

}

然後我們建立一個SimpleExecutor類來實現這個介面中的方法,後面我們將重點的分析一下查詢方法的實現

程式碼 13

public class SimpleExecutor implements Executor {
    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
        //1註冊驅動,獲取連線
        Connection connection = configuration.getDataSource().getConnection();
        //2獲取sql語句 轉換sql語句  #{} 轉換成 ? 轉換的過程對#{}的值解析出來並存儲
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        //3獲取預處理物件
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());

        //4設定引數
//        獲取到引數的全路徑
        String paramterType = mappedStatement.getParamterType();
        Class<?> paramterTypeClass = getClassType(paramterType);
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();
            //反射
            Field declaredField = paramterTypeClass.getDeclaredField(content);
//            設定暴力訪問
            declaredField.setAccessible(true);
            Object o = declaredField.get(params[0]);
            preparedStatement.setObject(i+1,o);

        }

        //5執行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = getClassType(resultType);


        ArrayList<Object> objects = new ArrayList<>();

        //6封裝返回結果集
        while(resultSet.next()){
            Object o = resultTypeClass.newInstance();
            //元資料
            ResultSetMetaData metaData = resultSet.getMetaData();
//            getColumnCount  獲取總列數  就是屬性的個數
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                //欄位名
                String columnName = metaData.getColumnName(i);
                //欄位的值
                Object value = resultSet.getObject(columnName);
                //使用反射或者內省,根據資料庫表和實體的對應關係,完成封裝
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultTypeClass);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(o,value);
            }
            objects.add(o);
        }

        return (List<E>) objects;
    }


    @Override
    public int update(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {

        //1註冊驅動,獲取連線
        Connection connection = configuration.getDataSource().getConnection();
        //2獲取sql語句 轉換sql語句  #{} 轉換成 ? 轉換的過程對#{}的值解析出來並存儲
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);
        //3獲取預處理物件
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());

        //4設定引數
//        獲取到引數的全路徑
        String paramterType = mappedStatement.getParamterType();
        Class<?> paramterTypeClass = getClassType(paramterType);
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();
            //反射
            Field declaredField = paramterTypeClass.getDeclaredField(content);
//            設定暴力訪問
            declaredField.setAccessible(true);
            Object o = declaredField.get(params[0]);
            preparedStatement.setObject(i+1,o);

        }

        //5執行sql
        int rows = preparedStatement.executeUpdate();
        return rows;
    }


    private Class<?> getClassType(String paramterType) throws ClassNotFoundException {
        if (paramterType!=null){
            Class<?> aClass = Class.forName(paramterType);
            return aClass;
        }
        return null;
    }

    //對#{}進行解析,將#{}使用?代替,解析#{}中的值進行儲存
    private BoundSql getBoundSql(String sql) {
        //標記處理類:配置標記解析器來完成對佔位符的解析處理工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        //標記解析器
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //用標記解析器解析出來的sql
        String parseSql = genericTokenParser.parse(sql);
//        #{}解析出來的引數名稱
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        BoundSql boundSql = new BoundSql(parseSql, parameterMappings);
        return boundSql;
    }
}

我們具體來分析下這個query方法,首先我們註冊驅動,獲取資料庫連線,然後,根據從MappedStatement中獲取sql語句,然後通過getBoundSql方法來解析我們的sql,實際上這一步就是來將我們sql中的#{}替換成?並將#{}中的屬性取出並存儲到parameterMappingList,也就是程式碼中getBoundSql方法的作用,該方法用到的工具類我後面會貼出來。

將sql處理好後,我們獲取預處理物件,然後開始設定引數,這裡我們將會用到反射。

首先我們獲取到入參的全路徑,比如com.xx.User,然後我們通過getClassType這個方法初始化這個類,然後我們迴圈遍歷parameterMappingList 取出裡面的每一個引數,並通過反射將我們傳遞進來的引數中的值取出來,並對應到?相應的位置。而後,我們執行查詢操作,獲取到查詢的結果。

既然獲取到結果,我們就要根據我們設定的resultType來對結果集進行封裝,我們同樣根據反射來取得我們resultType中的類,因為我們查詢的時候,可能是多條,可能是一條,所以我們建立一個Object集合來進行結果集的儲存和返回。

這裡我們通過while來遍歷結果集,每一次迴圈都是一條資料,所以我們沒迴圈一次結果集都要建立一個Object物件進行接收, 每遍歷一次結果集我們都要取出資料來源再進行每一列,也就是每個欄位的遍歷,此處我們for迴圈的 i 是從1開始的,因為我們在資料來源中取列名是從腳標1開始的,我們獲取到列名再通過列名獲取到列值,而後我們通過內省來將列的值賦值給我們建立的Object物件,每一次for迴圈就遍歷賦值一個列,這樣我們就將每個結果集的每一條資料都對映給了Object物件,最終生成一個集合最終返回給使用者。這樣,我們的Query方法就實現了。至此,傳統(後面我們還講分析一下傳統方法的不足,以及如何優化,程式碼中已經給出了優化的部分)的查詢方法就完成了,我們可以寫測試類進行測試。我將工具類貼上在下面,方便我們接下來的測試。

public class GenericTokenParser {

  private final String openToken; //開始標記
  private final String closeToken; //結束標記
  private final TokenHandler handler; //標記處理器

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  /**
   * 解析${}和#{}
   * @param text
   * @return
   * 該方法主要實現了配置檔案、指令碼等片段中佔位符的解析、處理工作,並返回最終需要的資料。
   * 其中,解析工作由該方法完成,處理工作是由處理器handler的handleToken()方法來實現
   */
  public String parse(String text) {
    // 驗證引數問題,如果是null,就返回空字串。
    if (text == null || text.isEmpty()) {
      return "";
    }

    // 下面繼續驗證是否包含開始標籤,如果不包含,預設不是佔位符,直接原樣返回即可,否則繼續執行。
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }

   // 把text轉成字元陣列src,並且定義預設偏移量offset=0、儲存最終需要返回字串的變數builder,
    // text變數中佔位符對應的變數名expression。判斷start是否大於-1(即text中是否存在openToken),如果存在就執行下面程式碼
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
     // 判斷如果開始標記前如果有轉義字元,就不作為openToken進行處理,否則繼續處理
      if (start > 0 && src[start - 1] == '\\') {
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        //重置expression變數,避免空指標或者老資料干擾。
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {////存在結束標記時
          if (end > offset && src[end - 1] == '\\') {//如果結束標記前面有轉義字元時
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {//不存在轉義字元,即需要作為引數進行處理
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //首先根據引數的key(即expression)進行引數處理,返回?作為佔位符
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}


public class ParameterMapping {

    private String content;

    public ParameterMapping(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}


public class ParameterMappingTokenHandler implements TokenHandler {
    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();

    // context是引數名稱 #{id} #{username}

    public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }

    private ParameterMapping buildParameterMapping(String content) {
        ParameterMapping parameterMapping = new ParameterMapping(content);
        return parameterMapping;
    }

    public List<ParameterMapping> getParameterMappings() {
        return parameterMappings;
    }

    public void setParameterMappings(List<ParameterMapping> parameterMappings) {
        this.parameterMappings = parameterMappings;
    }

}

public interface TokenHandler {
  String handleToken(String content);
}

整個工程結構如下

再次建立一個maven工程, 把我們建立的框架的工程install,然後引入到我們的測試工程中
 <dependency>
      <groupId>com.xx</groupId>
      <artifactId>框架工程名</artifactId>
      <version>1.0-SNAPSHOT</version>
 </dependency>

然後我們建立一個包com.xx.pojo來儲存我們的User類

public class User {
    private Integer id;

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

    public String getUsername() {
        return username;
    }

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

    private String username;

    public Integer getId() {
        return id;
    }

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

我們再建立一個 com.xx.dao包,然後在該包下建立UserDao介面以及他的實現類UserDaoImpl

public interface IUserDao {
//    查詢所有使用者
    public List<User> findAll() throws Exception;
//    根素條件進行查詢
    public User findByCondition(User user) throws Exception;

    public int save(User user) throws PropertyVetoException, DocumentException, Exception;

    public int delete(User user);

    public int update(User user);
}
public class UserDaoImpl implements IUserDao{
    @Override
    public List<User> findAll() throws Exception {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        List<User> users = sqlSession.selectList("user.selectList");
        for (User user : users) {
            System.out.println(user);
        }
        return users;
    }

    @Override
    public User findByCondition(User user) throws Exception {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user2 = sqlSession.selectOne("user.selectOne", user);
        System.out.println(user2);

        return user2;
    }

    @Override
    public int save(User user) throws Exception {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        int rows = sqlSession.insert("user.insert", user);
        return rows;
    }

    @Override
    public int delete(User user) {
        return 0;
    }

    @Override
    public int update(User user) {
        return 0;
    }
}

我們再建立一個com.xx.test包,在該包下建立Test類

public class Test {
    @Test
    public void test() throws Exception {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user = new User();
        user.setId(1);
        user.setUsername("lucy");
        User user2 = sqlSession.selectOne("com.hg.dao.IUserDao.findByCondition", user);
        System.out.println(user2);

//        List<User> users = sqlSession.selectList("user.selectList");
//        for (User user : users) {
//            System.out.println(user);
//        }

//        IUserDao userDao = sqlSession.getMapper(IUserDao.class);

//        List<User> all = userDao.findAll();
//        User user1 = userDao.findByCondition(user);
//        System.out.println(user1);
//        System.out.println(all);
    }

    @Test
    public void save() throws Exception {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user = new User();
        user.setId(5);
        user.setUsername("張三");
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        int rows = userDao.save(user);
        System.out.println(rows);
    }

    @Test
    public void delete() throws Exception {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        User user = new User();
        user.setId(3);
        int rows = userDao.delete(user);
        System.out.println(rows);
    }

    @Test
    public void update() throws Exception {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        User user = new User();
        user.setId(4);
        user.setUsername("李四");
        int rows = userDao.update(user);
        System.out.println(rows);
    }



}

我們還要將上面起初建立的兩個客戶端的配置檔案配置到該工程的resource下

整個工程目錄如下:

為了測試方便,我們的資料庫中的user表只建立id和username兩個欄位,

準備完畢,我們開始測試程式碼,因為該篇文章只是介紹了通過statementid的定位方式進行查詢。所以我們採用test方法進行測試,後面我還會介紹這個手寫框架的不足,以及給出解決方案(其實程式碼已經是優化過的程式碼了),有興趣的小夥伴可以研究一下,其實這個就是mybatis的基本雛形了。