MyBatis (八)—— 自定義一個小MyBatis

最近研究了一下Mybatis的底層代碼,準備寫一個操作數據庫的小工具,實現了Mybatis的部分功能:

1. SQL語句在mapper.xml中配置。
2. 支持int,String,自定義數據類型的入參。
3. 根據mapper.xml動態創建接口的代理實現對象。

功能有限,目的是搞清楚MyBatis框架的底層思想,多學習研究優秀框架的實現思路,對提升自己的編碼能力大有裨益。

小工具使用到的核心技術點:xml解析+反射+jdk動態代理

接下來,一步一步來實現。

首先來說爲什麼要使用jdk動態代理。

傳統的開發方式:

  1. 接口定義業務方法。
  2. 實現類實現業務方法。
  3. 實例化實現類對象來完成業務操作。

Mybatis的方式:

  • 開發者只需要創建接口,定義業務方法。
  • 不需要創建實現類。
  • 具體的業務操作通過配置xml來完成。

MyBatis的方式省去了實現類的創建,改爲用xml來定義業務方法的具體實現。

那麼問題來了。

我們知道Java是面向對象的編程語言,程序在運行時執行業務方法,必須要有實例化的對象。但是,接口是不能被實例化的,而且也沒有接口的實現類,那麼此時這個對象從哪來呢?

程序在運行時,動態創建代理對象。

所以我們要用JDK動態代理,運行時結合接口和mapper.xml來動態創建一個代理對象,程序調用該代理對象的方法來完成業務。

動態代理參考→動態代理

代碼實現

這是一個根據 MyBatis 源碼設計的一個簡單自定義 MyBatis:

一、數據準備

首先說一下我用到的 Bean 類和 Dao 層接口、以及對應的數據庫表,需要解析的配置文件:

POJO類 Student:

public class Student {
    private int id;  //編號
    private String name;  //名字
    private String sex;   //性別
    private int age;      //年齡
    private String grade;    //班級

     //get、set、constructor、toString略
}

Dao層接口:StudentMapper,我只寫了兩個查詢方法:

public interface StudentMapper {
    //根據編號查詢單個學生並返回
    Student getStudentInId(int id);
    //返回所有的學生列表
    List<Student> getStudentList();
}

對應的表 student:
在這裏插入圖片描述
需要解析的配置文件:

在這裏插入圖片描述

db.properties:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test1?useSSL=false
jdbc.username=root
jdbc.password=123456

mappers文件下我只放了一個 xml 文件:student-mapper.xml:

<?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="com.lzq.dao.StudentMapper">
    <select id="getStudentInId" parameterType="java.lang.Integer" resultType="com.lzq.bean.Student">
        select * from student where id = ?
    </select>

    <select id="getStudentList" resultType="com.lzq.bean.Student">
        select * from student
    </select>
</mapper>

注意:爲了方便,我後面用 PreparedStatement 執行 SQL 語句,所以我直接將 #{} 換成了 ?,不然的話,後面需要一個方法轉換,麻煩:
在這裏插入圖片描述
好了,就是這些數據和配置,現在就開始寫 MyBatis 的代碼了!

二、自定義的 MyBatis 實現

我們將 實現 MyBatis 的大概分三個階段,數據的初始化(就是解析那些配置文件),動態代理(根據傳的 class 生成對應的代理對象給用戶),使用階段;

1、初始化階段
在這裏插入圖片描述
以上就是一個 mapper.xml 中最重要的屬性了,一個命名空間(需要用這個反射來創建對象),多個方法 id (後面要用這個方法 id 去反射方法,所以這個地方的 id 必須和接口中方法的名字一樣,否則人家底層反射的時候就反射不到了!)、以及返回值、SQL語句,所以我們需要將這些信息轉換成一個對象,Java中萬物皆對象嘛,如下 MappedStatement :

/**
 * @類名 MappedStatement
 * @類說明 存儲 SQL 語句信息
 * @作者 中都
 * @時間 2020/2/9 16:43
 */
public class MappedStatement {
    private String namespase;  //命名空間
    private String sourceId;    //命名空間+方法名
    private String resultType;  //返回值
    private String sql;         //SQL語句
   
     //get、set略
}

這個對象儲存的是一個 mapper.xml 文件中的一條 SQL 語句信息,但是一個 mapper.xml 中是會有多條 SQL 語句的,並且很多時候會有多個 mapper.xml 文件,所以我們需要一個類把這些配置信息(包括所有的 mapper.xml 文件信息、每個 mapper.xml 文件的每條 SQL 信息以及數據庫的配置信息等等)彙總,即對象 Configuration:

/**
 * @類名 Configuration
 * @類說明 把所有的配置信息糅合在一起,一般在一個項目中這個的對象是一個單例
 * @作者 中都
 * @時間 2020/2/9 17:12
 */
public class Configuration {
    private String jdbcDriver;   //數據庫驅動
    private String jdbcUrl;      //數據庫密碼
    private String jdbcUserName;  //用戶名
    private String jdbcPassword;  //密碼

    //SQL信息
    private Map<String,MappedStatement> mappedStatementMaps = new HashMap<>();

    //get、set略    
}

因爲這個配置信息對象需要儲存多個 mapper.xml 文件中的多條 SQL 語句信息,所以拿一個 map 對象去儲存,key 是 namespace + id,即接口全路徑名+方法名,value 就是對應的要執行的 SQL 語句;

我們平時在使用 MyBatis 的時候會有一個 SqlSessionFactory 的工廠類創建我們需要的 SqlSession 對象供我們使用,所以我模仿他們的實現,也實現了一個 SqlSessionFactory 工廠類,這個類用來初始化配置信息和創建 SQLSession 對象:

/**
 * @類名 SqlSessionFactory
 * @類說明 完成 Configuration 實例化,生產 SqlSession
 * @作者 中都
 * @時間 2020/2/9 17:20
 */
public class SqlSessionFactory {
    //final修飾,單例,配置信息
    private final Configuration configuration = new Configuration();

    //記錄mapper.xml文件存放的位置,這個 resources 文件下的 mappers 訪問不到,我給了全路徑
    public static final String MAPPER_CONFIG = "G:\\idea工程\\框架複習\\src\\main\\resources\\mappers";
    //記錄數據庫連接信息文件存放的位置
    public static final String DB_CONFIG_FILE = "db.properties";


    public SqlSessionFactory() {
        loadDbInfo();
        loadMappersInfo();
    }

    /**
     * 加載數據庫信息
     */
    private void loadDbInfo() {
        //加載數據庫配置文件
        InputStream dbin = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
        Properties p = new Properties();
        try {
            p.load(dbin); //將配置信息寫入 Properties 對象
        } catch (IOException e) {
            e.printStackTrace();
        }
        //將數據庫配置信息寫入 config 對象
        configuration.setJdbcDriver(p.get("jdbc.driver").toString());
        configuration.setJdbcUrl(p.get("jdbc.url").toString());
        configuration.setJdbcUserName(p.get("jdbc.username").toString());
        configuration.setJdbcPassword(p.get("jdbc.password").toString());
    }

    /**
     * 加載所有文件夾下的 xml 信息
     */
    private void loadMappersInfo() {
        File mappers = new File(MAPPER_CONFIG);
        if(mappers.isDirectory()) {
            File[] files = mappers.listFiles();
            for (File f : files) {
                loadMapperInfo(f);
            }
        }
    }

    /**
     * 解析單個 xml 文件
     * @param f
     */
    private void loadMapperInfo(File f) {
        //創建 saxReader 對象
        SAXReader reader = new SAXReader();
        //通過 read 方法讀取一個文件,轉換成 Document 對象
        Document document = null;
        try {
            document = reader.read(f);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        //獲取根節點元素
        Element root = document.getRootElement();
        //獲取命名空間
        String namespace = root.attribute("namespace").getData().toString();
        //獲取 select 子節點列表
        List<Element> selects = root.elements("select");
        //遍歷 select 節點,將信息記錄到 MappedStatement 對象,並登記到 configuration
        for (Element e : selects) {
            MappedStatement mappedStatement = new MappedStatement();  //實例化一條 sql 語句記錄
            String id = e.attribute("id").getData().toString();
            String resultType = e.attribute("resultType").getData().toString();
            String sql = e.getData().toString();  //去取 sql 語句
            String sourceId = namespace +"."+id;
            //給 MappedStatement 對象 賦值
            mappedStatement.setNamespase(namespace);
            mappedStatement.setResultType(resultType);
            mappedStatement.setSourceId(sourceId);
            mappedStatement.setSql(sql);
            //註冊到 configuration
            configuration.getMappedStatementMaps().put(sourceId,mappedStatement);
        }
    }


    /**
     * 創建 SqlSession 對象
     * @return
     */
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }
}

至此,第一階段配置信息的初始化完成;

2、動態代理
既然要創建 SqlSession對象,那就需要 SqlSession 了,裏面也只是實現了兩個簡單的查詢方法:

/**
 * @類名 SqlSession
 * @類說明
 * 1、對外提供方法的接口
 * 2、對內將請求轉發給 executor 執行
 * @作者 中都
 * @時間 2020/2/9 18:10
 */
public interface SqlSession {
    /**
     * 根據傳入的條件查詢單一結果
     * @param statement sql語句
     * @param parameter 傳入的參數
     * @param <T> 返回值
     * @return
     */
    <T> T selectOne(String statement,Object parameter);

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

    <T> T getMapper(Class<T> type);
}

上面只是一個接口,這是他的實現類:

/**
 * @類名 DefaultSqlSession
 * @類說明
 *  1、對外提供方法的接口
 *  2、對內將請求轉發給 executor 執行
 * @作者 中都
 * @時間 2020/2/9 18:17
 */
public class DefaultSqlSession implements SqlSession {
    //final修飾,單例,配置信息
    private final Configuration conf;
    //真正的執行者,委託者
    private Executor executor;

    public DefaultSqlSession(Configuration conf) {
        this.conf = conf;
        this.executor = new DefaultExecutor(conf);
    }

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        List<Object> selectList = this.selectList(statement,parameter);
        if(selectList == null || selectList.size() == 0) {
            return null;
        }else if(selectList.size() == 1) {
            return (T)selectList.get(0);
        }else {
            throw new RuntimeException("這不是單條記錄!");
        }
    }

    @Override
    public <E> List<E> selectList(String statement, Object parameter) {
        MappedStatement mappedStatement = conf.getMappedStatementMaps().get(statement);
        return executor.query(mappedStatement,parameter);
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        MapperProxy mapperProxy = new MapperProxy(this);
        return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},mapperProxy);
    }
}

因爲 getMapper 是通過動態代理實現的,根據單一職責原則,SqlSession 主要是完成數據查詢的,那動態代理的實現就需要另外一個類來完成了,即 MapperProxy :

/**
 * @類名 MapperProxy
 * @類說明 完成動態代理
 * @作者 中都
 * @時間 2020/2/9 20:39
 */
public class MapperProxy implements InvocationHandler {
    private SqlSession sqlSession;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //判斷方法的返回值是否是集合類型
        if(Collection.class.isAssignableFrom(method.getReturnType())) {
            //method.getDeclaringClass().getName()+"."+method.getName() 即類名.方法名,即配置文件中的命名空間
            return sqlSession.selectList(method.getDeclaringClass().getName()+"."+method.getName(),args == null?null:args[0]);
        }else {
            return sqlSession.selectOne(method.getDeclaringClass().getName()+"."+method.getName(),args == null?null:args[0]);
        }
    }
}

在上面 SqlSession 的實現類中我們也看到,SqlSession 就是那個代理類,它每次操作都是調用 Executor 去實現的:

/**
 * @類名 Executor
 * @類說明 核心接口之一,定義了數據庫操作的最基本方法,SqlSession的功能都基於它來實現
 * @作者 中都
 * @時間 2020/2/9 20:01
 */
public interface Executor {
    /**
     * 查詢接口
     * @param ms 封裝有 SQL 語句的 MappedStatement 對象
     * @param parameter 傳入的 SQL 參數
     * @param <E> 將參數轉化成指定的結果集返回
     * @return
     */
    <E> List<E> query(MappedStatement ms, Object parameter);
}

下面是 Executor 的具體實現類,主要完成數據庫操作,並將結果映射成需要返回的對象類型:

public class DefaultExecutor implements Executor {
    private final Configuration configuration;

    public DefaultExecutor(Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * 用於查詢
     * @param ms 封裝有 SQL 語句的 MappedStatement 對象
     * @param parameter 傳入的 SQL 參數
     * @param <E>
     * @return
     */
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter) {
        List<E> ret = new ArrayList<>();//定義結果集;
        try {
            Class.forName(configuration.getJdbcDriver());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //獲取連接,從 MappedStatement 獲取數據庫信息
            connection = DriverManager.getConnection(configuration.getJdbcUrl(),configuration.getJdbcUserName(),configuration.getJdbcPassword());
            //創建 preparedStatement 對象,從MappedStatement獲取SQL語句
            preparedStatement = connection.prepareStatement(ms.getSql());
            //處理SQL語句中的佔位符
            parameterize(preparedStatement,parameter);
            //執行查詢操作獲取 resultSet
            resultSet = preparedStatement.executeQuery();
            //將結果集通過反射技術,填充到list集合中
            handlerResultSet(resultSet,ret,ms.getResultType());
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                resultSet.close();
                preparedStatement.close();
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return ret;
    }

    /**
     * 對佔位符進行處理
     * @param preparedStatement
     * @param parameter
     */
    private void parameterize(PreparedStatement preparedStatement,Object parameter) throws SQLException {
        if(parameter instanceof Integer) {
            preparedStatement.setInt(1,(int)parameter);
        }else if(parameter instanceof Long) {
            preparedStatement.setLong(1,(long)parameter);
        }else if(parameter instanceof String) {
            preparedStatement.setString(1,(String)parameter);
        }
    }

    /**
     * 讀取 resultset 中的數據,並轉換成目標對象
     * @param resultSet
     * @param ret
     * @param className
     * @param <E>
     */
    private <E> void handlerResultSet(ResultSet resultSet,List<E> ret,String className) {
        Class<E> eClass = null;
        try {
            //通過反射獲取類對象;
            eClass = (Class<E>)Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        try {
            while (resultSet.next()) {
                //通過反射實例化對象
                Object o = eClass.newInstance();
                //使用反射將 resultset 中的數據填充到 o 對象
                ReflectionUtil.setPropToBeanFromResultSet(o,resultSet);
                //將對象加入到返回集中
                ret.add((E)o);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

它用到的工具類:

/**
 * @類名 ReflectionUtil
 * @類說明 利用反射幫助創建對象
 * @作者 中都
 * @時間 2020/2/9 21:37
 */
public class ReflectionUtil {

    /**
     * 爲指定的 bean 的 proName 屬性賦值 value
     * @param bean 目標對象
     * @param propName  對象的屬性名
     * @param value 值
     */
    public static void setPropToBean(Object bean,String propName,Object value) {
        Field f;
        try {
            f = bean.getClass().getDeclaredField(propName);//獲得對象指定的屬性
            f.setAccessible(true); //可以訪問
            f.set(bean,value);//爲屬性賦值
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 將結果集映射到對象
     * @param entity
     * @param resultSet
     * @throws SQLException
     */
    public static void setPropToBeanFromResultSet(Object entity, ResultSet resultSet) throws SQLException {
        Field[] declaredFields = entity.getClass().getDeclaredFields(); //得到對象的所有屬性
        for (int i = 0; i < declaredFields.length; i++) {
            if(declaredFields[i].getType().getSimpleName().equals("String")) {  //字符串類型
                setPropToBean(entity,declaredFields[i].getName(),resultSet.getString(declaredFields[i].getName()));
            }else if(declaredFields[i].getType().getSimpleName().equals("int")) {
                setPropToBean(entity,declaredFields[i].getName(),resultSet.getInt(declaredFields[i].getName()));
            }else if(declaredFields[i].getType().getSimpleName().equals("long")) {
                setPropToBean(entity,declaredFields[i].getName(),resultSet.getLong(declaredFields[i].getName()));
            }
        }

    }
}

至此,整個自定義 MyBatis 就寫完了,現在只需要測試一下就好:

3、測試

public class Test2 {
    @Test
    public void Test2() {
        SqlSessionFactory factory = new SqlSessionFactory();
        SqlSession sqlSession =  factory.openSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        Student student = mapper.getStudentInId(3);
        System.out.println(student);

        System.out.println(" ======================== ");
        List<Student> studentList = mapper.getStudentList();
        for (Student stu : studentList) {
            System.out.println(stu);
        }
    }
}

結果如下:
在這裏插入圖片描述

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