浅谈MyBatis通用Mapper实现原理
本文会先介绍通用Mapper的简单原理,然后使用最简单的代码来实现这个过程。
基本原理
通用Mapper提供了一些通用的方法,这些通用方法是以接口的形式提供的,例如。
publicinterfaceSelectMapper{ /** *根据实体中的属性值进行查询,查询条件使用等号 */ @SelectProvider(type=BaseSelectProvider.class,method="dynamicSQL") List select(Trecord); }
接口和方法都使用了泛型,使用该通用方法的接口需要指定泛型的类型。通过Java反射可以很容易得到接口泛型的类型信息,代码如下。
Type[]types=mapperClass.getGenericInterfaces(); Class>entityClass=null; for(Typetype:types){ if(typeinstanceofParameterizedType){ ParameterizedTypet=(ParameterizedType)type; //判断父接口是否为SelectMapper.class if(t.getRawType()==SelectMapper.class){ //得到泛型类型 entityClass=(Class>)t.getActualTypeArguments()[0]; break; } } }
实体类中添加的JPA注解只是一种映射实体和数据库表关系的手段,通过一些默认规则或者自定义注解也很容易设置这种关系,获取实体和表的对应关系后,就可以根据通用接口方法定义的功能来生成和XML中一样的SQL代码。动态生成XML样式代码的方式有很多,最简单的方式就是纯Java代码拼字符串,通用Mapper为了尽可能的少的依赖选择了这种方式。如果使用模板(如FreeMarker,Velocity和beetl等模板引擎)实现,自由度会更高,也能方便开发人员调整。
在MyBatis中,每一个方法(注解或XML方式)经过处理后,最终会构造成MappedStatement实例,这个对象包含了方法id(namespace+id)、结果映射、缓存配置、SqlSource等信息,和SQL关系最紧密的是其中的SqlSource,MyBatis最终执行的SQL时就是通过这个接口的getBoundSql方法获取的。
在MyBatis中,使用@SelectProvider这种方式定义的方法,最终会构造成ProviderSqlSource,ProviderSqlSource是一种处于中间的SqlSource,它本身不能作为最终执行时使用的SqlSource,但是他会根据指定方法返回的SQL去构造一个可用于最后执行的StaticSqlSource,StaticSqlSource的特点就是静态SQL,支持在SQL中使用#{param}方式的参数,但是不支持
为了能根据实体类动态生成支持动态SQL的方法,通用Mapper从这里入手,利用ProviderSqlSource可以生成正常的MappedStatement,可以直接利用MyBatis各种配置和命名空间的特点(这是通用Mapper选择这种方式的主要原因)。在生成MappedStatement后,“过河拆桥”般的利用完就把ProviderSqlSource替换掉了,正常情况下,ProviderSqlSource根本就没有执行的机会。在通用Mapper定义的实现方法中,提供了MappedStatement作为参数,有了这个参数,我们就可以根据ms的id(规范情况下是接口名.方法名)得到接口,通过接口的泛型可以获取实体类(entityClass),根据实体和表的关系我们可以拼出XML方式的动态SQL,一个简单的方法如下。
/** *查询全部结果 * *@paramms *@return */ publicStringselectAll(MappedStatementms){ finalClass>entityClass=getEntityClass(ms); //修改返回值类型为实体类型 setResultType(ms,entityClass); StringBuildersql=newStringBuilder(); sql.append(SqlHelper.selectAllColumns(entityClass)); sql.append(SqlHelper.fromTable(entityClass,tableName(entityClass))); sql.append(SqlHelper.orderByDefault(entityClass)); returnsql.toString(); }
拼出的XML形式的动态SQL,使用MyBatis的XMLLanguageDriver中的createSqlSource方法可以生成SqlSource。然后使用反射用新的SqlSource替换ProviderSqlSource即可,如下代码。
/** *重新设置SqlSource * *@paramms *@paramsqlSource */ protectedvoidsetSqlSource(MappedStatementms,SqlSourcesqlSource){ MetaObjectmsObject=SystemMetaObject.forObject(ms); msObject.setValue("sqlSource",sqlSource); }
MetaObject是MyBatis中很有用的工具类,MyBatis的结果映射就是靠这种方式实现的。反射信息使用的DefaultReflectorFactory,这个类会缓存反射信息,因此MyBatis的结果映射的效率很高。
到这里核心的内容都已经说完了,虽然知道怎么去替换SqlSource了,但是!什么时候去替换呢?
这一直都是一个难题,如果不大量重写MyBatis的代码很难万无一失的完成这个任务。通用Mapper并没有去大量重写,主要是考虑到以后的升级,也因此在某些特殊情况下,通用Mapper的方法会在没有被替换的情况下被调用,这个问题在将来的MyBatis3.5.x版本中会以更友好的方式解决(目前的ProviderSqlSource已经比以前能实现更多的东西,后面会讲)。
针对不同的运行环境,需要用不同的方式去替换。当使用纯MyBatis(没有Spring)方式运行时,替换很简单,因为会在系统中初始化SqlSessionFactory,可以初始化的时候进行替换,这个时候也不会出现前面提到的问题。替换的方式也很简单,通过SqlSessionFactory可以得到SqlSession,然后就能得到Configuration,通过configuration.getMappedStatements()就能得到所有的MappedStatement,循环判断其中的方法是否为通用接口提供的方法,如果是就按照前面的方式替换就可以了。
在使用Spring的情况下,以继承的方式重写了MapperScannerConfigurer和MapperFactoryBean,在Spring调用checkDaoConfig的时候对SqlSource进行替换。在使用SpringBoot时,提供的mapper-starter中,直接注入List
下面我们按照这个思路,以最简练的代码,实现一个通用方法。
实现一个简单的通用Mapper
1.定义通用接口方法
publicinterfaceBaseMapper{ @SelectProvider(type=SelectMethodProvider.class,method="select") List select(Tentity); }
这里定义了一个简单的select方法,这个方法判断参数中的属性是否为空,不为空的字段会作为查询条件进行查询,下面是对应的Provider。
publicclassSelectMethodProvider{ publicStringselect(Objectparams){ return"什么都不是!"; } }
这里的Provider不会最终执行,只是为了在初始化时可以生成对应的MappedStatement。
2.替换SqlSource
下面代码为了简单,都指定的BaseMapper接口,并且没有特别的校验。
publicclassSimpleMapperHelper{ publicstaticfinalXMLLanguageDriverXML_LANGUAGE_DRIVER =newXMLLanguageDriver(); /** *获取泛型类型 */ publicstaticClassgetEntityClass(Class>mapperClass){ Type[]types=mapperClass.getGenericInterfaces(); Class>entityClass=null; for(Typetype:types){ if(typeinstanceofParameterizedType){ ParameterizedTypet=(ParameterizedType)type; //判断父接口是否为BaseMapper.class if(t.getRawType()==BaseMapper.class){ //得到泛型类型 entityClass=(Class>)t.getActualTypeArguments()[0]; break; } } } returnentityClass; } /** *替换SqlSource */ publicstaticvoidchangeMs(MappedStatementms)throwsException{ StringmsId=ms.getId(); //标准msId为包名.接口名.方法名 intlastIndex=msId.lastIndexOf("."); StringmethodName=msId.substring(lastIndex+1); StringinterfaceName=msId.substring(0,lastIndex); Class>mapperClass=Class.forName(interfaceName); //判断是否继承了通用接口 if(BaseMapper.class.isAssignableFrom(mapperClass)){ //判断当前方法是否为通用select方法 if(methodName.equals("select")){ ClassentityClass=getEntityClass(mapperClass); //必须使用"); //解析sqlSource SqlSourcesqlSource=XML_LANGUAGE_DRIVER.createSqlSource( ms.getConfiguration(),sqlBuilder.toString(),entityClass); //替换 MetaObjectmsObject=SystemMetaObject.forObject(ms); msObject.setValue("sqlSource",sqlSource); } } } }
changeMs方法简单的从msId开始,获取接口和实体信息,通过反射回去字段信息,使用