电竞比分网-中国电竞赛事及体育赛事平台

分享

源碼分析 | 基于jdbc實(shí)現(xiàn)一個Demo版的Mybatis

 小傅哥 2021-12-13


作者 | 小傅哥
博客 | https://

微信公眾號:bugstack蟲洞棧 | 博客:https://

沉淀、分享、成長,專注于原創(chuàng)專題案例,以最易學(xué)習(xí)編程的方式分享知識,讓自己和他人都能有所收獲。目前已完成的專題有;Netty4.x實(shí)戰(zhàn)專題案例、用Java實(shí)現(xiàn)JVM、基于JavaAgent的全鏈路監(jiān)控、手寫RPC框架、架構(gòu)設(shè)計(jì)專題案例、源碼分析等。

你用劍🗡、我用刀🔪,好的代碼都很燒😏,望你不吝出招💨!

一、前言介紹

在前面一篇分析了 mybatis 源碼,從它為什么之后接口但是沒有實(shí)現(xiàn)類就能執(zhí)行數(shù)據(jù)庫操作為入口,整個源碼核心流程完全解釋了一遍。對于一個3年以上的程序員來說,新知識的學(xué)習(xí)過程應(yīng)該是從最開始 helloworld 到熟練使用 api 完成業(yè)務(wù)功能。下一步為了深入了解就需要閱讀部分核心源碼,從而在出問題后可以快速定位,迅速排查。從而減少線上事故的持續(xù)時長,提升個人影響力。但!這不是學(xué)習(xí)終點(diǎn),因?yàn)闊o論是任何一個框架的源碼,如果只是看那么就很難學(xué)習(xí)到它的實(shí)用技術(shù)。紙上得來終覺淺,唯有實(shí)戰(zhàn)和操練。

那么,本章節(jié)我們?nèi)ズ唵螌?shí)現(xiàn)一個基于jdbc的demo版本Mybatis,從而更加清楚這樣框架的設(shè)計(jì)。與此同時這份思想會讓你可以在其他場景使用,比如給ES查詢寫一個EsBatis。實(shí)現(xiàn)了心情也好了;

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-8Ai2paSY-1578920519920)(https://fuzhengwei./assets/images/pic-content/2019/11/itstack-demo-mybatis-07.png)]

二、案例工程

擴(kuò)展上一篇源碼分析工程;itstack-demo-mybatis,增加 like 包,模仿 Mybatis 工程。完整規(guī)程下載,關(guān)注公眾號:bugstack蟲洞棧 | 回復(fù):源碼分析

itstack-demo-mybatis
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo
    │   │       ├── dao
    │   │       │├── ISchool.java
    │   │       │└── IUserDao.java
    │   │       ├── like
    │   │       │├── Configuration.java
    │   │       │├── DefaultSqlSession.java
    │   │       │├── DefaultSqlSessionFactory.java
    │   │       │├── Resources.java
    │   │       │├── SqlSession.java
    │   │       │├── SqlSessionFactory.java
    │   │       │├── SqlSessionFactoryBuilder.java
    │   │       │└── SqlSessionFactoryBuilder.java
    │   │       └── interfaces     
    │   │         ├── School.java
    │   │        └── User.java
    │   ├── resources
    │   │   ├── mapper
    │   │   │   ├── School_Mapper.xml
    │   │   │   └── User_Mapper.xml
    │   │   ├── props
    │   │   │   └── jdbc.properties
    │   │   ├── spring
    │   │   │   ├── mybatis-config-datasource.xml
    │   │   │   └── spring-config-datasource.xml
    │   │   ├── logback.xml
    │   │   ├── mybatis-config.xml
    │   │   └── spring-config.xml
    │   └── webapp
    │       └── WEB-INF
    └── test
         └── java
             └── org.itstack.demo.test
                 ├── ApiLikeTest.java
                 ├── MybatisApiTest.java
                 └── SpringApiTest.java

三、環(huán)境配置

  1. JDK1.8
  2. IDEA 2019.3.1
  3. dom4j 1.6.1

四、代碼講述

關(guān)于整個 Demo 版本,并不是把所有 Mybatis 全部實(shí)現(xiàn)一遍,而是撥絲抽繭將最核心的內(nèi)容展示給你,從使用上你會感受一模一樣,但是實(shí)現(xiàn)類已經(jīng)全部被替換,核心類包括;

  • Configuration
  • DefaultSqlSession
  • DefaultSqlSessionFactory
  • Resources
  • SqlSession
  • SqlSessionFactory
  • SqlSessionFactoryBuilder
  • XNode

1. 先測試下整個DemoJdbc框架

ApiLikeTest.test_queryUserInfoById()

@Test
public void test_queryUserInfoById() {
    String resource = "spring/mybatis-config-datasource.xml";
    Reader reader;
    try {
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();

        try {
            User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
            System.out.println(JSON.toJSONString(user));
        } finally {
            session.close();
            reader.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

一切順利結(jié)果如下(新人往往會遇到各種問題);

{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}

Process finished with exit code 0

可能乍一看這測試類完全和 MybatisApiTest.java 測試的代碼一模一樣呀,也看不出區(qū)別。其實(shí)他們的引入的包是不一樣;

MybatisApiTest.java 里面引入的包

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

ApiLikeTest.java 里面引入的包

import org.itstack.demo.like.Resources;
import org.itstack.demo.like.SqlSession;
import org.itstack.demo.like.SqlSessionFactory;
import org.itstack.demo.like.SqlSessionFactoryBuilder;

好!接下來我們開始分析這部分核心代碼。

2. 加載XML配置文件

這里我們采用 mybatis 的配置文件結(jié)構(gòu)進(jìn)行解析,在不破壞原有結(jié)構(gòu)的情況下,最大可能的貼近源碼。mybatis 單獨(dú)使用的使用的時候使用了兩個配置文件;數(shù)據(jù)源配置、Mapper 映射配置,如下;

mybatis-config-datasource.xml & 數(shù)據(jù)源配置

?xml version="1.0" encoding="UTF-8"?>
!DOCTYPE configuration PUBLIC "-////DTD Config 3.0//EN"
        "http:///dtd/mybatis-3-config.dtd">

configuration>
    environments default="development">
        environment id="development">
            transactionManager type="JDBC"/>
            dataSource type="POOLED">
                property name="driver" value="com.mysql.jdbc.Driver"/>
                property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/>
                property name="username" value="root"/>
                property name="password" value="123456"/>
            /dataSource>
        /environment>
    /environments>

    mappers>
        mapper resource="mapper/User_Mapper.xml"/>
        mapper resource="mapper/School_Mapper.xml"/>
    /mappers>

/configuration>

User_Mapper.xml & Mapper 映射配置

?xml version="1.0" encoding="UTF-8"?>
!DOCTYPE mapper PUBLIC "-////DTD Mapper 3.0//EN" "http:///dtd/mybatis-3-mapper.dtd">
mapper namespace="org.itstack.demo.dao.IUserDao">

    select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where id = #{id}
    /select>

    select id="queryUserList" parameterType="org.itstack.demo.po.User" resultType="org.itstack.demo.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where age = #{age}
    /select>

/mapper>

這里的加載過程與 mybaits 不同,我們采用 dom4j 方式。在案例中會看到最開始獲取資源,如下;

ApiLikeTest.test_queryUserInfoById() & 部分截取

String resource = "spring/mybatis-config-datasource.xml";
Reader reader;
try {
reader = Resources.getResourceAsReader(resource);
...

從上可以看到這是通過配置文件地址獲取到了讀取流的過程,從而為后面解析做基礎(chǔ)。首先我們先看 Resources 類,整個是我們的資源類。

Resources.java & 資源類

/**
 * 公眾號 | bugstack蟲洞棧
 * 博 客 | https://
 * Create by 小傅哥 @2020
 */
public class Resources {

    public static Reader getResourceAsReader(String resource) throws IOException {
        return new InputStreamReader(getResourceAsStream(resource));
    }

    private static InputStream getResourceAsStream(String resource) throws IOException {
        ClassLoader[] classLoaders = getClassLoaders();
        for (ClassLoader classLoader : classLoaders) {
            InputStream inputStream = classLoader.getResourceAsStream(resource);
            if (null != inputStream) {
                return inputStream;
            }
        }
        throw new IOException("Could not find resource " + resource);
    }

    private static ClassLoader[] getClassLoaders() {
        return new ClassLoader[]{
                ClassLoader.getSystemClassLoader(),
                Thread.currentThread().getContextClassLoader()};
    }

}

這段代碼方法的入口是getResourceAsReader,直到往下以此做了;

  1. 獲取 ClassLoader 集合,最大限度搜索配置文件
  2. 通過 classLoader.getResourceAsStream 讀取配置資源,找到后立即返回,否則拋出異常

3. 解析XML配置文件

配置文件加載后開始進(jìn)行解析操作,這里我們也仿照 mybatis 但進(jìn)行簡化,如下;

SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

SqlSessionFactoryBuilder.build() & 入口構(gòu)建類

public DefaultSqlSessionFactory build(Reader reader) {
    SAXReader saxReader = new SAXReader();
    try {
        Document document = saxReader.read(new InputSource(reader));
        Configuration configuration = parseConfiguration(document.getRootElement());
        return new DefaultSqlSessionFactory(configuration);
    } catch (DocumentException e) {
        e.printStackTrace();
    }
    return null;
}
  • 通過讀取流創(chuàng)建 xml 解析的 Document 類
  • parseConfiguration 進(jìn)行解析 xml 文件,并將結(jié)果設(shè)置到配置類中,包括;連接池、數(shù)據(jù)源、mapper關(guān)系

SqlSessionFactoryBuilder.parseConfiguration() & 解析過程

private Configuration parseConfiguration(Element root) {
    Configuration configuration = new Configuration();
    configuration.setDataSource(dataSource(root.selectNodes("http://dataSource")));
    configuration.setConnection(connection(configuration.dataSource));
    configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
    return configuration;
}
  • 在前面的 xml 內(nèi)容中可以看到,我們需要解析出數(shù)據(jù)庫連接池信息 datasource,還有數(shù)據(jù)庫語句映射關(guān)系 mappers

SqlSessionFactoryBuilder.dataSource() & 解析出數(shù)據(jù)源

private MapString, String> dataSource(ListElement> list) {
    MapString, String> dataSource = new HashMap>(4);
    Element element = list.get(0);
    List content = element.content();
    for (Object o : content) {
        Element e = (Element) o;
        String name = e.attributeValue("name");
        String value = e.attributeValue("value");
        dataSource.put(name, value);
    }
    return dataSource;
}
  • 這個過程比較簡單,只需要將數(shù)據(jù)源信息獲取即可

SqlSessionFactoryBuilder.connection() & 獲取數(shù)據(jù)庫連接

private Connection connection(MapString, String> dataSource) {
    try {
        Class.forName(dataSource.get("driver"));
        return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
    } catch (ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    }
    return null;
}
  • 這個就是jdbc最原始的代碼,獲取了數(shù)據(jù)庫連接池

SqlSessionFactoryBuilder.mapperElement() & 解析SQL語句

private MapString, XNode> mapperElement(ListElement> list) {
    MapString, XNode> map = new HashMap>();
    Element element = list.get(0);
    List content = element.content();
    for (Object o : content) {
        Element e = (Element) o;
        String resource = e.attributeValue("resource");
        try {
            Reader reader = Resources.getResourceAsReader(resource);
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(new InputSource(reader));
            Element root = document.getRootElement();
            //命名空間
            String namespace = root.attributeValue("namespace");
            // SELECT
            ListElement> selectNodes = root.selectNodes("select");
            for (Element node : selectNodes) {
                String id = node.attributeValue("id");
                String parameterType = node.attributeValue("parameterType");
                String resultType = node.attributeValue("resultType");
                String sql = node.getText();
                // ? 匹配
                MapInteger, String> parameter = new HashMap>();
                Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                Matcher matcher = pattern.matcher(sql);
                for (int i = 1; matcher.find(); i++) {
                    String g1 = matcher.group(1);
                    String g2 = matcher.group(2);
                    parameter.put(i, g2);
                    sql = sql.replace(g1, "?");
                }
                XNode xNode = new XNode();
                xNode.setNamespace(namespace);
                xNode.setId(id);
                xNode.setParameterType(parameterType);
                xNode.setResultType(resultType);
                xNode.setSql(sql);
                xNode.setParameter(parameter);
                
                map.put(namespace + "." + id, xNode);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    return map;
}
  • 這個過程首先包括是解析所有的sql語句,目前為了測試只解析 select 相關(guān)
  • 所有的 sql 語句為了確認(rèn)唯一,都是使用;namespace + select中的id進(jìn)行拼接,作為 key,之后與sql一起存放到 map 中。
  • 在 mybaits 的 sql 語句配置中,都有占位符,用于傳參。where id = #{id} 所以我們需要將占位符設(shè)置為問號,另外需要將占位符的順序信息與名稱存放到 map 結(jié)構(gòu),方便后續(xù)設(shè)置查詢時候的入?yún)ⅰ?/li>

4. 創(chuàng)建DefaultSqlSessionFactory

最后將初始化后的配置類 Configuration,作為參數(shù)進(jìn)行創(chuàng)建 DefaultSqlSessionFactory,如下;

public DefaultSqlSessionFactory build(Reader reader) {
    SAXReader saxReader = new SAXReader();
    try {
        Document document = saxReader.read(new InputSource(reader));
        Configuration configuration = parseConfiguration(document.getRootElement());
        return new DefaultSqlSessionFactory(configuration);
    } catch (DocumentException e) {
        e.printStackTrace();
    }
    return null;
}

DefaultSqlSessionFactory.java & SqlSessionFactory的實(shí)現(xiàn)類

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
private final Configuration configuration;
    
public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
    }

}
  • 這個過程比較簡單,構(gòu)造函數(shù)只提供了配置類入?yún)?/li>
  • 實(shí)現(xiàn) SqlSessionFactory 的 openSession(),用于創(chuàng)建 DefaultSqlSession,也就可以執(zhí)行 sql 操作

5. 開啟SqlSession

SqlSession session = sqlMapper.openSession();

上面這一步就是創(chuàng)建了DefaultSqlSession,比較簡單。如下;

@Override
public SqlSession openSession() {
    return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
}

6. 執(zhí)行SQL語句

User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);

在 DefaultSqlSession 中通過實(shí)現(xiàn) SqlSession,提供數(shù)據(jù)庫語句查詢和關(guān)閉連接池,如下;

SqlSession.java & 定義

public interface SqlSession {

    T> T selectOne(String statement);

    T> T selectOne(String statement, Object parameter);

    T> ListT> selectList(String statement);

    T> ListT> selectList(String statement, Object parameter);

    void close();
}

接下來看具體的執(zhí)行過程,session.selectOne

DefaultSqlSession.selectOne() & 執(zhí)行查詢

public T> T selectOne(String statement, Object parameter) {
    XNode xNode = mapperElement.get(statement);
    MapInteger, String> parameterMap = xNode.getParameter();
    try {
        PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
        buildParameter(preparedStatement, parameter, parameterMap);
        ResultSet resultSet = preparedStatement.executeQuery();
        ListT> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
        return objects.get(0);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}
  • selectOne 就objects.get(0);,selectList 就全部返回

  • 通過 statement 獲取最初解析 xml 時候的存儲的 select 標(biāo)簽信息;

    select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
    SELECT id, name, age, createTime, updateTime
    FROM user
    where id = #{id}
    select>
    
  • 獲取 sql 語句后交給 jdbc 的 PreparedStatement 類進(jìn)行執(zhí)行

  • 這里還需要設(shè)置入?yún)?,我們將入?yún)⒃O(shè)置進(jìn)行抽取,如下;

    private void buildParameter(PreparedStatement preparedStatement, Object parameter, MapInteger, String> parameterMap) throws SQLException, IllegalAccessException {
    
        int size = parameterMap.size();
        // 單個參數(shù)
        if (parameter instanceof Long) {
            for (int i = 1; i <> size; i++) {
                preparedStatement.setLong(i, Long.parseLong(parameter.toString()));
            }
            return;
        }
    
        if (parameter instanceof Integer) {
            for (int i = 1; i <> size; i++) {
                preparedStatement.setInt(i, Integer.parseInt(parameter.toString()));
            }
            return;
        }
    
        if (parameter instanceof String) {
            for (int i = 1; i <> size; i++) {
                preparedStatement.setString(i, parameter.toString());
            }
            return;
        }
    
        MapString, Object> fieldMap = new HashMap>();
        // 對象參數(shù)
        Field[] declaredFields = parameter.getClass().getDeclaredFields();
        for (Field field : declaredFields) {
            String name = field.getName();
            field.setAccessible(true);
            Object obj = field.get(parameter);
            field.setAccessible(false);
            fieldMap.put(name, obj);
        }
    
        for (int i = 1; i <> size; i++) {
            String parameterDefine = parameterMap.get(i);
            Object obj = fieldMap.get(parameterDefine);
    
            if (obj instanceof Short) {
                preparedStatement.setShort(i, Short.parseShort(obj.toString()));
                continue;
            }
    
            if (obj instanceof Integer) {
                preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
                continue;
            }
    
            if (obj instanceof Long) {
                preparedStatement.setLong(i, Long.parseLong(obj.toString()));
                continue;
            }
    
            if (obj instanceof String) {
                preparedStatement.setString(i, obj.toString());
                continue;
            }
    
            if (obj instanceof Date) {
                preparedStatement.setDate(i, (java.sql.Date) obj);
            }
    
        }
    
    }
    
    • 單個參數(shù)比較簡單直接設(shè)置值即可,Long、Integer、String …
    • 如果是一個類對象,需要通過獲取 Field 屬性,與參數(shù) Map 進(jìn)行匹配設(shè)置
  • 設(shè)置參數(shù)后執(zhí)行查詢 preparedStatement.executeQuery()

  • 接下來需要將查詢結(jié)果轉(zhuǎn)換為我們的類(主要是反射類的操作),resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));

    private T> ListT> resultSet2Obj(ResultSet resultSet, Class?> clazz) {
    ListT> list = new ArrayList>();
    try {
    ResultSetMetaData metaData = resultSet.getMetaData();
    int columnCount = metaData.getColumnCount();
    // 每次遍歷行值
    while (resultSet.next()) {
    T obj = (T) clazz.newInstance();
    for (int i = 1; i <> columnCount; i++) {
    Object value = resultSet.getObject(i);
    String columnName = metaData.getColumnName(i);
    String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
    Method method;
    if (value instanceof Timestamp) {
    method = clazz.getMethod(setMethod, Date.class);
    } else {
    method = clazz.getMethod(setMethod, value.getClass());
    }
    method.invoke(obj, value);
    }
    list.add(obj);
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    return list;
    }
    
    • 主要通過反射生成我們的類對象,這個類的類型定義在 sql 標(biāo)簽上
    • 時間類型需要判斷后處理,Timestamp,與 java 不是一個類型

7. Sql查詢補(bǔ)充說明

sql 查詢有入?yún)?、有不需要入?yún)?、有查詢一個、有查詢集合,只需要合理包裝即可,例如下面的查詢集合,入?yún)⑹菍ο箢愋停?/p>

ApiLikeTest.test_queryUserList()

@Test
public void test_queryUserList() {
    String resource = "spring/mybatis-config-datasource.xml";
    Reader reader;
    try {
        reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        
try {
            User req = new User();
            req.setAge(18);
            ListUser> userList = session.selectList("org.itstack.demo.dao.IUserDao.queryUserList", req);
            System.out.println(JSON.toJSONString(userList));
        } finally {
            session.close();
            reader.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

}

**測試結(jié)果:

[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]

Process finished with exit code 0

五、綜上總結(jié)

  • 學(xué)習(xí)完 Mybaits 核心源碼,再實(shí)現(xiàn)一下核心過程,那么就會很清晰這個過程是怎么個流程,也就不會覺得自己知識棧有漏洞
  • 只有深入的學(xué)習(xí)才能將這樣的技術(shù)賦能于其他開發(fā)上,例如給ES增加這樣查詢包,讓ES更加容易操作。其實(shí)還可以有很多創(chuàng)造
  • 知識往往是綜合的使用,將各個知識點(diǎn)綜合起來使用,才能更加熟練。不要總看不做,否則全套的流程不能在自己腦子流程下什么印象

六、文末驚喜

小傅哥 | 沉淀、分享、成長,讓自己和他人都能有所收獲!

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多