MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能。需要注 意的是,如果没有完全理解MyBatis 的运行原理和插件的工作方式,最好不要使用插件, 因为它会改变系底层的工作逻辑,给系统带来很大的影响。
MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心 对象的行为,比如处理参数,处理SQL,处理结果。
Mybatis插件典型适用场景
实现思考:
1
2 /***
3 * @Author 徐庶 QQ:1092002729
4 * @Slogan 致敬大师,致敬未来的你
5 *
6 * 自定义分页插件实现的简易版分页插件
7 */
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()方法处理。
1 <dependency>
2 <groupId>com.github.pagehelper</groupId>
3 <artifactId>pagehelper</artifactId>
4 <version>1.2.15</version>
5 </dependency>
1 <configuration>
2
3 <plugins>
4 <!‐‐ com.github.pagehelper为PageHelper类所在包名 ‐‐>
5 <plugin interceptor="com.github.pagehelper.PageHelper">
6 <property name="helperDialect" value="mysql" />
7 <!‐‐ 该参数默认为false ‐‐>
8 <!‐‐ 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 ‐‐>
9 <!‐‐ 和startPage中的pageNum效果一样 ‐‐>
10 <property name="offsetAsPageNum" value="true" />
11 <!‐‐ 该参数默认为false ‐‐>
12 <!‐‐ 设置为true时,使用RowBounds分页会进行count查询 ‐‐>13 <property name="rowBoundsWithCount" value="true" />
14 <!‐‐ 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结
果 ‐‐>
15 <!‐‐ (相当于没有执行分页查询,但是返回结果仍然是Page类型) ‐‐>
16 <property name="pageSizeZero" value="true" />
17 <!‐‐ 3.3.0版本可用 ‐ 分页参数合理化,默认false禁用 ‐‐>
18 <!‐‐ 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一
页 ‐‐>
19 <!‐‐ 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 ‐‐>
20 <property name="reasonable" value="true" />
21 <!‐‐ 3.5.0版本可用 ‐ 为了支持startPage(Object params)方法 ‐‐>
22 <!‐‐ 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值
‐‐>
23 <!‐‐ 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的
用默认值 ‐‐>
24 <!‐‐ 不理解该含义的前提下,不要随便复制该配置 ‐‐>
25 <property name="params" value="pageNum=start;pageSize=limit;" />
26 </plugin>
27 </plugins>
28 </configuration>
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<Emp> list=mapper.selectAll();
10 PageInfo<ServiceStation> info = new PageInfo<ServiceStation>(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);返回代理对象。
当个插件的情况下,代理能不能被代理?代理顺序和调用顺序的关系? 可以被代理。
先来看一下分页插件的简单用法:
1 PageHelper.startPage(1, 3);
2 List<Blog> 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 <T> Page<T> getLocalPage() {
2 return PageHelper.getLocalPage();
3 }
4 //线程独享5 protected static final ThreadLocal LOCAL_PAGE = new ThreadLocal();
6 public static <T> Page<T> getLocalPage() {
7 return (Page)LOCAL_PAGE.get();
8 }
可以发现这里是调用的是PageHelper的一个本地线程变量中的一个 Page对象,从其中获取 我们所设置的 PageSize 与 PageNum,那么他是怎么设置值的呢?请看:
1
2 PageHelper.startPage(1, 3);
3
4 public static <E> Page<E> startPage(int pageNum, int pageSize) {
5 return startPage(pageNum, pageSize, true);
6 }
7
8 public static <E> Page<E> startPage(int pageNum, int pageSize, boolean co
unt, Boolean reasonable, Boolean pageSizeZero) {
9 Page<E> page = new Page(pageNum, pageSize, count);
10 page.setReasonable(reasonable);
11 page.setPageSizeZero(pageSizeZero);
12 Page<E> 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学习资料,关注我,免费领取