從零開始手寫 mybatis(一)MVP 版本

什麼是 MyBatis ?

MyBatis 是一款優秀的持久層框架,它支持定製化 SQL、存儲過程以及高級映射。java

MyBatis 避免了幾乎全部的 JDBC 代碼和手動設置參數以及獲取結果集。mysql

MyBatis 可使用簡單的 XML 或註解來配置和映射原生信息,將接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java對象)映射成數據庫中的記錄。(這是官網解釋)git

MyBatis 運行原理

mybatis.png

當框架啓動時,經過configuration解析config.xml配置文件和mapper.xml映射文件,映射文件可使用xml方式或者註解方式,而後由configuration得到sqlsessionfactory對象,再由sqlsessionfactory得到sqlsession數據庫訪問會話對象,經過會話對象得到對應DAO層的mapper對象,經過調用mapper對象相應方法,框架就會自動執行SQL語句從而得到結果。github

手寫 mybatis

其實總體流程就是這麼簡單,咱們來一塊兒實現一個簡單版本的 mybatis。sql

創做目的

(1)深刻學習 mybatis 的原理數據庫

一千個讀者就有一千個哈姆雷特,一千個做者就有一千個莎士比亞。——老馬session

(2)實現屬於本身的 mybatis 工具。mybatis

數據庫的種類實際上有幾百種,好比工做中就用到過 GreenPlum 這種相對小衆的數據庫,這時候 mybatis 可能就不能使用了。app

感受大可沒必要,符合 SQL 標準都應該統一支持下,這樣更加方便實用。框架

實現方式

本系列目前共計 17 個迭代版本,基本完成了 mybatis 的核心特性。

耗時大概十天左右,相對實現的方式比較簡單。

採用 mvp 的開發策略,逐漸添加新的特性。

本系列將對核心代碼進行講解,完整代碼已經所有開源

https://github.com/houbb/mybatis

快速體驗

mysql 安裝

不是本系列重點,請自行找資料。

版本:使用的是 v5.7 版本,v8.0 以後依賴的驅動包會有所不一樣。

sql 執行

-- auto-generated definition
create table user
(
  id   int auto_increment
    primary key,
  name varchar(100) not null,
  password varchar(100) not null
);

insert into user (name, password) value ('ryo', '123456');

maven 引入

<dependency>
    <groupId>com.github.houbb</groupId>
    <artifactId>mybatis</artifactId>
    <version>0.0.1</version>
</dependency>

配置文件

  • mybatis-config-5-7.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <dataSource>
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/test"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </dataSource>

    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>

</configuration>

測試代碼

Config config = new XmlConfig("mybatis-config-5-7.xml");

SqlSession sqlSession = new DefaultSessionFactory(config).openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1L);
System.out.println(user);

輸出結果:

User{id=1, name='ryo', password='123456'}

是否是有種 mybatis 初戀般的感受呢?

到這裏都是引子,下面咱們來說述下一些核心實現。

代碼實現

maven 依賴

這裏咱們須要訪問 mysql,也須要解析 xml。

須要引入以下的依賴:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.29</version>
</dependency>
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>

接口定義

上述的測試代碼中,咱們演示用到的幾個核心接口以下:

  • Config.java

配置接口

/**
 * 配置信息
 * @author binbin.hou
 * @since 0.0.1
 */
public interface Config {

    /**
     * 獲取數據源信息
     * @return 數據源配置
     * @since 0.0.1
     */
    DataSource getDataSource();

    /**
     * 獲取映射類信息
     * @param clazz 類信息
     * @return 結果
     * @since 0.0.1
     */
    MapperClass getMapperData(final Class clazz);

    /**
     * 獲取映射類信息
     * @param clazz 類信息
     * @param methodName 方法名稱
     * @return 結果
     * @since 0.0.1
     */
    MapperMethod getMapperMethod(final Class clazz,
                                 final String methodName);

    /**
     * 數據庫鏈接信息
     * @return 鏈接信息
     * @since 0.0.1
     */
    Connection getConnection();
}
  • SqlSession.java
public interface SqlSession {

    /**
     * 查詢單個
     * @param mapperMethod 方法
     * @param args 參數
     * @param <T> 泛型
     * @return 結果
     * @since 0.0.1
     */
    <T> T selectOne(final MapperMethod mapperMethod, Object[] args);

    /**
     * Retrieves a mapper.
     * @param <T> the mapper type
     * @param type Mapper interface class
     * @return a mapper bound to this SqlSession
     * @since 0.0.1
     */
    <T> T getMapper(Class<T> type);

    /**
     * 獲取配置信息
     * @return 配置
     * @since 0.0.1
     */
    Config getConfig();

}
  • UserMapper.java

UserMapper 就是咱們常常定義的 mapper

public interface UserMapper {

    User selectById(final long id);

}

下面咱們來看看對應的幾個比較重要的實現。

xml 的配置初始化

咱們的不少配置放在 config.xml 文件中,確定是經過解析 xml 實現的。

基礎屬性

public class XmlConfig extends ConfigAdaptor {

    /**
     * 文件配置路徑
     *
     * @since 0.0.1
     */
    private final String configPath;

    /**
     * 配置文件信息
     *
     * @since 0.0.1
     */
    private Element root;

    /**
     * 數據源信息
     *
     * @since 0.0.1
     */
    private DataSource dataSource;

    /**
     * mapper 註冊類
     *
     * @since 0.0.1
     */
    private final MapperRegister mapperRegister = new MapperRegister();

    public XmlConfig(String configPath) {
        this.configPath = configPath;

        // 配置初始化
        initProperties();

        // 初始化數據鏈接信息
        initDataSource();

        // mapper 信息
        initMapper();
    }

    @Override
    public DataSource getDataSource() {
        return this.dataSource;
    }

    @Override
    public Connection getConnection() {
        try {
            Class.forName(dataSource.driver());
            return DriverManager.getConnection(dataSource.url(), dataSource.username(), dataSource.password());
        } catch (ClassNotFoundException | SQLException e) {
            throw new MybatisException(e);
        }
    }

    @Override
    public MapperMethod getMapperMethod(Class clazz, String methodName) {
        return this.mapperRegister.getMapperMethod(clazz, methodName);
    }
}

配置初始化

這裏就是解析 xml 文件的 root 節點,便於後續使用:

root 節點的初始化以下:

/**
 * 獲取根節點
 * @param path 配置路徑
 * @return 元素
 * @since 0.0.1
 */
public static Element getRoot(final String path) {
    try {
        // 初始化數據庫鏈接信息
        InputStream inputStream = StreamUtil.getInputStream(path);
        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        return document.getRootElement();
    } catch (DocumentException e) {
        throw new MybatisException(e);
    }
}

初始化數據鏈接信息

這就是解析 xml 中對於 dataSource 的配置信息:

/**
 * 初始化數據源
 *
 * @since 0.0.1
 */
private void initDataSource() {
    // 根據配置初始化鏈接信息
    this.dataSource = new DataSource();
    Element dsElem = root.element("dataSource");
    Map<String, String> map = new HashMap<>(4);
    for (Object property : dsElem.elements("property")) {
        Element element = (Element) property;
        String name = element.attributeValue("name");
        String value = element.attributeValue("value");
        map.put("jdbc." + name, value);
    }
    dataSource.username(map.get(DataSourceConst.USERNAME))
            .password(map.get(DataSourceConst.PASSWORD))
            .driver(map.get(DataSourceConst.DRIVER))
            .url(map.get(DataSourceConst.URL));
}

初始化 mapper

解析 xml 中的 mapper 配置。

/**
 * 初始化 mapper 信息
 *
 * @since 0.0.1
 */
private void initMapper() {
    Element mappers = root.element("mappers");
    // 遍歷全部須要初始化的 mapper 文件路徑
    for (Object item : mappers.elements("mapper")) {
        Element mapper = (Element) item;
        String path = mapper.attributeValue("resource");
        mapperRegister.addMapper(path);
    }
}

mapperRegister 就是對方法的元數據進行一些構建,好比出參,入參的類型,等等,便於後期使用。

好比咱們的 UserMapper.xml 方法內容以下:

<select id = "selectById" paramType="java.lang.Long" resultType = "com.github.houbb.mybatis.domain.User">
        select * from user where id = ?
</select>

sql 就是:select * from user where id = ?

方法標識:selectById

入參:Long

出參:User

建立 session

如何建立

SqlSession sqlSession = new DefaultSessionFactory(config).openSession();

這句話實際執行的是:

@Override
public SqlSession openSession() {
    return new DefaultSqlSession(config, new SimpleExecutor());
}

獲取 mapper 實現

UserMapper userMapper = sqlSession.getMapper(UserMapper.class)

這裏獲取 mapper,實際獲取的是什麼呢?

實際上獲取到的是一個代理。

mybatis 將咱們的接口,和實際 xml 中的 sql 兩者經過動態代理結合,讓咱們調用 xml 中的 sql 和使用接口方法同樣天然。

獲取代理

getMapper 其實是一個動態代理。

@Override
@SuppressWarnings("all")
public <T> T getMapper(Class<T> clazz) {
    MapperProxy proxy = new MapperProxy(clazz, this);
    return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, proxy);
}

動態代理的實現

MapperProxy 的實現以下:

public class MapperProxy implements InvocationHandler {

    /**
     * 類信息
     *
     * @since 0.0.1
     */
    private final Class clazz;

    /**
     * sql session
     *
     * @since 0.0.1
     */
    private final SqlSession sqlSession;

    public MapperProxy(Class clazz, SqlSession sqlSession) {
        this.clazz = clazz;
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MapperMethod mapperMethod = this.sqlSession.getConfig()
                .getMapperMethod(clazz, method.getName());
        if (mapperMethod != null) {
            return this.sqlSession.selectOne(mapperMethod, args);
        }
        return method.invoke(proxy, args);
    }

}

代理了什麼?

當咱們執行 userMapper.selectById(1L) 時,實際執行的是什麼?

實際執行的是 sqlSession.selectOne(mapperMethod, args)

代理實現

selectOne 是比較核心的內容了。

總體實現

總體以下

public <T> T query(final Config config,
                   MapperMethod method, Object[] args) {
    try(Connection connection = config.getConnection();
        PreparedStatement preparedStatement = connection.prepareStatement(method.getSql());) {
        // 2. 處理參數
        parameterHandle(preparedStatement, args);
        // 3. 執行方法
        preparedStatement.execute();
        // 4. 處理結果
        final Class resultType = method.getResultType();
        ResultSet resultSet = preparedStatement.getResultSet();
        ResultHandler resultHandler = new ResultHandler(resultType);
        Object result = resultHandler.buildResult(resultSet);
        return (T) result;
    } catch (SQLException ex) {
        throw new MybatisException(ex);
    }
}

咱們獲取到 xml 中的 sql,而後構建 jdbc 中你們比較熟悉的 PreparedStatement。

而後對出參和入參進行處理,最後返回結果。

入參設置

public void setParams(final Object[] objects) {
    try {
        for(int i = 0; i < objects.length; i++) {
            Object value = objects[i];
            // 目標類型,這個後期能夠根據 jdbcType 獲取
            // jdbc 下標從1開始
            statement.setObject(i+1, value);
        }
    } catch (SQLException throwables) {
        throw new MybatisException(throwables);
    }
}

針對咱們很是簡單的例子:

select * from user where id = ?

那就是直接把入參中的 1L 設置到佔位符 ? 便可。

出參處理

這裏主要用到反射,將查詢結果和 javaBean 作一一映射。

/**
 * 構建結果
 * @param resultSet 結果集合
 * @return 結果
 * @since 0.0.1
 */
public Object buildResult(final ResultSet resultSet) {
    try {
        // 基本類型,非 java 對象,直接返回便可。
        // 能夠進行抽象
        Object instance = resultType.newInstance();
        // 結果大小的判斷
        // 爲空直接返回,大於1則報錯
        if(resultSet.next()) {
            List<Field> fieldList = ClassUtil.getAllFieldList(resultType);
            for(Field field : fieldList) {
                Object value = getResult(field, resultSet);
                ReflectFieldUtil.setValue(field, instance, value);
            }
            // 返回設置值後的結果
            return instance;
        }
        return null;
    } catch (InstantiationException | IllegalAccessException | SQLException e) {
        throw new MybatisException(e);
    }
}

到這裏,一個簡易版的 myabtis 就能夠跑起來了。

固然這裏還有不少的不足之處,咱們後續都會一一優化。

完整代碼地址

爲了便於學習,完整版本代碼以開源:

https://github.com/houbb/mybatis

image

相關文章
相關標籤/搜索