简述
在我们使用 Mybatis 进行增删改查时, SqlSession 是核心, 它相当于一个数据库连接对象, 在一个 SqlSession 中可以执行多条 SQL 语句
SqlSession 本身是一个接口, 提供了很多种操作方法, 如 insert,select 等等, 我们可以直接调用, 但是这种方式是不推荐的, 可读性, 可维护性都不是很高, 推荐使用 Mapper 接口映射的方式去进行增删改查, 了解一下这种方式的运行过程也是有必要的
在了解 SqlSession 运行过程前, 我们需要具备动态代理以及 JDK 动态代理的相关知识
开始分析
我们在使用 Mapper 时, 基本流程就是在配置文件中指定好 Mapper 接口文件和 xml 文件的路径, 然后使用如下代码获取代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class)
然后调用接口中的方法即可调执行相关的 SQL 语句, 这一过程是具体怎么执行的呢? 下面开始分析
[getMapper 方法]
SqlSession 只是一个接口, Mybatis 中使用的是它的默认实现类 DefaultSqlSession
- @Override
- public <T> T getMapper(Class<T> type) {
- return configuration.<T>getMapper(type, this);
- }
调用 Configuration 的 getMapper 方法
- public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
- return mapperRegistry.getMapper(type, sqlSession);
- }
继续调用 MapperRegistry 的 getMapper 方法
- @SuppressWarnings("unchecked")
- 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);
- }
- }
这里需要介绍一下, 我们在写 xml 配置文件时注册了 Mapper, 被注册的 Mapper 就被维护在 MapperRegistry 的一个 HashMap 中
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
map 的 key 是 Class 类, value 是对应的一个代理工厂 MapperProxyFactory, 用来生产对应的代理类, 如果 key 没有对应的 value, 会抛出异常, 告诉我们并没有去注册这个 Mapper, 这时候就需要去检查配置文件了
调用 MapperProxyFactory 的 newInstance() 方法去生产代理对象
- public T newInstance(SqlSession sqlSession) {
- // 生产代理, 该对象必须实现 InvocationHandler 接口且实现 invoke 方法
- final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
- return newInstance(mapperProxy);
- }
- @SuppressWarnings("unchecked")
- protected T newInstance(MapperProxy<T> mapperProxy) {
- return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
- }
这里就涉及到了 JDK 动态代理
首先 new 一个 Mapper 代理, 由 JDK 动态代理可知, 代理对象必须实现 InvocationHandler 接口且实现 invoke 方法
调用 Proxy.newProxyInstance 方法产生代理对象
为什么要动态代理呢? 因为我们要执行 SQL! 在接口方法中, 我们是不需要自己写查询方法的, 而 SQL 方法的执行就依赖于动态代理!
动态代理的主要逻辑就是 invoke 方法中的内容, 我们来看 MapperProxy 的 invoke 方法
- @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);
- }
简单描述一些流程
我们知道 Java 中, 所有类的父类都是 Object, 因此所有类都会继承一些 Object 的方法, 如 toString() 之类的, 所以这里我们要进行判断, 如果我们的代理对象调用的是从 Object 那里继承来的方法, 我们就不去执行动态代理逻辑, 而是直接执行该方法
如果执行的方法归本类所有, 则通过 cachedMapperMethod 产生 MapperMethod 来执行 execute 代理逻辑, 即执行 SQL
[这里涉及到了一个很重要的问题!!!]
我们知道, xml 映射文件其实就是 Mapper 接口的实现类, 接口方法的全路径和 xml 映射文件中 namesapce + SQL 块的 id 一一对应, Mybatis 是如何找到并维护这种对应关系的呢? 就是通过 MapperProxy 中的一个 Map
private final Map<Method, MapperMethod> methodCache;
这个 Map 的 key 为传入的方法类型, value 为 MapperMethod, 也是执行 SQL 的关键, 其实这个 methodCache 主要是为了完成缓存的任务, 因为我们执行方法时, 需要从 Configuration 中去解析对应的 xml 文件中的 SQL 块, 多次解析会造成资源的浪费, 代码如下
- 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;
- }
若 key 对应的 value 存在, 则从 map 直接拿到并返回, 若不存在, 则通过
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
解析后, 放入 map, 并返回, 下面看是如何解析的
- 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);
- }
在 MapperMethod 中有两个成员变量 command 和 method,command 主要完成了根据 method 方法解析出对应的 SQL 块, 而 method 负责一些如传参的转化, 返回类型的判定等, 这里我们主要看 command 解析 (注释中)
- public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
- //1, 拿到接口全路径. 方法名称的字符串, 这不就是 xml 映射文件中的 namespace.id 吗?
- String statementName = mapperInterface.getName() + "." + method.getName();
- MappedStatement ms = null;
- if (configuration.hasStatement(statementName)) {
- //2, 判断 Configuration 中是否含有该配置, 有则拿到 MappedStatement
- ms = configuration.getMappedStatement(statementName);
- } else if (!mapperInterface.equals(method.getDeclaringClass())) { // issue #35
- //3, 如果不存在, 看看它的父类全路径. 方法名称是否存在
- String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
- if (configuration.hasStatement(parentStatementName)) {
- ms = configuration.getMappedStatement(parentStatementName);
- }
- }
- if (ms == null) {
- // 如果 MappedStatement 为空
- if(method.getAnnotation(Flush.class) != null){
- // 先瞅瞅执行的这个方法有木有 @Flush 注解呢? 有就执行下面操作
- name = null;
- type = SqlCommandType.FLUSH;
- } else {
- // 没有... 那就抛异常呗, 赶紧去检查是不是 xml 和接口对应写错了?
- throw new BindingException("Invalid bound statement (not found):" + statementName);
- }
- } else {
- // 不等于空就执行下面操作
- name = ms.getId();
- type = ms.getSqlCommandType();
- if (type == SqlCommandType.UNKNOWN) {
- throw new BindingException("Unknown execution method for:" + name);
- }
- }
- }
至此根据 method 解析 SQL 块完毕, 这一系列初始化过程在 new 这个 MapperMethod 对象后便完成了, 然后便调用 execute 方法进行执行代理方法, 即 SQL 执行!
- public Object execute(SqlSession sqlSession, Object[] args) {
- Object result;
- if (SqlCommandType.INSERT == command.getType()) {
- Object param = method.convertArgsToSqlCommandParam(args);
- result = rowCountResult(sqlSession.insert(command.getName(), param));
- } else if (SqlCommandType.UPDATE == command.getType()) {
- Object param = method.convertArgsToSqlCommandParam(args);
- result = rowCountResult(sqlSession.update(command.getName(), param));
- } else if (SqlCommandType.DELETE == command.getType()) {
- Object param = method.convertArgsToSqlCommandParam(args);
- result = rowCountResult(sqlSession.delete(command.getName(), param));
- } else if (SqlCommandType.SELECT == command.getType()) {
- 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);
- }
- } else if (SqlCommandType.FLUSH == command.getType()) {
- result = sqlSession.flushStatements();
- } else {
- throw new BindingException("Unknown execution method for:" + command.getName());
- }
- if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
- throw new BindingException("Mapper method'" + command.ge
- tName()
- + "attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
- }
- return result;
- }
这一段代码就不去一一解析了, 主要就是通过初始化好的 command, 拿到 SQL 块的 id 以及类型, method 拿到 SQL 参数以及返回类型, 通过调用 sqlSession 的原生方法如 select,selectOne,insert,update 等等去执行 SQL
总结
哇, 终于写完了, 这次的源码分析真的好爽, 一步步了解了 Mybatis 的 SqlSession 是如何 getMapper 并调用方法的, 如果你能看完, 真的十分感谢!
(这里最后只到了调用 SqlSession 原生的增删改查方法, 而并没有去深入这些方法是如何实现的, 大家有兴趣可以自行去查看源码, 有机会下次再去分析!)
如有错误一定要指出哦, 万分感谢!
来源: http://www.jianshu.com/p/d95c2bbb03a6