栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Mybatis笔记-07【MyBatis逆向工程&分页插件】

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Mybatis笔记-07【MyBatis逆向工程&分页插件】

1、分页插件

MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能。需要注 意的是,如果没有完全理解MyBatis 的运行原理和插件的工作方式,最好不要使用插件, 因为它会改变系底层的工作逻辑,给系统带来很大的影响。
MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心 对象的行为,比如处理参数,处理SQL,处理结果。

Mybatis插件典型适用场景

  1. 分页功能
    mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况 下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截 StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即 可;
  2. 公共字段统一赋值
    一般业务系统都会有创建者,创建时间,修改者,修改时间四个字段,对于这四 个字段的赋值,实际上可以在DAO层统一拦截处理,可以用mybatis插件拦截 Executor类的update方法,对相关参数进行统一赋值即可;
  3. 性能监控
    对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方 法,用日志记录每个方法执行的时间;
  4. 其它
    其实mybatis扩展性还是很强的,基于插件机制,基本上可以控制SQL执行的各 个阶段,如执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可
    以根据项目业务来实现对应业务逻辑。

实现思考:

  1. 第一个问题:
    不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事 情,在原来的方法后面做一点事情?
    答案:大家很容易能想到用代理模式,这个也确实是MyBatis 插件的原理。
  2. 第二个问题:
    我们可以定义很多的插件,那么这种所有的插件会形成一个链路,比如我们提交一个休 假申请,先是项目经理审批,然后是部门经理审批,再是HR 审批,再到总经理审批,怎么 实现层层的拦截?
    答案:插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。
    在之前的源码中我们也发现了,mybatis内部对于插件的处理确实使用的代理模式,既 然是代理模式,我们应该了解MyBatis 允许哪些对象的哪些方法允许被拦截,并不是每一 个运行的节点都是可以被修改的。只有清楚了这些对象的方法的作用,当我们自己编写插件 的时候才知道从哪里去拦截。在MyBatis 官网有答案,我们来看一下:
    https://mybatis.org/mybatis-3/zh/configuration.html#plugins

    Executor 会拦截到CachingExcecutor 或者BaseExecutor。因为创建Executor 时是先创 建CachingExcecutor,再包装拦截。从代码顺序上能看到。我们可以通过mybatis的分页插件来 看看整个插件从包装拦截器链到执行拦截器链的过程。
    在查看插件原理的前提上,我们需要来看看官网对于自定义插件是怎么来做的,官网上有介 绍:通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指 定想要拦截的方法签名即可。这里本人踩了一个坑,在Springboot中集成,同时引入了 pagehelper­spring­boot­starter 导致RowBounds参数的值被刷掉了,也就是走到了我的拦截
    其中没有被设置值,这里需要注意,拦截器出了问题,可以Debug看一下Configuration配置类中 拦截器链的包装情况。
1.自定义分页插件
1
2  
8 @Intercepts({ 
9 @Signature(type = Executor.class,method = "query" ,args ={MappedStatemen 
t.class, Object.class, RowBounds.class, ResultHandler.class} ), // 需要代理 
的对象和方法 
10 @Signature(type = Executor.class,method = "query" ,args ={MappedStateme 
nt.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.clas 
s, BoundSql.class} ) // 需要代理的对象和方法 
11 }) 
12 public class MyPageInterceptor implements Interceptor { 
13
14 @Override 
15 public Object intercept(Invocation invocation) throws Throwable { 
16 System.out.println("简易版的分页插件:逻辑分页改成物理分页"); 
17
18 // 修改sql 拼接Limit 0,10 
19 Object[] args = invocation.getArgs(); 
20 // MappedStatement 对mapper映射文件里面元素的封装 
21 MappedStatement ms= (MappedStatement) args[0]; 
22 // BoundSql 对sql和参数的封装 
23 Object parameterObject=args[1]; 
24 BoundSql boundSql = ms.getBoundSql(parameterObject); 
25 // RowBounds 封装了逻辑分页的参数 :当前页offset,一页数limit 
26 RowBounds rowBounds= (RowBounds) args[2]; 
27
28 // 拿到原来的sql语句 
29 String sql = boundSql.getSql(); 
30 String limitSql=sql+ " limit "+rowBounds.getOffset()+","+ rowBounds.get 
Limit(); 
31
32 //将分页sql重新封装一个BoundSql 进行后续执行33 BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), limitSql, b 
oundSql.getParameterMappings(), parameterObject); 
34
35 // 被代理的对象 
36 Executor executor= (Executor) invocation.getTarget(); 
37 CacheKey cacheKey = executor.createCacheKey(ms, parameterObject, rowBou 
nds, pageBoundSql); 
38 // 调用修改过后的sql继续执行查询 
39 return executor.query(ms,parameterObject,rowBounds, (ResultHandler) arg 
s[3],cacheKey,pageBoundSql); 
40 } 
41 }

拦截签名跟参数的顺序有严格要求,如果按照顺序找不到对应方法会抛出异常:

1 org.apache.ibatis.exceptions.PersistenceException: 
2 ### Error opening session. Cause: org.apache.ibatis.plugin.PluginEx 
ception: 
3 Could not find method on interface org.apache.ibatis.executor.Execu 
tor named query 

MyBatis 启动时扫描 标签, 注册到Configuration 对象的 InterceptorChain 中。property 里面的参数,会调用setProperties()方法处理。

2.分页插件使用
  1. 添加pom依赖:
1  
2 com.github.pagehelper 
3 pagehelper 
4 1.2.15 
5  
  1. 插件注册,在mybatis-config.xml 中注册插件:
1  
2
3  
4  
5  
6  
7  
8  
9  
10  
11  
12 13  
14  
15  
16  
17  
18 pages会查询最后一 
页 ‐‐> 
19 pages会返回空数据 ‐‐> 
20  
21  
22  
23  
24  
25  
26  
27  
28 
  1. 调用
1 // 获取配置文件 
2 InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis‐ 
config.xml"); 
3 // 通过加载配置文件获取SqlSessionFactory对象 
4 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStr 
eam); 
5 try (SqlSession sqlSession = sqlSessionFactory.openSession()) { 
6 // Mybatis在getMapper就会给我们创建jdk动态代理 
7 EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); 
8 PageHelper.startPage(1, 5); 
9 List list=mapper.selectAll(); 
10 PageInfo info = new PageInfo(list, 
3); 
11 System.out.println("当前页码:"+info.getPageNum()); 
12 System.out.println("每页的记录数:"+info.getPageSize()); 
13 System.out.println("总记录数:"+info.getTotal()); 
14 System.out.println("总页码:"+info.getPages()); 
15 System.out.println("是否第一页:"+info.isIsFirstPage()); 
16 System.out.println("连续显示的页码:"); 
17 int[] nums = info.getNavigatepageNums(); 
18 for (int i = 0; i < nums.length; i++) {19 System.out.println(nums[i]); 
20 } 
21 } 
22
23 

代理和拦截是怎么实现的?
上面提到的可以被代理的四大对象都是什么时候被代理的呢?Executor 是openSession() 的时候创建的; StatementHandler 是SimpleExecutor.doQuery()创建的;里面包含了处理参 数的ParameterHandler 和处理结果集的ResultSetHandler 的创建,创建之后即调用 InterceptorChain.pluginAll(),返回层层代理后的对象。代理是由Plugin 类创建。在我们重写的 plugin() 方法里面可以直接调用returnPlugin.wrap(target, this);返回代理对象。
当个插件的情况下,代理能不能被代理?代理顺序和调用顺序的关系? 可以被代理。

3.PageHelper 原理

先来看一下分页插件的简单用法:

1 PageHelper.startPage(1, 3); 
2 List blogs = blogMapper.selectBlogById2(blog); 
3 PageInfo page = new PageInfo(blogs, 3); 

对于插件机制我们上面已经介绍过了,在这里我们自然的会想到其所涉及的核心类 : PageInterceptor。拦截的是Executor 的两个query()方法,要实现分页插件的功能,肯定是要 对我们写的sql进行改写,那么一定是在 intercept 方法中进行操作的,我们会发现这么一行代 码:

1 String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBou nds, cacheKey);

调用到 AbstractHelperDialect 中的 getPageSql 方法:

1 public String getPageSql(MappedStatement ms, BoundSql boundSql, Object pa 
rameterObject, RowBounds rowBounds, CacheKey pageKey) { 
2 // 获取sql 
3 String sql = boundSql.getSql(); 
4 //获取分页参数对象 
5 Page page = this.getLocalPage(); 
6 return this.getPageSql(sql, page, pageKey); 
7 } 

这里可以看到会去调用 this.getLocalPage(),我们来看看这个方法:

1 public  Page getLocalPage() { 
2 return PageHelper.getLocalPage(); 
3 } 
4 //线程独享5 protected static final ThreadLocal LOCAL_PAGE = new ThreadLocal(); 
6 public static  Page getLocalPage() { 
7 return (Page)LOCAL_PAGE.get(); 
8 } 

可以发现这里是调用的是PageHelper的一个本地线程变量中的一个 Page对象,从其中获取 我们所设置的 PageSize 与 PageNum,那么他是怎么设置值的呢?请看:

1
2 PageHelper.startPage(1, 3); 
3
4 public static  Page startPage(int pageNum, int pageSize) { 
5 return startPage(pageNum, pageSize, true); 
6 } 
7
8 public static  Page startPage(int pageNum, int pageSize, boolean co 
unt, Boolean reasonable, Boolean pageSizeZero) { 
9 Page page = new Page(pageNum, pageSize, count); 
10 page.setReasonable(reasonable); 
11 page.setPageSizeZero(pageSizeZero); 
12 Page oldPage = getLocalPage(); 
13 if (oldPage != null && oldPage.isOrderByOnly()) { 
14 page.setOrderBy(oldPage.getOrderBy()); 
15 } 
16 //设置页数,行数信息 
17 setLocalPage(page); 
18 return page; 
19 } 
20
21 protected static void setLocalPage(Page page) { 
22 //设置值 
23 LOCAL_PAGE.set(page); 
24 } 

在我们调用 PageHelper.startPage(1, 3); 的时候,系统会调 用 LOCAL_PAGE.set(page) 进行设置,从而在分页插件中可以获取到这个本地变量对象中的参 数进行 SQL 的改写,由于改写有很多实现,我们这里用的Mysql的实现:

在这里我们会发现分页插件改写SQL的核心代码,这个代码就很清晰了,不必过多赘述:

1
2 public String getPageSql(String sql, Page page, CacheKey pageKey) { 
3 StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); 
4 sqlBuilder.append(sql); 
5 if (page.getStartRow() == 0) { 
6 sqlBuilder.append(" LIMIT "); 
7 sqlBuilder.append(page.getPageSize()); 
8 } else { 
9 sqlBuilder.append(" LIMIT "); 
10 sqlBuilder.append(page.getStartRow()); 
11 sqlBuilder.append(","); 
12 sqlBuilder.append(page.getPageSize()); 
13 pageKey.update(page.getStartRow()); 
14 } 
15
16 pageKey.update(page.getPageSize()); 
17 return sqlBuilder.toString(); 
18 } 

今天就分享到这里了,文章后续及更多java学习资料,关注我,免费领取

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/964436.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号