手寫源碼(四):自己實現Mybatis 手寫MyBatis

手寫MyBatis

如題,這次我又來作死試試編寫類似Mybatis的持久層框架了

MyBatis的難點

  • 如何在沒有實例的情況下創建Mapping接口的實現類並且調用接口中的方法
    • 使用字節技術創建子類
    • 使用匿名內部類
    • 使用動態代理創建對象(我們使用這個)

創建一個接口UserMapper,再創建一個實體類User

使用JDK的動態代理,創建一個代理處理器

public class InvocationHandlerMybatis implements InvocationHandler {
    /**
     *
     * @param proxy     代理對象
     * @param method    攔截的方法
     * @param args      方法上的參數
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("開始代理");
        return 1;
    }
}

包裝上面的代理

public class SqlSession {
    /**加載Mapper接口*/
    public static <T> T getMapper(Class clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandlerMybatis());
    }
}

使用測試類測試一下

    public static void main(String[] args) {
        UserMapper userMapper = SqlSession.getMapper(UserMapper.class);
        int i = userMapper.insertUser("", "");
        System.out.println(i);
    }

測試結果如下



這樣就可以實現拿到接口的方法參數並且自行控制對象的返回值

一、@Insert的實現步驟

  • 判斷方法上是否存在@Insert註解,存在的話,獲取上面的SQL語句
  • 獲取方法的參數,和SQL參數經行匹配,並且替換SQL的參數(變成問號)
  • 調用JDBC代碼執行語句並獲取返回值

具體實現如下

/**
 * @author libi
 * 用於動態代理,獲取方法的參數並且給返回值
 */
public class InvocationHandlerMybatis implements InvocationHandler {
    /**
     * @param proxy     代理對象
     * @param method    攔截的方法
     * @param args      方法上的參數
     * @return          方法的返回值
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("開始代理");
        //判斷方法上是否存在Insert註解
        ExtInsert extInsert = method.getDeclaredAnnotation(ExtInsert.class);
        if (extInsert != null) {
            //執行插入的操作,返回影響行數
            return doInsert(method, args, extInsert);
        }
        return null;
    }

    /**
     * 執行插入的操作
     * @param method
     * @param args
     * @param extInsert
     * @return
     */
    private int doInsert(Method method, Object[] args, ExtInsert extInsert) {
        //獲取Sql語句
        String sql = extInsert.value();
        System.out.println("insert sql:" + sql);
        //獲取方法參數和Sql語句進行匹配
        //定義一個Map,Key是參數名,Value是參數值
        ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
        //獲取方法上的參數
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            //獲取參數名稱和參數的值
            ExtParam param = parameters[i].getDeclaredAnnotation(ExtParam.class);
            if (param != null) {
                String name = param.value();
                Object value = args[i];
                System.out.println("paramName:"+name+",paramValue:"+value);
                map.put(name, value);
            }
        }
        //怕打亂順序而把sql語句的參數放在一個有序的數組裏
        List<Object> sqlParam = new ArrayList<>();
        String[] sqlInsertParameter = SQLUtils.sqlInsertParameter(sql);
        for (String paramName : sqlInsertParameter) {
            Object paramValue = map.get(paramName);
            sqlParam.add(paramValue);
        }
        System.out.println();
        //把參數替換成?
        sql = SQLUtils.parameQuestion(sql, sqlInsertParameter);
        System.out.println("new sql:"+sql);
        //執行JDBC
        return JDBCUtils.insert(sql, false, sqlParam);
    }
}

測試這個方法

我們定義一個Mapper

public interface UserMapper {
    @ExtInsert("insert into user(username,password) values (#{userName},#{password})")
    int insertUser(@ExtParam("userName") String userName, @ExtParam("password") String password);
}

然後再主函數裏使用代理調用這個方法

public class Cluster {
    public static void main(String[] args) {
        UserMapper userMapper = SqlSession.getMapper(UserMapper.class);
        int i = userMapper.insertUser("name", "123");
        System.out.println(i);
    }
}

然後運行結果如下


二、@Select的實現思路

  • 找到方法裏帶有@Select註解的方法,拿到Spl語句
  • 獲取方法上的參數,綁定,然後把參數替換成?
  • 調用JDBC調用底層
  • 使用反射機制實例化實體類對象(獲取方法返回的類型,使用反射實例化對象)

核心代碼如下

和上面不同的是,我重構了InvocationHandleMybatis類的代碼,複用了一些代碼

/**
     * 執行查詢的操作
     * @param method
     * @param args
     * @param extSelect
     * @return 查詢結果,可能是實體類對象,List或者基礎類型
     */
    private Object doSelect(Method method, Object[] args, ExtSelect extSelect) throws SQLException, IllegalAccessException, InstantiationException, NoSuchFieldException {
        //獲取Sql語句
        String sql = extSelect.value();
        System.out.println("select sql:" + sql);
        //獲取方法參數和Sql語句進行匹配
        ConcurrentHashMap<String, Object> paramMap = getParamMap(method, args);

        //怕Sql參數順序和@Param參數順序不一致而把sql語句的參數放在一個有序的數組裏
        List<Object> sqlParamValue = new ArrayList<>();
        List<String> sqlSelectParameter = SQLUtils.sqlSelectParameter(sql);
        for (String paramName : sqlSelectParameter) {
            Object paramValue = paramMap.get(paramName);
            sqlParamValue.add(paramValue);
        }
        //把參數替換成?
        sql = SQLUtils.parameQuestion(sql, sqlSelectParameter);
        System.out.println("new sql:"+sql);
        //執行JDBC
        ResultSet resultSet = JDBCUtils.query(sql, sqlParamValue);
        //判斷是否有結果集
        if (!resultSet.next()) {
            return null;
        }
        resultSet.previous();
        //使用反射獲取方法類型
        Class<?> returnType = method.getReturnType();
        //使用反射機制實例化對象
        Object result = returnType.newInstance();
        //遍歷這個結果集
        while (resultSet.next()) {
            for (String paramName : sqlSelectParameter) {
                //獲取參數的值
                Object resultValue = resultSet.getObject(paramName);
                //使用反射機制賦值
                Field field = returnType.getDeclaredField(paramName);
                field.setAccessible(true);
                field.set(result, resultValue);
            }
        }
        return result;
    }

在使用上面的代碼時,我會檢測方法上是否有@Select註解,有的話說明這個方法是用於查詢語句的,我們就把這個註解傳進來

我們改寫UserMapper類,增加Select方法,如下

public interface UserMapper {
    @ExtInsert("insert into user(username,password) values (#{userName},#{password})")
    int insertUser(@ExtParam("userName") String userName, @ExtParam("password") String password);

    @ExtSelect("select * from user where username=#{userName} and password=#{password}")
    User selectUser(@ExtParam("userName") String userName, @ExtParam("password") String password);
}

改寫測試用的代碼,如下

public class Cluster {
    public static void main(String[] args) {
        UserMapper userMapper = SqlSession.getMapper(UserMapper.class);
        User user = userMapper.selectUser("name", "123");
        System.out.println(user.getUserName());
    }
}

執行後我的運行結果如下


附:整個核心的代理類如下

/**
 * @author libi
 * 用於動態代理,獲取方法的參數並且給返回值
 */
public class InvocationHandlerMybatis implements InvocationHandler {
    /**
     * @param proxy     代理對象
     * @param method    攔截的方法
     * @param args      方法上的參數
     * @return          方法的返回值
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("開始代理");
        //判斷方法上是否存在Insert註解
        ExtInsert extInsert = method.getDeclaredAnnotation(ExtInsert.class);
        if (extInsert != null) {
            //執行插入的操作,返回影響行數
            return doInsert(method, args, extInsert);
        }
        //判斷方法上是否有Select註解
        ExtSelect extSelect = method.getDeclaredAnnotation(ExtSelect.class);
        if (extSelect != null) {
            //執行查詢的操作,返回實際實體類或者List
            return doSelect(method, args, extSelect);
        }
        return null;
    }

    /**
     * 執行插入的操作
     * @param method
     * @param args
     * @param extInsert
     * @return 影響行數
     */
    private int doInsert(Method method, Object[] args, ExtInsert extInsert) {
        //獲取Sql語句
        String sql = extInsert.value();
        System.out.println("insert sql:" + sql);
        //獲取方法參數和Sql語句進行匹配
        ConcurrentHashMap<String, Object> paramMap = getParamMap(method, args);

        //怕Sql參數順序和@Param參數順序不一致而把sql語句的參數放在一個有序的數組裏
        List<Object> sqlParamValue = new ArrayList<>();
        String[] sqlInsertParameter = SQLUtils.sqlInsertParameter(sql);
        for (String paramName : sqlInsertParameter) {
            Object paramValue = paramMap.get(paramName);
            sqlParamValue.add(paramValue);
        }
        //把參數替換成?
        sql = SQLUtils.parameQuestion(sql, sqlInsertParameter);
        System.out.println("new sql:"+sql);
        //執行JDBC
        return JDBCUtils.insert(sql, false, sqlParamValue);
    }

    /**
     * 執行查詢的操作
     * @param method
     * @param args
     * @param extSelect
     * @return 查詢結果,可能是實體類對象,List或者基礎類型
     */
    private Object doSelect(Method method, Object[] args, ExtSelect extSelect) throws SQLException, IllegalAccessException, InstantiationException, NoSuchFieldException {
        //獲取Sql語句
        String sql = extSelect.value();
        System.out.println("select sql:" + sql);
        //獲取方法參數和Sql語句進行匹配
        ConcurrentHashMap<String, Object> paramMap = getParamMap(method, args);

        //怕Sql參數順序和@Param參數順序不一致而把sql語句的參數放在一個有序的數組裏
        List<Object> sqlParamValue = new ArrayList<>();
        List<String> sqlSelectParameter = SQLUtils.sqlSelectParameter(sql);
        for (String paramName : sqlSelectParameter) {
            Object paramValue = paramMap.get(paramName);
            sqlParamValue.add(paramValue);
        }
        //把參數替換成?
        sql = SQLUtils.parameQuestion(sql, sqlSelectParameter);
        System.out.println("new sql:"+sql);
        //執行JDBC
        ResultSet resultSet = JDBCUtils.query(sql, sqlParamValue);
        //判斷是否有結果集
        if (!resultSet.next()) {
            return null;
        }
        resultSet.previous();
        //使用反射獲取方法類型
        Class<?> returnType = method.getReturnType();
        //使用反射機制實例化對象
        Object result = returnType.newInstance();
        //遍歷這個結果集
        while (resultSet.next()) {
            for (String paramName : sqlSelectParameter) {
                //獲取參數的值
                Object resultValue = resultSet.getObject(paramName);
                //使用反射機制賦值
                Field field = returnType.getDeclaredField(paramName);
                field.setAccessible(true);
                field.set(result, resultValue);
            }
        }
        return result;
    }

    /**
     * 建立方法上的參數和值@Param參數名的映射
     * @param method
     * @param args
     * @return
     */
    private ConcurrentHashMap<String, Object> getParamMap(Method method, Object[] args) {
        //定義一個Map,Key是參數名,Value是參數值
        ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
        //獲取方法上的參數
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            //獲取參數名稱和參數的值
            ExtParam param = parameters[i].getDeclaredAnnotation(ExtParam.class);
            if (param != null) {
                String name = param.value();
                Object value = args[i];
                System.out.println("paramName:"+name+",paramValue:"+value);
                map.put(name, value);
            }
        }
        return map;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章