1. 程式人生 > >學習Mybatis(5):編寫外掛

學習Mybatis(5):編寫外掛

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
    }
]