Análisis del código fuente de la interfaz Mybatis Mapper

Análisis del código fuente de la interfaz Mybatis Mapper

Programación de tecnología Java TSMYK

La primera dirección de este artículo es un blog personal
https://my.oschina.net/mengyuankan/blog/2873220

Artículos relacionados

Análisis de Mybatis Análisis de código fuente de archivo de configuración
Conversión de tipo
Mybatis Análisis de código fuente Análisis de código fuente de grupo de conexión de base de datos Mybatis

Prefacio

Al usar Mybatis, solo necesitamos escribir la interfaz correspondiente, es decir, la interfaz Mapper de la capa dao. Sin escribir la clase de implementación, Mybatis puede encontrar el SQL correspondiente configurado en el archivo xml según el nombre del método correspondiente en la interfaz. , parámetros de método y SQL En SQL en xml, podemos vincular parámetros a través de # {0}, # {1}, y también podemos vincular parámetros a través de # {arg0}, # {arg1}. Puede estar vinculado por el nombres de parámetros en el método, como nombre, edad, etc., y también pueden estar vinculados por # {param1}, # {param2}, etc. A continuación, veamos cómo se implementa el código fuente de Mybatis.

Análisis de código fuente

En Mybatis, el código fuente para analizar la interfaz Mapper se encuentra principalmente en el paquete de enlace. Hay 4 clases en este paquete, más una clase de herramienta ParamNameResolver para la resolución de nombres de parámetros de métodos. Hay 5 clases en total. La cantidad de código no es mucho. Analice estas categorías a la vez.
Análisis del código fuente de la interfaz Mybatis Mapper
Primero veamos brevemente estas categorías:

  1. BindingException: excepción personalizada, ignorada
  2. MapperMethod: En esta clase, se encapsula la información del método correspondiente de la interfaz Mapper y la información de la declaración SQL correspondiente. La clase MapperMethod puede considerarse como el puente de conexión entre la interfaz Mapper y la declaración SQL en el archivo de configuración. Es en estas clases La clase más importante tiene más códigos, mientras que las otras clases tienen muy pocos códigos.
  3. MapperProxy: Es el objeto proxy de la interfaz Mapper, al utilizar Mybatis no es necesario implementar la interfaz Mapper, se implementa mediante el proxy dinámico del JDK.
  4. MapperProxyFactory: clase de fábrica de la clase MapperProxy, utilizada para crear MapperProxy
  5. MapperRegistry: Es el centro de registro de la interfaz Mybatis y su fábrica de objetos proxy, cuando Mybatis se inicializa, cargará el archivo de configuración y la información de la interfaz Mapper para registrarse en esta clase.
  6. ParamNameResolver Esta clase no es una clase del paquete de enlace, es una clase de herramienta del paquete de reflexión, se utiliza principalmente para resolver parámetros de métodos de interfaz.
    A continuación, observe el proceso de implementación de cada clase:

MapperProxy

Primero observe la clase MapperProxy, que es el objeto proxy de la interfaz Mapper e implementa la interfaz InvocationHandler, es decir, el proxy dinámico del JDK se utiliza para generar objetos proxy para la interfaz Mapper.


public class MapperProxy<T> implements InvocationHandler, Serializable {

  // 关联的 sqlSession 对象
  private final SqlSession sqlSession;
  // 目标接口,即 Mapper 接口对应的 class 对象
  private final Class<T> mapperInterface;
  // 方法缓存,用于缓存 MapperMethod对象,key 为 Mapper 接口中对应方法的 Method 对象,value 则是对应的 MapperMethod,MapperMethod 会完成参数的转换和 SQL 的执行功能
  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;
  }

  // 代理对象执行的方法,代理以后,所有 Mapper 的方法调用时,都会调用这个invoke方法
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
     // 并不是每个方法都需要调用代理对象进行执行,如果这个方法是Object中通用的方法,则无需执行
     if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      // 如果是默认方法,则执行默认方法,Java 8 提供了默认方法
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
     // 从缓存中获取 MapperMethod 对象,如果缓存中没有,则创建一个,并添加到缓存中
     final MapperMethod mapperMethod = cachedMapperMethod(method);
     // 执行方法对应的 SQL 语句
     return mapperMethod.execute(sqlSession, args);
  }
  // 缓存 MapperMethod 
  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;
  }
}

Lo anterior es el código principal de la clase proxy MapperProxy. Cabe señalar que los métodos relevantes en MapperMethod se llaman. MapperMethod será analizado posteriormente. Primero, sepa que es el SQL correspondiente a la conversión de parámetros del método y el método de ejecución .Un punto más, al ejecutar el método de destino, si es un método en Objeto, el método de destino se ejecutará directamente, si es el método predeterminado, se ejecutará la lógica relevante del método predeterminado, de lo contrario se ejecutará el método de destino. ser ejecutado usando un objeto proxy

MapperProxyFactory

Después de leer la clase de proxy MapperProxy mencionada anteriormente, MapperProxyFactory se utiliza para crear la clase de proxy, que es una clase de fábrica.


public class MapperProxyFactory<T> {
  // 当前的 MapperProxyFactory 对象可以创建的 mapperInterface 接口的代理对象
  private final Class<T> mapperInterface;

  // MapperMetho缓存
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

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

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

Una clase de fábrica, utilizada principalmente para crear objetos proxy de la interfaz Mapper, el código es simple, no hay nada que decir.

MapeadorRegistro

MapperRegistry Es el centro de registro de la interfaz Mapper y su correspondiente objeto proxy factory. Cuando se inicializa Mybatis, cargará el archivo de configuración y la información de la interfaz Mapper y lo registrará en la clase MapperRegistry. Cuando sea necesario ejecutar un determinado SQL, se obtendrá primero del registro. El objeto proxy de la interfaz Mapper.


public class MapperRegistry {
  // 配置对象,包含所有的配置信息
  private final Configuration config;

  // 接口和代理对象工厂的对应关系,会用工厂去创建接口的代理对象
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

  // 注册 Mapper 接口
  public <T> void addMapper(Class<T> type) {
    // 是接口,才进行注册
    if (type.isInterface()) {
      // 如果已经注册过了,则抛出异常,不能重复注册
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      // 是否加载完成的标记
      boolean loadCompleted = false;
      try {
        // 会为每个 Mapper 接口创建一个代理对象工厂
        knownMappers.put(type, new MapperProxyFactory<T>(type));

        // 下面这个先不看,主要是xml的解析和注解的处理
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        // 如果注册失败,则移除掉该Mapper接口
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

  // 获取 Mapper 接口的代理对象,
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 从缓存中获取该 Mapper 接口的代理工厂对象
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
     // 如果该 Mapper 接口没有注册过,则抛异常
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    //使用代理工厂创建 Mapper 接口的代理对象
    return mapperProxyFactory.newInstance(sqlSession);
  }  
}

El anterior es el código registrado por MapperRegistry, que principalmente registra la interfaz Mapper y obtiene el objeto proxy de la interfaz Mapper, que es fácil de entender.

ParamNameResolver

ParamNameResolver no es una clase del paquete de enlace. Es una clase de herramienta del paquete de reflexión. Se utiliza principalmente para resolver parámetros de métodos de interfaz.
Es decir, en los parámetros del método, el parámetro es el primero, es decir, se analiza la relación correspondiente entre el nombre del parámetro y el índice; el código en esta clase es más complicado que las varias clases anteriores, y es un poco Enrevesado Escribí el método principal Para ayudar a entender, pero la cantidad de código no es mucho.

Los métodos principales de esta clase son principalmente el método de construcción y el método getNamedParams. Veamos primero los otros métodos:


public class ParamNameResolver {
  // 参数前缀,在 SQL 中可以通过 #{param1}之类的来获取
  private static final String GENERIC_NAME_PREFIX = "param";

  // 参数的索引和参数名称的对应关系,有序的,最重要的一个属性
  private final SortedMap<Integer, String> names;
  // 参数中是否有 @Param 注解
  private boolean hasParamAnnotation;

  // 获取对应参数索引实际的名称,如 arg0, arg1,arg2......
  private String getActualParamName(Method method, int paramIndex) {
      Object[] params = (Object[]) GET_PARAMS.invoke(method);
      return (String) GET_NAME.invoke(params[paramIndex]);
  }
  // 是否是特殊参数,如果方法参数中有 RowBounds 和 ResultHandler 则会特殊处理,不会存入到 names 集合中
  private static boolean isSpecialParameter(Class<?> clazz) {
    return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
  }

  // 返回所有的参数名称 (toArray(new String[0])又学习了一种新技能)
  public String[] getNames() {
    return names.values().toArray(new String[0]);
  }

El código anterior son algunos métodos auxiliares de ParamNameResolver. El más importante es el atributo de nombres, que se utiliza para almacenar la correspondencia entre el índice del parámetro y el nombre del parámetro. Es un mapa, pero hay excepciones. Si el parámetro contiene dos tipos de RowBounds y ResultHandler, no pondrán sus índices y relaciones correspondientes en la colección de nombres, de la siguiente manera:


aMethod(@Param("M") int a, @Param("N") int b) -- names = {{0, "M"}, {1, "N"}}
aMethod(int a, int b) -- names = {{0, "0"}, {1, "1"}}
aMethod(int a, RowBounds rb, int b) -- names = {{0, "0"}, {2, "1"}}

Método de construcción, ahora veamos el método de construcción de ParamNameResolver. Cuando se llama al método de construcción para crear este tipo de objeto, se analizarán los parámetros del método y el resultado del análisis se colocará en los datos de nombres. El código es como sigue:

Atención


  public ParamNameResolver(Configuration config, Method method) {
    // 方法所有参数的类型
    final Class<?>[] paramTypes = method.getParameterTypes();
    // 所有的方法参数,包括 @Param 注解的参数
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    // 参数索引和参数名称的对应关系
    final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      // 不处理 RowBounds 和 ResultHandler 这两种特殊的参数
      if (isSpecialParameter(paramTypes[paramIndex])) {
        continue;
      }
      String name = null;
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        // 如果参数被 @Param 修饰
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          // 则参数名称取其值
          name = ((Param) annotation).value();
          break;
        }
      }
      // 如果是一般的参数
      if (name == null) {
        // 是否使用真实的参数名称,true 使用,false 跳过
        if (config.isUseActualParamName()) {
          // 如果为 true ,则name = arg0, arg1 之类的
          name = getActualParamName(method, paramIndex);
        }
        // 如果上述为false,
        if (name == null) {
          // name为参数索引,0,1,2 之类的
          name = String.valueOf(map.size());
        }
      }
      // 存入参数索引和参数名称的对应关系
      map.put(paramIndex, name);
    }
    // 赋值给 names 属性
    names = Collections.unmodifiableSortedMap(map);
  }

Después de leer la estructura anterior, pruebe el método principal, existen los siguientes métodos:


Person queryPerson(@Param("age") int age, String name, String address, @Param("money") double money);

En el método anterior, si se usa el nombre real, es decir, cuando config.isUseActualParamName () es verdadero, después del análisis, el atributo de nombres se imprime de la siguiente manera:


{0=age, 1=arg1, 2=money, 3=arg3}

Si config.isUseActualParamName () es falso, después del análisis, el atributo de nombres se imprimirá de la siguiente manera:


{0=age, 1=1, 2=money, 3=3}

Ahora que se han analizado los parámetros del método, si entra el valor del parámetro del método entrante, ¿cómo se obtiene? Es el método getNamedParams en esta clase:


  public Object getNamedParams(Object[] args) {
    // 参数的个数
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
     // 如果参数没有被 @Param 修饰,且只有一个,则直接返回
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      // 参数名称和参数值的对应关系
      final Map<String, Object> param = new ParamMap<Object>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        // key = 参数名称,value = 参数值
        param.put(entry.getValue(), args[entry.getKey()]);

        final String genericParamName = "param"+ String.valueOf(i + 1);
        // 默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
     // 返回参数名称和参数值的对应关系,是一个 map 
      return param;
    }
  }

Ahora pruébelo en el método principal:

Si config.isUseActualParamName () es verdadero y el constructor analiza el método, la relación correspondiente entre el índice del parámetro y el nombre es:


{0=age, 1=arg1, 2=money, 3=arg3}

Ahora los parámetros son:


Object[] argsArr = {24, "zhangsan", 1000.0, "chengdou"};

Ahora llame al método getNamedParams para vincular el nombre del método y el valor del método. Los resultados obtenidos son los siguientes. Tenga en cuenta que el método devuelve un objeto, que en realidad es un mapa:


{age=24, param1=24, arg1=zhangsan, param2=zhangsan, money=1000.0,  param3=1000.0, arg3=chengdou,  param4=chengdou}

Entonces, en SQL en xml, puede obtener el valor a través del # {nombre} correspondiente, o puede obtener el valor a través del # {param1}, etc.

En otro caso, config.isUseActualParamName () es falso. Después del análisis, la correspondencia entre el índice del parámetro y el nombre es:


{0=age, 1=1, 2=money, 3=3}

Después de eso, ¿cómo se ve la vinculación de parámetros y valores de parámetros? El método principal todavía se prueba de la siguiente manera, y el parámetro es el parámetro argsArr anterior:


{age=24, param1=24, 1=zhangsan, param2=zhangsan, money=1000.0, param3=1000.0, 3=chengdou param4=chengdou}

En SQL, los valores de los parámetros también se pueden obtener mediante # {0}, # {1} y similares.

Por lo tanto, después de que la clase ParamNameResolver analiza la interfaz Mapper, podemos usar # {name}, # {param1}, # {0} y otros métodos para obtener el valor del parámetro correspondiente en SQL.

MapperMethod

Después de comprender la clase de herramienta ParamNameResolver anterior, es fácil comprender MapperMethod.

En esta clase, se encapsula la información del método correspondiente de la interfaz Mapper y la información de la declaración SQL correspondiente.La clase MapperMethod puede considerarse como el puente de conexión entre la interfaz Mapper y la declaración SQL en el archivo de configuración.

Solo hay dos propiedades en esta clase, que corresponden a dos clases internas, SqlCommand y MethodSignature respectivamente. SqlCommand registra el nombre y tipo de la declaración SQL, y MethodSignature es la información del método correspondiente en la interfaz del asignador:


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

Primero echemos un vistazo a estas dos clases internas.

SqlCommand


public static class SqlCommand {
// SQL 的名称,是接口的全限定名+方法名组成
private final String name;
// SQL 的类型,取值:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
private final SqlCommandType type;

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
  // SQL 名称
  String statementName = mapperInterface.getName() + "." + method.getName();
  // MappedStatement  封装了 SQL 语句的相关信息
  MappedStatement ms = null;
  // 在配置文件中检测是否有该 SQL 语句
  if (configuration.hasStatement(statementName)) {
    ms = configuration.getMappedStatement(statementName);
  } else if (!mapperInterface.equals(method.getDeclaringClass())) { 
    // 是否父类中有该 SQL 的语句
    String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
    if (configuration.hasStatement(parentStatementName)) {
      ms = configuration.getMappedStatement(parentStatementName);
    }
  }
  // 处理 @Flush 注解
  if (ms == null) {
    if(method.getAnnotation(Flush.class) != null){
      name = null;
      type = SqlCommandType.FLUSH;
    }
  } else {
    // 获取 SQL 名称和类型
    name = ms.getId();
    type = ms.getSqlCommandType();
  }
}

Método Firma


  public static class MethodSignature {

    private final boolean returnsMany; // 方法的返回值为 集合或数组
    private final boolean returnsMap;  // 返回值为 map
    private final boolean returnsVoid;  // void
    private final boolean returnsCursor; // Cursor
    private final Class<?> returnType;  // 方法的返回类型
    private final String mapKey; // 如果返回值为 map,则该字段记录了作为 key 的列名
    private final Integer resultHandlerIndex;  // ResultHandler 参数在参数列表中的位置
    private final Integer rowBoundsIndex;  // RowBounds参数在参数列表中的位置

    // 用来解析接口参数,上面已介绍过
    private final ParamNameResolver paramNameResolver;

    public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
      // 方法的返回值类型
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
      this.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
      this.returnsCursor = Cursor.class.equals(this.returnType);
      this.mapKey = getMapKey(method);
      this.returnsMap = (this.mapKey != null);
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }

    // 根据参数值来获取参数名称和参数值的对应关系 是一个 map,看上面的main方法测试
    public Object convertArgsToSqlCommandParam(Object[] args) {
      return paramNameResolver.getNamedParams(args);
    }
}

MapperMethod

A continuación, veamos MapperMethod. El método principal es el método de ejecución, que se utiliza para ejecutar el SQL correspondiente al método:


  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        // insert 语句,param 为 参数名和参数值的对应关系
        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()) {
          // void 类型且方法有 ResultHandler 参数,调用 sqlSession.select 执行
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          // 返回集合或数组,调用 sqlSession.<E>selectList 执行
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          // 返回 map ,调用 sqlSession.<K, V>selectMap
          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());
    }
    return result;
  }

Lo anterior es la implementación principal de la clase MapperMethod, que consiste en obtener el nombre y los parámetros de la interfaz correspondiente, llamar al valor del método correspondiente de sqlSession y ejecutar el SQL correspondiente para obtener el resultado. Hay algunos métodos auxiliares en esta clase, que se puede ignorar.

para resumir

Lo anterior es el análisis subyacente de la interfaz Mapper, es decir, el módulo de enlace. Mybatis usará el proxy dinámico del JDK para crear un objeto proxy para cada interfaz Mapper, y usará la clase de herramienta ParamNameResolver para analizar los parámetros del Mapper interfaz, de modo que el SQL en XML pueda usar tres Hay dos formas de obtener el valor del parámetro, # {nombre}, # {0} y # {param1}. Cuando se complete el análisis del parámetro de interfaz, habrá un ejecutar el método de MapperMethod para llamar al nombre de la interfaz, es decir, el nombre y los parámetros correspondientes del SQL Métodos relacionados de sqlSession para ejecutar SQL para obtener resultados.

Supongo que te gusta

Origin blog.51cto.com/15077536/2608582
Recomendado
Clasificación