MyBatis原理分析之获取Mapper接口的代理对象

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: MyBatis原理分析之获取Mapper接口的代理对象

本篇博文是原理分析的第三篇。当使用mapper接口进行CRUD时,其实是其代理对象在发挥作用,SQLsession获取mapper接口的代理对象时序图如下:


【1】DefaultSqlSession

如下代码所示,这里其实是调用了configuration实例的方法。该方法是一个泛型方法,参数有Class<T> type表示你的接口Class对象,比如UserMapper.class---interface com.jane.mapper.UserMapper

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

【2】Configuration


configuration有常量成员protected final MapperRegistry mapperRegistry = new MapperRegistry(this);实例,mapperRegistry 引用了当前configuration。

 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

MapperRegistry属性和构造方法如下:

public class MapperRegistry {
  private final Configuration config;
  //注意这个哦
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
  public MapperRegistry(Configuration config) {
    this.config = config;
  }
//..
}


一文中我们可以得知创建SqlSessionFactory时对所有的mapper(xml和接口)进行了解析并为每一个mapper接口创建了MapperProxyFactory对象放入knownMappers 中。



MapperRegistry的addMapper方法如下:

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 {
      //这里向knownMappers放入当前解析的mapper对应的MapperProxyFactory实例
       knownMappers.put(type, new MapperProxyFactory<>(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 {
       if (!loadCompleted) {
         knownMappers.remove(type);
       }
     }
   }
 }


【3】MapperRegistry

MapperRegistry的getMapper方法如下所示:

@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//首先获取MapperProxyFactory
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
  //触发MapperProxyFactory的newInstance
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

代码解释如下:

  • ① 从knownMappers中获取当前class对象的mapperProxyFactory实例;
  • ② 如果不存在则抛出异常;
  • ③ 如果存在则执行mapperProxyFactory.newInstance(sqlSession)来获取当前mapper的代理对象

【4】MapperProxyFactory

MapperProxyFactory主要属性和构造方法

public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
//...
}

可以看到其除了接口的class对象外还维护了一个私有ConcurrentHashMap类型常量methodCache

创建实例对象MapperProxy

public T newInstance(SqlSession sqlSession) {
  //首先实例化得到一个MapperProxy,然后创建得到其代理对象
 final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
 return newInstance(mapperProxy);
}

代码解释如下:


① 根据sqlsession、mapperInterface(接口的class对象),以及类型的methodCache创建MapperProxy

② 为Mapper创建代理对象

这里需要注意的是MapperProxy是一个InvocationHandler类型,需要实现Object invoke(Object proxy, Method method, Object[] args)方法。


MapperProxy的主要属性和构造方法

public class MapperProxy<T> implements InvocationHandler, Serializable {
  private static final long serialVersionUID = -6424540398559729838L;
  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;
  }
//...
}  


InvocationHandler是什么呢?InvocationHandler是由代理实例的调用处理程序实现的接口。也就是说一个类/接口的代理实例的调用处理程序必须实现InvocationHandler接口的invoke方法。可以理解为本文中MapperProxy就是Mapper代理实例的调用处理程序。

InvocationHandler的invoke方法如下:

Object invoke(Object proxy, Method method, Object[] args)
proxy:代理实例对象
method:目标方法
args:方法入参

Proxy是什么?Proxy是专门完成代理的操作类,是所有动态代理类的父类,通过此类为一个或多个接口动态地生成实现类。使用 Proxy 生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的意义。通常都是为指定的目标对象生成动态代理

//直接创建一个动态代理对象
static Object newProxyInstance( ClassLoader loader, Class<?>[] interfaces,InvocationHandler h ) 
loader :定义代理类的类加载器
interfaces:被代理类实现的所有接口
h:代理实例的调用处理程序
该方法将会返回一个代理对象,代理对象有代理调用处理程序--InvocationHandler 

根据mapperProxy创建Mapper的代理对象

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
//也就是说说触发mapper的方法时会交给mapperProxy来处理


可以看下最终得到的Mapper的代理对象如下(h 表示其是一个InvocationHandler也就是调用处理程序):


每个代理实例都有一个关联的调用处理程序InvocationHandler。在代理实例上调用方法时,方法调用将被编码并发送到其调用处理程序的invoke方法。


【5】MapperMethod

上面我们提到了MapperProxyFactory有常量成员methodCache ,在类加载过程中就进行了初始化。

 private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

根据MapperProxyFactory生成MapperProxy实例时,将ConcurrentHashMap类型的methodCache 传了过去。

final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);

MapperProxy实现了InvocationHandler接口的invoke方法,那么在使用Mapper进行CRUD时实际会调用对应的MapperProxyinvoke方法:

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

如上代码可以看到其先获取了MapperMethod 实例,然后调用了MapperMethod 实例的execute方法。

cachedMapperMethod方法

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

代码解释如下:


① 尝试从ConcurrentHashMap类型的methodCache获取当前method对应的MapperMethod

② 如果①没有获取到,则新建MapperMethod实例

③ 将{method=mapperMethod}放入methodCache中

④ 返回MapperMethod 实例


这里先看一下一个Method对象是个什么?


实例化MapperMethod

new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())构造方法如下:

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


这里我们可以看到,其新建了SqlCommandMethodSignature实例。

MapperMethod的UML图如下所示


再看一下MapperMethod实例对象

实例化SqlCommandmethod

SqlCommandmethod是MapperMethod静态嵌套类,主要属性是name和SqlCommandType。

 public static class SqlCommand {
    private final String name;
    private final SqlCommandType type;
    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      String statementName = mapperInterface.getName() + "." + method.getName();
      MappedStatement ms = null;
      if (configuration.hasStatement(statementName)) {
        ms = configuration.getMappedStatement(statementName);
      } else if (!mapperInterface.equals(method.getDeclaringClass())) { // issue #35
        String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
        if (configuration.hasStatement(parentStatementName)) {
          ms = configuration.getMappedStatement(parentStatementName);
        }
      }
      if (ms == null) {
        if(method.getAnnotation(Flush.class) != null){
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
          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);
        }
      }
    }
//...
  }

构造方法如下解释如下


① mapperInterface.getName() + "." + method.getName()解析获取到statementName ,如com.mybatis.dao.EmployeeMapper.getEmpById;

② 判断configuration实例的Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")成员对象中是否有①中的statementName;

③ 如果存在,则获取statementName对应的MappedStatement;

④ 如果不存在且当前接口Class不是方法的所属Class,则根据方法的所属Class的name与方法名字解析新的statementName

⑤ 如果configuration的Map<String, MappedStatement> mappedStatements成员中存在新的statementName,则返回对应的MappedStatement

⑥ 如果最终得到的MappedStatement为null,则判断方法上面是否有注解@Flush;

⑦ 如果有注解@Flush,则赋值name=null,type = SqlCommandType.FLUSH;

⑧ 如果没有注解@Flush,则抛出异常

⑨ 如果最终得到的MappedStatement不为null,则赋值 name = ms.getId(); type = ms.getSqlCommandType();。

⑩ 如果type为UNKNOWN,则抛出异常


SqlCommandType 是一个枚举类,主要有值 UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH

public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

hasStatement(statementName)与getMappedStatement(statementName)

configuration.hasStatement(statementName)) ;
configuration.getMappedStatement(statementName);

上面我们看到这样两句代码,代码表面本身很好理解,我们跟进去看:

hasStatement代码流程片段如下:

public boolean hasStatement(String statementName) {
   return hasStatement(statementName, true);
 }
public boolean hasStatement(String statementName, boolean validateIncompleteStatements) {
   if (validateIncompleteStatements) {
     buildAllStatements();
   }
   return mappedStatements.containsKey(statementName);
 }

getMappedStatement代码流程片段如下:

public MappedStatement getMappedStatement(String id) {
  return this.getMappedStatement(id, true);
}
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
  if (validateIncompleteStatements) {
    buildAllStatements();
  }
  return mappedStatements.get(id);
}

可以看到,其都调用了 buildAllStatements();。那么这个方法是什么呢?如下代码所示,其实就是mybatis提供的快速失败机制

  protected void buildAllStatements() {
    parsePendingResultMaps();
    if (!incompleteCacheRefs.isEmpty()) {
      synchronized (incompleteCacheRefs) {
        incompleteCacheRefs.removeIf(x -> x.resolveCacheRef() != null);
      }
    }
    if (!incompleteStatements.isEmpty()) {
      synchronized (incompleteStatements) {
        incompleteStatements.removeIf(x -> {
          x.parseStatementNode();
          return true;
        });
      }
    }
    if (!incompleteMethods.isEmpty()) {
      synchronized (incompleteMethods) {
        incompleteMethods.removeIf(x -> {
          x.resolve();
          return true;
        });
      }
    }
  }

在该方法上面有如下注释:


Parses all the unprocessed statement nodes in the cache. It is recommended to call this method once all the mappers are added as it provides fail-fast statement validation.


解析缓存中所有未处理的statement节点。建议在添加所有映射程序后调用此方法,因为它提供fail fast语句验证。

resolveCacheRef、parseStatementNode、resolve都会抛出异常

那么什么是fail fast 呢?


fail fast即快速失败。这是一种设计思想,即系统如果发现异常立即抛出异常结束任务。与快速失败对应的还有fail save也就是安全失败,简单解释即为系统发现异常时,并不抛出异常结束程序,而是捕获异常通常并写入到错误日志中。

目录
相关文章
|
2月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
28天前
|
SQL Java 数据库连接
mybatis使用四:dao接口参数与mapper 接口中SQL的对应和对应方式的总结,MyBatis的parameterType传入参数类型
这篇文章是关于MyBatis中DAO接口参数与Mapper接口中SQL的对应关系,以及如何使用parameterType传入参数类型的详细总结。
30 10
|
1月前
|
SQL XML Java
Mybatis的原理和MybaitsPlus
这篇文章对比分析了Mybatis和Mybatis Plus的特点与底层实现机制,探讨了两者之间的差异及各自的优势。
48 0
|
3月前
|
SQL Java 数据库连接
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
文章讲述了在使用Mybatis时遇到的资源文件找不到的问题,并提供了通过修改Maven配置来解决资源文件编译到target目录下的方法。
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
|
2月前
|
SQL XML Java
mybatis :sqlmapconfig.xml配置 ++++Mapper XML 文件(sql/insert/delete/update/select)(增删改查)用法
当然,这些仅是MyBatis功能的初步介绍。MyBatis还提供了高级特性,如动态SQL、类型处理器、插件等,可以进一步提供对数据库交互的强大支持和灵活性。希望上述内容对您理解MyBatis的基本操作有所帮助。在实际使用中,您可能还需要根据具体的业务要求调整和优化SQL语句和配置。
44 1
|
28天前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
84 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
28天前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
47 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
1月前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
240 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
|
28天前
|
SQL Java 数据库连接
mybatis使用二:springboot 整合 mybatis,创建开发环境
这篇文章介绍了如何在SpringBoot项目中整合Mybatis和MybatisGenerator,包括添加依赖、配置数据源、修改启动主类、编写Java代码,以及使用Postman进行接口测试。
15 0
mybatis使用二:springboot 整合 mybatis,创建开发环境
|
28天前
|
Java 数据库连接 API
springBoot:后端解决跨域&Mybatis-Plus&SwaggerUI&代码生成器 (四)
本文介绍了后端解决跨域问题的方法及Mybatis-Plus的配置与使用。首先通过创建`CorsConfig`类并设置相关参数来实现跨域请求处理。接着,详细描述了如何引入Mybatis-Plus插件,包括配置`MybatisPlusConfig`类、定义Mapper接口以及Service层。此外,还展示了如何配置分页查询功能,并引入SwaggerUI进行API文档生成。最后,提供了代码生成器的配置示例,帮助快速生成项目所需的基础代码。