Mybatis的插件用来对SQL语句进行修饰,或者在执行SQL语句前后进行一些工作
在Mybatis的配置文件中,<plugins>标签就是来配置自定义插件的
例如:
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
自定义插件需要有如下注解:
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
- type:拦截的组件类型,有Executor、StatementHandler、ResultSetHandler、ParameterHandler等,从名字可以看出来,插件可以作用于SQL执行器、SQL语句、SQL参数、结果集等
- method:四大组件的方法,如下:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
- args:方法参数,如StatementHandler的prepare方法,参数为Connection和Integer类型,就写{Connection.class,Integer.class}
编写插件需要实现Interceptor接口,该接口定义如下:
import java.util.Properties;
public interface Interceptor {
Object intercept(Invocation var1) throws Throwable;
Object plugin(Object var1);
void setProperties(Properties var1);
}
setProperties用来为实例变量赋值
plugin一般只要调用Plugin.wrap(var1,this)即可
重点是intercept方法,用来实现拦截逻辑,不过一般最后都要调用var1.proceed();
还有一个重要类——MetaObject,用来获取四大组件内部的实例变量(以及这些变量内部的变量),例如 StatementHandler 类,实际上框架调用的是 RoutingStatementHandler ,该类有一个StatementHandler类型的delegate变量,其内部有一个名为boundSql的对象,现在要获取该对象,可以这么写:
MetaObject stmtHandlerMeta=SystemMetaObject.forObject(statementHandler);
BoundSql boundSql= (BoundSql) stmtHandlerMeta.getValue("delegate.boundSql");
实际编写
以分页查询为例(结合Spring Boot):
1)编写Page类:
public class Page {
private Integer page;
private Integer pageSize;
public Integer getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
}
很简单的一个POJO类,包含两个参数:页码和页容量,及其getter、setter
2)编写PagePlugin类:
import Page;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.Configuration;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
//拦截StatementHandler的prepare方法
@Intercepts({
@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class,Integer.class})
})
public class PagePlugin implements Interceptor {
private Integer defaultPage;
private Integer defaultPageSize;
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler= (StatementHandler) invocation.getTarget();
MetaObject statementHandlerMetaObject= SystemMetaObject.forObject(statementHandler);
Object o=null;
//插件使用责任链模式重重代理,这里是用来还原到原始类
while(statementHandlerMetaObject.hasGetter("h")){
o=statementHandlerMetaObject.getValue("h");
statementHandlerMetaObject=SystemMetaObject.forObject(o);
}
if(o!=null)
statementHandler= (StatementHandler) o;
MetaObject stmtHandlerMeta=SystemMetaObject.forObject(statementHandler);
String sql= (String) stmtHandlerMeta.getValue("delegate.boundSql.sql");
//只有select方法才会拦截
if(sql.trim().toLowerCase().startsWith("select")!=0)
return invocation.proceed();
BoundSql boundSql= (BoundSql) stmtHandlerMeta.getValue("delegate.boundSql");
Object paramObject=boundSql.getParameterObject();
Page page=getPage(paramObject);
//没有带页面参数就直接反射
if (page==null)
return invocation.proceed();
int pageNum=page.getPage()==null?defaultPage:page.getPage();
int pageSize=page.getPageSize()==null?defaultPageSize:page.getPageSize();
int total=getTotal(invocation,stmtHandlerMeta,boundSql);
int totalPage=total/pageSize==0?total/pageSize:total/pageSize+1;
//页码过大(超过数据总数)就直接反射
if(!checkPage(pageNum,totalPage))
return invocation.proceed();
return changeSQL(invocation,stmtHandlerMeta,boundSql,pageNum,pageSize);
}
private Object changeSQL(Invocation invocation, MetaObject stmtHandlerMeta, BoundSql boundSql, int pageNum, int pageSize) throws InvocationTargetException, IllegalAccessException, SQLException {
String sql= (String) stmtHandlerMeta.getValue("delegate.boundSql.sql");
String newSql=sql+" limit ?,?";
stmtHandlerMeta.setValue("delegate.boundSql.sql",newSql);
PreparedStatement ps= (PreparedStatement) invocation.proceed();
int count=ps.getParameterMetaData().getParameterCount();
ps.setInt(count-1,(pageNum-1)*pageSize);
ps.setInt(count,pageSize);
return ps;
}
private boolean checkPage(int pageNum, Integer totalPage) throws Exception {
if (pageNum>totalPage)
return false;
return true;
}
private int getTotal(Invocation invocation, MetaObject stmtHandlerMeta, BoundSql boundSql) throws Throwable {
MappedStatement mappedStatement= (MappedStatement) stmtHandlerMeta.getValue("delegate.mappedStatement");
Configuration configuration=mappedStatement.getConfiguration();
String sql= (String) stmtHandlerMeta.getValue("delegate.boundSql.sql");
String countSql="select count(*) as total from ("+sql+") $_paging";
Connection conn= (Connection) invocation.getArgs()[0];
PreparedStatement ps=null;
int total=0;
try {
ps=conn.prepareStatement(countSql);
BoundSql bs=new BoundSql(configuration,countSql,boundSql.getParameterMappings(),boundSql.getParameterObject());
ParameterHandler ph=new DefaultParameterHandler(mappedStatement,boundSql.getParameterObject(),bs);
ph.setParameters(ps);
ResultSet rs=ps.executeQuery();
while (rs.next())
total=rs.getInt("total");
}finally {
if(ps!=null)
ps.close();
}
return total;
}
private Page getPage(Object paramObject) {
if(paramObject==null)
return null;
Page page=null;
if(paramObject instanceof Map){
Map<String,Object> paramMap= (Map<String, Object>) paramObject;
for(Map.Entry<String,Object> entry:paramMap.entrySet()){
if(entry.getValue() instanceof Page) {
return (Page) entry.getValue();
}
}
}else if(paramObject instanceof Page){
return (Page) paramObject;
}
return page;
}
@Override
public Object plugin(Object o) {
//Plugin工具类的wrap方法用来包装插件
return Plugin.wrap(o,this);
}
@Override
public void setProperties(Properties properties) {
defaultPage=1;
defaultPageSize=5;
}
}
3)配置:
首先将其注入为Bean:
@Bean
public PagePlugin pagePlugin(){
return new PagePlugin();
}
然后修改Mapper、Service、ServiceImpl,增加 getAllUsers(@Param Page page) 方法
然后写SQL语句(UserMapper.xml):
<select id="getAllUsers" resultMap="BaseResultMap">
select * from user
</select>
然后在Controller添加相应方法:
@GetMapping("/getAllUsers")
public List<User> getAll(@RequestParam int page,@RequestParam int pageSize){
Page page=new Page();
page.setPage(page);
page.setPageSize(pageSize);
return mapper.getAllUser(page);
}
4)测试:(共6条测试数据)
GET /getAllUsers?page=1&pageSize=4
[
{
"id": 1,
"userName": "zhangsan",
"age": 1
},
{
"id": 2,
"userName": "lisi",
"age": 2
},
{
"id": 3,
"userName": "wangwu",
"age": 3
},
{
"id": 4,
"userName": "zhaoliu",
"age": 4
}
]
GET /getAllUsers?page=2&pageSize=4
[
{
"id": 5,
"userName": "zhouqi",
"age": 5
},
{
"id": 6,
"userName": "wangba",
"age": 6
}
]
GET /getAllUsers?page=3&pageSize=4
[
{
"id": 1,
"userName": "zhangsan",
"age": 1
},
{
"id": 2,
"userName": "lisi",
"age": 2
},
{
"id": 3,
"userName": "wangwu",
"age": 3
},
{
"id": 4,
"userName": "zhaoliu",
"age": 4
},
{
"id": 5,
"userName": "zhouqi",
"age": 5
},
{
"id": 6,
"userName": "wangba",
"age": 6
}
]