mybatis源碼分析(一):自己動(dòng)手寫一個(gè)簡(jiǎn)單的mybaits框架

框架解決了什么問(wèn)題

目前主流的Java Web項(xiàng)目都采用SSM(spring springmvc mybatis)框架,其中mybatis框架源碼是最簡(jiǎn)單的,想入手源碼學(xué)習(xí)的同學(xué)很推薦從mybatis開始。本系列文章是我對(duì)mybatis源碼學(xué)習(xí)的一些梳理總結(jié),可以幫助你更高效得理解mybatis。
在學(xué)習(xí)一個(gè)框架源碼之前你首先要問(wèn)自己幾個(gè)問(wèn)題。
1.這個(gè)框架解決了什么問(wèn)題?
2.為了解決這個(gè)問(wèn)題,你是如何設(shè)計(jì)的?框架是如何設(shè)計(jì)的?
3.框架是怎么樣給你提供擴(kuò)展性的?

通常第一個(gè)問(wèn)題都很簡(jiǎn)單,可以直接在mybatis官網(wǎng)找到答案。mybatis官網(wǎng)是這么解釋的:

MyBatis 是一款優(yōu)秀的持久層框架,它支持自定義 SQL、存儲(chǔ)過(guò)程以及高級(jí)映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設(shè)置參數(shù)和獲取結(jié)果集的工作。MyBatis 可以通過(guò)簡(jiǎn)單的 XML 或注解來(lái)配置和映射原始類型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 對(duì)象)為數(shù)據(jù)庫(kù)中的記錄。

我的理解是mybatis是一個(gè)持久層框架,它解決了使用JDBC冗余復(fù)雜代碼操作數(shù)據(jù)庫(kù)的問(wèn)題,并且可以通過(guò)XML或者注解的方式來(lái)實(shí)現(xiàn)ORM(對(duì)象關(guān)系映射)技術(shù),使我們?cè)陧?xiàng)目中操作數(shù)據(jù)庫(kù)更為簡(jiǎn)單和耦合性更好。

自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的mybatis框架

在學(xué)習(xí)源碼之前先自己簡(jiǎn)單實(shí)現(xiàn)一遍類似的功能是很有必要的,可以幫助你更好的理解框架是解決問(wèn)題的。假如要你實(shí)現(xiàn)一個(gè)簡(jiǎn)單的mybatis框架你會(huì)怎么做?
先不用去看mybatis的細(xì)節(jié),我們知道使用mybatis的時(shí)候在注解或者xml中定義一個(gè)sql然后綁定到一個(gè)接口的方法上就能執(zhí)行這條sql,并給你封裝好結(jié)果返回。實(shí)現(xiàn)這個(gè)功能并不難,我們可以先嘗試實(shí)現(xiàn)一遍。
我們也定義一個(gè)接口,不需要寫實(shí)現(xiàn)類就可以執(zhí)行接口定義的方法,且這個(gè)方法幫我們?nèi)?zhí)行注解里的sql語(yǔ)句:

public interface DistrictDao {

    @MyQuery(" select * from district where id = ? ")
    List<District> getDistrictById(Integer id);

}

要實(shí)現(xiàn)這個(gè)功能用JDK的動(dòng)態(tài)代理就可以了,我們可以建一個(gè)工廠類,用這個(gè)工廠類可以生成任意接口的代理對(duì)象,代理對(duì)象幫我們執(zhí)行方法上定義的SQL并封裝成結(jié)果。

簡(jiǎn)單的實(shí)現(xiàn)了一下可以執(zhí)行任意查詢語(yǔ)句的代理工廠類:

public class DBHandlerProxyFactory {

    /**
     * 獲取代理對(duì)象
     *
     * @param dataSource
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getTargetClass(DataSource dataSource, Class<T> clazz) {
        return (T) Proxy.newProxyInstance(
                clazz.getClassLoader(), // 傳入ClassLoader
                new Class[]{clazz}, // 傳入要實(shí)現(xiàn)的接口
                getQueryInvHandler(dataSource)); // 傳入處理調(diào)用方法的InvocationHandler
    }

    /**
     * 獲取動(dòng)態(tài)代理執(zhí)行handler
     * @param dataSource
     * @return
     */
    public static InvocationHandler getQueryInvHandler(DataSource dataSource) {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                MyQuery myQuery = method.getAnnotation(MyQuery.class);
                if (myQuery != null) {
                    //1.拿到目標(biāo)方法上面的sql
                    String sql = myQuery.value();
                    //2.拿到數(shù)據(jù)庫(kù)連接并執(zhí)行sql
                    Connection connection = dataSource.getConnection();
                    Statement statement = connection.createStatement();
                    int i = 0;
                    while (sql.contains("?")) {
                        sql = sql.replaceFirst("\\?", args[i] + "");
                        i++;
                    }
                    System.out.println("執(zhí)行sql語(yǔ)句:" + sql);
                    ResultSet resultSet = statement.executeQuery(sql);
                    //3.獲取目標(biāo)方法的返回類型
                    Type returnType = method.getGenericReturnType();
                    Class<?> returnTypeClass = null;
                    if (returnType instanceof Class) {
                        returnTypeClass = (Class<?>) returnType;
                    } else {
                        ParameterizedTypeImpl r = (ParameterizedTypeImpl) method.getGenericReturnType();
                        returnTypeClass = r.getRawType();
                        if (Collection.class.isAssignableFrom(returnTypeClass)) {
                            Type[] types = r.getActualTypeArguments();
                            returnTypeClass = (Class<?>) types[0];
                        }
                    }
                    //4.封裝返回結(jié)果
                    List<?> list = getQueryResult(resultSet, returnTypeClass);
                    resultSet.close();
                    connection.close();
                    return list;
                }
                return new ArrayList<>();
            }
        };
        return handler;
    }


    /**
     * 獲取查詢結(jié)果
     *
     * @param resultSet
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> List<T> getQueryResult(ResultSet resultSet, Class<T> clazz) {
        List<T> list = new ArrayList<>();
        try {
            while (resultSet.next()) {
                T obj = clazz.newInstance();
                ResultSetMetaData rsmeta = resultSet.getMetaData();
                int count = rsmeta.getColumnCount();
                for (int i = 0; i < count; i++) {
                    String name = rsmeta.getColumnName(i + 1);
                    Field f = obj.getClass().getDeclaredField(name);
                    f.setAccessible(true);
                    f.set(obj, resultSet.getObject(name));
                }
                list.add(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

}

這里就不闡述動(dòng)態(tài)代理了,動(dòng)態(tài)代理handler執(zhí)行思路是:
1.拿到目標(biāo)方法上面的sql
2.拿到數(shù)據(jù)庫(kù)連接并使用JDBC的API來(lái)執(zhí)行sql
3.獲取目標(biāo)方法的返回類型
4.封裝返回結(jié)果

通過(guò)這四步就可以不用再寫冗余的JDBC代碼了,只用寫接口就可以查詢數(shù)據(jù)并返回封裝好的結(jié)果了。執(zhí)行代碼是這樣的:

@SpringBootTest
class MybatisStudyApplicationTests {

    @Autowired
    private DataSource dataSource;


    @Test
    void testProxy(){
        DistrictDao districtDao = DBHandlerProxyFactory.getTargetClass(dataSource,DistrictDao.class);
        List<District> districtList =  districtDao.getDistrictById(1);
        System.out.println(districtList);
    }
}

OK,到這里自己嘗試設(shè)計(jì)一個(gè)簡(jiǎn)單“MyBatis”功能就完成了,接下來(lái)我們需要分析一下我們自己寫的這個(gè)簡(jiǎn)單的框架有那些問(wèn)題和缺陷。我們帶著問(wèn)題再去看Mybatis源碼就會(huì)明白作者的設(shè)計(jì)意圖,再看看Mybatis作者是如何解決這些問(wèn)題的,這樣學(xué)習(xí)源碼會(huì)更有收獲一些。

自己實(shí)現(xiàn)mybatis框架的缺陷

1.操作數(shù)據(jù)庫(kù)只支持查詢的操作而且代碼復(fù)用性不高,如果要支持增刪改查的操作該如何設(shè)計(jì)

2.封裝結(jié)果集只支持集合類型,而且返回的實(shí)例字段只能和表字段名稱一致才能映射,如何把結(jié)果集封裝設(shè)計(jì)的更靈活一些

3.參數(shù)轉(zhuǎn)換如何實(shí)現(xiàn),例如傳入的是Java的日期類型,應(yīng)該怎么樣轉(zhuǎn)換成數(shù)據(jù)庫(kù)支持的格式

4.預(yù)編譯SQL以及數(shù)據(jù)庫(kù)的事務(wù)如何實(shí)現(xiàn)

5.如何加入緩存策略

目前能想到的就是這些,然后我們帶著這些問(wèn)題逐步深入mybatis的源碼來(lái)看一下mybatis是如何設(shè)計(jì)的。

本文涉及的源碼放在github上:

https://github.com/burgleaf/mybatis-study

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容