mybatis接口映射的实现原理

自作多情 提交于 2020-04-08 11:56:37

使用mybatis接口映射的示例用户代码:

try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    userMapper.saveUser(user);
}

UserMapper只是一个java接口,实现类都没有,mybatis竟然让我们直接调用一个接口方法就可以执行数据库操作,这背后的细节就是代理机制的使用,语句sqlSession.getMapper(UserMapper.class)实际上是利用java的动态代理机制返回一个实现了UserMapper接口的代理对象,可以打印userMapper.getClass().getName()的结果或者用IDE调试工具可以看到是一个代理对象。

那到底是如何获得一个代理对象的呢,SqlSession接口的默认实现是DefaultSqlSession,我们看看在DefaultSqlSessiongetMapper方法的实现:

// DefaultSqlSession#getMapper:

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

继续跟踪Configuration里的getMapper方法实现:

// Configuration#getMapper:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 直接调用了mapperRegistry的getMapper()
    // mapperRegistry是Configuration中的mapper注册对象
    return mapperRegistry.getMapper(type, sqlSession);
}
// MapperRegistry#getMapper:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 从knownMappers容器中读取与mapper类型匹配的MapperProxyFactory对象
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
        // throw e
    }
    try {
        // 调用工厂方法产生代理实例
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        // throw e
    }
}

上面列出代码的注释中标注的,程序流程从knownMappers容器中获取一个与mapper类型匹配的MapperProxyFactory实例,这是一个可以创建代理对象的工厂类,工厂方法newInstance获得的就是一个mapper类型的代理实例。这里knownMappers是MapperRegistry中维护的一个map结构的容器字段:

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

knownMappers容器中缓存了mapper类型和产生mapper类型实例的工厂,缓存内容是在mybatis初始化全局的Configuration对象的过程中设置的,我们可以在MapperRegistry中找到添加缓存条目的方法:

// MapperProxy#addMapper:

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        if (hasMapper(type)) {
            // throw e
        }
        boolean loadCompleted = false;
        try {
            // 在缓存中增加一个条目mapperType -> MapperProxyFactory
            knownMappers.put(type, new MapperProxyFactory<T>(type));
            // ...
        } finally {
            // ...
        }
    }
}

现在我们来看看MapperProxyFactory这个工厂,它的代码不多,如下所示是它的全部实现代码。

// MapperProxyFactroy:

public class MapperProxyFactory<T> {
    // mapperInterface字段维护着目标mapper类型
    private final Class<T> mapperInterface;
    // 缓存着已经构建好的mapper方法和和其对应的MapperMethod对象;
    // MapperMethod类似一个包含mapper方法的执行上下文信息的封装对象。
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

    // 构造方法,在前一个代码片段MapperProxy#addMapper里有调用体现
    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        // 动态代理创建代理实例
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }

    // 对外暴露的工厂方法
    public T newInstance(SqlSession sqlSession) {
        // 首先创建一个MapperProxy实例
        // MapperProxy事实上就是一个InvocationHandler实例
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        // 调用内部重载的newInstance方法返回mapper实例
        return newInstance(mapperProxy);
    }
}

注释里已经说明了MapperProxyFactory的整体结构和代码流程,主要就是两个重载的newInstance方法体现了利用动态代理来创建mapper代理实例,公开的newInstance方法内部先实例化了一个MapperProxy对象,MapperProxy其实就是一个InvocationHandler,通过动态代理创建的代理对象内部都维持一个InvocationHandler的引用,最终的数据库的操作就实现在InvocationHandler的invoke方法中,在创建MapperProxy对象时构造方法接受了几个参数,这些参数在invoke方法中操作数据库时都需要依赖,我们看看它声明的这几个字段以及构造方法:

// MapperProxy:
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;
}

methodCache字段就是前面说过的methodCache缓存,sqlSession指向的就是最开头的用户代码中使用的sqlSession对象,需要把sqlSession对象传递这么远,因为最终还是通过sqlSession的insert、update、select等方法来实现数据库操作的。

最后看看invoke方法:

// MapperProxy#invoke:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {// 如果不是接口
            return method.invoke(this, args);
        } else if (isDefaultMethod(method)) {// 如果是default方法,java 8支持默认方法
            return invokeDefaultMethod(proxy, method, args);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    // 这里才表示是接口代理
    // 从前面所说的methodCache中取MapperMethod,
    // 缓存中没有就创建一个,可以查看cachedMapperMethod方法的实现
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 通过MapperMethod对象执行实际操作
    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;
}

可以看到最终是调用mapperMethod.execute(sqlSession, args)执行数据库操作的,前面说过,MapperMethod其实像一个上下文对象,它封装的是正在执行的mapper方法的细节,还有就是方法执行数据库操作所需要的环境依赖信息等。

看看它的构造方法和字段:

// MapperMethod:

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

然后我们跟踪它的execute方法实现细节:

// MapperMethod#execute:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            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()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
                result = executeForCursor(sqlSession, args);
            } else {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
            }
            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;
}

execute方法中检测当前sql命令的形式,根据类型是select、insert、upadte、delete或者flush分别采取不同的执行过程,以insert为例,看看insert这个case分支里有:

result = rowCountResult(sqlSession.insert(command.getName(), param));

最终还是进入了sqlSession的insert方法过程,接口映射可以理解为就是利用动态代理包装了SqlSession实例,我们得到的是代理对象,在代理对象上进行crud操作,然后又把请求委托给SqlSession处理。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!