仓库
文章涉及的代码都将统一存放到此仓库,文章涉及的案例代码存放在包:com.hzchendou.blog.demo.example下
代码地址:Gitee
分支:lesson4
涉及案例:Case4
简介
Mybatis是一款优秀ORM框架,开发者只需要关注SQL,处理记录结果映射规则,同时Mybatis提供了许多功能特性帮助开发者编写高性能程序,本文将介绍Mybatis插件机制,实现分页查询功能。
插件
Mybatis提供了插件机制,可以让开发者更加灵活地使用Mybatis框架。插件接口Interceptor定义如下:
public interface Interceptor {
插件方法入口,在执行目标方法之前先调用intercept方法
Object intercept(Invocation invocation) throws Throwable;
对目标对象进行包装,底层使用了JDK的Proxy方法创建代理对象,实现方法拦截
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/// 可以给插件对象设置属性,方便在连接方法中使用
default void setProperties(Properties properties) {
// NOP
}
}
关于插件,我们需要知道Mybatis为我们提供了哪些扩展点,以及插件实现方法拦截的原理是什么。
插件扩展点
插件需要在Myabtis配置文件中进行配置,然后才能被Mybatis框架识别,如下所示:
<configuration>
<!-- 省略无关配置 -->
<!-- 插件配置 -->
<plugins>
<plugin interceptor="com.hzchendou.blog.demo.interceptor.xxxxxxxx"/>
</plugins>
<!-- 省略无关配置 -->
</configuration>
配置文件中的插件将统一注册到Configuration配置对象中的interceptorChain属性
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
/// 使用插件对目标进行包装,实现方法拦截
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
/// 注册插件
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
/// 获取所有插件
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
然后会在合适的时机调用pluginAll方法对目标对象进行代理,实现方法调用拦截,通过查看原代码,我们发现Interceptor会对四类组件实施代理操作:
- ParameterHandler,对输入参数进行处理,包含两个方法
- getParameterObject,获取参数对象
- setParameters,将参数配置到预处理器中,构建完整的SQL语句
- ResultSetHandler,对返回结果进行处理,将数据库记录转化为Java中的对象
- handleResultSets,处理SQL结果,以List方式返回查询结果
- handleCursorResultSets,处理SQL结果,以Cursor(游标)方式返回查询结果
- handleOutputParameters,处理存储过程结果,不返回数据
- StatementHandler,处理SQL语句,可以对输入参数进行处理, 实现参数化操作,所有的操作最后都是委托StatementHandler执行,底层调用JDBC的Statement对象
- prepare,预处理,获取Statement对象
- parameterize,参数化处理
- batch,执行批处理
- update 更新操作
- query 查询操作
- queryCursor 查询操作,返回游标
- getBoundSql 获取SQL语句操作对象
- getParameterHandler,获取参数处理器
- Executor,主要涉及数据库操作,增删改查,以及事务操作,提交回滚等,设计内容较多不便写,有兴趣可以查看代码
上述所有操作都能够使用Mybatis的插件机制进行拦截,每一个插件可以拦截一个或者多个方法,按照编程中的单一职责原则,建议一个拦截器实现一种方法,方便维护,下面我们来讨论一下拦截是如何实现的
插件方法拦截原理
我们知道Java提供了动态代理以及反射机制,Mybatis的插件就是用这两个技术实现的,首先我们定义一个插件:
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,
Integer.class})})
public class LogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("拦截StatementHandler.prepare方法, {}", invocation);
return invocation.proceed();
}
}
可以看出这个插件主要实现日志打印功能,在执行目标方法前打印日志,我们实现了Interceptor接口,同时使用了注解@Intercepts
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
/**
* Returns method signatures to intercept.
*
* @return method signatures
*/
Signature[] value();
}
注解只有一个属性,定义如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
///目标类型
Class<?> type();
方法名
String method();
///输入参数类型
Class<?>[] args();
}
Signature包含三个属性,目标类型、方法名称、方法输入参数,通过这三个属性可以唯一确定一个方法,这三个属性的组合可以称为方法签名,通过方法签名我们可以知道需要拦截哪个方法,Mybatis调用Inteceptor中的plugin方法生成代理对象:
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
方法中调用了Plugin中的wrap方法,Plugin实现了InvocationHandler接口,我们首先来看一下wrap方法
public static Object wrap(Object target, Interceptor interceptor) {
/// 解析@Intercepts注解信息,生成需要拦截方法集合
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
/// 查找目标类中的哪些接口需要被代理,实现方法拦截
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
使用JDK中的Proxy生成代理对象
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
生成的代理对象会调用Plugin的invoke方法进行拦截
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
/// 判断当前方法是否在插件配置的拦截方法列表中
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
/// 存在执行插件intercept方法进行拦截
return interceptor.intercept(new Invocation(target, method, args));
}
/// 不存在直接执行目标方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
至此我们揭秘了插件运行原理
分页插件
分页查询是很常见的数据库查询操作,我们可以使用Mybatis的插件机制完成分页SQL查询,分页操作主要包含两个SQL语句:
- 查询分页数据记录
- 查询总记录数
我们可以使用Mybatis插件机制在查询分页数据的时候自动完成数据记录数查询操作,拦截StatementHandler.prepare方法,在此方法中查询数据记录数
public Object intercept(Invocation invocation) throws Throwable {
log.info("拦截StatementHandler.prepare方法, {}", invocation);
///得到代理目标对象
StatementHandler statementHandler = realTarget(invocation.getTarget());
///MetaObject主要用于设置或者是获取对象属性
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject
.getValue("delegate.mappedStatement");
// 1、先判断是不是SELECT操作 (跳过存储过程和非Select操作)
if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
|| StatementType.CALLABLE == mappedStatement.getStatementType()) {
return invocation.proceed();
}
statementHandler = metaObject.hasGetter("delegate") ? (StatementHandler) metaObject
.getValue("delegate") : statementHandler;
if (!(statementHandler instanceof CallableStatementHandler)) {
BoundSql boundSql = (BoundSql) metaObject.getValue(DELEGATE_BOUNDSQL);
Object parameterObejct = boundSql.getParameterObject();
String sql = boundSql.getSql();
/// 1、查看是否有分页参数
if (parameterObejct != null && parameterObejct instanceof PageVO) {
/// 2、在原先SQL语句中加入limit分页参数
PageVO param = (PageVO) parameterObejct;
int offset = (param.getPage() - 1) * param.getSize();
int size = param.getSize();
sql = sql + " limit " + offset + "," + size;
/// 3、将带分页参数的SQL语句替换原先SQL语句
metaObject.setValue(DELEGATE_BOUNDSQL_SQL, sql);
log.info("完成分页SQL配置, {}", sql);
4、组装记录数查询SQL,5、将总记录数保存到PageVO对象中
Connection connection = (Connection)invocation.getArgs()[0];
countTotal(param, parameterObejct, statementHandler, connection, mappedStatement);
log.info("完成分页总记录数查询, {}", param);
}
}
return invocation.proceed();
}
实现思路如下:
- 1、当是Select操作,并且输入参数为PageVO类型时认为是分页查询,否则不查询记录数
- 2、同时对原先查询操作拼接limit 分页参数
- 3、将拼接后带有分页参数的SQL语句替换原先查询语句,然后执行原先查询操作
- 4、查询记录数SQL通过拼接完成
- 将原先的SQL进行切分,保留from关键字之后的内容
- 在分割后的SQL语句前加入“select count(1) ”语句,拼接成完整的查询记录数SQL
- 5、然后执行查询操作,将查询后的记录数保存到输入参数PageVO中
运行
在博客系统中分页查询文章列表记录,对应Mapper接口:
/**
* 分页查询
*/
List<BlogDO> selectPage(PageVO param);
对应的XML中的SQL语句:
<select id="selectPage" resultMap="blogDO">
SELECT <include refid="blog_columns"/> FROM blog
</select>
分页查询结果需要在业务层进行组装,代码如下
public void run() throws IOException {
/// 使用Java 7提供的 try-with-resource写法,让JVM自动完成资源关闭操作
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
获取Mapper接口示例(底层使用了JDK的Proxy.newProxyInstance方法创建代理)
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
PageVO<BlogDO> result = selectPage(blogMapper, 1, 2);
log.info("分页查询博客文章列表信息, {}", result);
}
}
private PageVO<BlogDO> selectPage(BlogMapper blogMapper, int page, int size) {
PageVO<BlogDO> param = new PageVO();
param.setPage(page);
param.setSize(size);
/// 执行查询操作,并将记录数保存到PageVO对象中
List<BlogDO> blogs = blogMapper.selectPage(param);
param.setRecords(blogs);
param.setPageSize(blogs == null ? 0 : blogs.size());
return param;
}
运行结果如下:
1.进入到分页插件方法
22:54:48.351 [main] INFO com.hzchendou.blog.demo.interceptor.PaginationInterceptor - 拦截StatementHandler.prepare方法, org.apache.ibatis.plugin.Invocation@16f7c8c1
完成带分页参数的SQL语句组装
22:54:48.354 [main] INFO com.hzchendou.blog.demo.interceptor.PaginationInterceptor - 完成分页SQL配置, SELECT
id, `title`, `author_id`, `tags`, `status`
FROM blog limit 0,2
22:54:48.354 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - ==> Preparing: select count(1) from blog limit 0,2
22:54:48.368 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - ==> Parameters:
完成记录数查询,总记录数为4
22:54:48.377 [main] INFO com.hzchendou.blog.demo.interceptor.PaginationInterceptor - 完成分页总记录数查询, PageVO(page=1, size=2, total=4, pageSize=null, records=null)
22:54:48.377 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - ==> Preparing: SELECT id, `title`, `author_id`, `tags`, `status` FROM blog limit 0,2
22:54:48.378 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - ==> Parameters:
22:54:48.380 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - <== Total: 2
/// 可以看出分页查询成功执行,总记录数:4,分页查询到2条记录
22:54:48.382 [main] INFO com.hzchendou.blog.demo.example.Case4 - 分页查询博客文章列表信息, PageVO(page=1, size=2, total=4, pageSize=2, records=[BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)])
22:54:48.390 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [org.sqlite.SQLiteConnection@4d1b0d2a]
22:54:48.390 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [org.sqlite.SQLiteConnection@4d1b0d2a]
22:54:48.390 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 1293618474 to pool.
总结
mybtais提供了插件机制,开发者可以使用插件实现功能扩展
- 插件机制针对的是4种组件,可以拦截4种组件中定义的方法,实现功能扩展
- ParameterHandler,参数处理器
- ResultSetHandler,结果集处理器
- StatementHandler,SQL语句执行器
- Executor,执行器,包括SQL执行、事务管理
- 插件实现的基础是代理和反射技术
- 插件实例需要实现Interceptor接口
- 需要使用注解声明拦截的方法
- 在代理类Plugin中实现拦截调用逻辑
- 在目标方法前执行Interceptor中的intercept方法
联系方式
技术更新换代速度很快,我们无法在有限时间掌握全部知识,但我们可以在他人的基础上进行快速学习,学习也是枯燥无味的,加入我们学习牛人经验:
QQ:901856121
点击:加群讨论