美文网首页
mybatis运行原理04-mapper代理对象方法调用之Map

mybatis运行原理04-mapper代理对象方法调用之Map

作者: 布拉德老瓜 | 来源:发表于2021-03-08 22:46 被阅读0次

    在上一篇文章中,讲解了由SqlSession获取到mapper代理对象的过程,在这个过程中,首先创建了一个与sqlSession关联的InvocationHandler: mapperProxy, 然后调Proxy.newInstance(loader, interfaces, ih)创建了指定mapper接口的代理对象proxyObj,这个代理对象实际上是通过mapperProxy.invoke(proxyObj, method, args)生效的。即当我们调用proxyObj.method(args)时,由mapperProxy.invoke(proxyObj, method, args)来进行处理。
    本文就来讲述mapperProxy.invoke(proxyObj, method, args)是如何工作的:包括为方法创建mapperMethod对象和mapperMethod.execute()方法。至于如何创建连接、sql语句参数处理、执行sql语句、事务控制、返回指定结果集,放到下一篇文章再说。

    对象关系

    先上代码:

        //mapperProxy.invoke(proxy, method, args)
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
            } catch (Throwable var5) {
                throw ExceptionUtil.unwrapThrowable(var5);
            }
        }
    

    上来先判断定义该方法的是不是Object类,如果是(比如hashCode()等方法),没什么好说的,让方法执行。如果不是,那就先执行cachedInvoker(method),给创建一个invoker,然后再调用invoker的invoke()方法。这里有两个问题:
    1.该方法对象可能没有实现逻辑,除了方法的参数类型和返回值类型外,还缺少要执行的的sql语句。那么就需要为其进行sql语句的封装、参数化的处理和结果集的处理。

    2.java8中接口可以有实现方法(接口中的default方法),也可以有抽象方法。该创建一个怎样的invoker呢?显然要判断一下,然后区别对待。cachedInvoker()方法把这件事情交给了this.methodCache.computeIfAbsent(method, mappingFunction)来完成。

        private MapperProxy.MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
            try {
                //  computeIfAbsent(method, mappingFunction):
                // method: 被调用的方法对象
                // mappingFunction: 对方法进行映射处理的函数
                return (MapperProxy.MapperMethodInvoker)this.methodCache.computeIfAbsent(method, (m) -> {
                    if (m.isDefault()) {
                        try {
                            return privateLookupInMethod == null ? new MapperProxy.DefaultMethodInvoker(this.getMethodHandleJava8(method)) : new MapperProxy.DefaultMethodInvoker(this.getMethodHandleJava9(method));
                        } catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException var4) {
                            throw new RuntimeException(var4);
                        }
                    } else {
               //通常在mapper接口的中要被调用的方法并不是default类型,走这里的流程
                        return new MapperProxy.PlainMethodInvoker(new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()));
                    }
                });
            } catch (RuntimeException var4) {
                Throwable cause = var4.getCause();
                throw (Throwable)(cause == null ? var4 : cause);
            }
        }
    

    看看this.methodCache.computeIfAbsent(method, mappingFunction)是如何做到的。
    methodCache对象是一个HashMap,最初key:value都是空的。computeIfAbsent方法接收两个参数,一个是方法对象method,另一个是对方法对象进行映射处理的函数对象mappingFunction。computeIfAbsent所做的事情就是重新计算method对应的value应该取什么值,并将key:value存入到Map里,然后返回value。

        default V computeIfAbsent(K key,
                Function<? super K, ? extends V> mappingFunction) {
            Objects.requireNonNull(mappingFunction);
            V v;
            if ((v = get(key)) == null) {
                V newValue;
                if ((newValue = mappingFunction.apply(key)) != null) {
                    put(key, newValue);
                    return newValue;
                }
            }
    
            return v;
        }
    

    现在我们知道了method,即methodCache里应该存放的key,那么value是谁呢?就是mappingFunction(method)计算出的值。mappingFunction的计算逻辑是什么呢?如下:如果方法在接口中被声明为default,即public且非抽象的实例方法,那么就创建MapperProxy.DefaultMethodInvoker;否则,创建MapperProxy.PlainMethodInvoker。。接口中的default方法:https://blog.csdn.net/wf13265/article/details/79363522

    a default method
    所以对于非default方法,将method: PlainMethodInvoker对象以key:value的形式存放到了methodCache中并返回了PlainMethodInvoker对象
      //mappingFunction
        (m) -> {
                if (m.isDefault()) {
            // Default methods are public non-abstract instance methods
            // declared in an interface.
                    try {
                        return privateLookupInMethod == null ? new MapperProxy.DefaultMethodInvoker(this.getMethodHandleJava8(method)) : new MapperProxy.DefaultMethodInvoker(this.getMethodHandleJava9(method));
                    } catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException var4) {
                        throw new RuntimeException(var4);
                    }
                } else {
                    return new MapperProxy.PlainMethodInvoker(new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()));
                }
            }
    
    经过了this.methodCache.computeIfAbsent(method, mappingFunction)之后,methodCache内就存入了method: methodInvoker,同时将methodInvoker返回给了调用方。如图: computeIfAbsent之后的methodCache 调用关系

    现在我们重新回到文章的第一段代码mapperProxy.invoke(...)内了。到此时,创建了一个PlainMethodInvoker,并将其与被调用的方法存到了methodCache内。

    //mapperProxy.invoke()
    return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
    //刚完成了this.cachedInvoker(method),得到了PlainMethodInvoker对象invoker。
    //现在需要做的是invoker.invoke(proxy, method, args, this.sqlSession)
    

    前面创建PlainMethodInvoker的时候还没有看到底是怎么new出来的,现在过一遍,构造方法很简单,复杂的地方在于mapperMethod的创建。

        private static class PlainMethodInvoker implements MapperProxy.MapperMethodInvoker {
            private final MapperMethod mapperMethod;
    
            public PlainMethodInvoker(MapperMethod mapperMethod) {
                this.mapperMethod = mapperMethod;
            }
    
            public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
                return this.mapperMethod.execute(sqlSession, args);
            }
        }
    

    当时创建是通过下面的这段代码完成PlainMethodInvoker创建的:先创建一个MapperMethod,将它作为invoker构造方法的参数。

        return new MapperProxy.PlainMethodInvoker(new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()));
    

    mapperMethod作为PlainMethodInvoker的唯一属性,显然非常关键,因为invoker.invoke()的功能就是通过mapperMethod.execute()来完成的。其构造方法如下:

        public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
            this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
            this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
        }
    

    在这个构造方法内,为该方法对象创建了sql语句的查询类型和name,为方法设置方法签名。

    1. 创建sql语句
        public static class SqlCommand {
            private final String name;
            private final SqlCommandType type;
           
            public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
                //获取方法名
                String methodName = method.getName();
                //获取方法所在的类
                Class<?> declaringClass = method.getDeclaringClass();
                //获取映射语句信息
                MappedStatement ms = this.resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
                ///若ms是NULL,则查看该方法上是否标注了FLUSH标签,如果存在,则设置查询类型为FLUSH,否则抛出异常表示接口找不到定义的方法。
                if (ms == null) {
                    if (method.getAnnotation(Flush.class) == null) {
                        throw new BindingException("Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName);
                    }
    
                    this.name = null;
                    this.type = SqlCommandType.FLUSH;
                } else {
                    // 找到了,为sqlCommand对象设置查询类型
                    this.name = ms.getId();
                    this.type = ms.getSqlCommandType();
                    if (this.type == SqlCommandType.UNKNOWN) {
                        throw new BindingException("Unknown execution method for: " + this.name);
                    }
                }
            }
            //......
    }
    

    获取方法名,然后根据类名和方法名去对应的mappedStatement.之前在解析mapper的时候,将mapper的信息都存放到了configuration中,并有部分已经解析完了。现在要做的事情就是利用configuration.getStatement(statementId)将剩余的部分resultMap、sql语句解析出来。

    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) {
                //statementID = 类名 + 方法名
                String statementId = mapperInterface.getName() + "." + methodName;
                // 如果有还没解析完的statement,那么接着解析完并返回是否包含当前statementId
                if (configuration.hasStatement(statementId)) {
                    // 解析出对应的mappedStatement
                    return configuration.getMappedStatement(statementId);
                } else if (mapperInterface.equals(declaringClass)) {
                    return null;
                } else {
                    // 如果在当前接口解析不到,且方法并不是在当前接口定义的,则去父接口中解析该statement
                    Class[] var6 = mapperInterface.getInterfaces();
                    int var7 = var6.length;
    
                    for(int var8 = 0; var8 < var7; ++var8) {
                        Class<?> superInterface = var6[var8];
                        if (declaringClass.isAssignableFrom(superInterface)) {
                            MappedStatement ms = this.resolveMappedStatement(superInterface, methodName, declaringClass, configuration);
                            if (ms != null) {
                                return ms;
                            }
                        }
                    }
    
                    return null;
                }
            }
    
        public MappedStatement getMappedStatement(String id) {
            return this.getMappedStatement(id, true);
        }
    
        public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
            if (validateIncompleteStatements) {
                this.buildAllStatements();
            }
    
            return (MappedStatement)this.mappedStatements.get(id);
        }
    

    buildAllStatements完成剩下的CacheRefs、Statements和methods的解析。重点关注parseStatementNode是如何创建出sql语句的:如果还没有解析完成,那么就对其解析并从未解析的map中移除。

    protected void buildAllStatements() {
            this.parsePendingResultMaps();
            if (!this.C.isEmpty()) {
                synchronized(this.incompleteCacheRefs) {
                    this.incompleteCacheRefs.removeIf((x) -> {
                        return x.resolveCacheRef() != null;
                    });
                }
            }
    
            if (!this.incompleteStatements.isEmpty()) {
                synchronized(this.incompleteStatements) {
                    this.incompleteStatements.removeIf((x) -> {
                        x.parseStatementNode();
                        return true;
                    });
                }
            }
    
            if (!this.incompleteMethods.isEmpty()) {
                synchronized(this.incompleteMethods) {
                    this.incompleteMethods.removeIf((x) -> {
                        x.resolve();
                        return true;
                    });
                }
            }
    
        }
    

    对于需要解析的,就像之前在构建SqlSessionFactory中做的那样,使用parseStatementNode()再解析一遍statement.
    至此,sqlCommand所需的mappedStatement就有了(还是放在Configuration中),然后就是为其命名(id|方法名)和指定类型(SELECT/UPDATE...)。

    1. 创建方法签名
      解析方法的返回值类型,参数名解析,为MapperMethod指定resultHandlerIndex(用于找到resultHandler)和paramNameResolver(用于参数名解析)。

    至此,mapperMethod创建完成,执行就交给了mapperMethod.execute().

        public Object execute(SqlSession sqlSession, Object[] args) {
            Object result;
            Object param;
            switch(this.command.getType()) {
            case INSERT:
                //将方法的参数解析为sql语句的参数
                param = this.method.convertArgsToSqlCommandParam(args);
                //解析返回结果
                result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
                break;
            case UPDATE:
                param = this.method.convertArgsToSqlCommandParam(args);
                result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
                break;
            case DELETE:
                param = this.method.convertArgsToSqlCommandParam(args);
                result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
                break;
            //对于增删改而言,方法的返回结果比较固定,就是影响的行数。
            //但对于查找来说,返回结果则复杂一点
            case SELECT:        
                //如果返回void类型且定义了ResultHandler,那么需要resultHandler去处理查找结果并返回void。
                if (this.method.returnsVoid() && this.method.hasResultHandler()) {
                    this.executeWithResultHandler(sqlSession, args);
                    result = null;
                //返回结果为多条记录
                } else if (this.method.returnsMany()) {
                    result = this.executeForMany(sqlSession, args);
                //返回结果为Map     
                } else if (this.method.returnsMap()) {
                    result = this.executeForMap(sqlSession, args);
                //返回游标位置
                } else if (this.method.returnsCursor()) {
                    result = this.executeForCursor(sqlSession, args);
                } else {
                //常规查找, 先解析参数, 若结果为null或返回结果类型与方法的返回类型不一致,则返回Optional.ofNullable.(看似非空,实则为空,避免空指针异常)
                    param = this.method.convertArgsToSqlCommandParam(args);
                    result = sqlSession.selectOne(this.command.getName(), param);
                    if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
                        result = Optional.ofNullable(result);
                    }
                }
                break;
            case FLUSH:
                result = sqlSession.flushStatements();
                break;
            default:
                throw new BindingException("Unknown execution method for: " + this.command.getName());
            }
    
            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;
            }
        }
    

    到这里,我们不难理解sqlCommand的作用了,那就是指定sql语句的类型。一共有5种,crud和Flush(flush cache)。而方法签名则定义了参数名解析器和返回值类型。

    本节的重点在于,mapper代理对象的方法在被调用时,发生了什么。了解一下动态代理,就知道实际上是与该代理对象关联的invocationHandler去invoke了该代理对象的方法。invoke的时候,创建了mapperMethod对象,该对象与mapper接口的方法、mapper.xml文件中的sql语句的<id>相对应。内部封装了mapper接口方法的签名、参数名解析器和与sql对应的sqlCommand对象。通过mapperMethod对象,可以完成方法参数解析,返回值类型判断和sql语句类型判断,最终将sql语句id和参数交给sqlSession去完成对数据的操作。
    所以现在问题又来了,参数是如何被解析到sql中的?这个execute()过程中mybatis是怎样与数据库交互的获取到结果集的?Object类型的结果集又是怎么被封装成了指定的类型的?ResultHandler是个什么东西,它又是怎样处理返回结果的?
    下篇文章再看吧。

    相关文章

      网友评论

          本文标题:mybatis运行原理04-mapper代理对象方法调用之Map

          本文链接:https://www.haomeiwen.com/subject/fjuyqltx.html