小伙伴们元宵节快乐,记得吃元宵哦~
在日常开发中,小伙伴们多多少少都有用过 MyBatis 插件,松哥猜测大家用的最多的就是 MyBatis 的分页插件!不知道小伙伴们有没有想过有一天自己也来开发一个 MyBatis 插件?
其实自己动手撸一个 MyBatis 插件并不难,今天松哥就把手带大家撸一个 MyBatis 插件!
1.MyBatis 插件接口
即使你没开发过 MyBatis 插件,估计也能猜出来,MyBatis 插件是通过拦截器来起作用的,MyBatis 框架在设计的时候,就已经为插件的开发预留了相关接口,如下:
- public interface Interceptor {
- Object intercept(Invocation invocation) throws Throwable;
- default Object plugin(Object target) {
- return Plugin.wrap(target, this);
- }
- default void setProperties(Properties properties) {
- // NOP
- }
- }
这个接口中就三个方法,第一个方法必须实现,后面两个方法都是可选的。三个方法作用分别如下:
2.MyBatis 拦截器签名
拦截器定义好了后,拦截谁?
这个就需要拦截器签名来完成了!
拦截器签名是一个名为 @Intercepts 的注解,该注解中可以通过 @Signature 配置多个签名。@Signature 注解中则包含三个属性:
一个简单的签名可能像下面这样:
- @Intercepts(@Signature(
- type = ResultSetHandler.class,
- method = "handleResultSets",
- args = {Statement.class}
- ))
- public class CamelInterceptor implements Interceptor {
- //...
- }
3.被拦截的对象
根据前面的介绍,被拦截的对象主要有如下四个:
Executor
- public interface Executor {
- ResultHandler NO_RESULT_HANDLER = null;
- int update(MappedStatement ms, Object parameter) throws SQLException;
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; - List
flushStatements() throws SQLException; - void commit(boolean required) throws SQLException;
- void rollback(boolean required) throws SQLException;
- CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
- boolean isCached(MappedStatement ms, CacheKey key);
- void clearLocalCache();
- void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class> targetType);
- Transaction getTransaction();
- void close(boolean forceRollback);
- boolean isClosed();
- void setExecutorWrapper(Executor executor);
- }
各方法含义分别如下:
ParameterHandler
- public interface ParameterHandler {
- Object getParameterObject();
- void setParameters(PreparedStatement ps) throws SQLException;
- }
各方法含义分别如下:
ResultSetHandler
- public interface ResultSetHandler {
List handleResultSets(Statement stmt) throws SQLException; Cursor handleCursorResultSets(Statement stmt) throws SQLException; - void handleOutputParameters(CallableStatement cs) throws SQLException;
- }
各方法含义分别如下:
StatementHandler
- public interface StatementHandler {
- Statement prepare(Connection connection, Integer transactionTimeout)
- throws SQLException;
- void parameterize(Statement statement)
- throws SQLException;
- void batch(Statement statement)
- throws SQLException;
- int update(Statement statement)
- throws SQLException;
List query(Statement statement, ResultHandler resultHandler) - throws SQLException;
Cursor queryCursor(Statement statement) - throws SQLException;
- BoundSql getBoundSql();
- ParameterHandler getParameterHandler();
- }
各方法含义分别如下:
在开发一个具体的插件时,我们应当根据自己的需求来决定到底拦截哪个方法。
4.开发分页插件
4.1 内存分页
MyBatis 中提供了一个不太好用的内存分页功能,就是一次性把所有数据都查询出来,然后在内存中进行分页处理,这种分页方式效率很低,基本上没啥用,但是如果我们想要自定义分页插件,就需要对这种分页方式有一个简单了解。
内存分页的使用方式如下,首先在 Mapper 中添加 RowBounds 参数,如下:
- public interface UserMapper {
- List
getAllUsersByPage(RowBounds rowBounds); - }
然后在 XML 文件中定义相关 SQL:
- select * from user
可以看到,在 SQL 定义时,压根不用管分页的事情,MyBatis 会查询到所有的数据,然后在内存中进行分页处理。
Mapper 中方法的调用方式如下:
- @Test
- public void test3() {
- UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
- RowBounds rowBounds = new RowBounds(1,2);
- List
list = userMapper.getAllUsersByPage(rowBounds); - for (User user : list) {
- System.out.println("user = " + user);
- }
- }
构建 RowBounds 时传入两个参数,分别是 offset 和 limit,对应分页 SQL 中的两个参数。也可以通过 RowBounds.DEFAULT 的方式构建一个 RowBounds 实例,这种方式构建出来的 RowBounds 实例,offset 为 0,limit 则为 Integer.MAX_VALUE,也就相当于不分页。
这就是 MyBatis 中提供的一个很不实用的内存分页功能。
了解了 MyBatis 自带的内存分页之后,接下来我们就可以来看看如何自定义分页插件了。
4.2 自定义分页插件
首先要声明一下,这里松哥带大家自定义 MyBatis 分页插件,主要是想通过这个东西让小伙伴们了解自定义 MyBatis 插件的一些条条框框,了解整个自定义插件的流程,分页插件并不是我们的目的,自定义分页插件只是为了让大家的学习过程变得有趣一些而已。
接下来我们就来开启自定义分页插件之旅。
首先我们需要自定义一个 RowBounds,因为 MyBatis 原生的 RowBounds 是内存分页,并且没有办法获取到总记录数(一般分页查询的时候我们还需要获取到总记录数),所以我们自定义 PageRowBounds,对原生的 RowBounds 功能进行增强,如下:
- public class PageRowBounds extends RowBounds {
- private Long total;
- public PageRowBounds(int offset, int limit) {
- super(offset, limit);
- }
- public PageRowBounds() {
- }
- public Long getTotal() {
- return total;
- }
- public void setTotal(Long total) {
- this.total = total;
- }
- }
可以看到,我们自定义的 PageRowBounds 中增加了 total 字段,用来保存查询的总记录数。
接下来我们自定义拦截器 PageInterceptor,如下:
- @Intercepts(@Signature(
- type = Executor.class,
- method = "query",
- args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
- ))
- public class PageInterceptor implements Interceptor {
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- Object[] args = invocation.getArgs();
- MappedStatement ms = (MappedStatement) args[0];
- Object parameterObject = args[1];
- RowBounds rowBounds = (RowBounds) args[2];
- if (rowBounds != RowBounds.DEFAULT) {
- Executor executor = (Executor) invocation.getTarget();
- BoundSql boundSql = ms.getBoundSql(parameterObject);
- Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
- additionalParametersField.setAccessible(true);
- Map
additionalParameters = (Map ) additionalParametersField.get(boundSql); - if (rowBounds instanceof PageRowBounds) {
- MappedStatement countMs = newMappedStatement(ms, Long.class);
- CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql);
- String countSql = "select count(*) from (" + boundSql.getSql() + ") temp";
- BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
- Set
keySet = additionalParameters.keySet(); - for (String key : keySet) {
- countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
- }
- List
- Long count = (Long) countQueryResult.get(0);
- ((PageRowBounds) rowBounds).setTotal(count);
- }
- CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
- pageKey.update("RowBounds");
- String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
- BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
- Set
keySet = additionalParameters.keySet(); - for (String key : keySet) {
- pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
- }
- List list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], pageKey, pageBoundSql);
- return list;
- }
- //不需要分页,直接返回结果
- return invocation.proceed();
- }
- private MappedStatement newMappedStatement(MappedStatement ms, Class
longClass) { - MappedStatement.Builder builder = new MappedStatement.Builder(
- ms.getConfiguration(), ms.getId() + "_count", ms.getSqlSource(), ms.getSqlCommandType()
- );
- ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), longClass, new ArrayList<>(0)).build();
- builder.resource(ms.getResource())
- .fetchSize(ms.getFetchSize())
- .statementType(ms.getStatementType())
- .timeout(ms.getTimeout())
- .parameterMap(ms.getParameterMap())
- .resultSetType(ms.getResultSetType())
- .cache(ms.getCache())
- .flushCacheRequired(ms.isFlushCacheRequired())
- .useCache(ms.isUseCache())
- .resultMaps(Arrays.asList(resultMap));
- if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
- StringBuilder keyProperties = new StringBuilder();
- for (String keyProperty : ms.getKeyProperties()) {
- keyProperties.append(keyProperty).append(",");
- }
- keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
- builder.keyProperty(keyProperties.toString());
- }
- return builder.build();
- }
- }
这是我们今天定义的核心代码,涉及到的知识点松哥来给大家一个一个剖析。
在前面的代码中,我们一共在两个地方重新组织了 SQL,一个是查询总记录数的时候,另一个则是分页的时候,都是通过 boundSql.getSql() 获取到 Mapper.xml 中的 SQL 然后进行改装,有的小伙伴在 Mapper.xml 中写 SQL 的时候不注意,结尾可能加上了 ;,这会导致分页插件重新组装的 SQL 运行出错,这点需要注意。松哥在 GitHub 上看到的其他 MyBatis 分页插件也是一样的,Mapper.xml 中 SQL 结尾不能有 ;。
如此之后,我们的分页插件就算是定义成功了。
5.测试
接下来我们对我们的分页插件进行一个简单测试。
首先我们需要在全局配置中配置分页插件,配置方式如下:
接下来我们在 Mapper 中定义查询接口:
- public interface UserMapper {
- List
getAllUsersByPage(RowBounds rowBounds); - }
接下来定义 UserMapper.xml,如下:
- select * from user
最后我们进行测试:
- @Test
- public void test3() {
- UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
- List
list = userMapper.getAllUsersByPage(new RowBounds(1,2)); - for (User user : list) {
- System.out.println("user = " + user);
- }
- }
这里在查询时,我们使用了 RowBounds 对象,就只会进行分页,而不会统计总记录数。需要注意的时,此时的分页已经不是内存分页,而是物理分页了,这点我们从打印出来的 SQL 中也能看到,如下:
可以看到,查询的时候就已经进行了分页了。
当然,我们也可以使用 PageRowBounds 进行测试,如下:
- @Test
- public void test4() {
- UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
- PageRowBounds pageRowBounds = new PageRowBounds(1, 2);
- List
list = userMapper.getAllUsersByPage(pageRowBounds); - for (User user : list) {
- System.out.println("user = " + user);
- }
- System.out.println("pageRowBounds.getTotal() = " + pageRowBounds.getTotal());
- }
此时通过 pageRowBounds.getTotal() 方法我们就可以获取到总记录数。
6.小结
好啦,今天主要和小伙伴们分享了我们如何自己开发一个 MyBatis 插件,插件功能其实都是次要的,最主要是希望小伙伴们能够理解 MyBatis 的工作流程。
本文转载自微信公众号「江南一点雨」,可以通过以下二维码关注。转载本文请联系江南一点雨公众号。
本文题目:手把手教你开发MyBatis插件
标题URL:http://www.shufengxianlan.com/qtweb/news48/386598.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联