MyBatis開放用戶實現本身的插件,從而對整個調用過程進行個性化擴展。mysql
這是MyBatis整個調用流程的主要參與者。git
咱們能夠對其中的一些過程進行攔截,添加本身的功能,好比重寫Sql添加分頁參數。github
MyBatis容許攔截的接口以下spring
Executorsql
public interface Executor { ResultHandler NO_RESULT_HANDLER = null; int update(MappedStatement var1, Object var2) throws SQLException; <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException; <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException; <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException; List<BatchResult> flushStatements() throws SQLException; void commit(boolean var1) throws SQLException; void rollback(boolean var1) throws SQLException; CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4); boolean isCached(MappedStatement var1, CacheKey var2); void clearLocalCache(); void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5); Transaction getTransaction(); void close(boolean var1); boolean isClosed(); void setExecutorWrapper(Executor var1); }
ParameterHandler數據庫
public interface ParameterHandler { Object getParameterObject(); void setParameters(PreparedStatement var1) throws SQLException; }
ResultSetHandler設計模式
public interface ResultSetHandler { <E> List<E> handleResultSets(Statement var1) throws SQLException; <E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException; void handleOutputParameters(CallableStatement var1) throws SQLException; }
StatementHandlermybatis
public interface StatementHandler { Statement prepare(Connection var1, Integer var2) throws SQLException; void parameterize(Statement var1) throws SQLException; void batch(Statement var1) throws SQLException; int update(Statement var1) throws SQLException; <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException; <E> Cursor<E> queryCursor(Statement var1) throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }
只要攔截器定義了攔截的接口和方法,後續調用該方法時,將會被攔截。app
若是要實現本身的攔截器,須要實現接口Interceptoride
@Slf4j @Intercepts(@Signature(type = Executor.class, method ="update", args ={MappedStatement.class,Object.class} )) public class MyIntercetor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { log.info("MyIntercetor ..."); Object result = invocation.proceed(); log.info("result = " + result); return result; } @Override public Object plugin(Object o) { return Plugin.wrap(o,this); } @Override public void setProperties(Properties properties) { } }
1. 攔截方法配置
Intercepts,Signature
public @interface Intercepts { Signature[] value(); }
public @interface Signature {
Class<?> type();
String method();
Class<?>[] args();
}
配置
@Intercepts(@Signature(type = Executor.class, method ="update", args ={MappedStatement.class,Object.class} ))
咱們知道Java中方法的簽名包括所在的類,方法名稱,入參。
@Signature定義方法簽名
type:攔截的接口,爲上節定義的四個接口
method:攔截的接口方法
args:參數類型列表,須要和方法中定義的順序一致。
也能夠配置多個
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
2. intercept(Invocation invocation)
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return this.target;
}
public Method getMethod() {
return this.method;
}
public Object[] getArgs() {
return this.args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return this.method.invoke(this.target, this.args);
}
}
經過Invocation能夠獲取到被攔截的方法的調用對象,方法,參數。
proceed()用於繼續執行並得到最終的結果。
這裏使用了設計模式中的責任鏈模式。
3.這裏不能返回null。
用於給被攔截的對象生成一個代理對象,並返回它。
@Override public Object plugin(Object o) { return Plugin.wrap(o,this); }
能夠看下wrap方法,其實現了JDK的接口InvocationHandler,也就是爲傳入的target建立了一個代理對象。這裏使用了JDK動態代理方式。也能夠本身實現其餘代理方式,好比cglib.
public class Plugin implements InvocationHandler { private final Object target; private final Interceptor interceptor; private final Map<Class<?>, Set<Method>> signatureMap; public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target; }
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
}
因爲使用了動態代理,方法執行時,將會被調用invoke方法,會先判斷是否設置了攔截器:methods != null && methods.contains(method),
若是設置了攔截器,則調用攔截器this.interceptor.intercept(new Invocation(this.target, method, args))
不然直接調用method.invoke(this.target, args);
4.攔截器在執行前輸出"MyIntercetor ...",在數據庫操做返回後輸出"result =xxx"
log.info("MyIntercetor ..."); Object result = invocation.proceed(); log.info("result = " + result);
插件實現完成!
在Spring中引入很簡單。
第一種方式:
建立攔截器的bean
@Slf4j @Configuration public class IntercetorConfiguration { @Bean public MyIntercetor myIntercetor(){ return new MyIntercetor(); } }
注意第一種方式和第二種方式僅適用於SpringBoot應用,而且引入如下依賴
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency>
第二種方式
手動往Configuration中添加攔截器。
@Slf4j @Configuration public class IntercetorConfiguration { @Autowired private List<SqlSessionFactory> sqlSessionFactoryList; @PostConstruct public void addPageInterceptor() { MyIntercetor interceptor = new MyIntercetor(); Iterator var3 = this.sqlSessionFactoryList.iterator(); while(var3.hasNext()) { SqlSessionFactory sqlSessionFactory = (SqlSessionFactory)var3.next(); sqlSessionFactory.getConfiguration().addInterceptor(interceptor); } } }
第三種方式
若是是純Spring應用,可在mybatis配置文件中配置
<plugins> <plugin intercetor="xxx.xxx.MyIntercetor"> <property name="xxx" value="xxx"> </plugin> </plugins>
因爲上面定義的攔截器是攔截Executor的update方法,因此在執行insert,update,delete的操做時,將會被攔截。
本例子使用insert來測試。具體代碼查看:GitHub
2019-06-10 16:08:03.109 INFO 20410 --- [nio-8110-exec-1] c.m.user.dao.intercetor.MyIntercetor : MyIntercetor ... 2019-06-10 16:08:03.166 INFO 20410 --- [nio-8110-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited 2019-06-10 16:08:03.267 DEBUG 20410 --- [nio-8110-exec-1] o.m.s.t.SpringManagedTransaction : JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5cb1c36e] will not be managed by Spring 2019-06-10 16:08:03.274 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.dao.mapper.UserMapper.insertList : ==> Preparing: insert into user (name) values (?) , (?) , (?) 2019-06-10 16:08:03.307 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.dao.mapper.UserMapper.insertList : ==> Parameters: name:58(String), name:64(String), name:69(String) 2019-06-10 16:08:03.355 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.dao.mapper.UserMapper.insertList : <== Updates: 3 2019-06-10 16:08:03.358 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.d.m.U.insertList!selectKey : ==> Preparing: SELECT LAST_INSERT_ID() 2019-06-10 16:08:03.358 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.d.m.U.insertList!selectKey : ==> Parameters: 2019-06-10 16:08:03.380 DEBUG 20410 --- [nio-8110-exec-1] c.m.u.d.m.U.insertList!selectKey : <== Total: 1
2019-06-10 16:08:03.381 INFO 20410 --- [nio-8110-exec-1] c.m.user.dao.intercetor.MyIntercetor : result = 3
能夠看到攔截器被調用了。
這裏攔截StatementHandler的prepare方法,也就是SQL語句預編譯以前進行SQL改寫。
@Slf4j @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class PageIntercetor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { log.info("StatementHandler prepare ..."); StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); ParameterHandler parameterHandler = statementHandler.getParameterHandler(); BoundSql boundSql = statementHandler.getBoundSql(); //獲取到原始sql語句 String sql = boundSql.getSql(); String mSql = sql + " limit 0,1"; //經過反射修改sql語句 Field field = boundSql.getClass().getDeclaredField("sql"); field.setAccessible(true); field.set(boundSql, mSql); return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { //此處能夠接收到配置文件的property參數 System.out.println(properties.getProperty("name")); } }
分頁插件實現的難點在於當使用不一樣的Statement時,執行流程是不同的。
Statement須要定義statementType="STATEMENT",這個時候SQL語句不須要進行預編譯處理,參數是與xml中配飾的SQL語句拼接在一塊兒的。
<select id="select" resultMap="BaseResultMap" statementType="STATEMENT">
select id, name
from user
where
name = '${name}'
</select>
而當使用PreparedStatement時須要定義statementType="PREPARED",這個時候SQL語句須要進行預編譯處理。CallableStatement(用於調用存儲過程)同理。
<select id="select" resultMap="BaseResultMap" statementType="PREPARED"> select id, name from user where name = #{name} </select>
所以須要考慮不一樣狀況下的SQL改寫。
雖然Mybatis給咱們實現了分頁,只要在接口上傳入RowBounds參數,便可實現分頁。
可是這個是內存分頁。也就是把全部的數據都讀到應用內存中,再進行分頁。形成了許多無效的讀取。
固然也不必搞的這麼複雜!能夠在mapper.xml中直接添加limit.
須要注意的是limit的參數的數據量不一樣,那麼效率是不同的,須要進行相關的優化。
結束!!!!!