1. 程式人生 > >通過模擬Mybatis動態代理生成Mapper代理類,講解Mybatis核心原理

通過模擬Mybatis動態代理生成Mapper代理類,講解Mybatis核心原理

本文將通過模擬Mybatis動態代理生成Mapper代理類,講解Mybatis原理

1.平常我們是如何使用Mapper的

先寫一個簡單的UserMapper,它包含一個全表查詢的方法,程式碼如下

public interface UserMapper {

    @Select("select * from user")
    public List<User> queryAll();
}

然後大家思考一個問題,我們平時是怎麼使用這個UserMapper的?

很多時候我們會把Mybatis和Spring整合起來一起使用,於是會有類似下面的程式碼:

@Service
public class UserServiceImpl {

    @Autowired
    private UserMapper userMapper;

    public List<User> queryAll(){
        return this.userMapper.queryAll();
    }
}

看到這段熟得不能再熟的程式碼不知道大家會不會有一絲疑惑:UserMapper明明是一個介面,為什麼可以直接呼叫他的queryAll方法呢?

這個問題其實也不難解,我們不能直接呼叫一個介面的方法,這背後肯定是有一個物件的,至於這個物件是怎麼來的,這裡直接告訴大家是通過動態代理生成的。只要弄懂了這個動態代理物件是怎麼生成的,整個Mybatis框架原理就就說清楚了。在模擬之前我們先驗證一下是不是使用JDK的動態代理。

2.驗證Mybatis是通過動態代理生成Mapper代理物件

由於上面一段程式碼整合了spring,spring又為我們封裝了許多細節,我們重新看一段程式碼,看看沒有spring的情況下我們怎麼獲得一個UserMapper

public static void main(String[] args){
    
    SqlSessionFactory sqlSessionFactory = ....  //這裡省略,官網給了很多配置SqlSessionFactory的方法(不一定是這麼獲得)

    SqlSession session = sqlSessionFactory.openSession();

    UserMapper userMapper = session.getMapper(UserMapper.class);
}

為了驗證獲得UserMapper採用的是動態代理,我們可以在IDE中對著session.getMapper(UserMapper.class)

一路按著Ctrl點進去,我們會發現最終呼叫的程式碼是這樣的:

  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);

最後果然呼叫的是JDK的動態代理

3.動手模擬一次Mybatis的動態代理

既然知道了原理,下面我們就動手驗證吧!

為了簡單明瞭,我們不寫SqlSessionFactory類了,直接自定義一個MySession類,在裡面給出模擬的getMapper方法:

public class MySession {
    public static Object getMapper(Class clazz){
        //呼叫newProxyInstance需要傳入class陣列
        Class[] clazzs = new Class[]{clazz};
        //把動態代理過程中生成的代理類保留下來,有助於新手理解動態代理(此行可省略)
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        //通過動態代理生成Mapper
        Object object = Proxy.newProxyInstance(MySession.class.getClassLoader(), clazzs, new MyInvocationHandler());
        return object;
    }
}

如果不熟悉JDK的動態代理也沒關係,下面我會逐步分析。

大家可以看到呼叫動態代理生成動態代理物件需要三個引數:

  1. 類載入器
  2. class陣列
  3. InvocationHandler例項

為什麼需要類載入器?

動態代理生成類和其呼叫類必須通過同一個類載入器載入,否則它們之間無法相互呼叫

為什麼有個class陣列?

JDK的動態代理是基於介面的,class陣列中存放的是動態代理類需要實現的介面。比如本文中的例子生成的動態代理類需要實現UserMapper介面,所以你得把介面告訴它。

為什麼會有InvocationHandler例項?

動態代理會在原有方法上實現增強,而增強的邏輯就寫在InvocationHandler類的invoke方法上,所以要有這麼個例項。

你想一想,當初的這段程式碼

public interface UserMapper {

    @Select("select * from user")
    public List<User> queryAll();
}

,我們想實現一個怎樣的功能?

無非就是給它一條sql語句,希望它能去資料庫中執行這條sql語句並返回結果。這個過程可以拆分成兩個部分:

  1. 得到sql語句
  2. 通過JDBC操作資料庫,並執行sql返回結果

這裡省略JDBC的過程,給出一個簡單的invoke方法示例(Mybatis為我們封裝了一切JDBC的處理細節):

public class MyInvocationHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //解析得到sql
        Select annotation = method.getAnnotation(Select.class);
        String sql = annotation.value()[0];

        //執行sql(模擬JDBC)
        System.out.println(sql + " executing...");
        return null;
    }
}

最終我們執行UserMapper的queryAll()方法時,就會出現如下結果:

public class Main {
    public static void main(String[] args) {
        UserMapper userMapper = (UserMapper) MySession.getMapper(UserMapper.class);
        userMapper.query();
    }
}

//列印:
//select * from user executing...

總結

最後我們總結一下Mybatis框架的核心原理:

  1. 使用者只需要建立Mapper介面,並使用Mapper介面即可。
  2. Mybatis會對Mapper介面產生動態代理物件,這個動態代理物件實現了Mapper介面,擁有Mapper中定義的所有方法,並對這些方法進行了增強。增強的邏輯是獲得sql語句和執行sql語句。

通過個核心原理我們也就知道了Mybatis為我們做了什麼:

  1. 讓方法和sql語句對應起來,操作資料庫就如同呼叫方法一般簡單
  2. 遮蔽掉JDBC的細節