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

NO IMAGE

手寫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);
    }

測試結果如下

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

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

一、@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);
    }
}

然後運行結果如下

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

二、@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());
    }
}

執行後我的運行結果如下

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

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

/**
* @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;
}
}

相關文章

MangoDB的下載和安裝

團長大人的學習筆記——RabbitMQ

團長大人的學習筆記——Hibernate

團長大人的學習筆記——自定義Java註解