1. 程式人生 > 實用技巧 >MyBatis-介面實現資料庫操作的原理

MyBatis-介面實現資料庫操作的原理

參考:

https://blog.csdn.net/luoposhushengyizhuce/article/details/83902433

https://www.cnblogs.com/williamjie/p/11188355.html

mybatis如何通過只需要配置介面就能實現資料庫的操作

在用mybatis的時候,我們只需要寫一個介面,然後服務層就能呼叫,在我們的認知中,是不能直接調介面的方法的,這個其中的原理是什麼呢?由於自己比較好奇,就取翻了一下mybatis的原始碼,一下是做的一些記錄。
通過一個最簡單的例子來揭開它的面目。

@Test
public void testDogSelect() throws IOException {
    String resource = "allconfig.xml";// ①
    InputStream inputStream = Resources.getResourceAsStream(resource);// ②
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);// ③
    SqlSession session = sqlSessionFactory.openSession();// ④
    DogMapper dogMapper = session.getMapper(DogMapper.class);// ⑤
    List<Dog> dogs = dogMapper.selectDog();//⑥
    System.out.println(dogs.size());// ⑦
}


首先就是①②兩行是沒什麼要解釋的,就從第三行開始,我們追蹤程式碼,進入SqlSessionFactoryBuilder的build(InputStream)的方法
public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}

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.
        }
    }
}


重要程式碼就兩行,其中XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);是構建解析器用的,主要看下一行build(parser.parse()),其中parser.parse()主要就是把我們的mybatis的配置檔案進行解析,並把解析的大部分內容儲存到Configuration中。然後看build的程式碼

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }


返回了一個DefaultSqlSessionFactory
繼續看④這一句,我們知道sqlSessionFactory實際上是DefaultSqlSessionFactory的一個例項,進入DefaultSqlSessionFactory的openSession()方法

@Override
public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}


其他我們都先不管,我們只要看到這個方法返回的是一個DefaultSqlSession的例項就好
我們繼續看⑤這一行,session是DefaultSqlSession的一個例項。我們進入DefaultSqlSession的getMapper(Class type)方法,

@Override
public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
}


其中configuration是Configuration的例項,在解析mybatis的配置檔案的時候進行的初始化。繼續追進去

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}


繼續進到MapperRegistry的getMapper(Class type, SqlSession sqlSession)方法

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    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);
    }
}


此方法根據傳進來的type生成對應的代理,我們進入看看MapperProxyFactory

public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}


到這裡已經看到已經完成代理的生成,MapperProxyFactory是個工廠。再繼續看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 {
        if (Object.class.equals(method.getDeclaringClass())) {
        try {
            return method.invoke(this, args);
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        }
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
        mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
        methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }

}

當我們通過生成的物件呼叫方法的時候,都會進入這個類的invoke方法方法,我看看到如果呼叫的是我們自定的方法,直接就是呼叫的mybatis的實現,通過介面找到配置資訊,然後根據我們的配置去操作資料庫。

最後總結一下,mybatis之所以配置介面以後就能執行是因為在生產mapper的時候實質上是生成的一個代理,然後通過mapper呼叫介面方法的時候直接被MapperProxy的invoke截斷了,直接去呼叫了mybatis為我們制定的實現,而沒有去回撥。

MyBatis你只寫了介面為啥就能執行SQL啊?

一、靜態代理

又是一年秋招季,很多小夥伴開始去大城市打拼。來大城市第一件事就是租房,免不了和中介打交道,因為很多房東很忙,你根本找不到他。從這個場景中就可以抽象出來代理模式:

  • ISubject:被訪問者資源的抽象

  • SubjectImpl:被訪問者具體實現類(房東)

  • SubjectProxy:被訪問者的代理實現類(中介)

UML圖如下:

舉個例子來理解一下這個設計模式:

老闆讓記錄一下使用者服務的響應時間,用代理模式來實現這個功能。

一切看起來都非常的美好,老闆又發話了,把產品服務的響應時間也記錄一下吧。又得寫如下3個類:

  • IProductService

  • ProductServiceImpl

  • ProductServiceProxy

UserServiceProxy和ProductServiceProxy這兩個代理類的邏輯都差不多,卻還得寫2次。其實這個還好,如果老闆說,把現有系統的幾十個服務的響應時間都記錄一下吧,你是不是要瘋了?這得寫多少代理類啊?

二、動態代理

黑暗總是暫時的,終究會迎來黎明,在JDK1.3之後引入了一種稱之為動態代理(Dynamic Proxy)的機制。使用該機制,我們可以為指定的介面在系統執行期間動態地生成代理物件,從而幫助我們走出最初使用靜態代理實現AOP的窘境

動態代理的實現主要由一個類和一個介面組成,即java.lang.reflect.Proxy類和java.lang.reflect.InvocationHandler介面。

讓我們用動態代理來改造一下上面記錄系統響應時間的功能。雖然要為IUserService和IProductService兩種服務提供代理物件,但因為代理物件中要新增的橫切邏輯是一樣的。所以我們只需要實現一個InvocationHandler就可以了。程式碼如下

UML圖如下。恭喜你,你現在已經理解了Spring AOP是怎麼回事了,就是這麼簡單,今天先不展開談Spring

先簡單談談動態代理在Mybatis中是如何被大佬玩的出神入化的

三、Mybatis核心設計思路

相信用過mybatis的小夥伴都能理解下面這段程式碼,通過roleMapper這個介面直接從資料庫中拿到一個物件

Role role = roleMapper.getRole(3L);

直覺告訴我,一個介面是不能執行的啊,一定有介面的實現類,可是這個實現類我自己沒寫啊,難道mybatis幫我們生成了?你猜的沒錯,mybatis利用動態代理幫我們生成了介面的實現類,這個類就是:

org.apache.ibatis.binding.MapperProxy,

我先畫一下UML圖,MapperProxy就是下圖中的SubjectProxy類

和上面的UML類圖對比一下,發現不就少了一個SubjectImpl類嗎?那應該就是SubjectProxy類把SubjectImple類要做的事情做了唄,猜對了。SubjectProxy通過SubjectImple和SubjectImple.xml之間的對映關係知道自己應該執行什麼SQL。所以mybatis最核心的思路就是這麼個意思,細節之類的可以看原始碼,理清最主要的思路,看原始碼就能把握住重點。

關於原始碼相關的內容,更進一步的解釋動態代理在MyBatis中的使用,可以參考以前的一篇文章:《動態代理之投鞭斷流!看一下MyBatis的底層實現原理!

附錄:

https://mp.weixin.qq.com/s/ToMvAD0-QyUJgg_1eFvcaA