本帖最后由 大蓝鲸Java 于 2018-4-13 12:14 编辑
Mybatis的插件概述
要理解mybatis的插件技术,必须首先知道Mybatis的执行过程:
- SqlSessionFactoryBuilder:加载mybatis配置信息,生成SqlSessionFactory
- SqlSessionFactory:创建SqlSession的工厂类
- SqlSession:与数据库交互,执行SQL,并返回结果
SqlSession是一个接口,它提供了对数据库的增删改查方法,我们构建SqlsessionFactory就可以轻易的拿到SqlSession对象。那么SqlSession的增删改查方法是如何执行的呢。SqlSession是通过Executor、StatementHandler、ParameterHandler和ResultHandler来完成数据库的操作和结果返回的。
- Executor执行器,由它来调度StatementHandler、ParameterHandler和ResultHandler等来执行对应的SQL。
- StatementHandler的作用是使用数据库的Statement(PreparaStatement)执行操作,它是四大对象的核心,起到承上启下的作用。
- ParameterHandler用于SQL对参数的处理。
- ResultSetHandler用于对最后查询结果集(ResultSet)的封装返回的。
通过以上描述我们知道mybatis的数据库访问是通过sqlSession的四大对象完成的。我们可以在四大对象的调用的时候插入我们自定义的代码逻辑,去执行一些特殊的要求以满足特殊场景的需求,这就是Mybatis的插件技术。
如何自定义Mybatis插件
- 步骤一、编写自己的插件类
-
这个类需要实现Mybatis提供的插件接口Interceptor。在这个接口中定义了三个方法。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
我们首先来看看这三个方法分别都是用来干什么的:
- intercept方法:它将直接覆盖你所拦截对象原有的方法,它是插件的核心方法。该方法会传入一个Invocation对象,通过它就可以反射调用原来对象的方法。
- Plugin方法:传入的target参数就是被拦截的对象。它的作用是给被拦截的对象生成一个代理对象,并返回它。
- setProperties方法:允许在配置插件时的plugin元素中配置所需要的参数,方法在插件初始化的时候就被调用了一次,然后把插件对象出入到配置中,以便后面再取出。
步骤二、确定插件需要拦截的签名
正如Mybatis插件可以拦截SqlSession中的四大对象中的任意一个一样。从Plugin源码中我们可以看出插件需要注册签名在能够运行。注册签名时需要确定以下几个因素:
1、确定需要拦截的对象。
首先根据功能来确定你需要拦截的对象。
- Executor执行器,它负责调度执行SQL的全过程,一般很少拦截它。
- StatementHandler,是执行SQL的过程,拦截它可以重写我们的SQL,这个是最常用的拦截对象。
- ParameterHandler,很明显它主要是拦截执行SQL的参数组装,你可以重写组装参数的规则。
- ResultSetHandler,用于拦截执行结果的组装,拦截它,我们可以重写组装结果的规则。
2、拦截方法和参数
当你确定了需要拦截的对象,接下来就要确定需要拦截该对象的什么方法及方法的参数。例如,我们现在需要自定义分页插件,那么我们肯定需要去修改执行的SQL,也就是说我们需要拦截StatementHandler对象。我们知道StatementHandler的prepare方法会预编译SQL,于是我们需要拦截的方法便是prepare方法,在此之前完成SQL的重写编写。StatementHandler的接口定义如下:
[AppleScript] 纯文本查看 复制代码 public interface StatementHandler {
Statement prepare(Connection connection)
throws SQLException;
void parameterize(Statement statement)
throws SQLException;
void batch(Statement statement)
throws SQLException;
int update(Statement statement)
throws SQLException;
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
那么我们现在已经确定了拦截StatementHandler的prepare方法,方法中有一个参数Connection对象,因此我们可以确定我们插件类的签名如下:
@Intercepts({
@Signature(type = StatementHandler.class, //插件拦截的对象
method = "prepare", //插件拦截的对象的哪个方法
args = { Connection.class }) //拦截的方法的参数
})
public class MyInterceptor implements Interceptor{......
步骤三、实现拦截的方法
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
}
定义插件实现分页功能
分页插件时Mybatis中最为经典和常用的插件,要定义插件首先需要确定插件需要拦截的对象。我们知道Mybatis中SQL的预编译是在StatementHandler对象的prepare方法中进行的,因此我们需要在此方法之前去创建计算总数的SQL,并且通过它查询总条数,然后将当前要运行的SQL改造为分页SQL,这样就能进行分页查询了。
1、为了方便分页插件的使用,这里需要先定义一个POJO对象,用来封装分页需要的数据。
[AppleScript] 纯文本查看 复制代码 public class PageParams {
private Integer page;//当前页码
private Integer pageSize;//每页条数
private Boolean useFlag;//是否启用插件
private Boolean checkFlag;//是否检查当前页码有效性
private Integer total;//当前SQL返回总数,插件回填
private Integer totalPage;//总页数,插件回填
// set和get方法。。。。。
这样就可以通过这个POJO去定义当前页码,每页条数,是否启用插件,是否检查当前页码的有效性,通过这些属性可以控制插件的行为。而total和totalPage则是需要插件回填的两个数据,通过回填数据,调用者可以轻易得到这条SQL运行的总数和总页数。
2、定义分页插件类PagingPlugin实现Interceptor接口,然后配置我们的分页插件标签。
[AppleScript] 纯文本查看 复制代码 @Intercepts({
@Signature(type = StatementHandler.class,//插件拦截的对象
method = "prepare", //插件拦截的对象的方法
args = { Connection.class }) //拦截的方法的参数
})
public class PagingPlugin implements Interceptor{
private Integer defaultPage;//默认分页
private Integer defaultPageSize;//默认每页条数
private Boolean defaultUseFlag;//默认是否启动插件
private Boolean defaultCheckFlag;//默认是否检测当前页码的有效性
3、实现Interceptor的plugin方法去生成代理对象
[AppleScript] 纯文本查看 复制代码 @Override
public Object plugin(Object statementHandler) {
return Plugin.wrap(statementHandler, this);
}
4、实现setProperties方法,去设置配置参数。
[AppleScript] 纯文本查看 复制代码 /**
* 初始化参数
*/
@Override
public void setProperties(Properties props) {
String defaultPage = props.getProperty("default.page","1");
String defaultPageSize = props.getProperty("default.pageSize","20");
String defaultUseFlag = props.getProperty("default.useFlag","false");
String defaultCheckFlag = props.getProperty("default.checkFlag","false");
this.defaultPage = Integer.parseInt(defaultPage);
this.defaultPageSize = Integer.parseInt(defaultPageSize);
this.defaultUseFlag = Boolean.parseBoolean(defaultUseFlag);
this.defaultCheckFlag = Boolean.parseBoolean(defaultCheckFlag);
}
5、实现intercept方法,这是实现我们分页逻辑的重点。
这里需要先统计当前SQL运行返回的总条数。因此,我们要先构造统计总条数的SQL,然后运行它得到总条数,在通过每页多少条的pageSize进而算出最大页数,回填之前定义的POJO。而拿到当前运行的SQL去构建统计总条数的SQL是比较容易的,但是要构造出分页查询的SQL,并且传入分页参数是比较困难的。此时我们可以通过ParameterHandler对象完成,因此需要构建一个新的ParameterHandler对象,在Mybatis中默认使用DefaultParameterHandler来实现ParameterHandler的,使用它就可以设置SQL参数。接下来让我们看看intercept方法的实现
[AppleScript] 纯文本查看 复制代码 @Override
public Object intercept(Invocation invocation) throws Throwable {
//此处我们拦截的StatementHandler对象,所以方法参数传入的invocation是可以直接强转为StatementHandler
//StatementHandler statementHandler = getUnProxyObject(invocation);
MetaObject metaStatementHandler= getUnProxyObject(invocation);
//分离代理对象,获取最原始的被代理类
//取出需要执行的sql
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
if(!checkSelect(sql)){
return invocation.proceed();
}
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
Object parameterObject = boundSql.getParameterObject();
PageParams pageParams = getPageParams(parameterObject);
if(pageParams == null){//没有分页参数,不启用插件
return invocation.proceed();
}
//获取分页参数,如果获取不到,则使用默认值
Integer pageNum = pageParams.getPage()==null?this.defaultPage:pageParams.getPage();
Integer pageSize = pageParams.getPageSize() == null? this.defaultPageSize:pageParams.getPageSize();
Boolean useFlag = pageParams.getUseFlag() == null?this.defaultUseFlag:pageParams.getUseFlag();
Boolean checkFlag = pageParams.getCheckFlag() == null?this.defaultCheckFlag:pageParams.getCheckFlag();
if(!useFlag){//不使用分页插件
return invocation.proceed();
}
//获取总记录数
int total = getToal(invocation,metaStatementHandler,boundSql);
//回填总数到分页参数中
this.SetTotalToPageParams(pageParams, total, pageSize);
//检查当前页码有效性
this.checkPage(checkFlag, pageNum, pageParams.getTotalPage());
return changeSQL(invocation, metaStatementHandler, boundSql, pageNum, pageSize);
}
首先需要从代理对象中分离出真实对象,通过MetaObject绑定这个非代理对象来获取各种参数,这是插件中常常用到的方法。getUnProxyObject方法就是用来获取真实对象的。
[AppleScript] 纯文本查看 复制代码 /**
* 从代理对象中分离出真实对象
* @param invocation
* @return
*/
private MetaObject getUnProxyObject(Invocation invocation) {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler= SystemMetaObject.forObject(statementHandler);
while(metaStatementHandler.hasGetter("h")){
Object object = metaStatementHandler.getValue("h");
metaStatementHandler = SystemMetaObject.forObject(object);
}
while(metaStatementHandler.hasGetter("target")){
Object object = metaStatementHandler.getValue("target");
metaStatementHandler = SystemMetaObject.forObject(object);
}
return metaStatementHandler;
}
这里从BoundSql中获取我们当前要执行的SQL,如果是select语句我们才会进行分页处理,否侧直接通过反射执行原有的prepare方法,所以这里需要做一个判断,代码如下
[AppleScript] 纯文本查看 复制代码 /**
* 判断是否是Select语句
* @param sql
* @return
*/
private boolean checkSelect(String sql) {
String trimSql = sql.trim();
int idx = trimSql.toLowerCase().indexOf("select");
return idx==0;
}
这个时候需要获取分页参数。参数可以是Map对象,也可以是POJO,或者是@Param朱姐。这里支持继承PageParams或者Map,其实@Param在Mybatis也是一种Map传参。获取分页参数的方法getPageParams
[AppleScript] 纯文本查看 复制代码 private PageParams getPageParams(Object parameterObject) {
if(parameterObject == null){
return null;
}
PageParams pageParams = null;
if(parameterObject instanceof Map){
@SuppressWarnings("unchecked")
Map<String,Object> paramMap = (Map<String, Object>) parameterObject;
Set<String> keySet = paramMap.keySet();
Iterator<String> iterator = keySet.iterator();
while(iterator.hasNext()){
String key = iterator.next();
Object value = paramMap.get(key);
if(value instanceof PageParams){
return (PageParams) value;
}
}
}else if(parameterObject instanceof PageParams){//继承方式
pageParams = (PageParams) parameterObject;
}
return pageParams;
}
该方法中判断参数是否是一个Map。如果是Map,则遍历Map找到分页参数;如果不是Map,就判断它是不是PageParams类,如果是则直接返回。一旦得到这个分页参数为null,或者分页参数指示不启用分页插件,那么就直接执行原来拦截的方法。
得到分页参数后,就要获取总数,方法如下
[AppleScript] 纯文本查看 复制代码 /**
* 获取总数
* @param invocation
* @param metaStatementHandler
* @param boundSql
* @return
*/
private int getTotal(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql) {
//获取当前的mappedStatement
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
//获取配置对象
Configuration cfg = mappedStatement.getConfiguration();
//获取当前需要执行的SQL
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
String countSql = "select count(*) as \"total\" from ( "+sql+" ) ";
//获取拦截方法参数,我们知道是Connection对象
Connection connection = (Connection) invocation.getArgs()[0];
PreparedStatement ps = null;
int total = 0;
try{
//预编译统计总数SQL
ps = connection.prepareStatement(countSql);
//构建统计总数BoundSql
BoundSql countBoundSql = new BoundSql(cfg,countSql,boundSql.getParameterMappings(),boundSql.getParameterObject());
//构建Mybatis的ParameterHandler用来设置查询总数SQL的参数
ParameterHandler handler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBoundSql);
//设置查询总数参数
handler.setParameters(ps);
//执行查询
ResultSet rs = ps.executeQuery();
while(rs.next()){
total = rs.getInt("total");
}
}catch(Exception e){
}finally{
if(ps != null){
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
System.err.println("总条数:"+total);
return total;
}
我们从BoundSql中获取当前需要执行的SQL,对它进行改写就可以得到我们需要统计的SQL,然后使用Connection预编译。之后采用JDBC的方式计算出总数并将结果返回,但是这个不能关闭Connection,因为后面的查询还需要用到它。
得到这个总数后将它回填到分页参数中,这样我们就得到了分页数据中两个很重要的参数了总记录数和总页数。
[AppleScript] 纯文本查看 复制代码 private void SetTotalToPageParams(PageParams pageParams,int total,int pageSize){
pageParams.setTotal(total);
int totalPage = total%pageSize==0?total/pageSize:total/pageSize+1;
pageParams.setTotalPage(totalPage);
}
然后,根据分页参数的设置判断是否启用检测页码正确性的处理,如果当前页码大于最大页码的时候抛出异常,提示错误,代码如下:
[AppleScript] 纯文本查看 复制代码 /**
* 校验当前页码的有效性
* @param checkFlag
* @param pageNum
* @param pageTotal
* @throws Exception
*/
private void checkPage(Boolean checkFlag,Integer pageNum,Integer pageTotal) throws Exception{
if(checkFlag){
if(pageNum > pageTotal){
throw new Exception("查询失败,查询页码"+pageNum+">查询总页数"+pageTotal);
}
}
}
最后我们修改当前SQL为分页SQL
[AppleScript] 纯文本查看 复制代码 /**
* 修改当前查询的sql
* @param invocation
* @param metaStatementHandler
* @param boundSql
* @param page
* @param pageSize
* @return
* @throws Exception
*/
private Object changeSQL(Invocation invocation,MetaObject metaStatementHandler,BoundSql boundSql,int page,int pageSize) throws Exception{
//获取当前需要执行的SQL
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
//修改SQL,变成分页SQL
String newSql = "";
if("mysql".equalsIgnoreCase(dbType)){
newSql = "select * from ( "+sql+" ) temp limit ?,?";
}else if("oracle".equalsIgnoreCase(dbType)){
newSql = "select * from ( select temp.*,rownum rw from ( "+ sql+" ) temp where rownum<=? ) where rw>?";
}
//修改当前需要执行的SQL
metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
//获取PrepareStatement,为其设置分页参数
PreparedStatement ps = (PreparedStatement) invocation.proceed();
int parameterCount = ps.getParameterMetaData().getParameterCount();
if("mysql".equalsIgnoreCase(dbType)){
ps.setInt(parameterCount-1, (page-1)*pageSize);
ps.setInt(parameterCount, pageSize);
}else if("oracle".equalsIgnoreCase(dbType)){
ps.setInt(parameterCount-1, page*pageSize);
ps.setInt(parameterCount, (page-1)*pageSize+1 );
}
return ps;
}
这样我们的分页插件就完成了。接下来就是需要去配置和运行我们的插件了
# 自定义分页插件的配置与运行 #
我们需要在Mybatis配置文件里面配置我们的插件。配置时需要注意plugins元素的配置顺序,如果配错了顺序系统就会报错。
<plugins>
<plugin interceptor="com.itcast.PagingPlugin">
<property name="dbType" value="mysql"/>
</plugin>
</plugins>
|