自己動手實現一個簡單的Mybatis(初級版本1.0)
阿新 • • 發佈:2018-12-06
手寫Mybatis-v1.0
原始碼連結(包括v1.0與v2.0): https://github.com/staticLin/customMyBatis.git
從上一個文章 ---Mybatis概述中瞭解到了Mybatis的主要架構與底層原理流程,結尾給出了一個巨集觀流程圖,可以知道,大致我們可以從三個模組入手:
- SqlSession:含有屬性Configuration、Excutor,含有方法getMapper,selectOne(先實現一個查詢方法)
- Configuration:含有MapperRegistry(mapper介面、方法、SQL),含有方法getMapper
- Executor:含有方法doQuery查詢資料庫
這樣來看,我們的MyBatis1.0的大致脈絡已經出來了。接下來就coding吧~
先是自定義的SqlSession (這裡忽略SqlSessionFactory解析xml資源過程,1.0版本簡化)
/** * @description: 自定義的SqlSession * @author: linyh * @create: 2018-10-31 16:31 **/ public class CustomSqlSession { //持有兩個關鍵物件 private CustomConfiguration configuration; private CustomExecutor executor; /** * 用構造器將兩個物件形成關係 */ public CustomSqlSession(CustomConfiguration configuration, CustomExecutor executor) { this.configuration = configuration; this.executor = executor; } public CustomConfiguration getConfiguration() { return configuration; } /** * 委派configuration獲取mapper */ public <T> T getMapper(Class<T> clazz){ return configuration.getMapper(clazz, this); } /** * 委派executor查詢 */ public <T> T selectOne(String statement, String parameter){ return executor.query(statement, parameter); } }
然後是自定義Configuration實現getMapper方法,這裡也初始化一個MapperProxyFactory為了存放所有的Mapper。再寫一個驗證Mapper是否存在的方法和根據Class獲取對應mapper的MapperProxyFactory。
/** * @description: * @author: linyh * @create: 2018-10-31 16:32 **/ public class CustomConfiguration { public final MapperRegistory mapperRegistory = new MapperRegistory(); public static final Map<String, String> mappedStatements = new HashMap<>(); //TODO 改用anontation掃描 (暫時HardCode) //初始化時Configuration載入所有Mapper方法與Sql語句 public CustomConfiguration() { mapperRegistory.addMapper(TestMapper.class); mappedStatements.put("com.test.mybatis.v1.mapper.TestCustomMapper.selectByPrimaryKey" , "select * from test where id = %d"); } //MapperProxy根據statementName查詢是否有對應SQL public boolean hasStatement(String statementName) { return mappedStatements.containsKey(statementName); } //MapperProxy根據statementID獲取SQL public String getMappedStatement(String id) { return mappedStatements.get(id); } public <T> T getMapper(Class<T> clazz, CustomSqlSession sqlSession) { return mapperRegistory.getMapper(clazz, sqlSession); } }
這裡完善MapperProxy與MapperRegistory(註冊所有的Mapper的Map)
/**
* @description: Mapper動態代理者
* @author: linyh
* @create: 2018-10-31 16:52
**/
public class MapperProxy implements InvocationHandler{
private CustomSqlSession sqlSession;
public MapperProxy(CustomSqlSession sqlSession) {
this.sqlSession = sqlSession;
}
/**
* 每一個Mapper的每個方法都將執行invoke方法,此方法判斷方法名是否維護在Configuration中,如有則取出SQL
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (sqlSession.getConfiguration().hasStatement(method.getDeclaringClass().getName()+"."+method.getName())) {
String sql = sqlSession.getConfiguration().getMappedStatement(method.getDeclaringClass().getName()+"."+method.getName());
return sqlSession.selectOne(sql, args[0].toString());
}
return method.invoke(proxy, args);
}
}
/**
* @description: mapper註冊類
* @author: linyh
* @create: 2018-10-31 16:51
**/
public class MapperRegistory {
//用一個Map維護所有Mapper
private final Map<Class<?>, MapperProxyFactory> knownMappers = new HashMap<>();
//TODO Configuration解析anontation之後呼叫方法初始化所有mapper
public <T> void addMapper(Class<T> clazz){
knownMappers.put(clazz, new MapperProxyFactory(clazz));
}
/**
* getMapper最底層執行者,獲取mapper的MapperProxyFactory物件
*/
public <T> T getMapper(Class<T> clazz, CustomSqlSession sqlSession) {
MapperProxyFactory proxyFactory = knownMappers.get(clazz);
if (proxyFactory == null) {
throw new RuntimeException("Type: " + clazz + " can not find");
}
return (T)proxyFactory.newInstance(sqlSession);
}
/**
* 內部類實現一個Factory生成Mapper的代理
*/
public class MapperProxyFactory<T>{
private Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public T newInstance(CustomSqlSession sqlSession) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, new MapperProxy(sqlSession));
}
}
}
到這裡getMapper就實現好了,每次getMapper都能生成對應的MapperProxy代理。接下來實現Executor的查詢方法。
先定義一個介面(2.0將加入CacheExecutor,面向介面程式設計便於擴充套件)。
/**
* @description:
* @author: linyh
* @create: 2018-10-31 16:32
**/
public interface CustomExecutor {
<T> T query(String statement, String parameter);
}
然後是具體的實現類,其中使用JDBC查詢。
/**
* @description: 自定義Executor
* @author: linyh
* @create: 2018-10-31 17:46
**/
public class CustomDefaultExecutor implements CustomExecutor{
@Override
public <T> T query(String statement, String parameter) {
Connection conn = null;
PreparedStatement preparedStatement = null;
Test test = null;
try {
conn = getConnection();
//TODO ParameterHandler
preparedStatement = conn.prepareStatement(String.format(statement, Integer.parseInt(parameter)));
preparedStatement.execute();
ResultSet rs = preparedStatement.getResultSet();
//TODO ObjectFactory
test = new Test();
//TODO ResultSetHandler
while (rs.next()) {
test.setId(rs.getInt(1));
test.setNums(rs.getInt(2));
test.setName(rs.getString(3));
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return (T)test;
}
public Connection getConnection() throws SQLException {
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/gp?serverTimezone=UTC";
String username = "root";
String password = "admin";
Connection conn = null;
try {
Class.forName(driver); //classLoader,載入對應驅動
conn = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}
這樣查詢方法也大致完成了,建立實體與Mapper,然後開始測試一下吧~
/**
* @description: 實體類
* @author: linyh
* @create: 2018-10-31 17:03
**/
public class Test {
private Integer id;
private Integer nums;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getNums() {
return nums;
}
public void setNums(Integer nums) {
this.nums = nums;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"id=" + id +
", nums=" + nums +
", name='" + name + '\'' +
'}';
}
}
/**
* @Author:linyh
* @Date: 2018/10/31 16:56
* @Modified By:
*/
public interface TestCustomMapper {
Test selectByPrimaryKey(int id);
}
/**
* @description:
* @author: linyh
* @create: 2018-10-31 18:05
**/
public class TestMybatis {
public static void main(String[] args) {
CustomSqlSession sqlSession = new CustomSqlSession(
new CustomConfiguration(), new CustomDefaultExecutor());
TestCustomMapper testCustomMapper = sqlSession.getMapper(TestCustomMapper.class);
System.out.println(testCustomMapper.selectByPrimaryKey(2));
}
}
控制檯列印:
這樣就完成了極簡版MyBatisv1.0了,其中還有很多不足之處,我用了TODO標記,下面總結幾點不足之處,統計2.0需要改進與增加的功能。
不足之處
- 解析Mapper資訊時用了HardCode,需改用anontation方式去掃描Mapper與對應SQL語句。
- Executor不夠單一職責,它不僅負責引數裝配還負責查詢語句加上結果集對映,不合理,需要細分職責。
增加的功能
- 新增anontation功能動態掃描Mapper類,去掉HardCode。
- 新增StatementHandler設定引數以及負責查詢工作。
- 新增ObjectFactory動態建立實體類。
- 新增ResultSetHandler將結果集對映到實體類中。
- 增加一個Plugin功能。
- 增加CacheExecutor裝飾者來為查詢增加一個快取功能。