主頁 > 後端開發 > 手寫 Mybatis,“整整” Mybatis原始碼

手寫 Mybatis,“整整” Mybatis原始碼

2020-10-31 13:31:41 後端開發

前言

前兩天寫了一個 手寫Spring ioc 框架,“擼擼”Spring 原始碼我們今天來整整Mybatis,

mybaits 在 ORM 框架中,可算是半壁江山了,由于它是輕量級,半自動加載,靈活性和易拓展性,深受廣大公司的喜愛,所以我們程式開發也離不開 mybatis ,

很多朋友對 mybatis 原始碼沒什么了解,或者想看但是不知道怎么看的苦惱嗎?

歸根結底,我們還是需要知道為什么會有 mybatis ,mybatis 解決了什么問題? 想要知道 mybatis 解決了什么問題,就要知道傳統的 JDBC 操作存在哪些痛點才促使 mybatis 的誕生, 我們帶著這些疑問,再來一步步學習吧,

內容稍微有點長!耐心閱讀!

另外本人整理收藏了20年多家公司面試知識點整理 ,以及各種Java核心知識點免費分享給大家,下方只是部分截圖
想要資料的話也可以點擊直接進入:暗號:csdn,免費獲取,

在這里插入圖片描述

傳統JDBC的弊端

所以我們先來來看下原始 JDBC 的操作

我們知道最原始的資料庫操作,分為以下幾步

  1. 獲取 connection 連接
  2. 獲取 preparedStatement
  3. 引數替代占位符
  4. 獲取執行結果 resultSet
  5. 決議封裝 resultSet 到物件中回傳,

如下是原始 JDBC 的查詢代碼,存在哪些問題?

public static void main(String[] args) {
        String dirver="com.mysql.jdbc.Driver";
        String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8";
        String userName="root";
        String password="123456";

        Connection connection=null;
        List<User> userList=new ArrayList<>();
        try {
            Class.forName(dirver);
            connection= DriverManager.getConnection(url,userName,password);

            String sql="select * from user where username=?";
            PreparedStatement preparedStatement=connection.prepareStatement(sql);
            preparedStatement.setString(1,"張三");
            System.out.println(sql);
            ResultSet resultSet=preparedStatement.executeQuery();

            User user=null;
            while(resultSet.next()){
                user=new User();
                user.setId(resultSet.getInt("id"));
                user.setUsername(resultSet.getString("username"));
                user.setPassword(resultSet.getString("password"));
                userList.add(user);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (!userList.isEmpty()) {
            for (User user : userList) {
                System.out.println(user.toString());
            }
        }

    }

大家是不是發現了上面有哪些不友好的地方?

我這里總結了以下幾點:

  1. 資料庫的連接資訊存在硬編碼,即是寫死在代碼中的,
  2. 每次操作都會建立和釋放 connection 連接,操作資源的不必要的浪費,
  3. sql 和引數存在硬編碼,
  4. 將回傳結果集封裝成物體類麻煩,要創建不同的物體類,并通過 set 方法一個個的注入,

存在上面的問題,所以 mybatis 就對上述問題進行了改進, 對于硬編碼,我們很容易就想到組態檔來解決,mybatis 也是這么解決的, 對于資源浪費,我們想到使用連接池,mybatis 也是這個解決的, 對于封裝結果集麻煩,我們想到是用 JDK 的反射機制,好巧,mybatis 也是這么解決的,

設計思路

既然如此,我們就來寫一個自定義持久層框架,來解決上述問題,當然是參照 mybatis 的設計思路,這樣我們在寫完之后,再來看 mybatis 的原始碼就恍然大悟,這個地方這樣配置原來是因為這樣啊,

我們分為使用端和框架端兩部分,

學海無涯,我們一起勉力前行!

Ps:有需要的小伙伴可以點擊直接進入:暗號:csdn,免費獲取,

在這里插入圖片描述

使用端

我們在使用 mybatis 的時候是不是需要使用 SqlMapConfig.xml 組態檔,用來存放資料庫的連接資訊,以及 mapper.xml 的指向資訊,mapper.xml 組態檔用來存放 sql 資訊,

所以我們在使用端來創建兩個檔案 SqlMapConfig.xml 和 mapper.xml,

框架端

框架端要做哪些事情呢?如下:

  1. 獲取組態檔,也就是獲取到使用端的 SqlMapConfig.xml 以及 mapper.xml 的檔案
  2. 決議組態檔,對獲取到的檔案進行決議,獲取到連接資訊,sql,引數,回傳型別等等,這些資訊都會保存在 configuration 這個物件中,
  3. 創建 SqlSessionFactory,目的是創建 SqlSession 的一個實體,
  4. 創建 SqlSession ,用來完成上面原始 JDBC 的那些操作,

那在 SqlSession 中 進行了哪些操作呢?

  1. 獲取資料庫連接
  2. 獲取 sql ,并對 sql 進行決議
  3. 通過內省,將引數注入到 preparedStatement 中
  4. 執行 sql
  5. 通過反射將結果集封裝成物件

使用端實作

好了,上面說了一下,大概的設計思路,主要也是仿照 mybatis 主要的類實作的,保證類名一致,方便我們后面閱讀原始碼,我們先來配置好使用端吧,我們創建一個 maven 專案,

在專案中,我們創建一個 User 物體類

public class User {
    private Integer id;
    private String username;
    private String password;
    private String birthday;
    //getter()和 setter()方法
}

創建 SqlMapConfig.xml 和 Mapper.xml SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false"></property>
    <property name="userName" value="root"></property>
    <property name="password" value="123456"></property>

    <mapper resource="UserMapper.xml">
    </mapper>
</configuration>

可以看到我們 xml 中就配置了資料庫的連接資訊,以及 mapper 一個索引,mybatis 中的 SqlMapConfig.xml 中還包含其他的標簽,只是豐富了功能而已,所以我們只用最主要的,

mapper.xml 是每個類的 sql 都會生成一個對應的 mapper.xml ,我們這里就用 User 類來說吧,所以我們就創建一個 UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="cn.quellanan.dao.UserDao">
    <select id="selectAll" resultType="cn.quellanan.pojo.User">
        select * from user
    </select>
    <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User">
        select * from user where username=#{username}
    </select>
</mapper>

可以看到有點 mybatis 里面檔案的味道,有 namespace 表示命名空間,id 唯一標識,resultType 回傳結果集的型別,paramType 引數的型別, 我們使用端先創建到這,主要是兩個組態檔,

我們接下來看看框架端是怎么實作的,

框架端實作

架端,我們按照上面的設計思路一步一步來,

獲取配置

怎么樣獲取組態檔呢?我們可以使用 JDK 自帶自帶的類 Resources 加載器來獲取檔案,我們創建一個自定義 Resource 類來封裝一下:

import java.io.InputStream;
public class Resources {
    public  static InputStream getResources(String path){
        //使用系統自帶的類 Resources 加載器來獲取檔案,
        return Resources.class.getClassLoader().getResourceAsStream(path);
    }
}

這樣通過傳入路徑,就可以獲取到對應的檔案流啦,

決議組態檔

上面獲取到了 SqlMapConfig.xml 組態檔,我們現在來決議它, 不過在此之前,我們需要做一點準備作業,就是決議的記憶體放到什么地方?

所以我們來創建兩個物體類 Mapper 和 Configuration ,

Mapper Mapper 物體類用來存放使用端寫的 mapper.xml 檔案的內容,我們前面說了里面有 id、sql、resultType 和 paramType .所以我們創建的 Mapper 物體如下:

public class Mapper {
    private String id;
    private Class<?> resultType;
    private Class<?> parmType;
    private String sql;
    //getter()和 setter()方法
}

這里我們為什么不添加 namespace 的值呢? 聰明的你肯定發現了,因為 mapper 里面這些屬性表明每個 sql 都對應一個 mapper , 而 namespace 是一個命名空間,算是 sql 的上一層,所以在 mapper 中暫時使用不到,就沒有添加了,

Configuration Configuration 物體用來保存 SqlMapConfig 中的資訊,所以需要保存資料庫連接,我們這里直接用 JDK 提供的 DataSource ,還有一個就是 mapper 的資訊,每個 mapper 有自己的標識,所以這里采用 hashMap 來存盤,如下:

public class Configuration {

    private DataSource dataSource;
    HashMap <String,Mapper> mapperMap=new HashMap<>();
    //getter()和 setter 方法
    }

XmlMapperBuilder

做好了上面的準備作業,我們先來決議 mapper 吧,我們創建一個 XmlMapperBuilder 類來決議,通過 dom4j 的工具類來決議 XML 檔案,我這里用的 dom4j 依賴為:

         <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.3</version>
        </dependency>

思路:

  1. 獲取檔案流,轉成 document,
  2. 獲取根節點,也就是 mapper,獲取根節點的 namespace 屬性值
  3. 獲取 select 節點,獲取其 id,sql , resultType ,paramType
  4. 將 select 節點的屬性封裝到 Mapper 物體類中,
  5. 同理獲取 update/insert/delete 節點的屬性值封裝到 Mapper 中
  6. 通過 namespace.id 生成 key 值將 mapper 物件保存到 Configuration 物體中的 HashMap 中,
  7. 回傳 Configuration 物體
    代碼如下:
public class XmlMapperBuilder {
    private Configuration configuration;
    public XmlMapperBuilder(Configuration configuration){
        this.configuration=configuration;
    }

    public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException {
        Document document=new SAXReader().read(in);

        Element rootElement=document.getRootElement();
        String namespace=rootElement.attributeValue("namespace");

        List<Node> list=rootElement.selectNodes("//select");

        for (int i = 0; i < list.size(); i++) {
            Mapper mapper=new Mapper();
            Element element= (Element) list.get(i);
            String id=element.attributeValue("id");
            mapper.setId(id);
            String paramType = element.attributeValue("paramType");
            if(paramType!=null && !paramType.isEmpty()){
                mapper.setParmType(Class.forName(paramType));
            }
            String resultType = element.attributeValue("resultType");
            if (resultType != null && !resultType.isEmpty()) {
                mapper.setResultType(Class.forName(resultType));
            }
            mapper.setSql(element.getTextTrim());
            String key=namespace+"."+id;
            configuration.getMapperMap().put(key,mapper);
        }
        return configuration;
    }

}

上面我只決議了 select 標簽,大家可以決議對應 insert/delete/uupdate 標簽,操作都是一樣的,

XmlConfigBuilder

我們再來決議一下 SqlMapConfig.xml 配置資訊思路是一樣的,

1、獲取檔案流,轉成 document,
2、獲取根節點,也就是 configuration,
3、獲取根節點中所有的 property 節點,并獲取值,也就是獲取資料庫連接資訊
4、創建一個 dataSource 連接池
5、將連接池資訊保存到 Configuration 物體中
6、獲取根節點的所有 mapper 節點
7、呼叫 XmlMapperBuilder 類決議對應 mapper 并封裝到 Configuration 物體中
8、完
代碼如下:

public class XmlConfigBuilder {
    private Configuration configuration;
    public XmlConfigBuilder(Configuration configuration){
        this.configuration=configuration;
    }

    public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {

        Document document=new SAXReader().read(in);

        Element rootElement=document.getRootElement();

        //獲取連接資訊
        List<Node> propertyList=rootElement.selectNodes("//property");
        Properties properties=new Properties();

        for (int i = 0; i < propertyList.size(); i++) {
            Element element = (Element) propertyList.get(i);
            properties.setProperty(element.attributeValue("name"),element.attributeValue("value"));
        }
        //是用連接池
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setDriverClass(properties.getProperty("driverClass"));
        dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        dataSource.setUser(properties.getProperty("userName"));
        dataSource.setPassword(properties.getProperty("password"));
        configuration.setDataSource(dataSource);

        //獲取 mapper 資訊
        List<Node> mapperList=rootElement.selectNodes("//mapper");
        for (int i = 0; i < mapperList.size(); i++) {
            Element element= (Element) mapperList.get(i);
            String mapperPath=element.attributeValue("resource");
            XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
            configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath));
        }
        return configuration;
    }
}

創建 SqlSessionFactory

完成決議后我們創建 SqlSessionFactory 用來創建 Sqlseesion 的物體,這里為了盡量還原 mybatis 設計思路,也也采用的工廠設計模式, SqlSessionFactory 是一個介面,里面就一個用來創建 SqlSessionf 的方法, 如下:

public interface SqlSessionFactory {
    public SqlSession openSqlSession();
}

單單這個介面是不夠的,我們還得寫一個介面的實作類,所以我們創建一個 DefaultSqlSessionFactory, 如下:

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }
    public SqlSession openSqlSession() {
        return new DefaultSqlSeeion(configuration);
    }
}

可以看到就是創建一個 DefaultSqlSeeion 并將包含配置資訊的 configuration 傳遞下去,DefaultSqlSeeion 就是 SqlSession 的一個實作類,

創建 SqlSession
在 SqlSession 中我們就要來處理各種操作了,比如 selectList,selectOne,insert, update , delete 等等, 如下:

public interface SqlSession {

    /**
     * 條件查找
     * @param statementid  唯一標識,namespace.selectid
     * @param parm  傳參,可以不傳也可以一個,也可以多個
     * @param <E>
     * @return
     */
    public <E> List<E> selectList(String statementid,Object...parm) throws Exception;

    public <T> T selectOne(String statementid, Object...parm) throws Exception;

    public int insert(String statementid, Object...parm) throws Exception;
    public int update(String statementid, Object...parm) throws Exception;
    public int delete(String statementid, Object...parm) throws Exception;
    public void commit() throws Exception;


    /**
     * 使用代理模式來創建介面的代理物件
     * @param mapperClass
     * @param <T>
     * @return
     */
    public <T> T getMapper(Class<T> mapperClass);

然后我們創建 DefaultSqlSeeion 來實作 SqlSeesion ,

public class DefaultSqlSeeion implements SqlSession {

    private Configuration configuration;

    private Executer executer=new SimpleExecuter();

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

    @Override
    public <E> List<E> selectList(String statementid, Object... parm) throws Exception {
        Mapper mapper=configuration.getMapperMap().get(statementid);
        List<E> query = executer.query(configuration, mapper, parm);
        return query;
    }

    @Override
    public <T> T selectOne(String statementid, Object... parm) throws Exception {
        List<Object> list =selectList(statementid, parm);
        if(list.size()==1){
            return (T) list.get(0);
        }else{
            throw new RuntimeException("回傳結果過多");
        }
    }

    @Override
    public int insert(String statementid, Object... parm) throws Exception {
        return update(statementid,parm);
    }

    @Override
    public int update(String statementid, Object... parm) throws Exception {
        Mapper mapper=configuration.getMapperMap().get(statementid);
        int update = executer.update(configuration, mapper, parm);
        return update;
    }

    @Override
    public int delete(String statementid, Object... parm) throws Exception {
        return update(statementid,parm);
    }

    @Override
    public void commit() throws Exception {
        executer.commit();
    }



    @Override
    public <T> T getMapper(Class<T> mapperClass) {

        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                //獲取到方法名
                String name = method.getName();
                //型別
                String className = method.getDeclaringClass().getName();
                String statementid=className+"."+name;

                Mapper mapper = configuration.getMapperMap().get(statementid);
                SqlCommandType sqlType = mapper.getSqlCommandType();
                Type genericReturnType = method.getGenericReturnType();


                switch (sqlType){
                    case SELECT:
                        //判斷是否實作泛型型別引數化
                        if(genericReturnType instanceof ParameterizedType){
                            return selectList(statementid,args);
                        }else {
                            return selectOne(statementid,args);
                        }
                    case INSERT:return insert(statementid,args);
                    case DELETE:return delete(statementid,args);
                    case UPDATE:return update(statementid,args);
                    default:break;
                }
                return null;
            }
        });


        return (T) proxyInstance;
    }

}

我們可以看到 DefaultSqlSeeion 獲取到了 configuration,并通過 statementid 從 configuration 中獲取 mapper, 然后具體實作交給了 Executer 類來實作,我們這里先不管 Executer 是怎么實作的,就假裝已經實作了,那么整個框架端就完成了,通過呼叫 Sqlsession.selectList() 方法,來獲取結果,

感覺我們都還沒有處理,就框架搭建好了?騙鬼呢,確實前面我們從獲取檔案決議檔案,然后創建工廠,都是做好準備作業,下面開始我們 JDBC 的實作,

SqlSession 具體實作

我們前面說 SqlSeesion 的具體實作有下面 5 步

  1. 獲取資料庫連接
  2. 獲取 sql,并對 sql 進行決議
  3. 通過內省,將引數注入到 preparedStatement 中
  4. 執行 sql
  5. 通過反射將結果集封裝成物件

但是我們在 DefaultSqlSeeion 中將實作交給了 Executer 來執行,所以我們就要在 Executer 中來實作這些操作,

我們首先來創建一個 Executer 介面,并寫一個 DefaultSqlSeeion 中呼叫的 query 方法,

public interface Executer {

    <E> List<E> query(Configuration configuration,Mapper mapper,Object...parm) throws Exception;

}

接著我們寫一個 SimpleExecuter 類來實作 Executer , 然后 SimpleExecuter.query() 方法中,我們一步一步的實作,

獲取資料庫連接
因為資料庫連接資訊保存在 configuration,所以直接獲取就好了,

//獲取連接
        connection=configuration.getDataSource().getConnection();

獲取 sql,并對 sql 進行決議

我們這里想一下,我們在 Usermapper.xml 寫的 sql 是什么樣子?

select * from user where username=#{username}

分兩步

  1. 將 sql 找到 #{***} ,并將這部分替換成 ?號

  2. 對 #{***} 進行決議獲取到里面的引數對應的 paramType 中的值,

具體實作用到下面幾個類, GenericTokenParser 類,可以看到有三個引數,開始標記,就是我們的 “#{” ,結束標記就是 “}” , 標記處理器就是處理標記里面的內容也就是 username,

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) {
      //具體實作
      }

主要的就是 parse() 方法,用來獲取操作 1 的 sql,獲取結果例如:

select * from user where username=?

那上面用到 TokenHandler 來處理引數, ParameterMappingTokenHandler 實作 TokenHandler 的類

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

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

    @Override
    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;
    }

}

可以看到將引數名稱存放 ParameterMapping 的集合中了, ParameterMapping 類就是一個物體,用來保存引數名稱的,

public class ParameterMapping {

    private String content;

    public ParameterMapping(String content) {
        this.content = content;
    }
    //getter()和 setter() 方法,
}

所以我們在我們通過 GenericTokenParser 類,就可以獲取到決議后的 sql,以及引數名稱,我們將這些資訊封裝到 BoundSql 物體類中,

public class BoundSql {

    private String sqlText;
    private List<ParameterMapping> parameterMappingList=new ArrayList<>();
    public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
        this.sqlText = sqlText;
        this.parameterMappingList = parameterMappingList;
    }
    getter()和 setter() 方法,
  }

好了,那么分兩步走,先獲取,后決議 獲取 獲取原始 sql 很簡單,sql 資訊就存在 mapper 物件中,直接獲取就好了,

String sql=mapper.getSql()

決議

1、創建一個 ParameterMappingTokenHandler 處理器
2、創建一個 GenericTokenParser 類,并初始化開始標記,結束標記,處理器
3、執行 genericTokenParser.parse(sql) ;獲取決議后的 sql‘’,以及在 parameterMappingTokenHandler 中存放了引數名稱的集合,
4、將決議后的 sql 和引數封裝到 BoundSql 物體類中,

/**
     * 決議自定義占位符
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql){
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);
        String parse = genericTokenParser.parse(sql);
        return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings());

    }

將引數注入到 preparedStatement 中

上面的就完成了 sql 的決議,但是我們知道上面得到的 sql 還是包含 JDBC 的 占位符,所以我們需要將引數注入到 preparedStatement 中,

  1. 通過 boundSql.getSqlText() 獲取帶有占位符的 sql .
  2. 接收引數名稱集合 parameterMappingList
  3. 通過 mapper.getParmType() 獲取到引數的類,
  4. 通過 getDeclaredField(content) 方法獲取到引數類的 Field,
  5. 通過 Field.get() 從引數類中獲取對應的值
  6. 注入到 preparedStatement 中
     BoundSql boundSql=getBoundSql(mapper.getSql());
        String sql=boundSql.getSqlText();
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();

        //獲取 preparedStatement,并傳遞引數值
        PreparedStatement preparedStatement=connection.prepareStatement(sql);
        Class<?> parmType = mapper.getParmType();

        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();
            Field declaredField = parmType.getDeclaredField(content);
            declaredField.setAccessible(true);
            Object o = declaredField.get(parm[0]);
            preparedStatement.setObject(i+1,o);
        }
        System.out.println(sql);
        return preparedStatement;

執行 sql

其實還是呼叫 JDBC 的 executeQuery() 方法或者 execute() 方法

//執行 sql
 ResultSet resultSet = preparedStatement.executeQuery();

通過反射將結果集封裝成物件

在獲取到 resultSet 后,我們進行封裝處理,和引數處理是類似的,

  1. 創建一個 ArrayList
  2. 獲取回傳型別的類
  3. 回圈從 resultSet 中取資料
  4. 獲取屬性名和屬性值
  5. 創建屬性生成器
  6. 為屬性生成寫方法,并將屬性值寫入到屬性中
  7. 將這條記錄添加到 list 中
  8. 回傳 list
/**
     * 封裝結果集
     * @param mapper
     * @param resultSet
     * @param <E>
     * @return
     * @throws Exception
     */
    private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{
        ArrayList<E> list=new ArrayList<>();
        //封裝結果集
        Class<?> resultType = mapper.getResultType();
        while (resultSet.next()) {
            ResultSetMetaData metaData = resultSet.getMetaData();
            Object o = resultType.newInstance();
            int columnCount = metaData.getColumnCount();
            for (int i = 1; i <= columnCount; i++) {
                //屬性名
                String columnName = metaData.getColumnName(i);
                //屬性值
                Object value = resultSet.getObject(columnName);
                //創建屬性描述器,為屬性生成讀寫方法
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(o,value);
            }
            list.add((E) o);
        }
        return list;
    }

創建 SqlSessionFactoryBuilder

我們現在來創建一個 SqlSessionFactoryBuilder 類,來為使用端提供一個人口,

public class SqlSessionFactoryBuilder {

    private Configuration configuration;

    public SqlSessionFactoryBuilder(){
        configuration=new Configuration();
    }

    public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
        XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration);
        configuration=xmlConfigBuilder.loadXmlConfig(in);

        SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return sqlSessionFactory;
    }
}

可以看到就一個 build 方法,通過 SqlMapConfig 的檔案流將資訊決議到 configuration ,創建并回傳一個 sqlSessionFactory ,

到此,整個框架端已經搭建完成了,但是我們可以看到,只實作了 select 的操作, update 、inster 、delete 的操作我們在我后面提供的原始碼中會有實作,這里只是將整體的設計思路和流程,

測驗

終于到了測驗的環節啦,我們前面寫了自定義的持久層,我們現在來測驗一下能不能正常的使用吧, 見證奇跡的時刻到啦

我們先引入我們自定義的框架依賴,以及資料庫和單元測驗

 <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>
        <dependency>
            <groupId>cn.quellanan</groupId>
            <artifactId>myself-mybatis</artifactId>
            <version>1.0.0</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>

然后我們寫一個測驗類

  1. 獲取 SqlMapperConfig.xml 的檔案流
  2. 獲取 Sqlsession
  3. 執行查找操作
@org.junit.Test
    public void test() throws Exception{
        InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
        SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();
        List<User> list = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll");

        for (User parm : list) {
            System.out.println(parm.toString());
        }
        System.out.println();

        User user=new User();
        user.setUsername("張三");
        List<User> list1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user);
        for (User user1 : list1) {
            System.out.println(user1);
        }

    }

在這里插入圖片描述

可以看到已經可以了,看來我們自定義的持久層框架生效啦,

優化

我們看上面的測驗方法,是不是感覺和平時用的不一樣,每次都都寫死 statementId ,這樣不太友好,所以我們接下來來點騷操作,通用 mapper 配置, 我們在 SqlSession 中增加一個 getMapper 方法,接收的引數是一個類,我們通過這個類就可以知道 statementId ,

/**
     * 使用代理模式來創建介面的代理物件
     * @param mapperClass
     * @param <T>
     * @return
     */
    public <T> T getMapper(Class<T> mapperClass);

具體實作就是利用 JDK 的動態代理機制,

  1. 通過 Proxy.newProxyInstance() 獲取一個代理物件
  2. 回傳代理物件 那代理物件執行了哪些操作呢? 創建代理物件的時候,會實作一個 InvocationHandler 介面,重寫 invoke() 方法,讓所有走這個代理的方法都會執行這個 i nvoke() 方法,那這個方法做了什么操作? 這個方法就是通過傳入的類物件,獲取到物件的類名和方法名,用來生成 statementid ,

所以我們在 mapper.xml 組態檔中的 namespace 就需要制定為類路徑,以及 id 為方法名, 實作方法:

@Override
    public <T> T getMapper(Class<T> mapperClass) {

        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                //獲取到方法名
                String name = method.getName();
                //型別
                String className = method.getDeclaringClass().getName();
                String statementid=className+"."+name;

                return selectList(statementid,args);
            }
        });


        return (T) proxyInstance;
    }

我們寫一個 UserDao

public interface UserDao {
    List<User> selectAll();

    List<User> selectByName(User user);
}

這個是不是我們熟悉的味道哈哈,就是 mapper 層的介面, 然后我們在 mapper.xml 中指定 namespace 和 id
在這里插入圖片描述
接下來我們在寫一個測驗方法

@org.junit.Test
    public void test2() throws Exception{
        InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
        SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();

        UserDao mapper = sqlSession.getMapper(UserDao.class);
        List<User> users = mapper.selectAll();
        for (User user1 : users) {
            System.out.println(user1);
        }

        User user=new User();
        user.setUsername("張三");
        List<User> users1 = mapper.selectByName(user);
        for (User user1 : users1) {
            System.out.println(user1);
        }

    }

在這里插入圖片描述

瞎比比

自定義的持久層框架,我們就寫完了,這個實際上就是 mybatis 的雛形,我們通過自己手動寫一個持久層框架,然后在來看 mybatis 的原始碼,就會清晰很多,下面這些類名在 mybatis 中都有體現,
在這里插入圖片描述

好了,本文到此就結束了!閱讀原始碼愉快, 覺得有用的兄弟們三連啊,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/196765.html

標籤:python

上一篇:Java大牛發布最新Java實戰筆記,從基礎知識到函式式編程

下一篇:阿里巴巴一個超級牛逼的混沌實驗實施工具【Chaosblade】

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more