springboot集成自定义mybatis分页插件
每天多学一点点~
好久没研究新东西了,最近看了下pagehelper插件,动态拼接sql,决心模仿下写一个简单的~
话不多说,这就开始吧…
1.实现目标
用户在接口中声明Page 对像实现后,由插件实现自动分页。
例如:
- page对象
public class Page implements java.io.Serializable {
private int szie; // 每页大
private int number; // 当前页码
}
... getset省略
- page参数声明
@Select("select * from user")
List<User> selectByPage(String name, Page page);
- 客户端调用
mapper.selectByPage(params, new Page(3, 2))
以上便是大概要实现的目标。
2.实现思路
1.修改 SQL 并添加 limit 语句
2.判断方法参数中是否有Page对象
3.取出Page对象 动态生成limit 语句
4.若既有page对象,又有limit语句,则抛出异常
5.上述操作必须在PreparedStatement 对像生成前完成(下文会说)
3.mybaits的Interceptor拦截器
拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。打个比方,对于Executor,Mybatis中有几种实现:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。这个时候如果你觉得这几种实现对于Executor接口的query方法都不能满足你的要求,那怎么办呢?是要去改源码吗?当然不。我们可以建立一个Mybatis拦截器用于拦截Executor接口的query方法,在拦截之后实现自己的query方法逻辑,之后可以选择是否继续执行原来的query方法。
该接口一共有三个方法,intercept、plugin和setProperties。plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法。setProperties方法是用于在Mybatis配置文件中指定一些属性的。
定义自己的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法。
这里博主在plugin方法中运用动态代理实现动态sql的拼接,直接返回代理类,在代理类中进行逻辑拼接
Mybatis拦截器只能拦截四种类型的接口:Executor、StatementHandler、ParameterHandler和ResultSetHandler。这是在Mybatis的Configuration中写死了的,如果要支持拦截其他接口就需要我们重写Mybatis的Configuration。Mybatis可以对这四个接口中所有的方法进行拦截。
Mybatis拦截器常常会被用来进行分页处理。我们知道要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis在执行Sql语句前也会产生一个包含Sql语句的Statement对象,而且对应的Sql语句是在Statement之前产生的,所以我们就可以在它成Statement之前对用来生成Statement的Sql语句下手。在Mybatis中Statement语句是通过RoutingStatementHandler对象的prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法,即在PreparedStatement对象生成之前完成,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用StatementHandler对象的prepare方法,即调用invocation.proceed()。
4.代码贴图
项目结构图
为了简单起见,博主直接用springboot封装了。(其实是ssm忘的差不多了23333)
- page类
/**
* @Classname Page
* @Description TODO
* @Date 2019/7/12 15:28
* @Created by 爆裂无球
*/
public class Page implements Serializable {
private int size; // 每页大小
private int number; // 当前页码
public Page(int size, int number) {
this.size = size;
this.number = number;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
// 获取开始 页数 因为mysql默认是0开始的
public int getBegin() {
return size * (number - 1);
}
//获取 最后 页数
public int getEnd() {
return size * number;
}
- PagePlugin类,实现mybatis的Interceptor类
/**
* @Classname PagePlugin
* @Description TODO
* @Date 2019/7/12 15:37
* @Created by 爆裂无球
*/
@Component
//@Intercepts({
// @Signature(method = "query", type = Executor.class, args = {
// MappedStatement.class, Object.class, RowBounds.class,
// ResultHandler.class}),
// @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class})})
public class PagePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return null;
}
/**
* 调用动态代理 拼接sql
*/
@Override
public Object plugin(Object o) {
if (o instanceof StatementHandler) {
return Proxy.newProxyInstance(PagePlugin.class.getClassLoader(),
new Class[]{StatementHandler.class},
new PageHandler((StatementHandler) o));
}
return o;
}
@Override
public void setProperties(Properties properties) {
}
- PageHandler类,具体的代理类
/**
* @Classname PageHandler InvocationHandler 动态代理
* @Description TODO
* @Date 2019/7/12 15:51
* @Created by 爆裂无球
*/
public class PageHandler implements InvocationHandler {
StatementHandler handler;
public PageHandler(StatementHandler handler) {
this.handler = handler;
}
/**
* 判断是否是StatementHandler对象 prepare 的方法 用于拦截拼接sql
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equalsIgnoreCase("prepare")) {
setParametersProxy();
}
return method.invoke(handler, args);
}
private void setParametersProxy() {
//1.若sql语句中有limit,则抛出异常
if (handler.getBoundSql().getSql().contains(" limit")) {
throw new IllegalArgumentException("请检查sql语句是否包含limit,若包含limit请不要使用自定义插件");
}
//若只传page对象 则进行sql拼接
else if (handler.getBoundSql().getParameterObject() instanceof Page) {
Page page = (Page) handler.getBoundSql().getParameterObject();
appendPageSql((Page) page);
}
//若既有page对象又有参数,则取出page对象进行拼接
else if (handler.getBoundSql().getParameterObject() instanceof Map) {
Map map = (Map) handler.getBoundSql().getParameterObject();
map.values().stream()
.filter(a -> a instanceof Page)
.findFirst()
.ifPresent(
page -> {
appendPageSql((Page) page);
}
);
}
}
/**
* sql拼接
* @param page
*/
private void appendPageSql(Page page) {
try {
BoundSql sql = handler.getBoundSql();
sql.getSql();
String limit = String.format(" limit %s,%s", page.getBegin(), page.getSize());
setFileValue("sql", sql, sql.getSql() + limit);
} catch (Exception e) {
throw new RuntimeException("拼接sql错误:"+e);
}
}
// private Object getFileValue(String fileName, Object target) throws NoSuchFieldException, IllegalAccessException {
// Field sqlField = target.getClass().getDeclaredField(fileName);
// sqlField.setAccessible(true); //关闭安全检查
// return sqlField.get(target);
// }
/**
* 通过反射获取方法 重新设置 value
*/
private Object setFileValue(String fileName, Object target, Object value) throws NoSuchFieldException, IllegalAccessException {
Field sqlField = target.getClass().getDeclaredField(fileName);
sqlField.setAccessible(true); //关闭安全检查
sqlField.set(target, value);
return sqlField.get(target);
}
}
5.测试
-
测试代码
-
测试结果
6.结语
世上无难事,只怕有心人,每天积累一点点,fighting!!!