这篇博客中来说一下对Mybatis动态代理接口方式的扩展,对于Mybatis动态代理接口不熟悉的朋友,可以参考前一篇博客,或者研读Mybatis源码。

  扩展11:动态代理接口扩展

  我们知道,真正在Mybatis动态代理接口方式背后起作用的是SqlSession接口,类似地,我们的动态代理接口扩展则是基于IDaoTemplate接口,同样的,也需要解决相同的三个基本问题:

问题1:确定需要执行的sqlId

  原生用法是根据包名、接口名、方法名去查找,但我们推荐添加一个sqlId的查找策略接口:

  1. public interface ISqlIdLookupStrategy {
  2. public String lookup(Method method);
  3. }

很简单,就只有一个方法,那就是根据接口中的方法查找需要执行的sqlId。Mybatis原生用法相当于如下实现:

  1. public class MybatisSqlIdLookupStrategy implements ISqlIdLookupStrategy{
  2.  
  3. @Override
  4. public String lookup(Method method) {
  5. return method.getDeclaringClass().getName()+"."+method.getName();
  6. }
  7. }

那么,怎么处理上一篇博客中所说的几个问题呢?我的做法是引入一个新的注解@SqlRef,这也是引入的第一个注解:

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface SqlRef {
  5. /**
  6. * 标识使用哪个SqlId
  7. * @return sqlId值
  8. */
  9. String value() default "";
  10.  
  11. /**
  12. * 指定相对于哪个类的路径,指定时,classpath选项将不起作用
  13. * @return 指定前面的value()是相对于哪个类而言
  14. */
  15. Class<?> cls() default Null.class;
  16.  
  17. /**
  18. * 是否为相对于当前类的路径
  19. * @return 是否相对于当前类路径,当前类是指标示该注解方法所在的类
  20. */
  21. boolean classpath() default true;
  22.  
  23. /**
  24. * 表示未配置类
  25. */
  26. public class Null{};
  27. }

这个注解只能加到方法上面,并且一直到运行时都可以获取到相关信息,其中包括三个属性方法:

  1. value:标识sqlId,如果为空,就按默认方法取
  2. cls:value表示的sqlId是相对于哪个类,其中SqlRef.Null表示没有配置值,如果不是SqlRef.Null,解析sqlId的时候就添加cls.getName()+"."
  3. classpath:和cls类似,也是指示value值的相对类,默认值为true,表示相对于@SqlRef注解所标示的那个方法所在类,如果配置为false,则忽略@SqlRef注解所标示的方法所在类,如果cls和classpath都配置,以cls为准。

相当于是如下实现ISqlIdLookupStrategy接口:

  1. public class SqlRefSqlIdLookupStrategy implements ISqlIdLookupStrategy{
  2.  
  3. @Override
  4. public String lookup(Method method) {
  5. SqlRef sqlRef = method.getAnnotation(SqlRef.class);
  6. if(null != sqlRef){
  7. String sqlId = sqlRef.value();
  8. if(null == sqlId || "".equals(sqlId.trim())){
  9. sqlId = method.getName();
  10. }
  11.  
  12. Class<?> cls = sqlRef.cls();
  13. if(null != cls && !SqlRef.Null.class.equals(cls)){
  14. sqlId = cls.getName() + "." + sqlId;
  15. }else if(sqlRef.classpath()){
  16. sqlId = method.getDeclaringClass().getName() + "." +sqlId;
  17. }
  18. return sqlId;
  19. }else{
  20. return method.getDeclaringClass().getName()+"."+method.getName();
  21. }
  22. }
  23. }

更进一步,还可以根据配置一个原sqlId和真正需要执行sqlId的映射关系,从而实现sqlId的偷梁换柱:

  1. public class MappingSqlIdLookupStrategy implements ISqlIdLookupStrategy{
  2.  
  3. private ISqlIdLookupStrategy proxy;
  4.  
  5. private Map<String, String> mapping;
  6.  
  7. @Override
  8. public String lookup(Method method) {
  9. String sqlId = null;
  10. ISqlIdLookupStrategy proxy = getProxy();
  11. if(null == proxy){
  12. sqlId = method.getDeclaringClass().getName()+"."+method.getName();
  13. }else{
  14. sqlId = proxy.lookup(method);
  15. }
  16.  
  17. Map<String, String> mapping = getMapping();
  18. if(null != sqlId && mapping.containsKey(sqlId)){
  19. return mapping.get(sqlId);
  20. }else{
  21. return sqlId;
  22. }
  23. }
  24.  
  25. // 省略 getter、setter方法
  26. }

这里的实现没有继承MybatisSqlIdLookupStrategy或SqlRefSqlIdLookupStrategy,而是使用内部代理的方式,算是聚合优于继承原则的体现吧。

  有了@SqlRef注解,对于执行单个sqlId的方法,就可以随心所欲的指向需要执行的sqlId了,很轻松的就解决了方法重载(方法名相同,但参数不同,执行的sqlId也不同)、不同名方法需要执行相同sqlId(比如查询列表、分页查询、查找等等)、需要执行其它命名空间中sqlId的问题了。但是@SqlRef对于需要一次性执行多个sqlId的批量,还是无能为力,这个问题我们等下再从另一个角度来看怎么处理,先接着看第2个问题:

问题2:确定需要执行的方法

  相比SqlSession,IDaoTemplate接口添加了分页查询(物理分页)、流式查询、调用存储过程、批量执行(还有merge、case等方法,因为评审的时候去掉了,这里也就不展开了),怎么确定动态代理接口时需要调用的方法呢?其实很简单,沿用Mybatis的思路,无非是根据SqlMapper元素标签、dao接口方法签名(特殊参数和返回值)、特殊注解、运行时参数等等。

  根据IDaoTemplate接口,目前确定执行方法的具体规则有:

  1. 如果返回值为整型数组int[],作为批量执行处理(注意,这里会覆盖掉Mybatis原本就返回int[]数组的情形,但原本就返回int[]极少用到,所以关系不大)
  2. 如果返回值为存储过程调用结果类型ICallResult,作为存储过程调用
  3. 如果返回值为流式查询结果类型IListStreamReader,作为流式查询调用
  4. 其它情形下,按Mybatis原生方法确定需要执行的方法,有一点不同的是,把IPage类型的参数和RowBounds类型的参数作为同一类处理,都视作分页查询

问题3:确定执行SQL时的参数

  除了批量执行,IDaoTemplate接口中方法形参和SqlSession中形参大同小异,所以执行SQL时的参数组装也是大同小异,大同就不说了,可参考上一篇博客,小异主要体现在:

  1. 分页参数IPage,作为特殊类型的参数,和RowBounds类型等同处理
  2. 流式查询selectListStream方法中还可以传入一个整型的fetchSize,表示每次读取的记录条数,因为整型参数和一般执行参数无法区分开来,所有我引入了@FetchSize注解,这也是引入的第2个注解:
  1. @Target({ElementType.METHOD, ElementType.PARAMETER})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface FetchSize {
  5.  
  6. /**
  7. * 每次获取的记录条数
  8. * <p>
  9. * 注意:每次获取的记录条数数值范围为 (0, 5000]
  10. * </p>
  11. * @return 每次读取记录数大小
  12. */
  13. int value() default -1;
  14. }

  这个注解可以添加到方法上,加到方法时需要指定value值,表示每次读取的记录条数,具有全局性,也就是说每次调用方法,这个值都一样;另外,这个注解还可以加在整型方法参数前,表示这个参数是每次读取的记录条数,具有局部性,每次调用方法,都使用新传入的参数值。

  到此为止,除了批量执行,三个基本问题都已经解决,而对于批量执行,我们也已经确定了,只有方法签名中返回值是整型数组int[],才算是批量执行。下面我们就从批量执行的角度,再次讨论三个基本问题的处理。

  批量执行三个基本问题的处理

  首先,引进一个概念:可转换为集合类型的参数,什么意思呢?其实很简单,就是说这个参数可以转换成集合类型,比如数组类型的参数、迭代器类型的、本身就是集合类型的等等。看下面的代码可能更清晰(具体怎么转换,就不赘述了):

  1. private boolean isCollectionType(Class<?> cls){
  2. return cls.isArray()
  3. || Iterator.class.isAssignableFrom(cls)
  4. || Enumeration.class.isAssignableFrom(cls)
  5. || Iterable.class.isAssignableFrom(cls); //因此包含Collection,从而也包含List、Set、Queue等常见集合类型
  6. }

  其次,我们把批量执行分一下类,分成如下三种:

  1. 一个sqlId,不同参数,执行多次

  2. 一组sqlId,相同参数,执行多次,这里又分为两种:

    2.1 一组sqlId,对应一个可转换为集合类型的参数,并且sqlId的个数和集合大小相同,一一对应的关系,每次执行不同sqlId,真正的执行参数也不相同

    2.2 一组sqlId,对应相同的参数,每次执行不同sqlId,真正的执行参数完全相同

  3.混合批量类型:一组sqlId,有的sqlId本身不是批量类型,有的sqlId本身又是一个批量类型(这个子批量类型就可以限制为批量类型1:即一个sqlId,不同参数多次执行,读者可以想想为什么?),他们的参数也不尽相同

  准备工作做好了,现在来分别看怎么处理三个基本问题的:

批量类型1:一个sqlId,不同参数,执行多次

问题1:确定sqlId

  因为是一个sqlId,所以很简单,和其它方法一样,使用ISqlIdLookupStrategy查找策略直接查找就行,可以使用原生用法,可以使用@SqlRef注解,还可以配置替换的映射关系,甚至可以同时使用其中的两种组合。

问题2:确定执行方法

  从IDaoTemplate接口来看,一个sqlId执行多次的只有一个方法,实际上也就自然而然的确定了执行的方法

问题3:确定执行参数

  一个sqlId执行多次,本质上就要求有一个可转换为集合类型的参数,然后将这个参数转换为真正的集合类型,从而可以循环迭代这个集合类型,每次使用其中的一个参数。这里会有一个问题,如果有多个可转换为集合类型的参数怎么办?为了防止歧义,我引入了@BatchParam注解,这也是引入的第三个注解:

  1. @Target({ElementType.PARAMETER, ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface BatchParam {
  5.  
  6. /**
  7. * 是否为批量,只适用于批量类型3
  8. * @return 是否为批量
  9. */
  10. boolean value() default true;
  11.  
  12. /**
  13. * 批量参数中每一项存入map结构时的名称
  14. * @return 数据项名称
  15. */
  16. String item() default "item";
  17.  
  18. /**
  19. * 表示批量参数的属性
  20. * @return 批量参数属性
  21. */
  22. String property() default "this";
  23.  
  24. /**
  25. * 当前索引存入map结构时的名称
  26. * @return 索引名称
  27. */
  28. String index() default "index";
  29. }

需要说明的是,虽然@BatchParam既可以添加到方法上,也可以添加到方法参数前,但是对于批量类型1,@BatchParam注解只有添加到方法参数前才生效,并且value属性方法不生效。看完这个注解的定义,再来说明怎么组装批量类型1的执行参数:

1、将所有执行参数按照Mybatis原生用法的方式组装成一个参数对象,记为P1

2、找出可转换为集合类型的参数,并真正转换为集合类型的参数,记为C2

  具体算法:找到标有@BatchParam参数的入参,如果property()属性等于this,就将这个入参作为集合类型的参数,否则的话,就解析这个入参的对应属性值作为集合类型的参数(属性可以是任意ognl表达式),如果入参或者从入参解析出的参数不是可转换为集合类型的参数,就抛出异常

3、循环迭代C2,每次循环,创建一个map对象PM,将C2中对应索引处的值以@BatchParam.item()为key存入PM,将循环索引以@BatchParam.index()为key存入PM,对于参数对象P1,则做如下处理:如果P1是一个Map结构,直接将Map结构合并到PM对象中,否则将整个P1参数以"param1"为key存入PM

  如果对上述过程不清楚的,建议多阅读几篇,编写几个实际例子,实际测试运行一下。

  好了,sqlId找到了,集合类型的参数也准备好了,直接调用IDaoTemplate中public int[] executeBatch(String sqlId, List<?> parameters)就可以了。

批量类型2:一组sqlId,相同参数,执行多次

问题1:确定sqlId

  因为有一组sqlId,所以原来的查找策略都失效了,这里我引入了@SqlRefs注解,这也是引入的第四个注解:

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface SqlRefs {
  5.  
  6. /**
  7. * @return {@link SqlRef}组,表示要执行的一组sqlId
  8. */
  9. SqlRef[] value();
  10. }

非常简单,@SqlRefs里面可以包含多个@SqlRef,而每一个@SqlRef可以根据查找查找策略找到一个需要执行的sqlId,因此第一个问题,需要执行的sqlId数组也就解决了。

问题2:确定执行方法

  从IDaoTemplate接口来看,一次执行一组sqlId的只有两个重置方法,没有本质上区别,只是执行时参数不同,因此执行方法结合第三个问题后也就解决了。

问题3:确定执行参数

  这里根据是否有含@BatchParam注解的方法形参分为两种情况:

  • 具有包含@BatchParam注解的方法形参:这种情况下作为批量执行2.1处理,按照批量执行1中逻辑一样,确定集合类型的参数,然后检查sqlId的个数和集合类型参数大小是否一致,不一致抛出异常,一致的话,就一一对应的去执行
  • 不具有包含@BatchParam注解的方法形参:这种情况下作为批量执行2.2处理,先按照Mybatis原生方法组装参数对象,然后循环sqlId,每次都传入相同的参数对象取执行

批量类型3:混合批量类型,一组sqlId,其中每一个sqlId可以是批量,也可以不是批量,而且参数可以不同

问题1:确定sqlId

  同样,有一组sqlId需要确定,使用原生方法或@SqlRef注解都无法解决问题,那使用@SqlRefs是否可以呢?如果只是确定一组sqlId,就想批量执行2中那样是没有问题的,但问题是,我们不但需要确定一组sqlId,而且还需要确定和每一个sqlId对应的参数,需要知道其中每一个sqlId本身是不是批量,因此不能简单实用@SqlRefs,为此,我引入了@Execute和@Executes注解,这也是引入的第五和第六个注解:

  1. @Target({ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Execute {
  5.  
  6. /**
  7. * SqlRef引用
  8. * @return SqlRef引用
  9. */
  10. SqlRef sqlRef();
  11.  
  12. /**
  13. * 批量参数
  14. * @return 批量参数
  15. */
  16. BatchParam param();
  17.  
  18. /**
  19. * 执行的条件
  20. * @return
  21. */
  22. String condition() default "";
  23. }
  24.  
  25. @Target({ElementType.METHOD})
  26. @Retention(RetentionPolicy.RUNTIME)
  27. @Documented
  28. public @interface Executes {
  29.  
  30. Execute[] value();
  31. }

这里@Execute标示一次执行所需的信息,一次执行可以是一个简单语句,也可以是一个批量类型1,@Execute不能单独使用,必须嵌套在@Executes中,而@Executes标示一组执行(需要说明的是,这里的一次执行概念@Execute并不是和物理上和数据库交互一次,而只是一个逻辑上的划分,实际上,整个批量类型3只会和数据库交互一次)。可以参考下面的例子熟悉一下@Execute和@Executes的使用:

  1. @Executes({
  2. //更新角色基本信息
  3. @Execute(sqlRef=@SqlRef("dUpdateRole"), param=@BatchParam(false)),
  4. //删除角色权限关系
  5. @Execute(sqlRef=@SqlRef("dDeleteRolePermissionByPermTypes"), param=@BatchParam(value=false)),
  6. //重新添加角色权限关系
  7. @Execute(sqlRef=@SqlRef("dInsertRolePermission"), param=@BatchParam(item="perm", property="permissions")),
  8. //如果页面加载过角色的分配角色信息,就先删除角色与分页角色关系,因为不是子批量类型,所以需要显示条件执行的条件
  9. @Execute(sqlRef=@SqlRef("dDeleteRoleRoleAllot"), param=@BatchParam(value=false), condition="roleAllotLoaded"),
  10. //新增角色与分配角色关系,这里不需要添加condition配置,因为会有一个隐式条件,如果是一个sqlId多次执行的子批量,而集合类型的参数为空,则不实际执行
  11. @Execute(sqlRef=@SqlRef("dInsertRoleRoleAllot"), param=@BatchParam(item="allot", property="roleAllots"))
  12. })
  13. public int[] dUpdate(RoleForm role);

这个例子的业务场景是:更新一个角色,其中角色包含角色基本信息,角色和权限的关系(1对多),角色与分配角色的关系(1对多),然后前端页面传入的参数是角色基本信息,角色权限信息,如果点击了角色与分配角色的关系,就再传入角色的分配角色信息,否则就没有角色的分配角色信息(这里没有不表示清除这个关系,而只是页面没有加载,不需要变化)。

  可以看到,每一个@Execute都有一个@SqlRef,从而也就解决了执行的sqlId数组问题。

问题2:确定执行方法

  同批量执行2,从IDaoTemplate接口来看,一次执行一组sqlId的只有两个重置方法,没有本质上区别,只是执行时参数不同,因此执行方法结合第三个问题后也就解决了。

问题3:确定执行参数

  这里相对批量执行2,情况反而简单一些,不过就是循环处理@Execute注解,而每一个@Execute的处理,和批量执行1非常类型,先根据sqlRef属性方法确定执行的sqlId,然后根据@BatchParam确定对应的执行参数,不同于批量执行1的有几点:

  1. @BatchParam注解的value()属性方法起作用了,表示当前sqlId是否为一个子批量类型
  2. 求子批量类型的集合类型参数时,根对象不同了,批量执行1的根对象就是@BatchParam注解所在的入参,而批量执行3的根对象,是整个入参按照Mybatis原生方式组装的包装对象
  3. @Execute注解还包含condition()的属性方法,表示需要执行的条件表达式,如果为空,表示需要执行,这个条件表达式的求值根对象也是整个入参按照Mybatis原生方式组装的包装对象

  到此,整个批量执行的三个基本问题也都已解决,但我们刚刚的说明有一个前提,那就是事先已经知道是什么批量类型了,而事实上,事先我们并不知道,那么怎么确定呢?

  回过头来看,实际上已经很简单了,具体规则如下:

  如果有@Executes注解,则为批量类型3;如果有@SqlRefs,则为批量类型2,其中含有@BatchParam注解的参数,则为批量类型2.1,否则为批量类型2.2;如果既没有@Executes,也没有@SqlRefs,则为批量类型1。

  最后,动态代理接口扩展还剩下一个问题,那就是怎么使用我们的动态代理逻辑替换Mybatis的动态代理逻辑?跟踪源码就知道,Mybatis的动态代理逻辑主要在类org.apache.ibatis.binding.MapperMethod中,而这个类被MapperProxy调用,进而被org.apache.ibatis.session.Configuration所使用:

  1. public class Configuration {
  2.  
  3. // ... 省略代码
  4. protected MapperRegistry mapperRegistry = new MapperRegistry(this);
  5. // ... 省略代码
  6. }

因此,我们只需要继承Configuration,替换属性mapperRegistry的初始化即可,或者在运行时修改mapperRegistry的值也可以。

Java EE开发平台随手记6——Mybatis扩展4的更多相关文章

  1. Java EE开发平台随手记4——Mybatis扩展3

    接着昨天的Mybatis扩展——IDaoTemplate接口. 扩展9:批量执行 1.明确什么是批量执行 首先说明一下,这里的批量执行不是利用<foreach>标签生成一长串的sql字符串 ...

  2. Java EE开发平台随手记3——Mybatis扩展2

    忙里偷闲,继续上周的话题,记录Mybatis的扩展. 扩展5:设置默认的返回结果类型 大家知道,在Mybatis的sql-mapper配置文件中,我们需要给<select>元素添加resu ...

  3. Java EE开发平台随手记2——Mybatis扩展1

    今天来记录一下对Mybatis的扩展,版本是3.3.0,是和Spring集成使用,mybatis-spring集成包的版本是1.2.3,如果使用maven,如下配置: <properties&g ...

  4. Java EE开发平台随手记5——Mybatis动态代理接口方式的原生用法

    为了说明后续的Mybatis扩展,插播一篇广告,先来简要说明一下Mybatis的一种原生用法,不过先声明:下面说的只是Mybatis的其中一种用法,如需要更深入了解Mybatis,请参考官方文档,或者 ...

  5. Java EE开发平台随手记1

    过完春节以来,一直在负责搭建公司的新Java EE开发平台,所谓新平台,其实并不是什么新技术,不过是将目前业界较为流行的框架整合在一起,做一些简单的封装和扩展,让开发人员更加易用. 和之前负责具体的项 ...

  6. Java EE开发课外事务管理平台

    Java EE开发课外事务管理平台 演示地址:https://ganquanzhong.top/edu 说明文档 一.系统需求 目前课外兴趣培训学校众多,完善,但是针对课外兴趣培训学校教务和人事管理信 ...

  7. Java EE开发环境——MyEclipse2017破解 和 Tomcat服务器配置

    Java EE开发,我们可以搭建如下开发环境: 底层运行环境:jdk 和 jre. Web服务器:Tomcat 后台数据库:SQL Server 可视化集成开发环境:MyEclipse Java EE ...

  8. JEECG 3.7.1 版本发布,企业级JAVA快速开发平台

    JEECG 3.7.1 版本发布,企业级JAVA快速开发平台 ---------------------------------------- Version:  Jeecg_3.7.1项 目:   ...

  9. JEECG 4.0 版本发布,JAVA快速开发平台

    JEECG 4.0 版本发布,系统全面优化升级,更快,更稳定!         导读                               ⊙平台性能优化,系统更稳定,速度闪电般提升      ...

随机推荐

  1. Python生成器的经典程序

    import random def get_data(): """返回0到9之间的3个随机数""" return random.sample ...

  2. linux修改主机名的方法

    linux修改主机名的方法 用hostname命令可以临时修改机器名,但机器重新启动之后就会恢复原来的值. #hostname   //查看机器名#hostname -i  //查看本机器名对应的ip ...

  3. JQuery按回车提交数据

    引入JQuery文件 <script src="JS/jquery-1.9.1.js" type="text/javascript"></sc ...

  4. c# WebBrower 与 HttpRequest配合 抓取数据

    今天研究一个功能,发现一个问题. 通过webbrower模拟用户自动登录可以完成,并且可以取到相对应的页面内容. 但是如果页面中通过ajax,动态加载的内容,这种方式是取不到的,于是用到了httpRe ...

  5. uva 11357 Matches

    // uva 11357 Matches // // 题目大意: // // 给你n个火柴,用这n个火柴能表示的非负数有多少个,不能含有前导零 // // 解题思路: // // f[i] 表示正好用 ...

  6. 第56讲:Scala中Self Types实战详解

    今天学习了self type的内容,让我们来看下代码 package scala.learn class Self{  self =>    val tmp = "Scala" ...

  7. 初步了解yield_python

    yield 关键字是在学习python生成器(Generator)时遇到的,对于它及Generator至今我还不能很深入的理解,当前只是把所理解的知识作下记录,以便以后翻查. yield关键字是用来定 ...

  8. Bullet物理引擎在OpenGL中的应用

    Bullet物理引擎在OpenGL中的应用 在开发OpenGL的应用之时, 难免要遇到使用物理来模拟OpenGL中的场景内容. 由于OpenGL仅仅是一个关于图形的开发接口, 因此需要通过第三方库来实 ...

  9. Paxos算法细节详解(一)--通过现实世界描述算法

    Paxos分析 最近研究paxos算法,看了许多相关的文章,概念还是很模糊,觉得还是没有掌握paxos算法的精髓,所以花了3天时间分析了libpaxos3的所有代码,此代码可以从https://bit ...

  10. 移动开发发展方向-----Hybird混合开发3大方案

    移动开发发展方向-----Hybird混合开发3大方案