Mybatis如何通過接口實(shí)現(xiàn)sql執(zhí)行原理解析
使用過 mybatis 框架的小伙伴們都知道,mybatis 是個半 orm 框架,通過寫 mapper 接口就能自動實(shí)現(xiàn)數(shù)據(jù)庫的增刪改查,但是對其中的原理一知半解,接下來就讓我們深入框架的底層一探究竟
1、環(huán)境搭建
首先引入 mybatis 的依賴,在 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 包下新建一個接口 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 方法測試
try(InputStream in = Resources.getResourceAsStream("com/myboy/demo/sqlsession/mybatis-config.xml")){ SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in); sqlSession = sqlSessionFactory.openSession(); # 拿到代理類對象 UserMapper mapper = sqlSession.getMapper(UserMapper.class); # 執(zhí)行方法 UserEntity userEntity = mapper.getById(2L); System.out.println(userEntity); sqlSession.close(); }catch (Exception e){ e.printStackTrace(); }
2、動態(tài)代理類的生成
?? 通過上面的示例,我們需要思考兩個問題:
- mybatis 如何生成 mapper 的動態(tài)代理類?
- 通過 sqlSession.getMapper 獲取到的動態(tài)代理類是什么內(nèi)容?
通過查看源碼,sqlSession.getMapper() 底層調(diào)用的是 mapperRegistry 的 getMapper 方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // sqlSessionFactory build 的時候,就已經(jīng)掃描了所有的 mapper 接口,并生成了一個 MapperProxyFactory 對象 // 這里根據(jù) mapper 接口類獲取 MapperProxyFactory 對象,這個對象可以用于生成 mapper 的代理對象 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)建代理對象 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
代碼注釋已經(jīng)寫的很清楚,每個 mapper 接口在解析時會對應(yīng)生成一個 MapperProxyFactory,保存到 knownMappers 中,mapper 接口的實(shí)現(xiàn)類(也就是動態(tài)代理類)通過這個 MapperProxyFactory 生成,mapperProxyFactory.newInstance(sqlSession)
代碼如下:
/** * 根據(jù) sqlSession 創(chuàng)建 mapper 的動態(tài)代理對象 * @param sqlSession sqlSession * @return 代理類 */ public T newInstance(SqlSession sqlSession) { // 創(chuàng)建 MapperProxy 對象,這個對象實(shí)現(xiàn) InvocationHandler 接口,里面封裝類 mapper 動態(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); }
代碼一目了然,通過 jdk 動態(tài)代理技術(shù)創(chuàng)建了 mapper 接口的代理對象,其 InvocationHandler 的實(shí)現(xiàn)是 MapperProxy,那么 mapper 接口中方法的執(zhí)行,最終都會被 MapperProxy 增強(qiáng)
3、MapperProxy 增強(qiáng) mapper 接口
MapperProxy 類實(shí)現(xiàn)了 InvocationHandler 接口,那么其核心方法必然是在其 invoke 方法內(nèi)部
/** * 所有 mapper 代理對象的方法的核心邏輯 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 如果執(zhí)行的方法是 Object 類的方法,則直接反射執(zhí)行 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { // 1、根據(jù)method創(chuàng)建方法執(zhí)行器對象 MapperMethodInvoker,用于適配不同的方法執(zhí)行過程 // 2、執(zhí)行方法邏輯 return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }
3.1、cachedInvoker(method)
由于 jdk8 對接口增加了 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í)行器,只有一個 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,這個類的 invoke 方法就是利用反射調(diào)用原始方法,沒什么好說的
如果是普通的接口方法,則將方法封裝成封裝成 MapperMethod,然后再將 MapperMethod 封裝到 PlainMethodInvoker 中,PlainMethodInvoker 沒什么好看的,底層的執(zhí)行方法還是調(diào)用 MapperMethod 的執(zhí)行方法,至于 MapperMethod,咱們放到下一章來看
3.2、MapperMethod
首先看下構(gòu)造方法
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { // 通過這個 SqlCommand 可以拿到 sql 類型和sql 對應(yīng)的 MappedStatement this.command = new SqlCommand(config, mapperInterface, method); // 包裝了 mapper 接口的一個方法,可以拿到方法的信息,比如方法返回值類型、返回是否集合、返回是否為空 this.method = new MethodSignature(config, mapperInterface, method); }
代碼里的注釋寫的很清楚了,MapperMethod 構(gòu)造方法創(chuàng)建了兩個對象 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ù)處理,單個參數(shù)直接返回,多個參數(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 { // 方法返回單個對象 // 將參數(shù)進(jìn)行轉(zhuǎn)換,如果是一個參數(shù),則原樣返回,如果多個參數(shù),則返回map,key是參數(shù)name(@Param注解指定 或 arg0、arg1 或 param1、param2 ),value 是參數(shù)值 Object param = method.convertArgsToSqlCommandParam(args); // selectOne 從數(shù)據(jù)庫獲取數(shù)據(jù),封裝成返回值類型,取出第一個 result = sqlSession.selectOne(command.getName(), param); // 如果返回值為空,并且返回值類型是 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 方法來看,他只做了兩件事
- 參數(shù)轉(zhuǎn)換
- 調(diào)用 sqlSession 對應(yīng)的 insert 方法
3.2.1、參數(shù)轉(zhuǎn)換 method.convertArgsToSqlCommandParam(args)
在 mapper 接口中,假設(shè)我們定義了一個 user 的查詢方法
List<User> find(@Param("name")String name, @Param("age")Integer age)
在我們的 mapper.xml 中,寫出來的 sql 可以是這樣的:
select * from user where name = #{name} and age > #{age}
當(dāng)然不使用 @Param 注解也可以的,按參數(shù)順序來
select * from user where name = #{arg0} and age > #{arg1} 或 select * from user where name = #{param1} and age > #{param2}
因此如果要通過占位符匹配到具體參數(shù),就要將接口參數(shù)封裝成 map 了,如下所示
{arg1=12, arg0="abc", param1="abc", param2=12} 或 {name="abc", age=12, param1="abc", param2=12} 復(fù)制代碼
這里的這個 method.convertArgsToSqlCommandParam(args) 就是這個作用,當(dāng)然只有一個參數(shù)的話就不用轉(zhuǎn)成 map 了, 直接就能匹配
3.2.2、調(diào)用 sqlSession 的方法獲取結(jié)果
真正要操作數(shù)據(jù)庫還是要借助 sqlSession,因此很快就看到了 sqlSession.insert(command.getName(), param)
方法的執(zhí)行,其第一個參數(shù)是 statement 的 id,就是 mpper.xml 中 namespace 和 insert 標(biāo)簽的 id的組合,如 com.myboy.demo.mapper.MoonAppMapper.getAppById
,第二個參數(shù)就是上面轉(zhuǎn)換過的參數(shù),至于 sqlSession 內(nèi)部處理邏輯,不在本章敘述范疇
sqlSession 方法執(zhí)行完后的執(zhí)行結(jié)果交給 rowCountResult 方法處理,這個方法很簡單,就是將數(shù)據(jù)庫返回的數(shù)據(jù)處理成接口返回類型,代碼很簡單,如下
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)搞清楚了通過 mapper 接口生成動態(tài)代理對象,以及代理對象調(diào)用 sqlSession 操作數(shù)據(jù)庫的邏輯,我總結(jié)出執(zhí)行邏輯圖如下:
總結(jié)
到此這篇關(guān)于Mybatis如何通過接口實(shí)現(xiàn)sql執(zhí)行原理的文章就介紹到這了,更多相關(guān)Mybatis接口實(shí)現(xiàn)sql執(zhí)行內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java利用delayedQueue實(shí)現(xiàn)本地的延遲隊(duì)列
這篇文章主要給大家介紹了java利用delayedQueue實(shí)現(xiàn)本地的延遲隊(duì)列的相關(guān)資料,文中介紹的非常詳細(xì),相信對大家具有一定的參考價值,需要的朋友們下面來一起看看吧。2017-04-04Spring Cloud Admin健康檢查 郵件、釘釘群通知的實(shí)現(xiàn)
這篇文章主要介紹了Spring Cloud Admin健康檢查 郵件、釘釘群通知的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08SpringBoot使用hutool-captcha實(shí)現(xiàn)驗(yàn)證碼生成與驗(yàn)證
在springboot的登陸頁面中為了防止機(jī)器大規(guī)模注冊,機(jī)器暴力破解數(shù)據(jù)密碼等危害,需要驗(yàn)證隨機(jī)生成的驗(yàn)證碼,本文主要介紹了SpringBoot使用hutool-captcha實(shí)現(xiàn)驗(yàn)證碼生成與驗(yàn)證,感興趣的可以了解一下2023-12-12springboot使用jasypt對配置文件加密加密數(shù)據(jù)庫連接的操作代碼
這篇文章主要介紹了springboot使用jasypt對配置文件加密加密數(shù)據(jù)庫連接的操作代碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-01-01SpringBoot返回Json對象報(bào)錯(返回對象為空{(diào)})
本文主要介紹介紹了SpringBoot返回Json對象報(bào)錯(返回對象為空{(diào)}),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01解析Spring Boot內(nèi)嵌tomcat關(guān)于getServletContext().getRealPath獲取得到臨時
大家都很糾結(jié)這個問題在使用getServletContext().getRealPath()得到的是臨時文件的路徑,每次重啟服務(wù),這個臨時文件的路徑還好變更,下面小編通過本文給大家分享Spring Boot內(nèi)嵌tomcat關(guān)于getServletContext().getRealPath獲取得到臨時路徑的問題,一起看看吧2021-05-05使用filebeat收集并解析springboot日志過程示例
這篇文章主要為大家介紹了使用filebeat收集并解析springboot日志實(shí)現(xiàn)過程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08