最近研究了一下Mybatis的底層代碼,準備寫一個操作數據庫的小工具,實現了Mybatis的部分功能:
1. SQL語句在mapper.xml中配置。
2. 支持int,String,自定義數據類型的入參。
3. 根據mapper.xml動態創建接口的代理實現對象。
功能有限,目的是搞清楚MyBatis框架的底層思想,多學習研究優秀框架的實現思路,對提升自己的編碼能力大有裨益。
小工具使用到的核心技術點:xml解析+反射+jdk動態代理
接下來,一步一步來實現。
首先來說爲什麼要使用jdk動態代理。
傳統的開發方式:
- 接口定義業務方法。
- 實現類實現業務方法。
- 實例化實現類對象來完成業務操作。
Mybatis的方式:
- 開發者只需要創建接口,定義業務方法。
- 不需要創建實現類。
- 具體的業務操作通過配置xml來完成。
MyBatis的方式省去了實現類的創建,改爲用xml來定義業務方法的具體實現。
那麼問題來了。
我們知道Java是面向對象的編程語言,程序在運行時執行業務方法,必須要有實例化的對象。但是,接口是不能被實例化的,而且也沒有接口的實現類,那麼此時這個對象從哪來呢?
程序在運行時,動態創建代理對象。
所以我們要用JDK動態代理,運行時結合接口和mapper.xml來動態創建一個代理對象,程序調用該代理對象的方法來完成業務。
動態代理參考→動態代理
代碼實現
這是一個根據 MyBatis 源碼設計的一個簡單自定義 MyBatis:
一、數據準備
首先說一下我用到的 Bean 類和 Dao 層接口、以及對應的數據庫表,需要解析的配置文件:
POJO類 Student:
public class Student {
private int id; //編號
private String name; //名字
private String sex; //性別
private int age; //年齡
private String grade; //班級
//get、set、constructor、toString略
}
Dao層接口:StudentMapper,我只寫了兩個查詢方法:
public interface StudentMapper {
//根據編號查詢單個學生並返回
Student getStudentInId(int id);
//返回所有的學生列表
List<Student> getStudentList();
}
對應的表 student:
需要解析的配置文件:
db.properties:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test1?useSSL=false
jdbc.username=root
jdbc.password=123456
mappers文件下我只放了一個 xml 文件:student-mapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lzq.dao.StudentMapper">
<select id="getStudentInId" parameterType="java.lang.Integer" resultType="com.lzq.bean.Student">
select * from student where id = ?
</select>
<select id="getStudentList" resultType="com.lzq.bean.Student">
select * from student
</select>
</mapper>
注意:爲了方便,我後面用 PreparedStatement 執行 SQL 語句,所以我直接將 #{} 換成了 ?,不然的話,後面需要一個方法轉換,麻煩:
好了,就是這些數據和配置,現在就開始寫 MyBatis 的代碼了!
二、自定義的 MyBatis 實現
我們將 實現 MyBatis 的大概分三個階段,數據的初始化(就是解析那些配置文件),動態代理(根據傳的 class 生成對應的代理對象給用戶),使用階段;
1、初始化階段
以上就是一個 mapper.xml 中最重要的屬性了,一個命名空間(需要用這個反射來創建對象),多個方法 id (後面要用這個方法 id 去反射方法,所以這個地方的 id 必須和接口中方法的名字一樣,否則人家底層反射的時候就反射不到了!)、以及返回值、SQL語句,所以我們需要將這些信息轉換成一個對象,Java中萬物皆對象嘛,如下 MappedStatement :
/**
* @類名 MappedStatement
* @類說明 存儲 SQL 語句信息
* @作者 中都
* @時間 2020/2/9 16:43
*/
public class MappedStatement {
private String namespase; //命名空間
private String sourceId; //命名空間+方法名
private String resultType; //返回值
private String sql; //SQL語句
//get、set略
}
這個對象儲存的是一個 mapper.xml 文件中的一條 SQL 語句信息,但是一個 mapper.xml 中是會有多條 SQL 語句的,並且很多時候會有多個 mapper.xml 文件,所以我們需要一個類把這些配置信息(包括所有的 mapper.xml 文件信息、每個 mapper.xml 文件的每條 SQL 信息以及數據庫的配置信息等等)彙總,即對象 Configuration:
/**
* @類名 Configuration
* @類說明 把所有的配置信息糅合在一起,一般在一個項目中這個的對象是一個單例
* @作者 中都
* @時間 2020/2/9 17:12
*/
public class Configuration {
private String jdbcDriver; //數據庫驅動
private String jdbcUrl; //數據庫密碼
private String jdbcUserName; //用戶名
private String jdbcPassword; //密碼
//SQL信息
private Map<String,MappedStatement> mappedStatementMaps = new HashMap<>();
//get、set略
}
因爲這個配置信息對象需要儲存多個 mapper.xml 文件中的多條 SQL 語句信息,所以拿一個 map 對象去儲存,key 是 namespace + id,即接口全路徑名+方法名,value 就是對應的要執行的 SQL 語句;
我們平時在使用 MyBatis 的時候會有一個 SqlSessionFactory 的工廠類創建我們需要的 SqlSession 對象供我們使用,所以我模仿他們的實現,也實現了一個 SqlSessionFactory 工廠類,這個類用來初始化配置信息和創建 SQLSession 對象:
/**
* @類名 SqlSessionFactory
* @類說明 完成 Configuration 實例化,生產 SqlSession
* @作者 中都
* @時間 2020/2/9 17:20
*/
public class SqlSessionFactory {
//final修飾,單例,配置信息
private final Configuration configuration = new Configuration();
//記錄mapper.xml文件存放的位置,這個 resources 文件下的 mappers 訪問不到,我給了全路徑
public static final String MAPPER_CONFIG = "G:\\idea工程\\框架複習\\src\\main\\resources\\mappers";
//記錄數據庫連接信息文件存放的位置
public static final String DB_CONFIG_FILE = "db.properties";
public SqlSessionFactory() {
loadDbInfo();
loadMappersInfo();
}
/**
* 加載數據庫信息
*/
private void loadDbInfo() {
//加載數據庫配置文件
InputStream dbin = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
Properties p = new Properties();
try {
p.load(dbin); //將配置信息寫入 Properties 對象
} catch (IOException e) {
e.printStackTrace();
}
//將數據庫配置信息寫入 config 對象
configuration.setJdbcDriver(p.get("jdbc.driver").toString());
configuration.setJdbcUrl(p.get("jdbc.url").toString());
configuration.setJdbcUserName(p.get("jdbc.username").toString());
configuration.setJdbcPassword(p.get("jdbc.password").toString());
}
/**
* 加載所有文件夾下的 xml 信息
*/
private void loadMappersInfo() {
File mappers = new File(MAPPER_CONFIG);
if(mappers.isDirectory()) {
File[] files = mappers.listFiles();
for (File f : files) {
loadMapperInfo(f);
}
}
}
/**
* 解析單個 xml 文件
* @param f
*/
private void loadMapperInfo(File f) {
//創建 saxReader 對象
SAXReader reader = new SAXReader();
//通過 read 方法讀取一個文件,轉換成 Document 對象
Document document = null;
try {
document = reader.read(f);
} catch (DocumentException e) {
e.printStackTrace();
}
//獲取根節點元素
Element root = document.getRootElement();
//獲取命名空間
String namespace = root.attribute("namespace").getData().toString();
//獲取 select 子節點列表
List<Element> selects = root.elements("select");
//遍歷 select 節點,將信息記錄到 MappedStatement 對象,並登記到 configuration
for (Element e : selects) {
MappedStatement mappedStatement = new MappedStatement(); //實例化一條 sql 語句記錄
String id = e.attribute("id").getData().toString();
String resultType = e.attribute("resultType").getData().toString();
String sql = e.getData().toString(); //去取 sql 語句
String sourceId = namespace +"."+id;
//給 MappedStatement 對象 賦值
mappedStatement.setNamespase(namespace);
mappedStatement.setResultType(resultType);
mappedStatement.setSourceId(sourceId);
mappedStatement.setSql(sql);
//註冊到 configuration
configuration.getMappedStatementMaps().put(sourceId,mappedStatement);
}
}
/**
* 創建 SqlSession 對象
* @return
*/
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
至此,第一階段配置信息的初始化完成;
2、動態代理
既然要創建 SqlSession對象,那就需要 SqlSession 了,裏面也只是實現了兩個簡單的查詢方法:
/**
* @類名 SqlSession
* @類說明
* 1、對外提供方法的接口
* 2、對內將請求轉發給 executor 執行
* @作者 中都
* @時間 2020/2/9 18:10
*/
public interface SqlSession {
/**
* 根據傳入的條件查詢單一結果
* @param statement sql語句
* @param parameter 傳入的參數
* @param <T> 返回值
* @return
*/
<T> T selectOne(String statement,Object parameter);
<E> List<E> selectList(String statement, Object parameter);
<T> T getMapper(Class<T> type);
}
上面只是一個接口,這是他的實現類:
/**
* @類名 DefaultSqlSession
* @類說明
* 1、對外提供方法的接口
* 2、對內將請求轉發給 executor 執行
* @作者 中都
* @時間 2020/2/9 18:17
*/
public class DefaultSqlSession implements SqlSession {
//final修飾,單例,配置信息
private final Configuration conf;
//真正的執行者,委託者
private Executor executor;
public DefaultSqlSession(Configuration conf) {
this.conf = conf;
this.executor = new DefaultExecutor(conf);
}
@Override
public <T> T selectOne(String statement, Object parameter) {
List<Object> selectList = this.selectList(statement,parameter);
if(selectList == null || selectList.size() == 0) {
return null;
}else if(selectList.size() == 1) {
return (T)selectList.get(0);
}else {
throw new RuntimeException("這不是單條記錄!");
}
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
MappedStatement mappedStatement = conf.getMappedStatementMaps().get(statement);
return executor.query(mappedStatement,parameter);
}
@Override
public <T> T getMapper(Class<T> type) {
MapperProxy mapperProxy = new MapperProxy(this);
return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},mapperProxy);
}
}
因爲 getMapper 是通過動態代理實現的,根據單一職責原則,SqlSession 主要是完成數據查詢的,那動態代理的實現就需要另外一個類來完成了,即 MapperProxy :
/**
* @類名 MapperProxy
* @類說明 完成動態代理
* @作者 中都
* @時間 2020/2/9 20:39
*/
public class MapperProxy implements InvocationHandler {
private SqlSession sqlSession;
public MapperProxy(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//判斷方法的返回值是否是集合類型
if(Collection.class.isAssignableFrom(method.getReturnType())) {
//method.getDeclaringClass().getName()+"."+method.getName() 即類名.方法名,即配置文件中的命名空間
return sqlSession.selectList(method.getDeclaringClass().getName()+"."+method.getName(),args == null?null:args[0]);
}else {
return sqlSession.selectOne(method.getDeclaringClass().getName()+"."+method.getName(),args == null?null:args[0]);
}
}
}
在上面 SqlSession 的實現類中我們也看到,SqlSession 就是那個代理類,它每次操作都是調用 Executor 去實現的:
/**
* @類名 Executor
* @類說明 核心接口之一,定義了數據庫操作的最基本方法,SqlSession的功能都基於它來實現
* @作者 中都
* @時間 2020/2/9 20:01
*/
public interface Executor {
/**
* 查詢接口
* @param ms 封裝有 SQL 語句的 MappedStatement 對象
* @param parameter 傳入的 SQL 參數
* @param <E> 將參數轉化成指定的結果集返回
* @return
*/
<E> List<E> query(MappedStatement ms, Object parameter);
}
下面是 Executor 的具體實現類,主要完成數據庫操作,並將結果映射成需要返回的對象類型:
public class DefaultExecutor implements Executor {
private final Configuration configuration;
public DefaultExecutor(Configuration configuration) {
this.configuration = configuration;
}
/**
* 用於查詢
* @param ms 封裝有 SQL 語句的 MappedStatement 對象
* @param parameter 傳入的 SQL 參數
* @param <E>
* @return
*/
@Override
public <E> List<E> query(MappedStatement ms, Object parameter) {
List<E> ret = new ArrayList<>();//定義結果集;
try {
Class.forName(configuration.getJdbcDriver());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//獲取連接,從 MappedStatement 獲取數據庫信息
connection = DriverManager.getConnection(configuration.getJdbcUrl(),configuration.getJdbcUserName(),configuration.getJdbcPassword());
//創建 preparedStatement 對象,從MappedStatement獲取SQL語句
preparedStatement = connection.prepareStatement(ms.getSql());
//處理SQL語句中的佔位符
parameterize(preparedStatement,parameter);
//執行查詢操作獲取 resultSet
resultSet = preparedStatement.executeQuery();
//將結果集通過反射技術,填充到list集合中
handlerResultSet(resultSet,ret,ms.getResultType());
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
resultSet.close();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return ret;
}
/**
* 對佔位符進行處理
* @param preparedStatement
* @param parameter
*/
private void parameterize(PreparedStatement preparedStatement,Object parameter) throws SQLException {
if(parameter instanceof Integer) {
preparedStatement.setInt(1,(int)parameter);
}else if(parameter instanceof Long) {
preparedStatement.setLong(1,(long)parameter);
}else if(parameter instanceof String) {
preparedStatement.setString(1,(String)parameter);
}
}
/**
* 讀取 resultset 中的數據,並轉換成目標對象
* @param resultSet
* @param ret
* @param className
* @param <E>
*/
private <E> void handlerResultSet(ResultSet resultSet,List<E> ret,String className) {
Class<E> eClass = null;
try {
//通過反射獲取類對象;
eClass = (Class<E>)Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
while (resultSet.next()) {
//通過反射實例化對象
Object o = eClass.newInstance();
//使用反射將 resultset 中的數據填充到 o 對象
ReflectionUtil.setPropToBeanFromResultSet(o,resultSet);
//將對象加入到返回集中
ret.add((E)o);
}
} catch (SQLException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
它用到的工具類:
/**
* @類名 ReflectionUtil
* @類說明 利用反射幫助創建對象
* @作者 中都
* @時間 2020/2/9 21:37
*/
public class ReflectionUtil {
/**
* 爲指定的 bean 的 proName 屬性賦值 value
* @param bean 目標對象
* @param propName 對象的屬性名
* @param value 值
*/
public static void setPropToBean(Object bean,String propName,Object value) {
Field f;
try {
f = bean.getClass().getDeclaredField(propName);//獲得對象指定的屬性
f.setAccessible(true); //可以訪問
f.set(bean,value);//爲屬性賦值
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
/**
* 將結果集映射到對象
* @param entity
* @param resultSet
* @throws SQLException
*/
public static void setPropToBeanFromResultSet(Object entity, ResultSet resultSet) throws SQLException {
Field[] declaredFields = entity.getClass().getDeclaredFields(); //得到對象的所有屬性
for (int i = 0; i < declaredFields.length; i++) {
if(declaredFields[i].getType().getSimpleName().equals("String")) { //字符串類型
setPropToBean(entity,declaredFields[i].getName(),resultSet.getString(declaredFields[i].getName()));
}else if(declaredFields[i].getType().getSimpleName().equals("int")) {
setPropToBean(entity,declaredFields[i].getName(),resultSet.getInt(declaredFields[i].getName()));
}else if(declaredFields[i].getType().getSimpleName().equals("long")) {
setPropToBean(entity,declaredFields[i].getName(),resultSet.getLong(declaredFields[i].getName()));
}
}
}
}
至此,整個自定義 MyBatis 就寫完了,現在只需要測試一下就好:
3、測試
public class Test2 {
@Test
public void Test2() {
SqlSessionFactory factory = new SqlSessionFactory();
SqlSession sqlSession = factory.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.getStudentInId(3);
System.out.println(student);
System.out.println(" ======================== ");
List<Student> studentList = mapper.getStudentList();
for (Student stu : studentList) {
System.out.println(stu);
}
}
}
結果如下: