如何开发自己的通用Mapper

如何开发自己的通用Mapper,第1张

如何开发自己的通用Mapper

HsqldbMapper实例

第一步,创建HsqldbMapper<T>

public interface HsqldbMapper<T>{

}

这个接口就是我们定义的通用Mapper,具体的接口方法在**第三步**写。其他的Mapper可以继承这个HsqldbMapper<T>。

第二部,创建HsqldbProvider

public class HsqldbProvider extends MapperTemplate {

//继承父类的方法

public HsqldbProvider(Class<?>mapperClass, MapperHelper mapperHelper) {

super(mapperClass, mapperHelper)

}

}

这个类是实际处理 *** 作的类,需要继承MapperTemplate,具体代码在**第四步**写。

第三步,在HsqldbMapper<T>中添加通用方法

这里以一个分页查询作为例子。 public interface HsqldbMapper { /** * 单表分页查询 * * @param object * @param offset * @param limit * @return */

@SelectProvider(type=HsqldbProvider.class,method = "dynamicSQL") List

selectPage(@Param("entity") T object, @Param("offset") int offset,

@Param("limit") int limit)}

返回结果为List,入参分别为查询条件和分页参数。在Mapper的接口方法中,当有多个入参的时候建议增加@Param注解,否则就得用param1,param2...来引用参数。

同时必须在方法上添加注解。查询使用SelectProvider,插入使用@InsertProvider,更新使用UpdateProvider,删除使用DeleteProvider。不同的Provider就相当于xml中不同的节点,如<select>,<insert>,<update>,<delete>。

因为这里是查询,所以要设置为SelectProvider,这4个Provider中的参数都一样,只有type和method。

type必须设置为实际执行方法的HasqldbProvider.class,method必须设置为"dynamicSQL"。

通用Mapper处理的时候会根据type反射HasqldbProvider查找方法,而Mybatis的处理机制要求method必须是type类中只有一个入参,且返回值为String的方法。"dynamicSQL"方法定义在MapperTemplate中,该方法如下:

public String dynamicSQL(Object record) {

return "dynamicSQL"

}

这个方法只是为了满足Mybatis的要求,没有任何实际的作用。

第四步,在HsqldbProvider中实现真正处理Sql的方法

在这里有一点要求,那就是HsqldbProvider处理HsqldbMapper<T>中的方法时,方法名必须一样,因为这里需要通过反射来获取对应的方法,方法名一致一方面是为了减少开发人员的配置,另一方面和接口对应看起来更清晰。

除了方法名必须一样外,入参必须是MappedStatement

ms,除此之外返回值可以是void或者SqlNode之一。

这里先讲一下通用Mapper的实现原理。通用Mapper目前是通过拦截器在通用方法第一次执行的时候去修改MappedStatement对象的SqlSource属性。而且只会执行一次,以后就和正常的方法没有任何区别。

使用Provider注解的这个Mapper方法,Mybatis本身会处理成ProviderSqlSource(一个SqlSource的实现类),由于之前的配置,这个ProviderSqlSource种的SQL是上面代码中返回的"dynamicSQL"。这个SQL没有任何作用,如果不做任何修改,执行这个代码肯定会出错。所以在拦截器中拦截符合要求的接口方法,遇到ProviderSqlSource就通过反射调用如HsqldbProvider中的具体代码去修改原有的SqlSource。

最简单的处理Mybatis SQL的方法是什么?就是创建SqlNode,使用DynamicSqlSource,这种情况下我们不需要处理入参,不需要处理代码中的各种类型的参数映射。比执行SQL的方式容易很多。

有关这部分的内容建议查看通用Mapper的源码和Mybatis源码了解,如果不了解在这儿说多了反而会乱。

下面在HsqldbProvider中添加public

SqlNode selectPage(MappedStatement ms)方法:

/**

* 分页查询

* @param ms

* @return

*/

public SqlNode selectPage(MappedStatement ms) {

Class<?>entityClass = getSelectReturnType(ms)

//修改返回值类型为实体类型

setResultType(ms, entityClass)

List<SqlNode>sqlNodes = new ArrayList<SqlNode>()

//静态的sql部分:select column ... from table

sqlNodes.add(new StaticTextSqlNode("SELECT "

+ EntityHelper.getSelectColumns(entityClass)

+ " FROM "

+ tableName(entityClass)))

//获取全部列

List<EntityHelper.EntityColumn>columnList = EntityHelper.getColumns(entityClass)

List<SqlNode>ifNodes = new ArrayList<SqlNode>()

boolean first = true

//对所有列循环,生成<if test="property!=null">[AND] column = #{property}</if>

for (EntityHelper.EntityColumn column : columnList) {

StaticTextSqlNode columnNode

= new StaticTextSqlNode((first ? "" : " AND ") + column.getColumn()

+ " = #{entity." + column.getProperty() + "} ")

if (column.getJavaType().equals(String.class)) {

ifNodes.add(new IfSqlNode(columnNode, "entity."+column.getProperty()

+ " != null and " + "entity."+column.getProperty() + " != '' "))

} else {

ifNodes.add(new IfSqlNode(columnNode, "entity."+column.getProperty() + " != null "))

}

first = false

}

//将if添加到<where>

sqlNodes.add(new WhereSqlNode(ms.getConfiguration(), new MixedSqlNode(ifNodes)))

//处理分页

sqlNodes.add(new IfSqlNode(new StaticTextSqlNode(" LIMIT #{limit}"),"offset==0"))

sqlNodes.add(new IfSqlNode(new StaticTextSqlNode(" LIMIT #{limit} OFFSET #{offset} "),"offset>0"))

return new MixedSqlNode(sqlNodes)

}

注:对这段代码感觉吃力的,可以对比本页最下面**结构**部分XML形式的查看。

首先这段代码要实现的功能是这样,根据传入的实体类参数中不等于null(字符串也不等于'')的属性作为查询条件进行查询,根据分页参数进行分页。

先看这两行代码:

//获取实体类型

Class<?>entityClass = getSelectReturnType(ms)

//修改返回值类型为实体类型

setResultType(ms, entityClass)

首先获取了实体类型,然后通过setResultType将返回值类型改为entityClass,就相当于resultType=entityClass。

这里为什么要修改呢?因为默认返回值是T,Java并不会自动处理成我们的实体类,默认情况下是Object,对于所有的查询来说,我们都需要手动设置返回值类型。

对于insert,update,delete来说,这些 *** 作的返回值都是int,所以不需要修改返回结果类型。

之后从List<SqlNode>

sqlNodes = new ArrayList<SqlNode>()代码开始拼写SQL,首先是SELECT查询头,在EntityHelper.getSelectColumns(entityClass)中还处理了别名的情况。

然后获取所有的列,对列循环创建<if

entity.property!=null>column = #{entity.property}</if>节点。最后把这些if节点组成的List放到一个<where>节点中。

这一段使用属性时用的是 entity.

+ 属性名,entity来自哪儿?来自我们前面接口定义处的Param("entity")注解,后面的两个分页参数也是。如果你用过Mybatis,相信你能明白。

之后在<where>节点后添加分页参数,当offset==0时和offset>0时的分页代码不同。

最后封装成一个MixedSqlNode返回。

返回后通用Mapper是怎么处理的,这里贴下源码:

SqlNode sqlNode = (SqlNode) method.invoke(this, ms)

DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(ms.getConfiguration(), sqlNode)

setSqlSource(ms, dynamicSqlSource)

返回SqlNode后创建了DynamicSqlSource,然后修改了ms原来的SqlSource。

第五步,配置通用Mapper接口到拦截器插件中

<plugins>

<plugin interceptor="com.github.abel533.mapper.MapperInterceptor">

<!--================================================-->

<!--可配置参数说明(一般无需修改)-->

<!--================================================-->

<!--UUID生成策略-->

<!--配置UUID生成策略需要使用OGNL表达式-->

<!--默认值32位长度:@java.util.UUID@randomUUID().toString().replace("-", "")-->

<!--<property name="UUID" value="@java.util.UUID@randomUUID().toString()"/>-->

<!--主键自增回写方法,默认值MYSQL,详细说明请看文档-->

<property name="IDENTITY" value="HSQLDB"/>

<!--序列的获取规则,使用{num}格式化参数,默认值为{0}.nextval,针对Oracle-->

<!--可选参数一共3个,对应0,1,2,分别为SequenceName,ColumnName,PropertyName-->

<property name="seqFormat" value="{0}.nextval"/>

<!--主键自增回写方法执行顺序,默认AFTER,可选值为(BEFORE|AFTER)-->

<!--<property name="ORDER" value="AFTER"/>-->

<!--支持Map类型的实体类,自动将大写下划线的Key转换为驼峰式-->

<!--这个处理使得通用Mapper可以支持Map类型的实体(实体中的字段必须按常规方式定义,否则无法反射获得列)-->

<property name="cameHumpMap" value="true"/>

<!--通用Mapper接口,多个用逗号隔开-->

<property name="mappers" value="com.github.abel533.mapper.Mapper,com.github.abel533.hsqldb.HsqldbMapper"/>

</plugin>

</plugins>

这里主要是**mappers**参数:

<property name="mappers" value="com.github.abel533.mapper.Mapper,com.github.abel533.hsqldb.HsqldbMapper"/>

多个通用Mapper可以用逗号隔开。

测试

接下来编写代码进行测试。

public interface CountryMapper extends Mapper<Country>,HsqldbMapper<Country>{

}

在CountryMapper上增加继承HsqldbMapper<Country>。

编写如下的测试:

@Test

public void testDynamicSelectPage() {

SqlSession sqlSession = MybatisHelper.getSqlSession()

try {

CountryMapper mapper = sqlSession.getMapper(CountryMapper.class)

//带查询条件的分页查询

Country country = new Country()

country.setCountrycode("US")

List<Country>countryList = mapper.selectPage(country, 0, 10)

//查询总数

Assert.assertEquals(1, countryList.size())

//空参数的查询

countryList = mapper.selectPage(new Country(), 100, 10)

Assert.assertEquals(10, countryList.size())

} finally {

sqlSession.close()

}

}

KeyWords: Mybatis 原理,源码,Mybatis Mapper 接口实现类,代理模式,动态代理,Java动态代理,Proxy.newProxyInstance,Mapper 映射,Mapper 实现

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。我们在使用 Mybaits 进行 ,通常只需要定义几个 Mapper 接口,然后在编写一个 xml 文件,我们在配置文件中写好 sql , Mybatis 帮我们完成 Mapper 接口道具体实现的调用。以及将结果映射到 model bean 中。

我们在项目中所编写的众多的 Mapper 类只是一个接口(interface ),根据 Java 的多态性我们知道,可以使用接口接口作为形参,进而在运行时确定具体实现的对象是什么。但是,对于 Mapper 接口,我们并没有编写其实现类!Mybatis是如何找到其实现类,进而完成具体的 CRUD 方法调用的呢?原理何在?

为了弄清楚 Mapper 接口是如何找到实现类的,我们先回忆一下 Mybatis 是怎么使用的,根据实际的例子,进而一点点的去分析。这里的使用指的是Mybatis 单独使用,而不是整合 spring , 因为整合 spring 的话,还需要涉及 Mapper dao 装载到 spring 容器的问题,spring 帮忙创建数据源配置等问题。

通常我们使用 Mybatis 的主要步骤是:

从一段代码看起

上面我们概括了使用 Mybatis 的4个步骤。这4个步骤看起来很简单,但是用代码写出来就很多。我们不妨先记着这4个步骤,再去看代码,会容易点。

在这块代码中,第 1 部分我们使用了 Java 编码的形式来实现 SqlSessionFactory ,也可以使用 xml 。如果使用xml的话,上面的第一部分代码就是这样的:

我们本次的目标是弄清楚 “ Mapper 是如何找到实现类的 ”,我们注意上面代码 3 , 4 的位置:

这里 mapper 可以调用selectBlog(1) 这个方法,说明 mapper 是个对象,因为对象才具有方法行为实现啊。BlogMapper接口是不能实例化的,更没有具体方法实现。我们并没有定义一个类,让它实现BlogMapper接口,而在这里它只是通过调用session.getMapper() 所得到的。由此,我们可以推断:肯定是session.getMapper() 方法内部产生了BlogMapper的实现类。有什么技术可以根据BlogMapper 接口生成了一个实现类呢?想到这里,对于有动态代理 使用经验的程序员来说,很容易想到,这背后肯定是基于动态代理技术,具体怎么实现的呢?下面我们来根据源码一探究竟。

Mapper 接口的注册

从上面的代码中,我们知道 BlogMapper 接口的实现类是从session.getMapper中得来的,大概是基于动态代理技术实现。我们既然能够从SqlSession中得到BlogMapper接口的,那么我们肯定需要先在哪里把它放进去了,然后 SqlSession 才能生成我们想要的代理类啊。上面代码中有这么一行:

跟着这个 addMapper 方法的代码实现是这样的:

我们看到这里 mapper 实际上被添加到 mapperRegissry 中。继续跟进代码:

看到这里我们知道上面所执行的configuration.addMapper(BlogMapper.class)其实最终被放到了HashMap中,其名为knownMappers ,knowMappers是MapperRegistry 类的一个私有属性,它是一个HashMap 。其Key 为当前Class对象,value 为一个MapperProxyFactory 实例。

这里我们总结一下: 诸如BlogMapper 之类的Mapper接口被添加到了MapperRegistry 中的一个HashMap中。并以 Mapper 接口的 Class 对象作为 Key , 以一个携带Mapper接口作为属性的MapperProxyFactory 实例作为value 。MapperProxyFacory从名字来看,好像是一个工厂,用来创建Mapper Proxy的工厂。我们继续往下看。

Mapper接口的动态代理类的生成

上面我们已经知道,Mapper 接口被到注册到了MapperRegistry中——放在其名为knowMappers 的HashMap属性中,我们在调用Mapper接口的方法的时候,是这样的:

这里,我们跟踪一下session.getMapper() 方法的代码实现,这里 SqlSession 是一个接口,他有两个实现类,一个是DefaultSqlSession,另外一个是SqlSessionManager,这里我们用的是DefaultSqlSession. 为什么是DefaultSqlSession呢?因为我们在初始化SqlSessionFactory的时候所调用的SqlSessionFactoryBuilder的build()方法里边配置的就是DefaultSqlSession, 所以,我们进入到DefaultSession类中,看看它对session.getMapper(BlogMapper.class)是怎么实现的:

如代码所示,这里的 getMapper 调用了 configuration.getMapper , 这一步 *** 作其实最终是调用了MapperRegistry,而此前我们已经知道,MapperRegistry是存放了一个HashMap的,我们继续跟踪进去看看,那么这里的get,肯定是从这个hashMap中取数据。我们来看看代码:

我们调用的session.getMapper(BlogMapper.class)最终会到达上面这个方法,这个方法,根据BlogMapper的class对象,以它为key在knowMappers 中找到了对应的value —— MapperProxyFactory(BlogMapper) 对象,然后调用这个对象的newInstance()方法。根据这个名字,我们就能猜到这个方法是创建了一个对象,代码是这样的:

看到这里,就清楚了,最终是通过Proxy.newProxyInstance产生了一个BlogMapper的代理对象。Mybatis 为了完成 Mapper 接口的实现,运用了代理模式。具体是使用了JDK动态代理,这个Proxy.newProxyInstance方法生成代理类的三个要素是:

代理模式中,代理类(MapperProxy)中才真正的完成了方法调用的逻辑。我们贴出MapperProxy的代码,如下:

我们调用的 Blog blog = mapper.selectBlog(1)实际上最后是会调用这个MapperProxy的invoke方法。这段代码中,if 语句先判断,我们想要调用的方法是否来自Object类,这里的意思就是,如果我们调用toString()方法,那么是不需要做代理增强的,直接还调用原来的method.invoke()就行了。只有调用selectBlog()之类的方法的时候,才执行增强的调用——即mapperMethod.execute(sqlSession, args)这一句代码逻辑。

而mapperMethod.execute(sqlSession, args)这句最终就会执行增删改查了,代码如下:

再往下一层,就是执行JDBC那一套了,获取链接,执行,得到ResultSet,解析ResultSet映射成JavaBean。

至此,我们已经摸清楚了Blog blog = mapper.selectBlog(1)中,BlogMapper接口调用到得到数据库数据过程中,Mybaitis 是如何为接口生成实现类的,以及在哪里出发了最终的CRUD调用。实际上,如果我们在调用Blog blog = mapper.selectBlog(1)之前,把从slqSession中得到的 mapper 对象打印出来就会看到,输出大概是这样的:

动态代理没错吧,Java动态代理实在是太美妙了。

上面我们用层层深入的方式摸清楚了 Mapper接口是如何找到实现类的。我们分析了 Mapper接口是如何注册的,Mapper接口是如何产生动态代理对象的,Maper接口方法最终是如何执行的。总结起来主要就是这几个点:


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/bake/11582608.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-17
下一篇 2023-05-17

发表评论

登录后才能评论

评论列表(0条)

保存