MyBatis中Mapper的动态代理模式(深入源码+言简意赅)

MyBatis中Mapper的动态代理模式

0 疑问

在 Mybatis 中,我们只需要定义 ⌈Mapper接口⌋⌈该Mapper接口所对应的XML配置文件⌋,就可以完成对数据库的 “增删查改” 操作,获取结果集。但是,从始至终我们都没有定义 Mapper 接口的实现类,且 Java 中也不允许使用接口来创建实例对象,那么当我们来调用接口中的方法时,到底是由谁来执行这个方法呢?

  • 答案就是 Mybatis 借助设计模式中的 ⌈动态代理模式⌋ 帮助我们在底层创建了 Mapper 接口的实现类,从而调用该实现类的方法完成了我们所需要的操作;
  • 对设计模式中的 ⌈代理模式⌋ 不熟悉的小伙伴可以看我的另一篇 CSDN 博客:设计模式之代理模式
  • 在此过程中,主要涉及三个至关重要的类:MapperRegistryMapperProxyMapperMethod

1 示例

我们以下面的代码为例,深入源码且言简意赅地说明 Mybatis 中的 Mapper 动态代理的实现过程,其中重点是第 5、6 点。

  @Test
  public void test() throws IOException {
      //1.读取MyBatis的核心配置文件
      InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

      //2.创建SqlSessionFactoryBuilder对象
      SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();

      //3.通过核心配置文件所对应的字节输入流创建工厂类SqlSessionFactory,生产SqlSession对象
      SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);

      //4.创建SqlSession对象,此时通过SqlSession对象所操作的sql都会自动提交
      SqlSession sqlSession = sqlSessionFactory.openSession(true);

      //5.通过动态代理模式创建UserMapper接口的代理实现类对象
      UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

      //6.调用UserMapper接口中的方法
      User user = userMapper.getUserById();
      System.out.println(user.toString());
  }

2 实现过程

2.1 代理对象的生成
  1. 当执行到示例代码的第 5 点时,Mybatis 在底层借助 MapperRegistry 类生成 UserMapper 接口的具体实现类,即 userMapper 对象的代理对象。下面是 MapperRegistry 类的源码,其中最重要的是 getMapper() 方法,下面对其进行详细解释;
public class MapperRegistry {
    private final Configuration config;
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();

    public MapperRegistry(Configuration config) {
        this.config = config;
    }

    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);
            }
        }
    }

    public <T> boolean hasMapper(Class<T> type) {
        return this.knownMappers.containsKey(type);
    }

    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (this.hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }

            boolean loadCompleted = false;

            try {
                this.knownMappers.put(type, new MapperProxyFactory(type));
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    this.knownMappers.remove(type);
                }

            }
        }

    }

    public Collection<Class<?>> getMappers() {
        return Collections.unmodifiableCollection(this.knownMappers.keySet());
    }

    public void addMappers(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
        resolverUtil.find(new IsA(superType), packageName);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        Iterator var5 = mapperSet.iterator();

        while(var5.hasNext()) {
            Class<?> mapperClass = (Class)var5.next();
            this.addMapper(mapperClass);
        }

    }

    public void addMappers(String packageName) {
        this.addMappers(packageName, Object.class);
    }
}
  1. 从源码中我们可以看到,MapperRegistry 中使用一个 Map 容器 knownMappers 存储已经注册过的 Mapper 接口,该容器的 key 值为 Mapper 接口,value 值为可以生成该 Mapper 接口具体实现类(代理对象)的 MapperProxyFactory 工厂,如下图所示;
    在这里插入图片描述
  2. 所以,代理对象的生成过程可以描述为:我们首先调用 SqlSession 对象的 getMapper() 方法(传入 Mapper 接口作为参数)。然后,Mybatis 底层实际去执行 MapperRegistry 类的 getMapper() 方法,在该类的 knownMappers 容器中以我们传入的 Mapper 接口为 key 值找到可以生成该 Mapper 接口所对应的代理对象的 MapperProxyFactory 工厂。最后,MapperProxyFactory 工厂通过 newInstance() 方法生成 Mapper 接口的代理对象 MapperProxy 类实例,至此进入下一阶段。
2.2 Mapper接口方法的执行
  1. 在第 5 点中的步骤执行完毕之后,我们成功获得了 UserMapper 接口的代理对象,下面解释在第 6 点的步骤中如何执行 Mapper 接口的 getUserById() 方法。首先是由 UserMapper 接口的代理对象 MapperProxy 类实例在其内部通过反射机制调用 invoke() 方法来生成 getUserById() 方法所对应的 MapperMethod 类实例对象,由 MapperMethod 类实例作为执行者完成我们的数据库操作;
/*
 * 代码有删减,只截取核心代码
 */
public class MapperProxy<T> implements InvocationHandler, Serializable {
    //生成getUserById()方法对应的MapperMethod类实例对象
    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);
        }
    }

    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 {
            //调用MapperMethod的execute()方法完成实际的数据库操作
            return this.mapperMethod.execute(sqlSession, args);
        }
    }
}
  1. 然后,getUserById() 方法所对应的 MapperMethod 类实例对象执行自己的 execute() 方法,解析 UserMapper 接口的 getUserById() 方法,并找到执行对应的 SQL 语句的方法(在 switch…case…中查找)。最终,getUserById() 方法会对应到 “SELECT” 中的 executeForMany() 方法,该方法与数据库进行交互,查询到我们所需要的数据,并返回结果,结束整个过程。
/*
 * 代码有删减,只截取核心代码
 */
public class MapperMethod {
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        Object param;
        switch(this.command.getType()) {
        case INSERT:
            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:
            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()) {
                result = this.executeForMap(sqlSession, args);
            } else if (this.method.returnsCursor()) {
                result = this.executeForCursor(sqlSession, args);
            } else {
                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;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值