MyBatis源碼(1)—— MyBatis架構梗概

目錄

一、 我是如何看源碼的

二、MyBastis架構

1. 預先分析一下MyBatis的骨架

2. 根據這個架構寫出一個MyBatis的1.0版本Demo

3. 看1.0版本有哪些不足,該添加些什麼,該怎樣升級

4. 開始看真實的MyBatis源碼, 理出其骨架

5. 查看MyBatis源碼中的各模塊代碼,查看相互聯繫和代碼實現


一、 我是如何看源碼的

   

     看框架原碼是提高代碼設計能力的一個重要的途徑。通過學習大牛的優秀代碼和設計思想,我們既能夠更深地理解框架的底層原理,又能夠強化代碼的架構能力。

    但是開源框架通常都是比較大的項目,盲目地一頭扎到代碼中去逐行看,可能會摸不着頭腦,沒有成就感,而一點點地耗盡熱情,最終收效甚微。

    我的建議是—— 先抓梗概,後看模塊,帶着問題看代碼

 

    我通常的做法是

    1. 在看源碼之前,設想如果自己來設計這個框架的話會怎麼寫

         根據框架的功能和暴露的接口,寫一個小Demo, 可以寫的很簡單,接口裏甚至只打印一句話都行,關鍵是思考着對框架進行分層和功能模塊劃分,只實現最核心的邏輯。  

         這樣能讓自己提起好奇心,以便看代碼時不會倦怠。同時這是自己設計能力的一個答卷,後面將對照着標準答案(真實的框架代碼設計情況)進行評判,找到自己的不足和更好的優化方式。

    2. 先看框架梗概,不陷入具體細節,列出整個框架的骨架圖和各模塊之間的關係;之後再按功能看各個模塊的代碼實現

         過早地陷入代碼細節,會導致只見樹木不見森林;先搭好骨架再去填充細節要相對容易得多。

 

    3. 看完代碼之後按真實的框架再寫一個demo,前面寫的demo哪個地方不好?該怎樣優化? 

         不懂的地方可以猜測再debug一下。

         多問些關鍵問題,找關鍵類聯繫起來。

 

二、MyBastis架構

    我們以MyBastis爲例來看一下框架源碼。

1. 預先分析一下MyBatis的骨架

      我們先看一下MyBatis的常用例子:

SqlSession session = null;
try{
      //創建SessionFactory對象,讀取配置信息
      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(
                                      Resources.getResourceAsStream("mybatis-config.xml"));

      //創建一個session
      session = factory.openSession();
      
      PersonMapper personMapper = session.getMapper(PersonMapper.class);

      //Mybatis通過mapper代理直接執行查詢方法
      Person person= personMapper.selectOne(1001);
      System.out.println(person);
}
catch(Exception e){
    e.printStackTrace();
}
finally{
    if(null != session){
        session.close();
    }
}

    從這些應用信息來看,我們能知道MyBatis:

    (1)要有一個Configuration類,負責解析xml配置文件, 將相應的配置讀進來並存儲到Map中,包括DB相應的配置、包名.類名.方法名 與SQL語句之間的轉換配置、以及其他的配置等。

    (2)Session類, 要建立與DB的連接,根據調用的情況執行相應的SQL語句並返回相應的結果;如果Session類的功能太多會導致耦合,不妨將與DB打交道的功能單獨封裝到一個Executor類中(至於叫什麼無所謂)。

    (3)Session類需要提供一個getMapper() 方法, 根據類名生成相應的代理來調用想要的方法,肯定要用到動態代理,再封裝一個動態代理類MapperProxy。

      至於SessionFactory和SessionFactoryBuilder不屬於核心功能,後續再去添加。

    這些是最基本的骨架,我們根據這些基本的功能來搭建MyBatis的1.0版本的Demo。

2. 根據這個架構寫出一個MyBatis的1.0版本Demo

      Session類:

package mybatisdemo;

import java.lang.reflect.Proxy;

public class MSession {
    private String xmlPath;

    public MSession(String xmlPath) {
        this.xmlPath = xmlPath;
    }

    public <T> T getMapper(Class clazz){
        return (T) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                new Class[]{clazz},
                new MProxyHandler(this.xmlPath));
    }
}

ProxyHandler類:

package mybatisdemo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MProxyHandler implements InvocationHandler {
    private MConfiguration config;
    private MExecutor executor;

    public MProxyHandler(String xmlPath) {
        config = new MConfiguration(xmlPath);
        executor = new MExecutor(config);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (this.config.getNameSpace().equals(method.getDeclaringClass().getName())){
            //這裏應該要根據所調用的方法選擇不同的調用分支,先寫個最簡單版本,之後再豐富
            return executor.selectOne(method.getName(), args);
        }
        //不屬於配置的方法,則去調用Object原有的方法
        return method.invoke(this, args);
    }
}

Configuration類

package mybatisdemo;

import java.util.HashMap;

public class MConfiguration{
    //存儲數據庫配置信息
    private HashMap<String, String> DBMap;
    //存儲類名.方法名 與SQL之間的映射
    private HashMap<String, String> SQLMap;
    //包名.類名,本應讀xml配置,這裏模擬直接寫死
    private String nameSpace;

    public MConfiguration(String xml) {
        //直接在這裏賦值模擬讀xml文件
        DBMap = new HashMap<>();
        DBMap.put("user", "root");
        DBMap.put("password", "passwd");
        DBMap.put("driver", "com.mysql.jdbc.Driver");
        DBMap.put("url", "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8");

        //SQLMap的內部也應該是讀xml進來的,這裏直接賦值模擬
        SQLMap = new HashMap<>();
        SQLMap.put("selectOne", "select * from person where id = ?");

        this.nameSpace = "mybatisdemo.PersonMapper";
    }

    public HashMap<String, String> getDBMap() {
        return DBMap;
    }

    public HashMap<String, String> getSQLMap() {
        return SQLMap;
    }

    public String getNameSpace() {
        return nameSpace;
    }
}

Executor類:

package mybatisdemo;

import java.sql.*;

public class MExecutor {
    private static MConfiguration config;
    private Connection connection = null;
    private PreparedStatement statement = null;

    public MExecutor(final MConfiguration config) {
        this.config = config;
    }

    public void init(){
        //獲取連接
        try {
            Class.forName(config.getDBMap().get("driver"));
            if (null == this.connection){
                String url = config.getDBMap().get("url");
                String user = config.getDBMap().get("user");
                String passwd = config.getDBMap().get("password");
                connection = DriverManager.getConnection(url, user, passwd);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public <T> T selectOne(String methodName, Object[] args){
        String sql = config.getSQLMap().get(methodName);
        if (null != sql && 0 != sql.length()){
            return query(sql, args);
        }
        return null;
    }

    public <T> T query(String sql, Object[] args){
        //這裏也是用的硬編碼,後續需要改進
        PersonInfo person = new PersonInfo();
        try {
            init();
            statement = connection.prepareStatement(sql);
            statement.setLong(1, (Long) args[0]);
            ResultSet rs = statement.executeQuery();
            //這裏結果集處理用的硬編碼,需改進
            while (rs.next()){
                person.setId(rs.getLong(1));
                person.setName(rs.getString(2));
                person.setAge(rs.getInt(3));
                person.setGender( rs.getByte("gender"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            try {
                connection.close();
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return (T) person;
    }
}

PersonInfo類: 一個信息封裝類

package mybatisdemo;

public class PersonInfo {
    private long id;
    private String name;
    private int age;
    private byte gender;

    public long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public byte getGender() {
        return gender;
    }

    public void setGender(byte gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "PersonInfo{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                '}';
    }
}

PersonMapper interface: 一個用來測試的接口,不做implements, 

package mybatisdemo;

import java.util.List;

public interface PersonMapper {
    PersonInfo selectOne(Long id);
    List<PersonInfo> selectAll();
    int insert(PersonInfo info);
    int delete(Long id);
    int update(PersonInfo info);
}

Main函數, 直接session調用PersonMapper.class 生成動態代理,不用寫實現直接根據文件就能查出結果

package mybatisdemo;

public class TestMain {
    public static void main(String[] args) {
        MSession session = new MSession("mybatis.xml");
        PersonMapper mapper = session.getMapper(PersonMapper.class);
        PersonInfo info = mapper.selectOne(1L);
        System.out.println(info);
    }
}

運行結果:

 

3. 看1.0版本有哪些不足,該添加些什麼,該怎樣升級

     1.0版本能夠運行了,已經能夠像MyBatis那樣,不用寫實現、直接通過配置文件就能夠在調用方法時輸出結果。

     但這個簡陋的版本,也存在着不少的問題,還需要繼續優化。我們看看存在哪些問題:

    (1)讀取xml部分還沒有寫

    (2)ProxyHandler 的invoke() 裏只寫了一個方法,這裏需要根據不同的調用情況選擇不同的調用分支

    (3)結果集處理寫的是硬編碼,這裏需要由xml配置給出resultSet或 resultMap,並且加上ORM 進行自動處理

    (4)Session加上工廠類SessionFactory、 SessionFactoryBuilder

    (5)加上一級緩存、二級緩存,提高查詢性能, Cache類及Cache過期策略, LRU/FIFO, 以及增/刪/改時Cache失效

    (6)連接管理、事務管理

    (7)日誌,插件,註解,異常處理,以及其他的擴展功能

 

 

 

4. 開始看真實的MyBatis源碼, 理出其骨架

    別急着深入看各個類,先粗看整理出枝節,不要陷入細節

    這是一個MyBatis的層級架構和功能模塊圖

 

5. 查看MyBatis源碼中的各模塊代碼,查看相互聯繫和代碼實現

    

    根據功能模塊,查看各代碼。 看細節代碼時,時刻記得整體的架構,自己寫的與它們的差距在哪裏,怎樣寫的更好。並注意各模塊之間的聯繫。

     後面博文,將分別解讀各核心模塊, 共包括 Configuration、Plugin、  MapperProxy、 SqlSession、 Executor 幾個部分。

    謝謝!

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章