@
前言
MyBatis 是一個非常優秀的持久層應用框架,目前幾乎已經一統天下。既然是持久層框架,那么一定是對於數據庫的操作,Java 中談到數據庫操作,一定少不了 JDBC。那么 MyBatis 比傳統的 JDBC 好在哪那?MyBatis 又在哪方面做了優化那?
JDBC
如果我們需要查詢所有用戶,傳統的 JDBC 會這樣寫。
public static void main(String[] args) {
//聲明Connection對象
Connection con = null;
try {
//加載驅動程序
Class.forName("com.mysql.jdbc.Driver");
//創建 connection 對象
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/db","username","password");
//使用 connection 對象創建statement 或者 PreparedStatement 類對象,用來執行SQL語句
Statement statement = con.createStatement();
//要執行的SQL語句
String sql = "select * from user";
//3.ResultSet類,用來存放獲取的結果集!!
ResultSet rs = statement.executeQuery(sql);
String job = "";
String id = "";
while(rs.next()){
//獲取job這列數據
job = rs.getString("job");
//獲取userId這列數據
id = rs.getString("userId");
//輸出結果
System.out.println(id + "\t" + job);
}
} catch(ClassNotFoundException e) {
e.printStackTrace();
} catch(SQLException e) {
//數據庫連接失敗異常處理
e.printStackTrace();
}catch (Exception e) {
e.printStackTrace();
}finally{
rs.close();
con.close();
}
}
通過上面的代碼,我們可以將 JDBC 對於數據庫的操作總結為以下幾個步驟:
- 加載驅動
- 創建連接,Connection 對象
- 根據 Connection 創建 Statement 或者 PreparedStatement 來執行 SQL 語句
- 返回結果集到 ResultSet 中
- 手動將 ResultSet 映射到 JavaBean 中
傳統的 JDBC 操作的問題也一目了然,整體非常繁瑣,也不夠靈活,執行一個 SQL 查詢就要寫一堆代碼。
MyBatis
來看看 MyBatis 代碼如何查詢數據庫。幾行代碼就完成了數據庫查詢操作,並且將數據庫查詢出來的結果映射到了 JavaBean 中了。我們的代碼沒有加入 Spring Mybatis,加入 Spring 后整體流程會復雜很多,不方便我們理解。
//獲取 sqlSession,sqlSession 相當於傳統 JDBC 的 Conection
public static SqlSession getSqlSession(){
InputStream configFile = new FileInputStream(filePath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);
return sqlSessionFactory.openSession();
}
//使用 sqlSession 獲得對應的 mapper,mapper 用來執行 sql 語句。
public static User get(SqlSession sqlSession, int id){
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
return userMapper.selectByPrimaryKey(id);
}
我們來對 MyBatis 操作數據庫做一個總結:
- 使用配置文件構建 SqlSessionFactory
- 使用 SqlSessionFactory 獲得 SqlSession,SqlSession 相當於傳統 JDBC 的 Conection
- 使用 SqlSession 得到 Mapper
- 用 Mapper 來執行 SQL 語句,並返回結果直接封裝到 JavaBean 中
源碼分析
大家平時應該經常使用 MyBatis 框架,對於 SqlSessionFactory、SqlSession、Mapper 等也有一些概念。下面我們從源碼來分析怎么實現這些概念。
前置知識
先給出一個大部分框架的代碼流程,方便大家理解框架。下面的圖片就說明了接口、抽象類和實現類的關系,我們自己寫代碼時也要多學習這種思想。
帶着結果看過程
看源碼對於很多人來說都是一個比較枯燥和乏味的過程,如果不做抽象和總結,會覺得非常亂。另外,看源碼不要去扣某個細節,盡量從宏觀上理解它。這樣帶着結果看過程你就會知道設計者為什么這么做。
先給出整個 MyBatis 框架的架構圖,大家先有一個印象:
原理分析
說明,我們講解的是原生的 MyBatis 框架,並不是與 Spring 結合的 MyBatis 框架。
還是把上面 MyBatis 操作數據庫的代碼拿過來,方便我們與源碼對照。
//獲取 sqlSession,sqlSession 相當於傳統 JDBC 的 Conection
public static SqlSession getSqlSession(){
//步驟一
InputStream configFile = new FileInputStream(filePath);
//步驟二
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);
return sqlSessionFactory.openSession();
}
//使用 sqlSession 獲得對應的 mapper,mapper 用來執行 sql 語句。
public static User get(SqlSession sqlSession, int id){
//步驟三
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
return userMapper.selectByPrimaryKey(id);
}
MyBatis 框架的第一步就是加載我們數據庫的相關信息,比如用戶名、密碼等。以及我們在 XML 文件中寫的 SQL 語句。
//配置文件中指定了數據庫相關的信息和寫 sql 語句的 mapper 相關信息,稍后我們需要讀取並加載到我們的配置類中。
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/db"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</transactionManager>
</environment>
</environments>
</configuration>
<mappers>
<mapper resource="xml/UserMapper.xml"/>
</mappers>
第二步就是通過讀取到的配置文件信息,構建一個 SqlSessionFactory。
通過 openSession 方法返回了一個 sqlSession,我們來看看 openSession 方法做了什么。
//我們來重點看看 openSession 做了什么操作, DefaultSqlSessionFactory.java
@Override
public SqlSession openSession() {
return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}
public Configuration getConfiguration() {
return this.configuration;
}
//這個函數里面有着事務控制相關的代碼。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
//根據上面的參數得到 TransactionFactory,通過 TransactionFactory 生成一個 Transaction,可以理解為這個 SqlSession 的事務控制器
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 將這個事務控制器封裝在 Executor 里
Executor executor = this.configuration.newExecutor(tx, execType);
// 使用 configuration 配置類,Executor,和 configuration(是否自動提交) 來構建一個 DefaultSqlSession。
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
看了上面的一大段代碼你可能會覺得蒙,沒關系,我們來划重點,最終結果返回了一個 DefaultSqlsession。
// 使用 configuration 配置類(我們上面讀取的配置文件就需要加載到這個類中),Executor(包含了數據事務控制相關信息),和 autoCommit(是否自動提交) 來構建一個 DefaultSqlSession。
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
有了這個 sqlSession 之后,我們就可以實現所有對數據庫的操作了,因為我們已經把所有的信息加載到這里面了。數據庫信息、SQL 信息、SQL 語句執行器等。當然我們一般使用這個 sqlSession 獲得對應的 mapper 接口類,然后用這個接口類查詢數據庫。
既然所有東西都封裝在 sqlSession 中,先來看看 sqlSession 的組成部分。
SqlSession 的接口定義:里面定義了增刪改查和提交回滾等方法。
public interface SqlSession extends Closeable {
<T> T selectOne(String var1);
<T> T selectOne(String var1, Object var2);
<E> List<E> selectList(String var1);
<E> List<E> selectList(String var1, Object var2);
<E> List<E> selectList(String var1, Object var2, RowBounds var3);
<K, V> Map<K, V> selectMap(String var1, String var2);
<K, V> Map<K, V> selectMap(String var1, Object var2, String var3);
<K, V> Map<K, V> selectMap(String var1, Object var2, String var3, RowBounds var4);
<T> Cursor<T> selectCursor(String var1);
<T> Cursor<T> selectCursor(String var1, Object var2);
<T> Cursor<T> selectCursor(String var1, Object var2, RowBounds var3);
void select(String var1, Object var2, ResultHandler var3);
void select(String var1, ResultHandler var2);
void select(String var1, Object var2, RowBounds var3, ResultHandler var4);
int insert(String var1);
int insert(String var1, Object var2);
int update(String var1);
int update(String var1, Object var2);
int delete(String var1);
int delete(String var1, Object var2);
void commit();
void commit(boolean var1);
void rollback();
void rollback(boolean var1);
List<BatchResult> flushStatements();
void close();
void clearCache();
Configuration getConfiguration();
<T> T getMapper(Class<T> var1);
Connection getConnection();
}
接下來用 sqlSession 獲取對應的 Mapper。
DefaultSqlSession 的 getMapper 實現:
public <T> T getMapper(Class<T> type) {
return this.configuration.getMapper(type, this);
}
//從 configuration 里面 getMapper,Mapper 就在 Configuration 里
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return this.mapperRegistry.getMapper(type, sqlSession);
}
MapperRegistry 里 getMapper 的最終實現,同時我們需要思考一個問題,我們的 sqlSession 接口里面只定義了抽象的增刪改查,而這個接口並沒有任何實現類,那么這個 XML 到底是如何與接口關聯起來並生成實現類那?通過 MapperRegistry 可以得出答案,那就是動態代理。
public class MapperRegistry {
private final Configuration config;
// 用一個 Map 來存儲接口和 xml 文件之間的映射關系,key 應該是接口,但是 value 是 MapperProxyFactory
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();
public MapperRegistry(Configuration config) {
this.config = config;
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//獲取到這個接口對應的 MapperProxyFactory。
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
//用上一步獲取的 MapperProxyFactory 和 sqlSession 構建對應的 Class
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
}
最終的結果是生成一個 mapper 接口的動態代理類,通過這個類,我們實現對數據庫的增刪改查。
接下來我們看看 newInstance 的具體實現:
public T newInstance(SqlSession sqlSession) {
// mapperInterface 就是接口
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
//動態代理,這里的動態代理有一些不一樣
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
為什么說這里的動態代理有一些不一樣那?我們先看看正常流程的動態代理,接口,和接口實現類是必須的。而我們的 Mapper 接口只有充滿了 SQL 語句的 XML 文件,沒有具體實現類。
與傳統的動態代理相比,MyBatis 的 Mapper 接口是沒有實現類的,那么它又是怎么實現動態代理的那?
我們來看一下 MapperProxy 的源碼:
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
// 正常的動態代理中 Object proxy 這個參數應該是接口的實現類
// com.paul.pkg.UserMapper@5a123uf
// 現在里面是 org.apache.ibatis.binding.MapperProxy@6y213kn, 這倆面
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
if (this.isDefaultMethod(method)) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
// Mapper 走這個流程,先嘗試在緩存里獲取 method
MapperMethod mapperMethod = this.cachedMapperMethod(method);
return mapperMethod.execute(this.sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);
if (mapperMethod == null) {
// mapperMethod 的構建,通過接口名,方法,和 xml 配置(通過 sqlSession 的 Configuration 獲得)
mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());
//通過 execute 執行方法,因為 sqlSession 封裝了 Executor,所以還要傳進來,execute 方法使用
//sqlSession 里面的方法。
this.methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
來看 MapperMethod 的定義:
// command 里面包含了方法名,比如 com.paul.pkg.selectByPrimaryKey
// type, 表示是 SELECT,UPDATE,INSERT,或者 DELETE
// method 是方法的簽名
public class MapperMethod {
private final MapperMethod.SqlCommand command;
private final MapperMethod.MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
}
}
```
```java
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;//返回結果
//INSERT操作
if (SqlCommandType.INSERT == command.getType()) {
//處理參數
Object param = method.convertArgsToSqlCommandParam(args);
//調用sqlSession的insert方法
result = rowCountResult(sqlSession.insert(command.getName(), param));
}
.....
.....
}
通過 sqlSession 來執行我們的 SQL 語句,返回結果,動態代理的方法調用結束。
進入 DefaultSqlSession 執行對應的 SQL 語句。
public <T> T selectOne(String statement, Object parameter) {
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
// 這里又需要 configuration 來獲取對應的 statement
// MappedStatement 里面有 xml 文件,和要執行的方法,就是 xml 里面的 id,statementType,以及 sql 語句。
MappedStatement ms = this.configuration.getMappedStatement(statement);
// 用 executor 執行 query,executor 里面應該是包裝了 JDBC。
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}
return var5;
}
Executor 的實現類里面執行 query 方法。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
// 使用 delegate 去 query,delegate 是 SimpleExecutor。里面使用 JDBC 進行數據庫操作。
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
自己實現一個 MyBatis 框架
整體流程
- 首先創建 SqlSessionFactory 實例,SqlSessionFactory 就是創建 SqlSession 的工廠類。
- 加載配置文件創建 Configuration 對象,配置文件包括數據庫相關配置文件以及我們在 XML 文件中寫的 SQL。
- 通過 SqlSessionFactory 創建 SqlSession。
- 通過 SqlSession 獲取 mapper 接口動態代理。
- 動態代理回調 SqlSession 中某查詢方法。
- SqlSession 將查詢方法轉發給 Executor。
- Executor 基於 JDBC 訪問數據庫獲取數據,最后還是通過 JDBC 操作數據庫。
- Executor 通過反射將數據轉換成 POJO 並返回給 SqlSession。
- 將數據返回給調用者。
項目整體使用 Maven 構建,mybatis-demo 是脫離 Spring 的 MyBatis 使用的例子,大家可以先熟悉以下 Mybatis 框架如何使用,代碼就不在講解了。paul-mybatis 是我們自己實現的 MyBatis 框架。
首先按照我們以前的使用 MyBatis 代碼時的流程,創建 Mapper 接口、XML 文件,和 POJO 以及集一些配置文件,這幾個文件我們和 mybatis-demo 創建一樣的即可,方便我們比較結果。
Mapper 接口,這里面定義兩個抽象方法,根據主鍵查找用戶和查找所有用戶:
package com.paul.mybatis.mapper;
import com.paul.mybatis.entity.User;
import java.util.List;
public interface UserMapper {
User selectByPrimaryKey(long userId);
List<User> selectAll();
}
XML 文件,里面是上面兩個抽象方法的具體 SQL 實現,完全消防官方 XML 文件的寫法,需要注意 namespace、id、resultType、SQL 語句這幾個點,都是我們后面代碼需要處理的。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.paul.mybatis.mapper.UserMapper">
<select id="selectByPrimaryKey" resultType="User">
select *
from t_user
where userId = #{userId}
</select>
<select id="selectAll" resultType="User">
select *
from t_user
</select>
</mapper>
最后是我們的實體類,它的屬性與數據庫的表相對應:
package com.paul.mybatis.entity;
public class User {
private long userId;
private String userName;
private int sex;
private String role;
public long getUserId() {
return userId;
}
public void setUserId(long userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
最后一個配置文件,數據庫連接配置文件 db.propreties:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root
配置文件和一些測試的必須類已經寫完了,首先我們需要把這些配置信息加載到 Configuration 配置類中。
先定義一個類來加載寫 SQL 語句的 XML 文件,上面我們說過要注意四個點,namespace、id、resultType、SQL 語句,我們寫對應的屬性來保存它,代碼很簡單,就不多講了。
package com.paul.mybatis.confiuration;
/**
*
* XML 中的 sql 配置信息加載到這個類中
*
*/
public class MappedStatement {
private String namespace;
private String id;
private String resultType;
private String sql;
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
}
接下來我們定義一個 Configuration 總配置類,來保存 db.propeties 里面的屬性和 XML 文件的 SQL 信息,Configuration 類里面的文件對應我們配置文件中的屬性。
package com.paul.mybatis.confiuration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
* 所有的配置信息
*
*/
public class Configuration {
private String jdbcDriver;
private String jdbcUrl;
private String jdbcPassword;
private String jdbcUsername;
private Map<String,MappedStatement> mappedStatement = new HashMap<>();
public Map<String, MappedStatement> getMappedStatement() {
return mappedStatement;
}
public void setMappedStatement(Map<String, MappedStatement> mappedStatement) {
this.mappedStatement = mappedStatement;
}
public String getJdbcDriver() {
return jdbcDriver;
}
public void setJdbcDriver(String jdbcDriver) {
this.jdbcDriver = jdbcDriver;
}
public String getJdbcUrl() {
return jdbcUrl;
}
public void setJdbcUrl(String jdbcUrl) {
this.jdbcUrl = jdbcUrl;
}
public String getJdbcPassword() {
return jdbcPassword;
}
public void setJdbcPassword(String jdbcPassword) {
this.jdbcPassword = jdbcPassword;
}
public String getJdbcUsername() {
return jdbcUsername;
}
public void setJdbcUsername(String jdbcUsername) {
this.jdbcUsername = jdbcUsername;
}
}
按照上面的流程圖,我們來創建一個 SqlSessionFactory 工廠類,這個類有兩個功能,一個是加載配置文件信息到 Configuration 類中,另一個是創建 SqlSession。
SqlSessionFactory 抽象模版:
package com.paul.mybatis.factory;
import com.paul.mybatis.sqlsession.SqlSession;
public interface SqlSessionFactory {
SqlSession openSession();
}
創建 SqlSessionFactory 的 Default 實現類,Default 實現類主要完成了兩個功能,加載配置信息到 Configuration 對象里,實現創建 SqlSession 的功能。
package com.paul.mybatis.factory;
import com.paul.mybatis.confiuration.Configuration;
import com.paul.mybatis.confiuration.MappedStatement;
import com.paul.mybatis.sqlsession.DefaultSqlSession;
import com.paul.mybatis.sqlsession.SqlSession;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
*
* 1.初始化時就完成了 configuration 的實例化
* 2.工廠類,生成 sqlSession
*
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory{
private final Configuration configuration = new Configuration();
// xml 文件存放的位置
private static final String MAPPER_CONFIG_LOCATION = "mappers";
// 數據庫信息存放的位置
private static final String DB_CONFIG_FILE = "db.properties";
public DefaultSqlSessionFactory() {
loadDBInfo();
loadMapperInfo();
}
private void loadDBInfo() {
InputStream db = this.getClass().getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
Properties p = new Properties();
try {
p.load(db);
} catch (IOException e) {
e.printStackTrace();
}
//將配置信息寫入Configuration 對象
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 loadMapperInfo(){
URL resources = null;
resources = this.getClass().getClassLoader().getResource(MAPPER_CONFIG_LOCATION);
File mappers = new File(resources.getFile());
//讀取文件夾下面的文件信息
if(mappers.isDirectory()){
File[] files = mappers.listFiles();
for(File file:files){
loadMapperInfo(file);
}
}
}
private void loadMapperInfo(File file){
SAXReader reader = new SAXReader();
//通過read方法讀取一個文件轉換成Document 對象
Document document = null;
try {
document = reader.read(file);
} catch (DocumentException e) {
e.printStackTrace();
}
//獲取根結點元素對象<mapper>
Element e = document.getRootElement();
//獲取命名空間namespace
String namespace = e.attribute("namespace").getData().toString();
//獲取select,insert,update,delete子節點列表
List<Element> selects = e.elements("select");
List<Element> inserts = e.elements("insert");
List<Element> updates = e.elements("update");
List<Element> deletes = e.elements("delete");
List<Element> all = new ArrayList<>();
all.addAll(selects);
all.addAll(inserts);
all.addAll(updates);
all.addAll(deletes);
//遍歷節點,組裝成 MappedStatement 然后放入到configuration 對象中
for(Element ele:all){
MappedStatement mappedStatement = new MappedStatement();
String id = ele.attribute("id").getData().toString();
String resultType = ele.attribute("resultType").getData().toString();
String sql = ele.getData().toString();
mappedStatement.setId(namespace+"."+id);
mappedStatement.setResultType(resultType);
mappedStatement.setNamespace(namespace);
mappedStatement.setSql(sql);
// xml 文件中的每個 sql 方法都組裝成 mappedStatement 對象,以 namespace+"."+id 為 key, 放入
// configuration 配置類中。
configuration.getMappedStatement().put(namespace+"."+id,mappedStatement);
}
}
@Override
public SqlSession openSession() {
// openSession 方法創建一個 DefaultSqlSession,configuration 配置類作為 構造函數參數傳入
return new DefaultSqlSession(configuration);
}
}
在 SqlSessionFactory 里創建了 DefaultSqlSession,我們看看它的具體實現。SqlSession 里面應該封裝了所有數據庫的具體操作和一些獲取 mapper 實現類的方法。
SqlSession 接口,定義模版方法
package com.paul.mybatis.sqlsession;
import java.util.List;
/**
*
* 封裝了所有數據庫的操作
* 所有功能都是基於 Excutor 來實現的,Executor 封裝了 JDBC 操作
*
*
*/
public interface SqlSession {
/**
* 根據傳入的條件查詢單一結果
* @param statement namespace+id,可以用做 key,去 configuration 里面獲取 sql 語句,resultType
* @param parameter 要傳入 sql 語句中的查詢參數
* @param <T> 返回指定的結果對象
* @return
*/
<T> T selectOne(String statement, Object parameter);
<T> List<T> selectList(String statement, Object parameter);
<T> T getMapper(Class<T> type);
}
Default 的 SqlSession 實現類。里面需要傳入 Executor,這個 Executor 里面封裝了 JDBC 操作數據庫的流程。我們重點關注 getMapper 方法,使用動態代理生成一個加強類。這里面最終還是把數據庫的相關操作轉給 SqlSession,使用 Mapper 能使編程更加優雅。
package com.paul.mybatis.sqlsession;
import com.paul.mybatis.bind.MapperProxy;
import com.paul.mybatis.confiuration.Configuration;
import com.paul.mybatis.confiuration.MappedStatement;
import com.paul.mybatis.executor.Executor;
import com.paul.mybatis.executor.SimpleExecutor;
import java.lang.reflect.Proxy;
import java.util.List;
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration) {
super();
this.configuration = configuration;
executor = new SimpleExecutor(configuration);
}
@Override
public <T> T selectOne(String statement, Object parameter) {
List<T> selectList = this.selectList(statement,parameter);
if(selectList == null || selectList.size() == 0){
return null;
}
if(selectList.size() == 1){
return (T) selectList.get(0);
}else{
throw new RuntimeException("too many result");
}
}
@Override
public <T> List<T> selectList(String statement, Object parameter) {
MappedStatement ms = configuration.getMappedStatement().get(statement);
// 我們的查詢方法最終還是交給了 Executor 去執行,Executor 里面封裝了 JDBC 操作。傳入參數包含了 sql 語句和 sql 語句需要的參數。
return executor.query(ms,parameter);
}
@Override
public <T> T getMapper(Class<T> type) {
//通過動態代理生成了一個實現類,我們重點關注,動態代理的實現,它是一個 InvocationHandler,傳入參數是 this,就是 sqlSession 的一個實例。
MapperProxy mp = new MapperProxy(this);
//給我一個接口,還你一個實現類
return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},mp);
}
}
來看看我們的 InvocationHandler 如何實現 invoke 方法:
package com.paul.mybatis.bind;
import com.paul.mybatis.sqlsession.SqlSession;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
/**
*
* 將請求轉發給 sqlSession
*
*/
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 {
System.out.println(method.getDeclaringClass().getName()+"."+method.getName());
//最終還是將執行方法轉給 sqlSession,因為 sqlSession 里面封裝了 Executor
//根據調用方法的類名和方法名以及參數,傳給 sqlSession 對應的方法
if(Collection.class.isAssignableFrom(method.getReturnType())){
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]);
}
}
}
獲取 Mapper 接口的實現類我們已經實現了,通過動態代理調用 sqlSession 的方法。那么就剩最后一個重要的工作了,那就是實現 Exectuor 類去操作數據庫,封裝 JDBC。
Executor 抽象模版,我們只實現了 query、update 等操作慢慢增加。
package com.paul.mybatis.executor;
import com.paul.mybatis.confiuration.MappedStatement;
import java.util.List;
/**
*
* mybatis 核心接口之一,定義了數據庫操作的最基本的方法,JDBC,sqlSession的所有功能都是基於它來實現的
*
*/
public interface Executor {
/**
*
* 查詢接口
* @param ms 封裝sql 語句的 mappedStatemnet 對象,里面包含了 sql 語句,resultType 等。
* @param parameter 傳入sql 參數
* @param <E> 將數據對象轉換成指定對象結果集返回
* @return
*/
<E> List<E> query(MappedStatement ms, Object parameter);
}
```
**Executor 接口的實現類,主要是對 JDBC 的封裝,和利用反射方法將結果映射到 resultType 對應的實體類中**
```java
package com.paul.mybatis.executor;
import com.paul.mybatis.confiuration.Configuration;
import com.paul.mybatis.confiuration.MappedStatement;
import com.paul.mybatis.util.ReflectionUtil;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class SimpleExecutor implements Executor {
private final Configuration configuration;
public SimpleExecutor(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter) {
System.out.println(ms.getSql().toString());
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 {
connection = DriverManager.getConnection(configuration.getJdbcUrl(), configuration.getJdbcUsername(), configuration.getJdbcPassword());
String regex = "#\\{([^}])*\\}";
// 將 sql 語句中的 #{userId} 替換為 ?
String sql = ms.getSql().replaceAll(regex,"");
preparedStatement = connection.prepareStatement(sql);
//處理占位符,把占位符用傳入的參數替換
parametersize(preparedStatement, parameter);
resultSet = preparedStatement.executeQuery();
handlerResultSet(resultSet, ret,ms.getResultType());
}catch (SQLException e){
e.printStackTrace();
}finally {
try {
resultSet.close();
preparedStatement.close();
connection.close();
}catch (Exception e){
e.printStackTrace();
}
}
return ret;
}
private void parametersize(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);
}
}
private <E> void handlerResultSet(ResultSet resultSet, List<E> ret,String className){
Class<E> clazz = null;
//通過反射獲取類對象
try {
clazz = (Class<E>)Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
while (resultSet.next()) {
Object entity = clazz.newInstance();
//通過反射工具 將 resultset 中的數據填充到 entity 中
ReflectionUtil.setPropToBeanFromResultSet(entity, resultSet);
ret.add((E) entity);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
到目前未知,我們簡單版的 MyBatis 框架已經實現了,我們來寫一個測試類測試一下。
package com.paul.mybatis;
import com.paul.mybatis.entity.User;
import com.paul.mybatis.factory.DefaultSqlSessionFactory;
import com.paul.mybatis.factory.SqlSessionFactory;
import com.paul.mybatis.mapper.UserMapper;
import com.paul.mybatis.sqlsession.SqlSession;
public class TestDemo {
public static void main(String[] args) {
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByPrimaryKey(121312312313L);
System.out.println(user.toString());
}
}
看一下測試的結果,整個 MyBatis 框架已經實現完成了,當然有很多地方需要完善,比如 XML 中的 SQL 語句處處理還缺很多功能,目前只支持 select 等,希望大家能通過源碼解讀和自己寫的過程明白 MyBatis 的具體實現要點。
最后給出源碼地址:源碼,碼字不易,如果您覺得學到了東西,請在 Chat 或 GitHub 點贊,不明白的可以評論或者留言。QQ 群:725758660