Mybatis源码 - Mapper实现类

Mybatis-Mapper实现类

摘要

说到Mybatis,我们都知道这是一个与数据库交互的持久层框架,它能提供可自定义的数据库查询接口,并且封装了查询细节,让我们专注于业务开发的优秀框架。

但说到动态代理,大部分刚出来同学可能就有点疑惑了,因为在工作中我不止一次被刚参加工作的同事问道:“Mapper接口的实现是放在那个包下?我怎么找不到呢?”。然后我会毫不犹豫的告诉他:“Mapper接口的实现类是由动态代理技术生成的,是放在内存中的,你是看不到的”,然后他们带着一脸问号回到了工位。

接下来让我们来看看Mybatis是如何通过动态代理技术来把Mapper实现类生成并放到内存中的,竟然不用写代码也能生成实现类,而且还能连接数据库。

知识预备-动态代理

关于动态代理技术,在网上有一大堆相关的解读,我们先来看看网上的大佬是咋说的

知乎什么是动态代理?

动态代理实战

小试牛刀

看的多不如敲的多,我们来看看如何的基于动态代理技术获取一个接口的实现类。

开始之前我们先来整理一下需求

1、定义一个接口,并且在该接口中编写一个sayHello()方法

2、基于动态代理技术获取接口的实现类

3、实现sayHello方法

好了,知道要干什么了,来就干活吧

  • 定义接口,定义方法
interface CustomInterface {
    void sayHello();
}
  • 基于动态代理获取实现类

请注意,这一步是最重要的一步,也是最难理解的一步,但是我们不用着急,我们一步一步来

  1. 编写实现类

    虽然我们的接口可以不编写实现类,但是方法的实现的逻辑也需要我们指定。我们需要实现jdk动态代理的重要接口InvocationHandler 接口,该接口只有一个方法需要我们实现,我们的方法实现逻辑就可以写在其中

class CustomInterfaceProxy implements InvocationHandler{

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName());
        return null;
    }
}
  1. 生成指定接口的实现类
        /**
         *  生成实现类,并且用接口去接收该实现类
         *  Proxy.newProxyInstance方法需要接收三个参数
         *  第一个参数: 需要传递一个类加载器实例,在这里我们如果需要给那个生成实现类,就需要传递那个接口的  类加载器
         *  第二个参数:  需要传递一个Class数组,在这里我们直接把需要代理的接口的类型信息传递进去
         *  第三个参数: 需要传递一个 InvocationHandler的实现类
         *
         *  通过解读该方法的三个参数,我们可以大概的了解到,该方法通过接口的类加载器,加载该接口的类型信息,
         *  然后与InvocationHandler的实现类进行绑定,之后就会得到一个指定接口的实现类
         *
         */
        CustomInterface customInterface = (CustomInterface) Proxy.newProxyInstance(CustomInterface.class.getClassLoader(), new Class[]{CustomInterface.class}, new CustomInterfaceProxy());

        //调用方法
        customInterface.sayHello();

调用方法后的执行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kIzEId7I-1643359779757)(C:\Users\pp\AppData\Local\Temp\1643342142328.png)]

打印了sayHello,这个输出是在我们编写的InvocationHandler实现类中打印的,执行代码如下

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println(method.getName());
    return null;
}

在该实现类中我们可以看到打印的是方法名称,不是实现了sayHello方法。不急,我们再往接口中加个方法看看

  1. 验证动态代理的运行模式
interface CustomInterface {

    void sayHello();

    //增加一个求和方法
    Integer sum(Integer v1,Integer v2);
}

我们调用该方法

        //调用求和方法
        Integer v1 = 1;
        Integer v2 = 2;
        Integer sum = customInterface.sum(v1, v2);
        System.out.println(String.format("调用求和方法: 求和参数%s,%s, 求和结果:%s ",v1,v2,sum));

然后我们改写一下InvocationHandler实现类中的invoke实现

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        String name = method.getName();
        System.out.println(method.getName());

        //如果为求和方法
        if(name.equals("sum")) {
            Integer v1 = (Integer) args[0];
            Integer v2 = (Integer) args[1];
            return v1 + v2;
        }
        return null;
    }

	// 或者这样
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        String name = method.getName();
        System.out.println(method.getName());

        //如果为CustomInterface的求和方法
        if(CustomInterface.class.getMethod("sum",Integer.class,Integer.class).equals(method)) {
            Integer v1 = (Integer) args[0];
            Integer v2 = (Integer) args[1];
            return v1 + v2;
        }

        return null;
    }

输出结果

在这里插入图片描述

进行到这里,我们可以得出得结论是,对于InvocationHandler的invoke方法,我们可以把这个方法看作是所有接口的统一入口,我们要区分这个方法是通过Method实例进行区分,当我们知道是那个方法后,就可以知道其参数类型,然后编写具体实现,返回该方法所需的返回值。

可能有同学会问,你编写代码就是判断了一下方法名字,方法名字可以重复啊,或者是其他接口的方法名字。这里同学们可以试想一下,当我们调用了接口方法之后,就执行了invoke方法,就说明他们必定具有联系。接下来更深层次的可以通过Debug的方式,查看Method实例的信息,args参数信息就会发现答案

实践是检验真理的唯一标准

我们在上一章中学习了如何使用jdk动态代理技术,了解了其基本原理,我们就一起来使用该技术实现个小需求吧

需求如下:

1、在接口中编写一个方法

2、在调用方法的实际代码之前,校验方法参数是否为null

我们以 CustomInterface.sum(Interger v1,Integer v2)为例

实现思路:

要实现上面的需求,如果是编写实现类的话,我们可以在每个方法进入之前编写参数校验的逻辑,并且需要每个方法都编写类似于 if(parameter == null) 的逻辑,会产生很多冗余的代码,并且代码阅读性很差。

但是现在我们使用了动态代理技术,我们只需要实现InvocationHandler的invoke方法,所有的方法都会经过该方法,这样就有利于我们在该方法编写一个通用的参数校验的方法。说干就干,我们来试试吧,体会下动态代理技术的神奇

1、编写一个校验参数是否为空的方法

    /**
     * 校验调用的方法参数是否为空
     * @param method  方法实例
     * @param args    传递的实参列表
     */
    private void checkParameterHaNull(Method method,Object[] args) {
        //获取该方法的参数数量
        int parameterCount = method.getParameterCount();
        //没有参数不做处理
        if(parameterCount == 0) {
            return;
        }
        //获取该方法的参数封装数据
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameterCount; i++) {
            Parameter parameter = parameters[i];
            //基础数据类型则不校验是否为空
            if (checkBaseType(parameter)) {
                continue;
            }
            //如果对应的参数为空
            if(args[i] == null) {
                String name = method.getName();
                String msg = "方法" + name + "的第" + i + "个参数" + parameter.getName() + "不能为空";
                throw new RuntimeException(msg);
            }
        }
    }

2、如果有基本数据类型,我们需要单独校验是否是基本数据类型

/**
 * 校验这个参数是否为基本数据类型的参数
 * @param parameter     基于反射封装的参数信息
 * @return              boolean   true-是  false-不是
 */
private boolean checkBaseType(Parameter parameter) {
    Class<?> type = parameter.getType();
    return Byte.TYPE.equals(type)
            || Short.TYPE.equals(type)
            || Integer.TYPE.equals(type)
            || Long.TYPE.equals(type)
            || Character.TYPE.equals(type)
            || Float.TYPE.equals(type)
            || Double.TYPE.equals(type)
            || Boolean.TYPE.equals(type);

}

3、调用方法

在这里插入图片描述

4、调用测试

第一个参数我们设置为null

在这里插入图片描述

5、执行结果

在这里插入图片描述

小结

我们在这一章简单并实践了一下基于Jdk的动态代理技术,但是需要注意的是使用Jdk的动态代理技术,只能代理接口,如果要代理非接口类,需要使用cglb动态代理技术,我们的SpringAOP就是基于它实现的。

接下来我们趁热打铁,马上来看看Mybatis是如何使用jdk动态代理技术来实现Mapper接口的

Mybatis动态代理技术实践

概述

mybatis针对如何实现动态代理根据自身的需求又进行了封装,封装的模块包为 org.apache.ibatis.bingding 包,让我们来看看这个包中有些类,然后这些类具体是干啥的

BindingException.java      
MapperMethod.java			Mapper方法的封装类
MapperProxy.java			Mapper接口的代理类
MapperProxyFactory.java	     Mapper代理类工厂
MapperRegistry.java			Mapper工厂注册	

核心类解读

Mapper的实现类-MapperProxy

首先就来介绍让很多刚工作的同学常问的问题,Mapper的实现类。为了便于理解,我们先大概的了解下该类是干啥的。

该类是实现InvocationHandler接口并抽象出了Mapper接口中所有方法的执行过程的类,一个MapperProxy实例就代表一个Mapper接口的实现类,说白了就是Mapper接口的实现类。

这样说起来有点抽象,接下来让我们来看看核心属性和核心方法

核心属性
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  //SqlSession对象,访问数据库
  private final SqlSession sqlSession;
  //Class类型信息,即Mapper接口的Class实例
  private final Class<T> mapperInterface;
  //Mapper方法与我们编写的XML的各种方法的对应关系
  // 使用Map进行一一对应,这就是为什么Mapper接口方法名称要与对应的XML文件的sql标签的id相同
  // 这里的Map其实是一个ConcurrentHashMap,一个Mapper接口维护一个methodCache
  private final Map<Method, MapperMethod> methodCache;
}
核心方法

构造方法

//methodCache是由MapperProxyFactory传递进来的  
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

调用Mapper方法

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
    //并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    //这里优化了,去缓存中找MapperMethod
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //执行
    return mapperMethod.execute(sqlSession, args);
  }

缓存MethodMapper

  //去缓存中找MapperMethod
  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      //找不到才去new
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }
小结

怎么就完了,这就是Mapper的实现类?那么多Mapper方法是如何执行的,没看到什么逻辑呢?

没错,这就是Mapper的实现类,并且是基于动态代理技术实现的,它在invoke方法中其实就只是调用了MeperMethod的execute方法而已。

所以想要了解具体的逻辑我们还需要深究MapperMethod,但是具体的代码实现逻辑不是我们本次的重点,Mybatis的执行逻辑非常复杂,我们深究会碰到JDBC的底层,Mybatis的核心组件Cache、ResultSetHandler、Executor、ResultMap,这些组件,随便搞一个出来,都够同学们弄很久了,所以我们这里先告诉同学们,到底Mybatis是如何生成是实现类的

所以,我们接下来看看MapperProxyFactory,MapperProxy的生产者

Mapper实现类工厂----MapperProxyFactory

MapperProxyFactory,首先根据类名我们知道这是一个使用工厂模式设计类的,它的职责是用于生成MapperProxy,而MapperProxy其实就是Mapper接口的实现类,并且是基于动态代理的实现类,我们要理解它其实很简单,当然它的代码也很简单,不信你看

核心属性
/**
 * @author Lasse Voss
 */
/**
 * 映射器代理工厂
 */
public class MapperProxyFactory<T> {

  private static Logger logger = LoggerFactory.getLogger(MapperProxyFactory.class); 
 // Mapper接口类型信息,和MapperProxy中的	mapperInterface一样
  private final Class<T> mapperInterface;
  // Mapper接口方法与Mabatis的XML  select、delete、等定义的标签的对应关系
  // 这里我们可以看到这里直接  new了一个ConsurrentHashMap
  // 它的作用是传递给MapperProxy,这里很高明的是,它是基于一个工厂维护一个引用,在初始化时容器为空,
  // 但是当对应的Mapper调用方法时就会往该容器中加入映射
  // 即一个Mapper接口 对应 一个MapperProxyFactory 对应 一个methodCache 对应多个 MapperProxy
  // 简单来说就是 多个MapperProxy通过引用的方式共用一个 methodCache
  // 这样的好处是即可以实现懒加载,第一次加载后,第二次就不需要再加载  
  private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
 // 此处省略了方法   
}
核心方法
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
  //用JDK自带的动态代理生成映射器
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

//获取 Mapper实现类
public T newInstance(SqlSession sqlSession) {
  // 请注意,这里始终把   methodCache传递给MapperProxy就证实了,多个MapperProxy共享一个methodCache
  final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}
总结

就这,Mapper的实现类就是这样创造出来的?

哈哈,没错,就是这样创造出来的,只不过Mybatis是基于自己的实现方式进行封装

好了,到了这里是不是就知道Mapper的实现类在哪里了,当然如果各位同学需要真正的理解,需要去过一下关于Java反射的知识,这部分知识真的很重要。

注册Mapper - MapperRegistry

上一章我们说到Mapper实现类工厂类是如何产生实现类的,并且还告知了同学了要过一下Java反射的知识,那这个类这次我们就用到了,是关于类加载的

核心属性
/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 * @author Lasse Voss
 */
/**
 * 映射器注册机
 *
 */
public class MapperRegistry {
 // Mybatis 配置信息类,该类包含了所有的Mybatis配置信息
 // 类似于 数据源配置、事务管理器、插件、别名注册信息等等,很多,关于该类,我们会在Mybatis配置中去深究
 // 该类几乎贯穿了整个 Mybatis的生命周期   
  private Configuration config;
  //将已经添加的映射都放入HashMap
  // 这里我们可以看到,该类只维护了一个Class实例与MapperProxyFactory的映射关系
  // 那很明显,  knownMappers 的 key值就是Mapper接口的Class信息
  // 再次强调,Class实例信息在一个虚拟机,即一个Java应用中始终只有一个  
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
  }
核心方法
  //通过 Class类型实例添加Mapper实现类工厂
  public <T> void addMapper(Class<T> type) {
    //mapper必须是接口!才会添加
    if (type.isInterface()) {
      if (hasMapper(type)) {
        //如果重复添加了,报错
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 直接创建一个MapperProxyFactory放入容器中  
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        //如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }



  /** 
  * 通过包名添加Mapper实现类工厂
   * @since 3.2.2
   */
  public void addMappers(String packageName, Class<?> superType) {
    //查找包下所有是superType的类
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }

  @SuppressWarnings("unchecked")
  //返回Mapper实现类
  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 {
      // 获取到Mapper实现类工厂后,直接创建一个并返回  
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }


小结

到了这里,其实整个Mapper接口的创建与获取方法我们已经了解得七七八八了,最主要的还是Java基础,基础意味着更高层次的抽象,更高层次的封装。

其实对Mapper的操作,Class类型实例贯穿到底,从添加一个Mapper实现类工厂,还是获取一个Mapper实现类,并且实现的方式很简单,就是我们最常用到的HashMap,读下来还是蛮有收获的

Mapper接口方法的实际执行者-MapperMethod

这里我们不深究该类的实现方式,该类的复杂程度,三言两语是说不清的,我们可以简单的看看,他的核心方法

核心方法
//执行
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  //可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
  if (SqlCommandType.INSERT == command.getType()) {
    logger.info("执行Insert");
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.insert(command.getName(), param));
  } else if (SqlCommandType.UPDATE == command.getType()) {
    logger.info("执行Update");
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.update(command.getName(), param));
  } else if (SqlCommandType.DELETE == command.getType()) {
    logger.info("执行Delete");
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.delete(command.getName(), param));
  } else if (SqlCommandType.SELECT == command.getType()) {
    logger.info("执行Select");
    if (method.returnsVoid() && method.hasResultHandler()) {
      //如果有结果处理器
      executeWithResultHandler(sqlSession, args);
      result = null;
    } else if (method.returnsMany()) {
      //如果结果有多条记录
      result = executeForMany(sqlSession, args);
    } else if (method.returnsMap()) {
      //如果结果是map
      result = executeForMap(sqlSession, args);
    } else {
      //否则就是一条记录
      Object param = method.convertArgsToSqlCommandParam(args);
      result = sqlSession.selectOne(command.getName(), param);
    }
  } else {
    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;
}
小结

嘿嘿,是不是有点JDBC的味道了,没错,我们已经快要触碰到底层了,JDBC的知识需要重新拾起来才能理解接下来的内容

总结

Mybatis的buiding模块是我们学习动态代理的技术的范例,他实现方式简单,容易理解,但最重要的还是要的我们动手写,多多Debug,是学习动态代理技术的起点,也能帮助我们理解SpringAOP的实现。

好好学习,天天向上

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MyBatisMapper 模块主要由两部分组成: 1. Mapper 接口:Mapper 接口是一个 Java 接口,其中定义了各种 SQL 操作的方法,这些方法都有对应的 SQL 语句。 2. Mapper XML 文件:Mapper XML 文件是一个独立的 XML 文件,其中定义了与 Mapper 接口中方法对应的 SQL 语句以及参数的映射关系。 MyBatis 在解析 Mapper 接口和 Mapper XML 文件时,会通过 Java 动态代理技术动态生成 Mapper 接口的实现,同时会将 Mapper XML 文件中定义的 SQL 语句解析成相应的 SQL 语句对象并存放在内存中,方便后续的操作。 Mapper 接口的源码: ```java public interface UserMapper { // 根据 ID 查询用户 @Select("SELECT * FROM user WHERE id = #{id}") User getUserById(Integer id); // 添加用户 @Insert("INSERT INTO user(username,password) VALUES(#{username},#{password})") int addUser(User user); // 更新用户信息 @Update("UPDATE user SET username = #{username},password = #{password} WHERE id = #{id}") int updateUser(User user); // 根据 ID 删除用户 @Delete("DELETE FROM user WHERE id = #{id}") int deleteUser(Integer id); } ``` Mapper XML 文件的源码: ```xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mapper.UserMapper"> <!-- 根据 ID 查询用户 --> <select id="getUserById" resultType="com.example.entity.User"> SELECT * FROM user WHERE id = #{id} </select> <!-- 添加用户 --> <insert id="addUser" parameterType="com.example.entity.User"> INSERT INTO user(username,password) VALUES(#{username},#{password}) </insert> <!-- 更新用户信息 --> <update id="updateUser" parameterType="com.example.entity.User"> UPDATE user SET username = #{username},password = #{password} WHERE id = #{id} </update> <!-- 根据 ID 删除用户 --> <delete id="deleteUser" parameterType="java.lang.Integer"> DELETE FROM user WHERE id = #{id} </delete> </mapper> ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值