Mybatis如何通過(guò)接口實(shí)現(xiàn)sql執(zhí)行原理解析
使用過(guò) mybatis 框架的小伙伴們都知道,mybatis 是個(gè)半 orm 框架,通過(guò)寫(xiě) mapper 接口就能自動(dòng)實(shí)現(xiàn)數(shù)據(jù)庫(kù)的增刪改查,但是對(duì)其中的原理一知半解,接下來(lái)就讓我們深入框架的底層一探究竟
1、環(huán)境搭建
首先引入 mybatis 的依賴(lài),在 resources 目錄下創(chuàng)建 mybatis 核心配置文件 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 環(huán)境、事務(wù)工廠、數(shù)據(jù)源 -->
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="UNPOOLED">
<property name="driver" value="org.apache.derby.jdbc.EmbeddedDriver"/>
<property name="url" value="jdbc:derby:db-user;create=true"/>
</dataSource>
</environment>
</environments>
<!-- 指定 mapper 接口-->
<mappers>
<mapper class="com.myboy.demo.mapper.user.UserMapper"/>
</mappers>
</configuration>在 com.myboy.demo.mapper.user 包下新建一個(gè)接口 UserMapper
public interface UserMapper {
UserEntity getById(Long id);
void insertOne(@Param("id") Long id, @Param("name") String name, @Param("json") List<String> json);
}在 resources 的 com.myboy.demo.mapper.user 包下創(chuàng)建 UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myboy.demo.mapper.user.UserMapper">
<select id="getById" resultType="com.myboy.demo.db.entity.UserEntity">
select * from demo_user where id = #{id}
</select>
<insert id="insertOne">
insert into demo_user (id, name, json) values (#{id}, #{name}, #{json})
</insert>
</mapper>創(chuàng)建 main 方法測(cè)試
try(InputStream in = Resources.getResourceAsStream("com/myboy/demo/sqlsession/mybatis-config.xml")){
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
sqlSession = sqlSessionFactory.openSession();
# 拿到代理類(lèi)對(duì)象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
# 執(zhí)行方法
UserEntity userEntity = mapper.getById(2L);
System.out.println(userEntity);
sqlSession.close();
}catch (Exception e){
e.printStackTrace();
}2、動(dòng)態(tài)代理類(lèi)的生成
?? 通過(guò)上面的示例,我們需要思考兩個(gè)問(wèn)題:
- mybatis 如何生成 mapper 的動(dòng)態(tài)代理類(lèi)?
- 通過(guò) sqlSession.getMapper 獲取到的動(dòng)態(tài)代理類(lèi)是什么內(nèi)容?
通過(guò)查看源碼,sqlSession.getMapper() 底層調(diào)用的是 mapperRegistry 的 getMapper 方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// sqlSessionFactory build 的時(shí)候,就已經(jīng)掃描了所有的 mapper 接口,并生成了一個(gè) MapperProxyFactory 對(duì)象
// 這里根據(jù) mapper 接口類(lèi)獲取 MapperProxyFactory 對(duì)象,這個(gè)對(duì)象可以用于生成 mapper 的代理對(duì)象
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 創(chuàng)建代理對(duì)象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}代碼注釋已經(jīng)寫(xiě)的很清楚,每個(gè) mapper 接口在解析時(shí)會(huì)對(duì)應(yīng)生成一個(gè) MapperProxyFactory,保存到 knownMappers 中,mapper 接口的實(shí)現(xiàn)類(lèi)(也就是動(dòng)態(tài)代理類(lèi))通過(guò)這個(gè) MapperProxyFactory 生成,mapperProxyFactory.newInstance(sqlSession) 代碼如下:
/**
* 根據(jù) sqlSession 創(chuàng)建 mapper 的動(dòng)態(tài)代理對(duì)象
* @param sqlSession sqlSession
* @return 代理類(lèi)
*/
public T newInstance(SqlSession sqlSession) {
// 創(chuàng)建 MapperProxy 對(duì)象,這個(gè)對(duì)象實(shí)現(xiàn) InvocationHandler 接口,里面封裝類(lèi) mapper 動(dòng)態(tài)代理方法的執(zhí)行的核心邏輯
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}代碼一目了然,通過(guò) jdk 動(dòng)態(tài)代理技術(shù)創(chuàng)建了 mapper 接口的代理對(duì)象,其 InvocationHandler 的實(shí)現(xiàn)是 MapperProxy,那么 mapper 接口中方法的執(zhí)行,最終都會(huì)被 MapperProxy 增強(qiáng)
3、MapperProxy 增強(qiáng) mapper 接口
MapperProxy 類(lèi)實(shí)現(xiàn)了 InvocationHandler 接口,那么其核心方法必然是在其 invoke 方法內(nèi)部
/**
* 所有 mapper 代理對(duì)象的方法的核心邏輯
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果執(zhí)行的方法是 Object 類(lèi)的方法,則直接反射執(zhí)行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 1、根據(jù)method創(chuàng)建方法執(zhí)行器對(duì)象 MapperMethodInvoker,用于適配不同的方法執(zhí)行過(guò)程
// 2、執(zhí)行方法邏輯
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}3.1、cachedInvoker(method)
由于 jdk8 對(duì)接口增加了 default 關(guān)鍵字,使接口中的方法也可以有方法體,但是默認(rèn)方法和普通方法的反射執(zhí)行方式不同,需要用適配器適配一下才能統(tǒng)一執(zhí)行,具體代碼如下
/**
* 適配器模式,由于默認(rèn)方法和普通方法反射執(zhí)行的方式不同,所以用 MapperMethodInvoker 接口適配下
* DefaultMethodInvoker 用于執(zhí)行默認(rèn)方法
* PlainMethodInvoker 用于執(zhí)行普通方法
*/
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return MapUtil.computeIfAbsent(methodCache, method, m -> {
// 返回默認(rèn)方法執(zhí)行器 DefaultMethodInvoker
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
// 返回普通方法執(zhí)行器,只有一個(gè) invoke 執(zhí)行方法,實(shí)際上就是調(diào)用 MapperMethod 的執(zhí)行方法
else {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}如果判定執(zhí)行的是接口的默認(rèn)方法,則原始方法封裝成 DefaultMethodInvoker,這個(gè)類(lèi)的 invoke 方法就是利用反射調(diào)用原始方法,沒(méi)什么好說(shuō)的
如果是普通的接口方法,則將方法封裝成封裝成 MapperMethod,然后再將 MapperMethod 封裝到 PlainMethodInvoker 中,PlainMethodInvoker 沒(méi)什么好看的,底層的執(zhí)行方法還是調(diào)用 MapperMethod 的執(zhí)行方法,至于 MapperMethod,咱們放到下一章來(lái)看
3.2、MapperMethod
首先看下構(gòu)造方法
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 通過(guò)這個(gè) SqlCommand 可以拿到 sql 類(lèi)型和sql 對(duì)應(yīng)的 MappedStatement
this.command = new SqlCommand(config, mapperInterface, method);
// 包裝了 mapper 接口的一個(gè)方法,可以拿到方法的信息,比如方法返回值類(lèi)型、返回是否集合、返回是否為空
this.method = new MethodSignature(config, mapperInterface, method);
}代碼里的注釋寫(xiě)的很清楚了,MapperMethod 構(gòu)造方法創(chuàng)建了兩個(gè)對(duì)象 SqlCommand 和 MethodSignature
mapper 接口的執(zhí)行核心邏輯在其 execute() 方法中:
/**
* 執(zhí)行 mapper 方法的核心邏輯
* @param sqlSession sqlSession
* @param args 方法入?yún)?shù)組
* @return 接口方法返回值
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
// 參數(shù)處理,單個(gè)參數(shù)直接返回,多個(gè)參數(shù)封裝成 map
Object param = method.convertArgsToSqlCommandParam(args);
// 調(diào)用 sqlSession 的插入方法
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
// 方法返回值為 void,但是參數(shù)里有 ResultHandler
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 方法返回集合
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 方法返回 map
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 方法返回指針
result = executeForCursor(sqlSession, args);
} else {
// 方法返回單個(gè)對(duì)象
// 將參數(shù)進(jìn)行轉(zhuǎn)換,如果是一個(gè)參數(shù),則原樣返回,如果多個(gè)參數(shù),則返回map,key是參數(shù)name(@Param注解指定 或 arg0、arg1 或 param1、param2 ),value 是參數(shù)值
Object param = method.convertArgsToSqlCommandParam(args);
// selectOne 從數(shù)據(jù)庫(kù)獲取數(shù)據(jù),封裝成返回值類(lèi)型,取出第一個(gè)
result = sqlSession.selectOne(command.getName(), param);
// 如果返回值為空,并且返回值類(lèi)型是 Optional,則將返回值用 Optional.ofNullable 包裝
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;
}代碼邏輯很清晰,拿 Insert 方法來(lái)看,他只做了兩件事
- 參數(shù)轉(zhuǎn)換
- 調(diào)用 sqlSession 對(duì)應(yīng)的 insert 方法
3.2.1、參數(shù)轉(zhuǎn)換 method.convertArgsToSqlCommandParam(args)
在 mapper 接口中,假設(shè)我們定義了一個(gè) user 的查詢(xún)方法
List<User> find(@Param("name")String name, @Param("age")Integer age)在我們的 mapper.xml 中,寫(xiě)出來(lái)的 sql 可以是這樣的:
select * from user where name = #{name} and age > #{age}當(dāng)然不使用 @Param 注解也可以的,按參數(shù)順序來(lái)
select * from user where name = #{arg0} and age > #{arg1}
或
select * from user where name = #{param1} and age > #{param2}因此如果要通過(guò)占位符匹配到具體參數(shù),就要將接口參數(shù)封裝成 map 了,如下所示
{arg1=12, arg0="abc", param1="abc", param2=12}
或
{name="abc", age=12, param1="abc", param2=12}
復(fù)制代碼這里的這個(gè) method.convertArgsToSqlCommandParam(args) 就是這個(gè)作用,當(dāng)然只有一個(gè)參數(shù)的話就不用轉(zhuǎn)成 map 了, 直接就能匹配
3.2.2、調(diào)用 sqlSession 的方法獲取結(jié)果
真正要操作數(shù)據(jù)庫(kù)還是要借助 sqlSession,因此很快就看到了 sqlSession.insert(command.getName(), param) 方法的執(zhí)行,其第一個(gè)參數(shù)是 statement 的 id,就是 mpper.xml 中 namespace 和 insert 標(biāo)簽的 id的組合,如 com.myboy.demo.mapper.MoonAppMapper.getAppById,第二個(gè)參數(shù)就是上面轉(zhuǎn)換過(guò)的參數(shù),至于 sqlSession 內(nèi)部處理邏輯,不在本章敘述范疇
sqlSession 方法執(zhí)行完后的執(zhí)行結(jié)果交給 rowCountResult 方法處理,這個(gè)方法很簡(jiǎn)單,就是將數(shù)據(jù)庫(kù)返回的數(shù)據(jù)處理成接口返回類(lèi)型,代碼很簡(jiǎn)單,如下
private Object rowCountResult(int rowCount) {
final Object result;
if (method.returnsVoid()) {
result = null;
} else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
result = rowCount;
} else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
result = (long) rowCount;
} else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
result = rowCount > 0;
} else {
throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
}
return result;
}4、小結(jié)
到目前為止,我們已經(jīng)搞清楚了通過(guò) mapper 接口生成動(dòng)態(tài)代理對(duì)象,以及代理對(duì)象調(diào)用 sqlSession 操作數(shù)據(jù)庫(kù)的邏輯,我總結(jié)出執(zhí)行邏輯圖如下:

總結(jié)
到此這篇關(guān)于Mybatis如何通過(guò)接口實(shí)現(xiàn)sql執(zhí)行原理的文章就介紹到這了,更多相關(guān)Mybatis接口實(shí)現(xiàn)sql執(zhí)行內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
RabbitMQ消費(fèi)者限流實(shí)現(xiàn)消息處理優(yōu)化
這篇文章主要介紹了RabbitMQ消費(fèi)者限流實(shí)現(xiàn)消息處理優(yōu)化,消費(fèi)者限流是用于消費(fèi)者每次獲取消息時(shí)限制條數(shù),注意前提是手動(dòng)確認(rèn)模式,并且在手動(dòng)確認(rèn)后才能獲取到消息,感興趣想要詳細(xì)了解可以參考下文2023-05-05
SSH框架網(wǎng)上商城項(xiàng)目第2戰(zhàn)之基本增刪查改、Service和Action的抽取
SSH框架網(wǎng)上商城項(xiàng)目第2戰(zhàn)之基本增刪查改、Service和Action的抽取,感興趣的小伙伴們可以參考一下2016-05-05
Spring Cloud入門(mén)系列服務(wù)提供者總結(jié)
這篇文章主要介紹了Spring Cloud入門(mén)系列之服務(wù)提供者總結(jié),服務(wù)提供者使用Eureka Client組件創(chuàng)建 ,創(chuàng)建完成以后修改某文件,具體操作方法及實(shí)例代碼跟隨小編一起看看吧2021-06-06
基于Apache組件分析對(duì)象池原理的實(shí)現(xiàn)案例分析
本文從對(duì)象池的一個(gè)簡(jiǎn)單案例切入,主要分析common-pool2組件關(guān)于:池、工廠、配置、對(duì)象管理幾個(gè)角色的源碼邏輯,并且參考其在Redis中的實(shí)踐,對(duì)Apache組件分析對(duì)象池原理相關(guān)知識(shí)感興趣的朋友一起看看吧2022-04-04
SpringBoot整合WebSocket的客戶端和服務(wù)端的實(shí)現(xiàn)代碼
這篇文章主要介紹了SpringBoot整合WebSocket的客戶端和服務(wù)端的實(shí)現(xiàn),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07
Java從控制臺(tái)讀入數(shù)據(jù)的幾種方法總結(jié)
下面小編就為大家?guī)?lái)一篇Java從控制臺(tái)讀入數(shù)據(jù)的幾種方法總結(jié)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-10-10
JAVA中的函數(shù)式接口Function和BiFunction詳解
這篇文章主要介紹了JAVA中的函數(shù)式接口Function和BiFunction詳解,JDK的函數(shù)式接口都加上了@FunctionalInterface注解進(jìn)行標(biāo)識(shí),但是無(wú)論是否加上該注解只要接口中只有一個(gè)抽象方法,都是函數(shù)式接口,需要的朋友可以參考下2024-01-01
Spring中的循環(huán)依賴(lài)問(wèn)題
在Spring框架中,循環(huán)依賴(lài)是指兩個(gè)或多個(gè)Bean相互依賴(lài),這導(dǎo)致在Bean的創(chuàng)建過(guò)程中出現(xiàn)依賴(lài)死鎖,為了解決這一問(wèn)題,Spring引入了三級(jí)緩存機(jī)制,包括singletonObjects、earlySingletonObjects和singletonFactories2024-09-09
Java修改eclipse中web項(xiàng)目的server部署路徑問(wèn)題
這篇文章主要介紹了Java修改eclipse中web項(xiàng)目的server部署路徑,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11

