源碼分析 | 基於jdbc實現一個Demo版的Mybatis

image

做者:小傅哥
博客:https://bugstack.cn - 原創系列優質專題文章html

沉澱、分享、成長,讓本身和他人都能有所收穫!

1、前言介紹

在前面一篇分析了 mybatis 源碼,從它爲何以後接口可是沒有實現類就能執行數據庫操做爲入口,整個源碼核心流程徹底解釋了一遍。對於一個3年以上的程序員來講,新知識的學習過程應該是從最開始 helloworld 到熟練使用 api 完成業務功能。下一步爲了深刻了解就須要閱讀部分核心源碼,從而在出問題後能夠快速定位,迅速排查。從而減小線上事故的持續時長,提高我的影響力。但!這不是學習終點,由於不管是任何一個框架的源碼,若是隻是看那麼就很難學習到它的實用技術。紙上得來終覺淺,惟有實戰和操練。java

那麼,本章節咱們去簡單實現一個基於jdbc的demo版本Mybatis,從而更加清楚這樣框架的設計。與此同時這份思想會讓你能夠在其餘場景使用,好比給ES查詢寫一個EsBatis。實現了心情也好了;node

image

2、案例工程

擴展上一篇源碼分析工程;itstack-demo-mybatis,增長 like 包,模仿 Mybatis 工程。完整規程下載,關注公衆號:bugstack蟲洞棧 | 回覆:源碼分析mysql

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

3、環境配置

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

4、代碼講述

關於整個 Demo 版本,並非把全部 Mybatis 所有實現一遍,而是撥絲抽繭將最核心的內容展現給你,從使用上你會感覺如出一轍,可是實現類已經所有被替換,核心類包括;程序員

  • 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();
    }
}

一切順利結果以下(新人每每會遇到各類問題);web

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

Process finished with exit code 0

可能乍一看這測試類徹底和 MybatisApiTest.java 測試的代碼如出一轍呀,也看不出區別。其實他們的引入的包是不同;spring

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;

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

2. 加載XML配置文件

這裏咱們採用 mybatis 的配置文件結構進行解析,在不破壞原有結構的狀況下,最大可能的貼近源碼。mybatis 單獨使用的使用的時候使用了兩個配置文件;數據源配置、Mapper 映射配置,以下;數據庫

mybatis-config-datasource.xml & 數據源配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/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 "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/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 方式。在案例中會看到最開始獲取資源,以下;apache

ApiLikeTest.test_queryUserInfoById() & 部分截取
String resource = "spring/mybatis-config-datasource.xml";
    Reader reader;
    try {
        reader = Resources.getResourceAsReader(resource);
    ...

從上能夠看到這是經過配置文件地址獲取到了讀取流的過程,從而爲後面解析作基礎。首先咱們先看 Resources 類,整個是咱們的資源類。

Resources.java & 資源類
/**
 * 公衆號 | bugstack蟲洞棧
 * 博 客 | https://bugstack.cn
 * 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配置文件

配置文件加載後開始進行解析操做,這裏咱們也仿照 mybatis 但進行簡化,以下;

SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactoryBuilder.build() & 入口構建類
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;
}
  • 經過讀取流建立 xml 解析的 Document 類
  • parseConfiguration 進行解析 xml 文件,並將結果設置到配置類中,包括;鏈接池、數據源、mapper關係
SqlSessionFactoryBuilder.parseConfiguration() & 解析過程
private Configuration parseConfiguration(Element root) {
    Configuration configuration = new Configuration();
    configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
    configuration.setConnection(connection(configuration.dataSource));
    configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
    return configuration;
}
  • 在前面的 xml 內容中能夠看到,咱們須要解析出數據庫鏈接池信息 datasource,還有數據庫語句映射關係 mappers
SqlSessionFactoryBuilder.dataSource() & 解析出數據源
private Map<String, String> dataSource(List<Element> list) {
    Map<String, 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;
}
  • 這個過程比較簡單,只須要將數據源信息獲取便可
SqlSessionFactoryBuilder.connection() & 獲取數據庫鏈接
private Connection connection(Map<String, 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最原始的代碼,獲取了數據庫鏈接池
SqlSessionFactoryBuilder.mapperElement() & 解析SQL語句
private Map<String, XNode> mapperElement(List<Element> list) {
    Map<String, 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
            List<Element> 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();
                // ? 匹配
                Map<Integer, 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 相關
  • 全部的 sql 語句爲了確認惟一,都是使用;namespace + select中的id進行拼接,做爲 key,以後與sql一塊兒存放到 map 中。
  • 在 mybaits 的 sql 語句配置中,都有佔位符,用於傳參。where id = #{id} 因此咱們須要將佔位符設置爲問號,另外須要將佔位符的順序信息與名稱存放到 map 結構,方便後續設置查詢時候的入參。

4. 建立DefaultSqlSessionFactory

最後將初始化後的配置類 Configuration,做爲參數進行建立 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的實現類
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);
    }
    
}
  • 這個過程比較簡單,構造函數只提供了配置類入參
  • 實現 SqlSessionFactory 的 openSession(),用於建立 DefaultSqlSession,也就能夠執行 sql 操做

5. 開啓SqlSession

SqlSession session = sqlMapper.openSession();

上面這一步就是建立了DefaultSqlSession,比較簡單。以下;

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

6. 執行SQL語句

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

在 DefaultSqlSession 中經過實現 SqlSession,提供數據庫語句查詢和關閉鏈接池,以下;

SqlSession.java & 定義
public interface SqlSession {

    <T> T selectOne(String statement);

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

    <T> List<T> selectList(String statement);

    <T> List<T> selectList(String statement, Object parameter);

    void close();
}

接下來看具體的執行過程,session.selectOne

DefaultSqlSession.selectOne() & 執行查詢
public <T> T selectOne(String statement, Object parameter) {
    XNode xNode = mapperElement.get(statement);
    Map<Integer, String> parameterMap = xNode.getParameter();
    try {
        PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
        buildParameter(preparedStatement, parameter, parameterMap);
        ResultSet resultSet = preparedStatement.executeQuery();
        List<T> 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 標籤信息;

    <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 類進行執行
  • 這裏還須要設置入參,咱們將入參設置進行抽取,以下;

    private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException {
    
        int size = parameterMap.size();
        // 單個參數
        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;
        }
    
        Map<String, Object> fieldMap = new HashMap<>();
        // 對象參數
        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);
            }
    
        }
    
    }
    • 單個參數比較簡單直接設置值便可,Long、Integer、String ...
    • 若是是一個類對象,須要經過獲取 Field 屬性,與參數 Map 進行匹配設置
  • 設置參數後執行查詢 preparedStatement.executeQuery()
  • 接下來須要將查詢結果轉換爲咱們的類(主要是反射類的操做),resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));

    private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
        List<T> 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 標籤上
    • 時間類型須要判斷後處理,Timestamp,與 java 不是一個類型

7. Sql查詢補充說明

sql 查詢有入參、有不須要入參、有查詢一個、有查詢集合,只須要合理包裝便可,例以下面的查詢集合,入參是對象類型;

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);
            List<User> 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();
    }
    
}

*測試結果:*

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

Process finished with exit code 0

5、綜上總結

  • 學習完 Mybaits 核心源碼,再實現一下核心過程,那麼就會很清晰這個過程是怎麼個流程,也就不會以爲本身知識棧有漏洞
  • 只有深刻的學習才能將這樣的技術賦能於其餘開發上,例如給ES增長這樣查詢包,讓ES更加容易操做。其實還能夠有不少創造
  • 知識每每是綜合的使用,將各個知識點綜合起來使用,才能更加熟練。不要總看不作,不然全套的流程不能在本身腦子流程下什麼印象

6、推薦閱讀

相關文章
相關標籤/搜索