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

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

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

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

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

public interface ISqlIdLookupStrategy {
public String lookup(Method method);
}

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

 public class MybatisSqlIdLookupStrategy implements ISqlIdLookupStrategy{

     @Override
public String lookup(Method method) {
return method.getDeclaringClass().getName()+"."+method.getName();
}
}

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

 @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SqlRef {
/**
* 标识使用哪个SqlId
* @return sqlId值
*/
String value() default ""; /**
* 指定相对于哪个类的路径,指定时,classpath选项将不起作用
* @return 指定前面的value()是相对于哪个类而言
*/
Class<?> cls() default Null.class; /**
* 是否为相对于当前类的路径
* @return 是否相对于当前类路径,当前类是指标示该注解方法所在的类
*/
boolean classpath() default true; /**
* 表示未配置类
*/
public class Null{};
}

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

  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接口:

 public class SqlRefSqlIdLookupStrategy implements ISqlIdLookupStrategy{

     @Override
public String lookup(Method method) {
SqlRef sqlRef = method.getAnnotation(SqlRef.class);
if(null != sqlRef){
String sqlId = sqlRef.value();
if(null == sqlId || "".equals(sqlId.trim())){
sqlId = method.getName();
} Class<?> cls = sqlRef.cls();
if(null != cls && !SqlRef.Null.class.equals(cls)){
sqlId = cls.getName() + "." + sqlId;
}else if(sqlRef.classpath()){
sqlId = method.getDeclaringClass().getName() + "." +sqlId;
}
return sqlId;
}else{
return method.getDeclaringClass().getName()+"."+method.getName();
}
}
}

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

 public class MappingSqlIdLookupStrategy implements ISqlIdLookupStrategy{

     private ISqlIdLookupStrategy proxy;

     private Map<String, String> mapping;

     @Override
public String lookup(Method method) {
String sqlId = null;
ISqlIdLookupStrategy proxy = getProxy();
if(null == proxy){
sqlId = method.getDeclaringClass().getName()+"."+method.getName();
}else{
sqlId = proxy.lookup(method);
} Map<String, String> mapping = getMapping();
if(null != sqlId && mapping.containsKey(sqlId)){
return mapping.get(sqlId);
}else{
return sqlId;
}
} // 省略 getter、setter方法
}

这里的实现没有继承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个注解:
 @Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FetchSize { /**
* 每次获取的记录条数
* <p>
* 注意:每次获取的记录条数数值范围为 (0, 5000]
* </p>
* @return 每次读取记录数大小
*/
int value() default -1;
}

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

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

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

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

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

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

  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注解,这也是引入的第三个注解:

 @Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BatchParam { /**
* 是否为批量,只适用于批量类型3
* @return 是否为批量
*/
boolean value() default true; /**
* 批量参数中每一项存入map结构时的名称
* @return 数据项名称
*/
String item() default "item"; /**
* 表示批量参数的属性
* @return 批量参数属性
*/
String property() default "this"; /**
* 当前索引存入map结构时的名称
* @return 索引名称
*/
String index() default "index";
}

需要说明的是,虽然@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注解,这也是引入的第四个注解:

 @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SqlRefs { /**
* @return {@link SqlRef}组,表示要执行的一组sqlId
*/
SqlRef[] value();
}

非常简单,@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注解,这也是引入的第五和第六个注解:

 @Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Execute { /**
* SqlRef引用
* @return SqlRef引用
*/
SqlRef sqlRef(); /**
* 批量参数
* @return 批量参数
*/
BatchParam param(); /**
* 执行的条件
* @return
*/
String condition() default "";
} @Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Executes { Execute[] value();
}

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

 @Executes({
//更新角色基本信息
@Execute(sqlRef=@SqlRef("dUpdateRole"), param=@BatchParam(false)),
//删除角色权限关系
@Execute(sqlRef=@SqlRef("dDeleteRolePermissionByPermTypes"), param=@BatchParam(value=false)),
//重新添加角色权限关系
@Execute(sqlRef=@SqlRef("dInsertRolePermission"), param=@BatchParam(item="perm", property="permissions")),
//如果页面加载过角色的分配角色信息,就先删除角色与分页角色关系,因为不是子批量类型,所以需要显示条件执行的条件
@Execute(sqlRef=@SqlRef("dDeleteRoleRoleAllot"), param=@BatchParam(value=false), condition="roleAllotLoaded"),
//新增角色与分配角色关系,这里不需要添加condition配置,因为会有一个隐式条件,如果是一个sqlId多次执行的子批量,而集合类型的参数为空,则不实际执行
@Execute(sqlRef=@SqlRef("dInsertRoleRoleAllot"), param=@BatchParam(item="allot", property="roleAllots"))
})
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所使用:

 public class Configuration {

 // ... 省略代码
protected MapperRegistry mapperRegistry = new MapperRegistry(this);
// ... 省略代码
}

因此,我们只需要继承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. ANSI C 所有的转义字符

    \a 响铃符 \b 回退符 \f 换页符 \n 换行符 \r 回车符 \t 横向制表符 \v 纵向制表符 \\ 反斜杠 \? 问号 \' 单引号 \" 双引号 \000 八进制数 \xhh ...

  2. ofbiz 代码日记

    写代码一定要尽善尽美.. //修改方法 //条件查询 用于修改 List<GenericValue> stoList = delegator.findByAnd("YcrossS ...

  3. EasyUI需注意的问题01

    一.EasyUI-Datagrid分页 在创建数据表格(DataGrid)的时候,通过设置'pagination' 属性为 true,可以在数据表格的底部生成一个分页工具栏. <table id ...

  4. 成功转移安卓手机QQ聊天记录

    废话先不说,直接上干货: 只要把两个地方的数据完整的复制到新手机对应位置就可以了,但过程相当坎坷: /data/data/com.tencent.mobileqq /sdcard/Tencent/Mo ...

  5. 在VS2010配置MPI--win7下64位系统

    配置MPI经历了不少波折,把这些经历记录下来,告诫后来人. 1.版本要对 下载MPI,去官方网站 http://www.mpich.org/downloads/ 选择x86-64版本 2.步骤要对 1 ...

  6. 各廠商ERP系統架構圖連結 (ERP流程圖)(轉)

    各廠商ERP系統架構圖連結 (ERP流程圖)   資料來源 Google圖片搜尋ERP整理而來 資通電腦 ArgoERP 資通電腦 Oracle ERP 鼎新電腦 Workflow ERP鼎新電腦 S ...

  7. 决策树 -- ID3算法小结

          ID3算法(Iterative Dichotomiser 3 迭代二叉树3代),是一个由Ross Quinlan发明的用于决策树的算法:简单理论是越是小型的决策树越优于大的决策树. 算法归 ...

  8. gulp实用插件总结

    gulp-sass:预编译sass; gulp-imagemin:压缩png.jpj.git.svg格式图片 gulp-minfy-css:压缩css文件 gulp-rename 重命名文件,把一个文 ...

  9. 一个demo让你彻底理解Android触摸事件的并发

    注:本文涉及的demo的地址:https://github.com/absfree/TouchDispatch 1. 触摸动作及事件序列 (1)触摸事件的动作 触摸动作一共有三种:ACTION_DOW ...

  10. 【整理】--linux指令

    1.压缩 解压 .tar 解包:tar xvf FileName.tar打包:tar cvf FileName.tar DirName(注:tar是打包,不是压缩!)———————————————.g ...