手寫一個簡化版Mybatis

1、引包

引入dom4j包以及數據庫連接包,我用的是mysql數據庫,因此引入mysql-connector包

2、數據庫創建

數據庫比較簡單,創建sql如下

CREATE DATABASE db_test;

use db_test;

CREATE TABLE `tb_user` (
  `id` int(11) NOT NULL,
  `name` varchar(20) NOT NULL,
  `age` tinyint(4) DEFAULT '0',
  `addr` varchar(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

然後隨便插入幾條數據

+----+---------+------+-----------+
| id | name    | age  | addr      |
+----+---------+------+-----------+
|  1 | Kurozaki|   19 | Guangdong |
|  2 | Kanako  |   20 | Japan     |
|  3 | Lee     |   20 | Malaysia  |
|  4 | Kintoki |   28 | Shenzhen  |
+----+---------+------+-----------+

3、模仿Mybatis編寫mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="test.dao.UserDao">
    <select id="getUserInfo" resultType="test.entity.User">
        select * from tb_user
        where id = ?;
    </select>

    <update id="updateUserName">
        update tb_user set name = ?
        where id = ?
    </update>

    <insert id="insertUser">
        insert into tb_user
        values(?, ?, ?, ?);
    </insert>
</mapper>

4、聲明查詢接口與實體類

package test.dao;

import test.entity.User;

/**
 * Created by YotWei on 2018/8/6.
 */
public interface UserDao {

    User getUserInfo(int id);

    int updateUserName(String newName, int id);

    int insertUser(int id, String name, int age, String addr);
}
package test.entity;

/**
 * Created by YotWei on 2018/8/6.
 */
public class User {
    private int id;
    private String name;
    private int age;
    private String addr;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }


    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", addr='" + addr + '\'' +
                '}';
    }
}

5、讀取mapper文件

mapper文件作用是管理sql語句與接口方法的映射,在使用Mybatis框架的時候,會先從mapper中讀取映射信息,包括接口名,方法名,查詢返回的數據類型,SQL語句的內容等等,MapperInfo定義如下

package com.yotwei.core;

/**
 * Created by YotWei on 2018/8/6.
 */
public class MapperInfo {

    private QueryType queryType;
    private String interfaceName;
    private String methodName;
    private String sql;
    private String resultType;

    public QueryType getQueryType() {
        return queryType;
    }

    public void setQueryType(QueryType queryType) {
        this.queryType = queryType;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }

    public String getInterfaceName() {
        return interfaceName;
    }

    public void setInterfaceName(String interfaceName) {
        this.interfaceName = interfaceName;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    @Override
    public String toString() {
        return "MapperInfo{" +
                "queryType=" + queryType +
                ", interfaceName='" + interfaceName + '\'' +
                ", methodName='" + methodName + '\'' +
                ", sql='" + sql + '\'' +
                ", resultType='" + resultType + '\'' +
                '}';
    }
}

其中QueryType是一個枚舉類型

package com.yotwei.core;

/**
 * Created by YotWei on 2018/8/6.
 */
public enum QueryType {
    SELECT, UPDATE, INSERT, DELETE;

    public static QueryType value(String v) {
        return valueOf(v.toUpperCase());
    }
}

下面是用一個類來讀取mapper的信息,這個類可以用枚舉單例實現

package com.yotwei.core;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by YotWei on 2018/8/6.
 */
public enum SqlMappersHolder {

    INSTANCE;

    private Map<String, MapperInfo> mi = null;

    SqlMappersHolder() {
        if (mi != null)
            return;
        mi = new HashMap<>();

        File dir = new File(SqlMappersHolder.class
                .getClassLoader()
                .getResource(Config.DEFAULT.getMapperPath())
                .getFile());

        // 用dom4j解析
        SAXReader reader = new SAXReader();
        try {
            for (String file : dir.list()) {
                Document doc = reader.read(new File(dir, file));
                Element root = doc.getRootElement();
                String className = root.attributeValue("namespace");

                for (Object o : root.elements()) {
                    Element e = (Element) o;

                    MapperInfo info = new MapperInfo();
                    info.setQueryType(QueryType.value(e.getName()));
                    info.setInterfaceName(className);
                    info.setMethodName(e.attributeValue("id"));
                    info.setResultType(e.attributeValue("resultType"));
                    info.setSql(e.getText());

                    mi.put(idOf(className, e.attributeValue("id")), info);
                }
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    public MapperInfo getMapperInfo(String className, String methodName) {
        return mi.get(idOf(className, methodName));
    }

    /*
     * 類名+"."+方法名作爲唯一id
     */
    private String idOf(String className, String methodName) {
        return className + "." + methodName;
    }
}

6、動態代理創建一個查詢接口

SqlSession提供一個getMapper方法來獲取一個DAO接口,DAO由代理類動態創建,傳入一個核心的Sql執行類SqlExecuteHandler,該類實現InvocationHandler接口

package com.yotwei.core;

import java.lang.reflect.Proxy;

/**
 * Created by YotWei on 2018/8/6.
 */
public class SqlSession {

    @SuppressWarnings("unchecked")
    public <T> T getMapper(Class<T> cls) {
        return (T) Proxy.newProxyInstance(cls.getClassLoader(),
                new Class[]{cls},
                new SqlExecuteHandler());
    }
}

SqlExecuteHandler的代碼如下,它的主要任務有

1、在invoke方法中,根據傳入的方法類獲取接口名與方法名,進而通過SqlMappersHolder獲取MapperInfo

2、根據配置連接數據庫,獲取到一個PreparedStatement對象

3、結合MapperInfo和參數列表設置PreparedStatement的參數,執行

4、獲取執行結果,通過反射技術將查詢結果映射到對應的實體類

package com.yotwei.core;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * Created by YotWei on 2018/8/6.
 */
public class SqlExecuteHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // get mapper info
        MapperInfo info = getMapperInfo(method);

        // execute sql
        return executeSql(info, args);
    }

    private MapperInfo getMapperInfo(Method method) throws Exception {
        MapperInfo info = SqlMappersHolder.INSTANCE.getMapperInfo(
                method.getDeclaringClass().getName(),
                method.getName());
        if (info == null) {
            throw new Exception("Mapper not found for method: " +
                    method.getDeclaringClass().getName() + "." + method.getName());
        }
        return info;
    }

    private Object executeSql(MapperInfo info, Object[] params)
            throws SQLException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Object result = null;
        PreparedStatement pstat = ConnectionManager.get().prepareStatement(info.getSql());
        for (int i = 0; i < params.length; i++) {
            pstat.setObject(i + 1, params[i]);
        }


        if (info.getQueryType() == QueryType.SELECT) {
            ResultSet rs = pstat.executeQuery();
            rs.first();
            // 將查詢結果映射爲Java類或基本數據類型)
            // 目前簡化版僅支持String和int兩種類型
            if (rs.getMetaData().getColumnCount() == 1) {
                switch (info.getResultType()) {
                    case "int":
                        result = rs.getInt(1);
                        break;
                    default:
                        result = rs.getString(1);
                }
            } else {
                Class<?> resTypeClass = Class.forName(info.getResultType());
                Object inst = resTypeClass.newInstance();
                for (Field field : resTypeClass.getDeclaredFields()) {
                    String setterName = "set" +
                            field.getName().substring(0, 1).toUpperCase() +
                            field.getName().substring(1);
                    Method md;

                    switch (field.getType().getSimpleName()) {
                        case "int":
                            md = resTypeClass.getMethod(setterName, new Class[]{int.class});
                            md.invoke(inst, rs.getInt(field.getName()));
                            break;

                        default:
                            md = resTypeClass.getMethod(setterName, new Class[]{String.class});
                            md.invoke(inst, rs.getString(field.getName()));
                    }
                }
                result = inst;
            }
        } else {
            result = pstat.executeUpdate();
        }
        pstat.close();
        return result;
    }

}

 

其中ConnectionManager的邏輯就是獲取到一個Connection,我的邏輯比較簡單,可以改用更好的方法替代,例如使用c3p0連接池。

package com.yotwei.core;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * Created by YotWei on 2018/8/7.
 */
public class ConnectionManager {

    public static Connection get() throws SQLException {
        return DriverManager.getConnection(
                Config.DEFAULT.getUrl(),
                Config.DEFAULT.getUser(),
                Config.DEFAULT.getPwd()
        );
    }
}

Config也是我自己定義的,主要就是放一些配置,我寫死在代碼裏了,可以改用讀取配置文件的方式

package com.yotwei.core;

/**
 * Created by YotWei on 2018/8/6.
 */
public class Config {

    public static final Config DEFAULT = new Config();

    private Config() {

    }

    private String url = "jdbc:mysql://localhost/db_test";
    private String user = "root";
    private String pwd = "root";

    private String mapperPath = "mapper/";

    public String getUrl() {
        return url;
    }

    public String getUser() {
        return user;
    }

    public String getPwd() {
        return pwd;
    }

    public String getMapperPath() {
        return mapperPath;
    }
}

7、測試

測試類如下

package test;

import com.yotwei.core.*;
import test.dao.UserDao;


/**
 * Created by YotWei on 2018/8/6.
 */
public class TestClient {

    public static void main(String[] args) {

        SqlSessionFactory factory = new SqlSessionFactory();
        SqlSession sqlSession = factory.openSession();

        UserDao userDao = sqlSession.getMapper(UserDao.class);

        System.out.println(userDao.getUserInfo(3));
    }
}
User{id=3, name='Lee', age=20, addr='Malaysia'}

可以看到代理類成功創建,並且查詢後成功映射了

源碼

源碼在Github上 

https://github.com/Kurozaki/mybatis-simple

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章