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