原文部落格地址: pjmike的部落格
前言
一直以來都在使用MyBatis做持久化框架,也知道當我們定義XXXMapper介面類並利用它來做CRUD操作時,Mybatis是利用了動態代理的技術幫我們生成代理類。那麼動態代理內部的實現細節到底是怎麼的呀?XXXMapper.java類和XXXMapper.xml到底是如何關聯起來的呀?本篇文章就來詳細剖析下MyBatis的動態代理的具體實現機制。
MyBatis的核心元件及應用
在詳細探究MyBatis中動態代理機制之前,先來補充一下基礎知識,認識一下MyBatis的核心元件。
- SqlSessionFactoryBuilder(構造器): 它可以從XML、註解或者手動配置Java程式碼來建立SqlSessionFactory。
- SqlSessionFactory: 用於建立SqlSession (會話) 的工廠
- SqlSession: SqlSession是Mybatis最核心的類,可以用於執行語句、提交或回滾事務以及獲取對映器Mapper的介面
- SQL Mapper: 它是由一個Java介面和XML檔案(或註解)構成的,需要給出對應的SQL和對映規則,它負責傳送SQL去執行,並返回結果
注意: 現在我們使用Mybatis,一般都是和Spring框架整合在一起使用,這種情況下,SqlSession將被Spring框架所建立,所以往往不需要我們使用SqlSessionFactoryBuilder或者SqlSessionFactory去建立SqlSession
下面展示一下如何使用MyBatis的這些元件,或者如何快速使用MyBatis:
- 資料庫表
CREATE TABLE user(
id int,
name VARCHAR(255) not NULL ,
age int ,
PRIMARY KEY (id)
)ENGINE =INNODB DEFAULT CHARSET=utf8;
複製程式碼
- 宣告一個User類
@Data
public class User {
private int id;
private int age;
private String name;
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
複製程式碼
- 定義一個全域性配置檔案mybatis-config.xml (關於配置檔案中具體屬性標籤解釋參閱官方文件)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--全域性配置檔案的根元素-->
<configuration>
<!--enviroments表示環境配置,可以配置成開發環境(development)、測試環境(test)、生產環境(production)等-->
<environments default="development">
<environment id="development">
<!--transactionManager: 事務管理器,屬性type只有兩個取值:JDBC和MANAGED-->
<transactionManager type="MANAGED" />
<!--dataSource: 資料來源配置-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<!--mappers檔案路徑配置-->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
複製程式碼
- UserMapper介面
public interface UserMapper {
User selectById(int id);
}
複製程式碼
- UserMapper檔案
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace屬性表示命令空間,不同xml對映檔案namespace必須不同-->
<mapper namespace="com.pjmike.mybatis.UserMapper">
<select id="selectById" parameterType="int"
resultType="com.pjmike.mybatis.User">
SELECT id,name,age FROM user where id= #{id}
</select>
</mapper>
複製程式碼
- 測試類
public class MybatisTest {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1);
System.out.println("User : " + user);
}
}
}
// 結果:
User : User{id=1, age=21, name='pjmike'}
複製程式碼
上面的例子簡單的展示瞭如何使用MyBatis,與此同時,我也將用這個例子來進一步探究MyBatis動態原理的實現。
MyBatis動態代理的實現
public static void main(String[] args) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);// <1>
User user = userMapper.selectById(1);
System.out.println("User : " + user);
}
}
複製程式碼
在前面的例子中,我們使用sqlSession.getMapper()方法獲取UserMapper物件,實際上這裡我們是獲取了UserMapper介面的代理類,然後再由代理類執行方法。那麼這個代理類是如何生成的呢?在探究動態代理類如何生成之前,我們先來看下SqlSessionFactory工廠的建立過程做了哪些準備工作,比如說mybatis-config配置檔案是如何讀取的,對映器檔案是如何讀取的?
mybatis全域性配置檔案解析
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
複製程式碼
我們使用new SqlSessionFactoryBuilder().build()的方式建立SqlSessionFactory工廠,走進build方法
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
複製程式碼
對於mybatis的全域性配置檔案的解析,相關解析程式碼位於XMLConfigBuilder的parse()方法中:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//解析全域性配置檔案
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//解析mapper對映器檔案
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
複製程式碼
從parseConfiguration方法的原始碼中很容易就可以看出它對mybatis全域性配置檔案中各個元素屬性的解析。當然最終解析後返回一個Configuration物件,Configuration是一個很重要的類,它包含了Mybatis的所有配置資訊,它是通過XMLConfigBuilder取錢構建的,Mybatis通過XMLConfigBuilder讀取mybatis-config.xml中配置的資訊,然後將這些資訊儲存到Configuration中
對映器Mapper檔案的解析
//解析mapper對映器檔案
mapperElement(root.evalNode("mappers"));
複製程式碼
該方法是對全域性配置檔案中mappers屬性的解析,走進去:
mapperParser.parse()
方法就是XMLMapperBuilder對Mapper對映器檔案進行解析,可與XMLConfigBuilder進行類比
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper")); //解析對映檔案的根節點mapper元素
configuration.addLoadedResource(resource);
bindMapperForNamespace(); //重點方法,這個方法內部會根據namespace屬性值,生成動態代理類
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
複製程式碼
- configurationElement(XNode context)方法
該方法主要用於將mapper檔案中的元素資訊,比如insert
、select
這等資訊解析到MappedStatement物件,並儲存到Configuration類中的mappedStatements屬性中,以便於後續動態代理類執行CRUD操作時能夠獲取真正的Sql語句資訊
buildStatementFromContext方法就用於解析insert、select
這類元素資訊,並將其封裝成MappedStatement物件,具體的實現細節這裡就不細說了。
- bindMapperForNamespace()方法
該方法是核心方法,它會根據mapper檔案中的namespace屬性值,為介面生成動態代理類,這就來到了我們的主題內容——動態代理類是如何生成的。
動態代理類的生成
bindMapperForNamespace方法原始碼如下所示:
private void bindMapperForNamespace() {
//獲取mapper元素的namespace屬性值
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 獲取namespace屬性值對應的Class物件
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//如果沒有這個類,則直接忽略,這是因為namespace屬性值只需要唯一即可,並不一定對應一個XXXMapper介面
//沒有XXXMapper介面的時候,我們可以直接使用SqlSession來進行增刪改查
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
//如果namespace屬性值有對應的Java類,呼叫Configuration的addMapper方法,將其新增到MapperRegistry中
configuration.addMapper(boundType);
}
}
}
}
複製程式碼
這裡提到了Configuration的addMapper方法,實際上Configuration類裡面通過MapperRegistry物件維護了所有要生成動態代理類的XxxMapper介面資訊,可見Configuration類確實是相當重要一類
public class Configuration {
...
protected MapperRegistry mapperRegistry = new MapperRegistry(this);
...
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
...
}
複製程式碼
其中兩個重要的方法:getMapper()和addMapper()
- getMapper(): 用於建立介面的動態類
- addMapper(): mybatis在解析配置檔案時,會將需要生成動態代理類的介面註冊到其中
1. Configuration#addMappper()
Configuration將addMapper方法委託給MapperRegistry的addMapper進行的,原始碼如下:
public <T> void addMapper(Class<T> type) {
// 這個class必須是一個介面,因為是使用JDK動態代理,所以需要是介面,否則不會針對其生成動態代理
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 生成一個MapperProxyFactory,用於之後生成動態代理類
knownMappers.put(type, new MapperProxyFactory<>(type));
//以下程式碼片段用於解析我們定義的XxxMapper介面裡面使用的註解,這主要是處理不使用xml對映檔案的情況
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
複製程式碼
MapperRegistry內部維護一個對映關係,每個介面對應一個MapperProxyFactory(生成動態代理工廠類)
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
複製程式碼
這樣便於在後面呼叫MapperRegistry的getMapper()時,直接從Map中獲取某個介面對應的動態代理工廠類,然後再利用工廠類針對其介面生成真正的動態代理類。
2. Configuration#getMapper()
Configuration的getMapper()方法內部就是呼叫MapperRegistry的getMapper()方法,原始碼如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//根據Class物件獲取建立動態代理的工廠物件MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//這裡可以看到每次呼叫都會建立一個新的代理物件返回
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
複製程式碼
從上面可以看出,建立動態代理類的核心程式碼就是在MapperProxyFactory.newInstance方法中,原始碼如下:
protected T newInstance(MapperProxy<T> mapperProxy) {
//這裡使用JDK動態代理,通過Proxy.newProxyInstance生成動態代理類
// newProxyInstance的引數:類載入器、介面類、InvocationHandler介面實現類
// 動態代理可以將所有介面的呼叫重定向到呼叫處理器InvocationHandler,呼叫它的invoke方法
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
複製程式碼
PS: 關於JDK動態代理的詳細介紹這裡就不再細說了,有興趣的可以參閱我之前寫的文章:動態代理的原理及其應用
這裡的InvocationHandler介面的實現類是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;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//如果呼叫的是Object類中定義的方法,直接通過反射呼叫即可
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//呼叫XxxMapper介面自定義的方法,進行代理
//首先將當前被呼叫的方法Method構造成一個MapperMethod物件,然後掉用其execute方法真正的開始執行。
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
...
}
複製程式碼
最終的執行邏輯在於MapperMethod類的execute方法,原始碼如下:
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
//insert語句的處理邏輯
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
//update語句的處理邏輯
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
//delete語句的處理邏輯
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
//select語句的處理邏輯
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
//呼叫sqlSession的selectOne方法
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
...
}
複製程式碼
在MapperMethod中還有兩個內部類,SqlCommand和MethodSignature類,在execute方法中首先用switch case語句根據SqlCommand的getType()方法,判斷要執行的sql型別,比如INSET、UPDATE、DELETE、SELECT和FLUSH,然後分別呼叫SqlSession的增刪改查等方法。
慢著,說了這麼多,那麼這個getMapper()方法什麼時候被呼叫呀?實際是一開始我們呼叫SqlSession的getMapper()方法:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
...
}
複製程式碼
所以getMapper方法的大致呼叫邏輯鏈是: SqlSession#getMapper() ——> Configuration#getMapper() ——> MapperRegistry#getMapper() ——> MapperProxyFactory#newInstance() ——> Proxy#newProxyInstance()
還有一點我們需要注意:我們通過SqlSession的getMapper方法獲得介面代理來進行CRUD操作,其底層還是依靠的是SqlSession的使用方法。
小結
根據上面的探究過程,簡單畫了一個邏輯圖(不一定準確):
本篇文章主要介紹了MyBatis的動態原理,回過頭來,我們需要知道我們使用UserMapper的動態代理類進行CRUD操作,本質上還是通過SqlSession這個關鍵類執行增刪改查操作,但是對於SqlSession如何具體執行CRUD的操作並沒有仔細闡述,有興趣的同學可以查閱相關資料。