框架解決了什么問(wèn)題
目前主流的Java Web項(xiàng)目都采用SSM(spring springmvc mybatis)框架,其中mybatis框架源碼是最簡(jiǎn)單的,想入手源碼學(xué)習(xí)的同學(xué)很推薦從mybatis開始。本系列文章是我對(duì)mybatis源碼學(xué)習(xí)的一些梳理總結(jié),可以幫助你更高效得理解mybatis。
在學(xué)習(xí)一個(gè)框架源碼之前你首先要問(wèn)自己幾個(gè)問(wèn)題。
1.這個(gè)框架解決了什么問(wèn)題?
2.為了解決這個(gè)問(wèn)題,你是如何設(shè)計(jì)的?框架是如何設(shè)計(jì)的?
3.框架是怎么樣給你提供擴(kuò)展性的?
通常第一個(gè)問(wèn)題都很簡(jiǎn)單,可以直接在mybatis官網(wǎng)找到答案。mybatis官網(wǎng)是這么解釋的:
MyBatis 是一款優(yōu)秀的持久層框架,它支持自定義 SQL、存儲(chǔ)過(guò)程以及高級(jí)映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設(shè)置參數(shù)和獲取結(jié)果集的工作。MyBatis 可以通過(guò)簡(jiǎn)單的 XML 或注解來(lái)配置和映射原始類型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 對(duì)象)為數(shù)據(jù)庫(kù)中的記錄。
我的理解是mybatis是一個(gè)持久層框架,它解決了使用JDBC冗余復(fù)雜代碼操作數(shù)據(jù)庫(kù)的問(wèn)題,并且可以通過(guò)XML或者注解的方式來(lái)實(shí)現(xiàn)ORM(對(duì)象關(guān)系映射)技術(shù),使我們?cè)陧?xiàng)目中操作數(shù)據(jù)庫(kù)更為簡(jiǎn)單和耦合性更好。
自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的mybatis框架
在學(xué)習(xí)源碼之前先自己簡(jiǎn)單實(shí)現(xiàn)一遍類似的功能是很有必要的,可以幫助你更好的理解框架是解決問(wèn)題的。假如要你實(shí)現(xiàn)一個(gè)簡(jiǎn)單的mybatis框架你會(huì)怎么做?
先不用去看mybatis的細(xì)節(jié),我們知道使用mybatis的時(shí)候在注解或者xml中定義一個(gè)sql然后綁定到一個(gè)接口的方法上就能執(zhí)行這條sql,并給你封裝好結(jié)果返回。實(shí)現(xiàn)這個(gè)功能并不難,我們可以先嘗試實(shí)現(xiàn)一遍。
我們也定義一個(gè)接口,不需要寫實(shí)現(xiàn)類就可以執(zhí)行接口定義的方法,且這個(gè)方法幫我們?nèi)?zhí)行注解里的sql語(yǔ)句:
public interface DistrictDao {
@MyQuery(" select * from district where id = ? ")
List<District> getDistrictById(Integer id);
}
要實(shí)現(xiàn)這個(gè)功能用JDK的動(dòng)態(tài)代理就可以了,我們可以建一個(gè)工廠類,用這個(gè)工廠類可以生成任意接口的代理對(duì)象,代理對(duì)象幫我們執(zhí)行方法上定義的SQL并封裝成結(jié)果。
簡(jiǎn)單的實(shí)現(xiàn)了一下可以執(zhí)行任意查詢語(yǔ)句的代理工廠類:
public class DBHandlerProxyFactory {
/**
* 獲取代理對(duì)象
*
* @param dataSource
* @param clazz
* @param <T>
* @return
*/
public static <T> T getTargetClass(DataSource dataSource, Class<T> clazz) {
return (T) Proxy.newProxyInstance(
clazz.getClassLoader(), // 傳入ClassLoader
new Class[]{clazz}, // 傳入要實(shí)現(xiàn)的接口
getQueryInvHandler(dataSource)); // 傳入處理調(diào)用方法的InvocationHandler
}
/**
* 獲取動(dòng)態(tài)代理執(zhí)行handler
* @param dataSource
* @return
*/
public static InvocationHandler getQueryInvHandler(DataSource dataSource) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MyQuery myQuery = method.getAnnotation(MyQuery.class);
if (myQuery != null) {
//1.拿到目標(biāo)方法上面的sql
String sql = myQuery.value();
//2.拿到數(shù)據(jù)庫(kù)連接并執(zhí)行sql
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
int i = 0;
while (sql.contains("?")) {
sql = sql.replaceFirst("\\?", args[i] + "");
i++;
}
System.out.println("執(zhí)行sql語(yǔ)句:" + sql);
ResultSet resultSet = statement.executeQuery(sql);
//3.獲取目標(biāo)方法的返回類型
Type returnType = method.getGenericReturnType();
Class<?> returnTypeClass = null;
if (returnType instanceof Class) {
returnTypeClass = (Class<?>) returnType;
} else {
ParameterizedTypeImpl r = (ParameterizedTypeImpl) method.getGenericReturnType();
returnTypeClass = r.getRawType();
if (Collection.class.isAssignableFrom(returnTypeClass)) {
Type[] types = r.getActualTypeArguments();
returnTypeClass = (Class<?>) types[0];
}
}
//4.封裝返回結(jié)果
List<?> list = getQueryResult(resultSet, returnTypeClass);
resultSet.close();
connection.close();
return list;
}
return new ArrayList<>();
}
};
return handler;
}
/**
* 獲取查詢結(jié)果
*
* @param resultSet
* @param clazz
* @param <T>
* @return
*/
public static <T> List<T> getQueryResult(ResultSet resultSet, Class<T> clazz) {
List<T> list = new ArrayList<>();
try {
while (resultSet.next()) {
T obj = clazz.newInstance();
ResultSetMetaData rsmeta = resultSet.getMetaData();
int count = rsmeta.getColumnCount();
for (int i = 0; i < count; i++) {
String name = rsmeta.getColumnName(i + 1);
Field f = obj.getClass().getDeclaredField(name);
f.setAccessible(true);
f.set(obj, resultSet.getObject(name));
}
list.add(obj);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
這里就不闡述動(dòng)態(tài)代理了,動(dòng)態(tài)代理handler執(zhí)行思路是:
1.拿到目標(biāo)方法上面的sql
2.拿到數(shù)據(jù)庫(kù)連接并使用JDBC的API來(lái)執(zhí)行sql
3.獲取目標(biāo)方法的返回類型
4.封裝返回結(jié)果
通過(guò)這四步就可以不用再寫冗余的JDBC代碼了,只用寫接口就可以查詢數(shù)據(jù)并返回封裝好的結(jié)果了。執(zhí)行代碼是這樣的:
@SpringBootTest
class MybatisStudyApplicationTests {
@Autowired
private DataSource dataSource;
@Test
void testProxy(){
DistrictDao districtDao = DBHandlerProxyFactory.getTargetClass(dataSource,DistrictDao.class);
List<District> districtList = districtDao.getDistrictById(1);
System.out.println(districtList);
}
}
OK,到這里自己嘗試設(shè)計(jì)一個(gè)簡(jiǎn)單“MyBatis”功能就完成了,接下來(lái)我們需要分析一下我們自己寫的這個(gè)簡(jiǎn)單的框架有那些問(wèn)題和缺陷。我們帶著問(wèn)題再去看Mybatis源碼就會(huì)明白作者的設(shè)計(jì)意圖,再看看Mybatis作者是如何解決這些問(wèn)題的,這樣學(xué)習(xí)源碼會(huì)更有收獲一些。
自己實(shí)現(xiàn)mybatis框架的缺陷
1.操作數(shù)據(jù)庫(kù)只支持查詢的操作而且代碼復(fù)用性不高,如果要支持增刪改查的操作該如何設(shè)計(jì)
2.封裝結(jié)果集只支持集合類型,而且返回的實(shí)例字段只能和表字段名稱一致才能映射,如何把結(jié)果集封裝設(shè)計(jì)的更靈活一些
3.參數(shù)轉(zhuǎn)換如何實(shí)現(xiàn),例如傳入的是Java的日期類型,應(yīng)該怎么樣轉(zhuǎn)換成數(shù)據(jù)庫(kù)支持的格式
4.預(yù)編譯SQL以及數(shù)據(jù)庫(kù)的事務(wù)如何實(shí)現(xiàn)
5.如何加入緩存策略
目前能想到的就是這些,然后我們帶著這些問(wèn)題逐步深入mybatis的源碼來(lái)看一下mybatis是如何設(shè)計(jì)的。
本文涉及的源碼放在github上: