您现在的位置是:首页 > 文章详情

Mybatis Mapper 接口源码解析(binding包)

日期:2018-11-07点击:366

相关文章

Mybatis 解析配置文件的源码解析

Mybatis 类型转换源码分析

Mybatis 数据源和数据库连接池源码解析(DataSource)

前言

在使用 Mybatis 的时候,我们只需要写对应的接口,即dao层的Mapper接口,不用写实现类,Mybatis 就能根据接口中对应的方法名称找到 xml 文件中配置的对应SQL,方法的参数和 SQL 的参数一一对应,在 xml 里面的 SQL 中,我们可以通过 #{0},#{1},来绑定参数,也可以通过 #{arg0},##{arg1}来绑定参数,还可以通过方法中真正的参数名称如 name,age之类的进行绑定,此外还可通过 #{param1},#{param2}等来绑定,接下来看下 Mybatis的源码是如何实现的。

源码分析

在 Mybatis 中,解析 Mapper 接口的源码主要是在 binding 包下,该包下就 4 个类,再加上一个方法参数名称解析的工具类 ParamNameResolver ,一共 5 个类,代码量不多,下面就来一次分析这几个类。

先来简单看下这几个类:

1. BindingException :自定义异常,忽略

2. MapperMethod :在该类中封装了Mapper 接口对应方法的信息,以及对应的 SQL 语句的信息,MapperMethod 类可以看做是 Mapper 接口和配置文件中的 SQL 语句之间的连接桥梁,是这几个类中最主要的一个类,代码也较多,其他几个类的代码就很少。

3. MapperProxy :它是 Mapper 接口的代理对象,在使用 Mybatis 的时候,不需要我们实现 Mapper 接口,是使用 JDK 的动态代理来实现。

4. MapperProxyFactory :MapperProxy类的工厂类,用来创建 MapperProxy 

5. MapperRegistry :它是 Mybatis 接口及其代理对象工厂的注册中心,在 Mybatis 初始化的时候,会加载配置文件和 Mapper 接口信息注册到该类中来。

6. ParamNameResolver  该类不是 binding 包下的类,它是 reflection 包下的一个工具类,主要用来解析接口方法参数的。

接下来看下每个类的实现过程:

MapperProxy

首先来看下 MapperProxy 类,它是 Mapper 接口的代理对象,实现了 InvocationHandler 接口,即使用了 JDK 的动态代理为 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; } }

以上就是 MapperProxy 代理类的主要代码,需要注意的是调用了 MapperMethod 中的相关方法,MapperMethod 在后面进行分析,这里先知道它是完成方法参数的转换和执行方法对应的SQL即可,还有一点,在执行目标方法的时候,如果是 Object 中的方法,则直接执行目标方法,如果是默认方法,则会执行默认方法的相关逻辑,否则在使用代理对象执行目标方法

MapperProxyFactory

在看了上述的 MapperProxy 代理类之后, MapperProxyFactory 就是用来创建该代理类的,是一个工厂类。

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

 一个工厂类,主要用于创建 Mapper 接口的代理对象,代码简单,没什么好说的。

MapperRegistry

MapperRegistry 它是 Mapper 接口及其对应代理工厂对象的注册中心,在 Mybatis 初始化的时候,会加载配置文件及Mapper接口信息,注册到 MapperRegistry  类中,在需要执行某 SQL 的时候,会先从注册中心获取 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); } }

以上就是 MapperRegistry Mapper 注册的代码,主要注册 Mapper 接口,和获取 Mapper 接口的代理对象,很好理解。

ParamNameResolver  

ParamNameResolver  它不是 binding 包下的类,它是 reflection 包下的一个工具类,主要用来解析接口方法参数的。也就是方法参数中,该参数是第几个,即解析出参数的名称和索引的对应关系;该类中的代码相比于以上几个类要负责些,有点绕,我是通过写 main 方法来辅助理解的,不过代码量也不多。

该类中的主要方法主要是构造方法和 getNamedParams 方法,先来看下其他的方法:

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

上述代码是 ParamNameResolver  的一些辅助方法,最重要的是 names 属性,它用来存放参数索引于参数名称的对应关系,是一个 map,但是有例外,如果参数中含有 RowBounds 和 ResultHandler 这两种类型,则不会把它们的索引和对应关系放入到 names 集合中,如下:

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

构造方法,现在来看下 ParamNameResolver 的构造方法,在调用构造方法创建该类对象的时候会对方法的参数进行解析,解析结果放到 names 数据中去,代码如下:

重点

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

看了上述的构造以后,main 方法测试一下,有如下方法:

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

如上方法,如果使用真实名称,即 config.isUseActualParamName() 为 true 的时候,解析之后,names 属性的为打印出来如下:

{0=age, 1=arg1, 2=money, 3=arg3}
如果 config.isUseActualParamName() 为 false的时候,解析之后,names 属性的为打印出来如下:

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

现在解析了方法的参数后,如果传入方法的参数值进来,怎么获取呢?就是该类中的   getNamedParams 方法:

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

   现在在了 main 方法测试一下:

如果 config.isUseActualParamName() 为 true,且方法经过构造方法解析后,参数索引和名称的对应关系为:

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

现在参数为:Object[] argsArr = {24, "zhangsan", 1000.0, "chengdou"};

现在调用 getNamedParams  方法来绑定方法名和方法值,获取的结果如下,注意该方法返回的是 Object,其实它是一个 map:

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

所以在 xml 中的 SQL 中,可以通过 对应的 #{name} 来获取值,也可以通过 #{param1} 等来获取值。

 还有一种情况,就是  config.isUseActualParamName() 为 false,解析后,参数索引和名称的对应关系为:

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

之后,参数和参数值的绑定又是什么样子的呢?还是 main 方法测试如下,参数还是上面的 argsArr 参数:

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

在 SQL 中也可以通过 #{0},#{1} 之类的来获取参数值。

所以,综上 ParamNameResolver 类解析 Mapper 接口后,我们在 SQL 中可以通过 #{name}, #{param1},#{0} 之类的方式来获取对应的参数值的原因。

MapperMethod

在理解了上述的  ParamNameResolver 工具类之后,来看 MapperMethod 就很好理解了。

在该类中封装了Mapper 接口对应方法的信息,以及对应的 SQL 语句的信息,MapperMethod 类可以看做是 Mapper 接口和配置文件中的 SQL 语句之间的连接桥梁,

该类中只有两个属性,分别对应两个内部类,SqlCommand 和 MethodSignature ,其中 SqlCommand  记录了 SQL 语句的名称和类型,MethodSignature 就是 Mapper 接口中对应的方法信息:

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

先来看看这两个内部类。

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

MethodSignature

 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

    接下来看下 MapperMethod,核心的方法为 execute 方法,用于执行方法对应的 SQL :

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

   以上就是 MapperMethod 类的主要实现,就是获取对应接口的名称和参数,调用 sqlSession 的对应方法值执行对应的 SQL 来获取结果,该类中还有一些辅助方法,可以忽略。

总结

以上就是 Mapper 接口底层的解析,即 binding 模块,Mybatis 会 使用 JDK 的动态代理来为 每个 Mapper 接口创建一个代理对象,通过 ParamNameResolver 工具类来解析 Mapper 接口的参数,是的在 XML 中的 SQL 可以使用 三种方式来获取参数的值,#{name},#{0} 和 #{param1} 的方法,当接口参数解析完成后,会有 MapperMethod 的 execute 方法来把 接口的名称 即 SQL 对应的名称和参数通过调用 sqlSession 的相关方法去执行 SQL 获取结果。

 

原文链接:https://my.oschina.net/mengyuankan/blog/2873220
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章