1. 程式人生 > >Mybatis深入原始碼分析之Mapper與介面繫結原理原始碼分析

Mybatis深入原始碼分析之Mapper與介面繫結原理原始碼分析

緊接上篇文章:Mybatis深入原始碼分析之SqlSessionFactoryBuilder原始碼分析,這裡再來分析下,Mapper與介面繫結原理。

本章疑問:

// 5.操作Mapper介面
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
public interface UserMapper {
    public UserEntity getUser(int id);
}

為什麼UserMapper是介面,沒用實現類,那麼他是怎麼初始化的?getMapper()方法為什麼可以呼叫?

mapper介面是怎麼初始化的?是反射?不是的,介面是不能反射初始化。揭祕:其實是代理設計模式【動態代理】,底層使用AOP實現。

另外MyBayis中最重要的是SqlSession:操縱SQL語句。

分析原始碼前,我們先回顧下動態代理技術,在我的這篇部落格中詳細介紹了:淺談Java【代理設計模式】——看這篇文章就懂了

思考問題:動態代理分為:jdk動態代理和CGLIB動態代理,那麼Mybatis使用了那種代理設計模式?

答案:MyBatis採用的jdk動態代理,因為代理的是介面。

回顧jdk動態代理

JDK動態代理的一般步驟如下:

1.建立被代理的介面和類;

2.實現InvocationHandler介面,對目標介面中宣告的所有方法進行統一處理;

3.呼叫Proxy的靜態方法,建立代理類並生成相應的代理物件;

程式碼實現jdk動態代理:

/**
 * 1.建立被代理的介面和類;
 */
public interface OrderService {
    public String add();
}
public class OrderServiceImpl implements OrderService {
    public String add() {
        System.out.println("OrderServiceImpl add。。。");
        return "success";
    }
}
/**
 * 2.實現InvocationHandler介面,對目標介面中宣告的所有方法進行統一處理;
 */
public class JdkMapperProxy implements InvocationHandler {
    //目標物件,被代理物件
    private Object targect;
    public JdkMapperProxy(Object targect){
        this.targect=targect;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置通知...在代理方法之前處理");
        //目標方法,目標方法引數
        Object result = method.invoke(targect, args);//被執行目標方法,被代理的方法
        System.out.println("後置通知...在代理方法之後處理");
        return null;
    }
}
/**
 * 3.呼叫Proxy的靜態方法,建立代理類並生成相應的代理物件;
 */
public class TestMybatis02 {
    public static void main(String[] args) {

        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        OrderService orderService = (OrderService) Proxy.newProxyInstance(OrderServiceImpl.class.getClassLoader()
                , OrderServiceImpl.class.getInterfaces(), new JdkMapperProxy(new OrderServiceImpl()));
        orderService.add();
    }
}

執行TestMybatis02 結果如下:

前置通知...在代理方法之前處理
OrderServiceImpl add。。。
後置通知...在代理方法之後處理

生成的代理類

通過反編譯工具檢視生成的代理類,可知,代理類實現了OrderService被代理類介面,add()方法中,呼叫h.invoke()方法,其中this.h指的是InvocationHandler,本質就是呼叫下面的這個方法

回顧了下jdk動態代理,下面我們開始原始碼分析

思考問題:會不會把下面這段配置轉為實體類

<select id="getUser" parameterType="int"
        resultType="com.mayikt.entity.UserEntity">
    select * from user where id=#{id}
</select>

答案是肯定的,在那裡進行解析的呢?下面開始分析原始碼:下面就是解析的地方

private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace != null && !namespace.equals("")) {
         ....
            //進入這裡
            this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } else {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
    } catch (Exception var3) {
        throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);
    }
}

重點這段程式碼:

this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
private void buildStatementFromContext(List<XNode> list) {
    if (this.configuration.getDatabaseId() != null) {
        //會進入到這裡
        this.buildStatementFromContext(list, this.configuration.getDatabaseId());
    }
    this.buildStatementFromContext(list, (String)null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    Iterator i$ = list.iterator();
    while(i$.hasNext()) {
        XNode context = (XNode)i$.next();
        XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);
        try {
            //進入到這裡
            statementParser.parseStatementNode();
        } catch (IncompleteElementException var7) {
            this.configuration.addIncompleteStatement(statementParser);
        }
    }
}
public void parseStatementNode() {
    String id = this.context.getStringAttribute("id");
    String databaseId = this.context.getStringAttribute("databaseId");
    if (this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
         ....
        if (this.configuration.hasKeyGenerator(keyStatementId)) {
            keyGenerator = this.configuration.getKeyGenerator(keyStatementId);
        } else {
            keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
        }
        //最終到這裡了
        this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }
}
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
    if (this.unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    } else {
      .....
        //進入這裡
        this.configuration.addMappedStatement(statement);
        return statement;
    }
}
public void addMappedStatement(MappedStatement ms) {
    //最終結果
    this.mappedStatements.put(ms.getId(), ms);
}
protected final Map<String, MappedStatement> mappedStatements;
this.mappedStatements = new Configuration.StrictMap("Mapped Statements collection");
protected static class StrictMap<V> extends HashMap<String, V> {

通過上面的程式碼執行流程,最終我們知道,mapper.xml中的配置檔案裡的每條sql語句是如何轉化為物件儲存起來的。最終都是封裝成一個MappedStatement物件,再通過一個HashMap集合儲存起來。

通過原始碼可知:HadhMap被put了兩次

後面我們來分析getMapper()方法:預設走的是DefaultSqlSession

// 5.操作Mapper介面
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

public <T> T getMapper(Class<T> type) {
    return this.configuration.getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return this.mapperRegistry.getMapper(type, sqlSession);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    } else {
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception var5) {
            throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
        }
    }
}

由上面程式碼可知:通過configuration.getMapper()去查下我們之前有沒有註冊過mapper介面,沒有則會報:沒用繫結介面錯誤。

再看看上篇文章中介紹的mapperRegistery裡面的東西:存放的是mapper介面,key為:介面,value為:MapperProxyFactory

這裡我們mapper介面註冊過,會進入else分支的這段程式碼:使用mapperProxyFactory建立代理類:

return mapperProxyFactory.newInstance(sqlSession);
public T newInstance(SqlSession sqlSession) {
    MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
    return this.newInstance(mapperProxy);
}

對比:mybatis的jdk動態代理和我們自己實現的jdk動態代理:

public class MapperProxy<T> implements InvocationHandler, Serializable {//mybatis的實現
public class JdkMapperProxy implements InvocationHandler {//我們的實現
protected T newInstance(MapperProxy<T> mapperProxy) {//mybatis的實現
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
OrderService orderService = (OrderService) Proxy.newProxyInstance(OrderServiceImpl.class.getClassLoader()//我們的實現
        , OrderServiceImpl.class.getInterfaces(), new JdkMapperProxy(new OrderServiceImpl()));

最後返回mapper資訊如下:mapper為:我們通過:mapperProxyFactory建立的代理類MapperProxy

所以當我們呼叫mapper的getUser()方法時候,就會執行MapperProxy代理類的invoke()方法

UserEntity user = mapper.getUser(2);
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {    //判斷mapper介面有沒有實現類,顯然我們mapper沒用實現類
        try {
            return method.invoke(this, args);
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    } else {    //會執行這個分支
        MapperMethod mapperMethod = this.cachedMapperMethod(method);    //快取中獲取method
        return mapperMethod.execute(this.sqlSession, args);    //執行sql語句
    }
}

思考問題:Mybatis裡面,mapper介面中有多個方法,每次呼叫會走同一個invoke()方法嗎?

答案:不會的,因為你的每個MapperRegistry裡面的class為mapper介面,都有獨立的MapperProxyFactory,因為MapperRegistry中key存放的是mapper介面,value為MapperProxyFactory。

我們使用MapperProxyFactory建立MapperProxy去建立的代理,所以每次呼叫getMapper()方法取到同一個mapper則會走同一個invoke()方法,反過來每次呼叫mapper時候,就會走不同invoke()方法。

一般我們把Mapper介面定義為全域性,則會走同一個invoke()方法,除非設=設定為多例,就每次都會new 不同,走不同invoke()方法。

Mybatis是基於多個不同的mapper介面生產的代理類,不同的mapper介面走不同的invoke方法,如果是相同的mapper介面,不同的方法,肯定是走同一個invoke方法。

那麼就有問題了,多個不同mapper介面會產生多個代理類( new MapperProxy()),佔太多的記憶體,後面會詳解。

MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);

上面我們把mapper介面看完了,執行 mapper.getUser(2) 會走invoke(),下面看invoke()方法

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 var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    } else {
        //進入這裡
        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        return mapperMethod.execute(this.sqlSession, args);
    }
}
private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);    //去快取中檢視是否有method,我們這裡是沒用的
        if (mapperMethod == null) {
            mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());    //會走到這裡
            this.methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
    this.method = new MapperMethod.MethodSignature(config, method);
}

先看下這塊

this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
public enum SqlCommandType {
    UNKNOWN,
    INSERT,
    UPDATE,
    DELETE,
    SELECT,
    FLUSH;

SqlCommandType 是和sql語句相關的

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    String statementName = mapperInterface.getName() + "." + method.getName();
    MappedStatement ms = null;
    if (configuration.hasStatement(statementName)) {//進入這裡
 
        ms = configuration.getMappedStatement(statementName);
    } else if (!mapperInterface.equals(method.getDeclaringClass())) {
        String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
        if (configuration.hasStatement(parentStatementName)) {
            ms = configuration.getMappedStatement(parentStatementName);
        }
    }
    if (ms == null) {
        if (method.getAnnotation(Flush.class) == null) {
            throw new BindingException("Invalid bound statement (not found): " + statementName);
        }
        this.name = null;
        this.type = SqlCommandType.FLUSH;
    } else {    //ms不為null,則執行到這裡
        this.name = ms.getId();
        this.type = ms.getSqlCommandType();
        if (this.type == SqlCommandType.UNKNOWN) {
            throw new BindingException("Unknown execution method for: " + this.name);
        }
    }
}
configuration.hasStatement(statementName)
public boolean hasStatement(String statementName) {
    return this.hasStatement(statementName, true);
}

getId()為namespace+id

將mapper.xml裡面配置的sql語句和對應的mapper介面方法進行關聯並放入map快取中,後期直接走快取了。最後執行execute()方法

public Object execute(SqlSession sqlSession, Object[] args) {
    Object param;
    Object result;
    if (SqlCommandType.INSERT == this.command.getType()) {
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
    } else if (SqlCommandType.UPDATE == this.command.getType()) {
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
    } else if (SqlCommandType.DELETE == this.command.getType()) {
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
    } else if (SqlCommandType.SELECT == this.command.getType()) {    //select型別走這裡
        if (this.method.returnsVoid() && this.method.hasResultHandler()) {    //判斷方法是否沒用返回結果的,不是
            this.executeWithResultHandler(sqlSession, args);
            result = null;
        } else if (this.method.returnsMany()) {    //判斷返回結果是不是返回多個結果集,不是
            result = this.executeForMany(sqlSession, args);
        } else if (this.method.returnsMap()) {    //是否返回map集合?不是
            result = this.executeForMap(sqlSession, args);
        } else {                //所以走這裡
            param = this.method.convertArgsToSqlCommandParam(args);    //轉換引數
            result = sqlSession.selectOne(this.command.getName(), param); //重點在這:selectOne()
        }
    } else {
        if (SqlCommandType.FLUSH != this.command.getType()) {
            throw new BindingException("Unknown execution method for: " + this.command.getName());
        }

        result = sqlSession.flushStatements();
    }

    if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
        throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
    } else {
        return result;
    }
}
public <T> T selectOne(String statement, Object parameter) {
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
        return list.get(0);
    } else if (list.size() > 1) {
        throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
        return null;
    }
}

通過原始碼我們可以改成下面這樣:selectOne(),後面我們針對selectOne()進行原始碼分析

//UserEntity user = mapper.getUser(2);
sqlSession.selectOne("com.mayikt.mapper.UserMapper.getUser()",2);

總結:

MybatisMapper介面繫結原理分析流程

1、mapper.xml中的配置檔案裡的每條sql語句,最終都是封裝成一個MappedStatement物件,再通過一個HashMap集合儲存起來。

2、執行getMapper()方法,判斷是否註冊過mapper介面,註冊了就會使用mapperProxyFactory去生成代理類MapperProxy

3、執行目標方法時,會呼叫MapperProxy代理類的invoke()方法

4、將mapper.xml裡面配置的sql語句和對應的mapper介面方法進行關聯並放入map快取中,後期直接走快取了。最後執行execute()方法

5、執行execute()方法最終呼叫selectOne()方法,執行結果。

 

本文參考

螞蟻課堂:http://www.mayikt.co