面試官:小夥子,Mybatis的本質和原理說一下

背景

項目須要,咱們須要本身作一套mybatis,或者使用大部分mybatis地原始內容。對其改造,以適應須要。這就要求我再次學習一下mybatis,對它有更深刻的瞭解。java

是什麼

MyBatis是一個持久層框架,用來處理對象關係映射。說白了就是以相對面向對象的方式來提交sql語句給jdbc。若是想找個簡單、快速上手的例子,最好是和spring相結合的。整理了一份272頁MybatisPDF文檔mysql

爲何

Java開發都是面向對象的思惟,若是用傳統下面本身去調用鏈接拼裝sql的方式,維護成本高,代碼可讀性差。git

public static void main(String[] args) {
    //數據庫鏈接對象
    Connection conn = null;
    //數據庫操做對象
    PreparedStatement stmt = null;
    //一、加載驅動程序
    try {
        Class.forName(DBDRIVER);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    //二、鏈接數據庫
    //經過鏈接管理器鏈接數據庫
    try {
        //在鏈接的時候直接輸入用戶名和密碼才能夠鏈接
        conn = DriverManager.getConnection(DBURL, USERNAME, PASSWORD);
    } catch (SQLException e) {
        e.printStackTrace();
    }
    //三、向數據庫中插入一條數據
    String sql = "INSERT INTO person(name,age) VALUES (?,?)";
    try {
        stmt = conn.prepareStatement(sql);
        stmt.setString(1,"陳崑崙");
        stmt.setInt(2,21);
        stmt.executeQuery();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    //四、執行語句
    try {
        ResultSet resultSet = stmt.executeQuery();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    //五、關閉操做,步驟相反哈~
    try {
        stmt.close();
        conn.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

怎麼作

咱們來看一下底層是怎麼處理和交互的。基本流程以下:github

面試官:小夥子,Mybatis的本質和原理說一下

看着頭大?沒事,咱們先從最簡化的版本開始添枝加葉。MyBatis能夠用配置文件或者註解形式注入sql。由於配置文件方式能夠方便的處理動態SQL(動態SQL就是sql語句裏有if else for這些的,能夠根據參數的變化最終sql也跟着變化)等優勢,用得更爲廣泛。面試

假設如今是2000年,Clinton Begin尚未發起ibatis(mybatis的前身)項目。而apache基金會內部發起了討論要設計這樣一個產品,指派你做爲項目負責人。如今思考,你的思路是什麼?正則表達式

通常思路是先把架構搭建起來,作成一個MVP最小可行性版本,而後再作功能加強。spring

從功能最簡化方面來看,須要兩步:第一步要將sql及所須要的元素以對象的形式輸入,第二步是獲取到這些信息轉換成jdbc信息處理。sql

這樣拆解後的思路是將sql及所須要的元素拆解成類方法的參數形式,方法自己要作的事情就是將這些參數以jdbc編程須要的形式傳給jdbc執行。這裏方法內部作的事情是同樣的,那就天然而然地想到不用每一個類都有一個實現。只要定義好接口,把實現用代理或者上層切面的方式統一處理就能夠了。數據庫

根據這個思路,首先要用代理來獲取參數。我設計使用方式是Insert、Select等註解裏寫sql元語句。經過方法參數注入參數。最終返回結果。以下apache

public interface UserMapper {
@Insert("INSERT INTO person(name,age) VALUES (#{name},#{age})")
    Integer insertUser(User user);
}

要實現接口的解析。先創建一個類,裏面構造一個代理類,實現相似於SqlSession,因此起名叫YunaSession(yuna是我給經典java學習場景工程https://github.com/xiexiaojing/yuna 起的名字)

public class YunaSession {
   public static Object dealSql(Class clazz) {
       Class c[] = new Class[]{clazz};
return Proxy. newProxyInstance (YunaSession. class .getClassLoader() , c ,
new YunaInvocationHandler()) ;
}
}

下面要實現的是代理中YunaInvocationHandler真正要實現的邏輯:將這些參數以jdbc編程須要的形式傳給jdbc執行。也就是說把上面【爲何】部分一開始的那段執行jdbc的代碼貼進去,將sql和參數的部分作替換。

咱們把關鍵再貼一遍便於說明問題

//三、向數據庫中插入一條數據
String sql = "INSERT INTO person(name,age) VALUES (?,?)";
try {
    stmt = conn.prepareStatement(sql);
    stmt.setString(1,"陳崑崙");
    stmt.setInt(2,21);
    stmt.executeQuery();
} catch (SQLException e) {
    e.printStackTrace();
}

這裏有兩個?,而jdbc的預處理語句傳入參數的時候要明確地知道第一個參數的類型是什麼,若是傳過來是對象的話,要知道對應對象的哪一個值。這就是爲何接口裏的預處理語句傳入是

INSERT INTO person(name,age) VALUES (#{name},#{age})

由於能夠經過匹配#{XX}這樣的肯定都是哪些參數,由於User對象裏有定義參數的類型。因此類型和值都肯定了。這個就是MappedStatement對象作的事情。如下是用正則表達式匹配+反射來達到解析sql並和對象值作匹配的實現:

public static void main(String[] args) throws Exception{
    Matcher m= pattern.matcher("INSERT INTO person(name,age) VALUES (#{name},#{age})");
    User user1 = new User();
    user1.setId(1);
    user1.setName("賈元春");
    user1.setAge(27);
    int i=1;
    while(m.find()) {
        System.out.println(m.group());
        String group = m.group();
        String fieldName = group.replace("#{","").replace("}","");
        Field field = User.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        if("java.lang.Integer".equals(field.getType().getName())) {
            System.out.println("stmt.setInt("+i+","+field.get(user1)+")");
        } else if("java.lang.String".equals(field.getType().getName())) {
            System.out.println(" stmt.setString("+i+","+field.get(user1)+")");
        }
        i++;
    }
}

運行結果是

面試官:小夥子,Mybatis的本質和原理說一下

能夠看到實現了效果。下面就是和jdbc鏈接結合起來。

public class YunaInvocationHandler implements InvocationHandler {
    public static final String DBDRIVER = "org.xx.mm.mysql.Driver";
    public static final String DBURL = "jdbc:mysql://localhost:3306/mydb";
    //如今使用的是mysql數據庫,是直接鏈接的,因此此處必須有用戶名和密碼
    public static final String USERNAME = "root";
    public static final String PASSWORD = "mysqladmin";
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Exception{
        Object result = null;
        Insert insert = method.getAnnotation(Insert.class);
        if (insert != null) {
            String sql = insert.value()[0];
            System.out.println("插入語句爲"+s);
            YunaSqlDeal yunaSqlDeal = new YunaSqlDeal();
            yunaSqlDeal.insert(s, Arrays.toString(args));
            //一、加載驅動程序
            try {
                Class.forName(DBDRIVER);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            //二、鏈接數據庫
            //經過鏈接管理器鏈接數據庫
            //數據庫鏈接對象
            Connection conn = null;
            try {
                //在鏈接的時候直接輸入用戶名和密碼才能夠鏈接
                conn = DriverManager.getConnection(DBURL, USERNAME, PASSWORD);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            composeStatement(sql, args[0], conn);
        }
        return 1;
    }
    private static final String PATTERN = "#\\{[A-Za-z0-9]+\\}";
    private static Pattern pattern = Pattern.compile("("+PATTERN+")");
    public static void composeStatement(String sql, Object obj, Connection conn) throws Exception{
        PreparedStatement stmt = conn.prepareStatement(sql.replaceAll(PATTERN, ""));
        Matcher m= pattern.matcher(sql);
        int i=1;
        while(m.find()) {
            System.out.println(m.group());
            String group = m.group();
            String fieldName = group.replace("#{","").replace("}","");
            Field field = User.class.getDeclaredField(fieldName);
            field.setAccessible(true);
            if("java.lang.Integer".equals(field.getType().getName())) {
                System.out.println("stmt.setInt("+i+","+field.get(obj)+")");
                stmt.setInt(i, Integer.parseInt(field.get(obj).toString()));
            } else if("java.lang.String".equals(field.getType().getName())) {
                stmt.setString(i, field.get(obj).toString());
            }
            i++;
        }
        stmt.execute();
        stmt.close();
        conn.close();
    }
}

這個實現的是insert的,返回值類型固定,若是是select查詢語句,涉及到返回的結果封裝成對象。思路也是經過反射,和參數轉換步驟差很少,就不貼代碼了。

到此,咱們實現了一個簡化版的mybatis框架。比貼的架構圖簡化在少用了不少設計模式的東西,和出於性能考慮重用的東西。mybatis的核心就實現完了。

總結

本文從mybatis的設計者角度出發,構造了一個簡化的mybatis框架。整理了一份272頁MybatisPDF文檔

不少原理性的東西看過以後會忘,可是若是真正站在設計者角度實現過一個簡化的版本,相信會加強記憶。同時也能和真正的實現作對比,更深層學習技術大牛們的設計精華。

相關文章
相關標籤/搜索