北京土巴兔裝修公司電話關(guān)鍵詞優(yōu)化報價怎么樣
MyBatis插件介紹
MyBatis提供了一種插件(plugin)的功能,雖然叫做插件,但其實這是攔截器功能。
MyBatis允許使用者在映射語句執(zhí)行過程中的某一些指定的節(jié)點進行攔截調(diào)用,通過織入攔截器,在不同節(jié)點修改一些執(zhí)行過程中的關(guān)鍵屬性,從而影響SQL的生成、執(zhí)行和返回結(jié)果,如:來影響Mapper.xml到SQL語句的生成、執(zhí)行SQL前對預(yù)編譯的SQL執(zhí)行參數(shù)的修改、SQL執(zhí)行后返回結(jié)果到Mapper接口方法返參POJO對象的類型轉(zhuǎn)換和封裝等。
根據(jù)上面的對Mybatis攔截器作用的描述,可以分析其可能的用途;最常見的就是Mybatis自帶的分頁插件PageHelper或Rowbound參數(shù),通過打印實際執(zhí)行的SQL語句,發(fā)現(xiàn)我們的分頁查詢之前,先執(zhí)行了COUNT(*)語句查詢數(shù)量,然后再執(zhí)行查詢時修改了SQL語句即在我們寫的SQL語句后拼接上了分頁語句LIMIT(offset, pageSize);
此外,實際工作中,可以使用Mybatis攔截器來做一些數(shù)據(jù)過濾、數(shù)據(jù)加密脫敏、SQL執(zhí)行時間性能監(jiān)控和告警等;既然要準備使用它,下面先來了解下其原理;
默認情況下,MyBatis 允許使用插件來攔截的四種相關(guān)操作類方法:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
這幾個接口之間的關(guān)系大概是這樣的:
Mybatis整體執(zhí)行流程:
核心對象
- Configuration:初始化基礎(chǔ)配置,比如MyBatis的別名等,一些重要的類型對象,如插件,映射器,ObjectFactory和typeHandler對象,MyBatis所有的配置信息都維持在Configuration對象之中。
- SqlSessionFactory:SqlSession工廠。
- SqlSession:作為MyBatis工作的主要頂層API,表示和數(shù)據(jù)庫交互的會話,完成必要的數(shù)據(jù)庫增刪改查功能。
- Executor:MyBatis的內(nèi)部執(zhí)行器,它負責(zé)調(diào)用StatementHandler操作數(shù)據(jù)庫,并把結(jié)果集通過ResultSetHandler進行自動映射,另外,它還處理二級緩存的操作。
- StatementHandler:MyBatis直接在數(shù)據(jù)庫執(zhí)行SQL腳本的對象。另外它也實現(xiàn)了MyBatis的一級緩存。
- ParameterHandler:負責(zé)將用戶傳遞的參數(shù)轉(zhuǎn)換成JDBC Statement所需要的參數(shù)。是MyBatis實現(xiàn)SQL入?yún)⒃O(shè)置的對象。
- ResultSetHandler:負責(zé)將JDBC返回的ResultSet結(jié)果集對象轉(zhuǎn)換成List類型的集合。是MyBatis把ResultSet集合映射成POJO的接口對象。
- TypeHandler:負責(zé)Java數(shù)據(jù)類型和JDBC數(shù)據(jù)類型之間的映射和轉(zhuǎn)換。
- MappedStatement:MappedStatement維護了一條<select|update|delete|insert>節(jié)點的封裝。
- SqlSource :負責(zé)根據(jù)用戶傳遞的parameterObject,動態(tài)地生成SQL語句,將信息封裝到BoundSql對象中,并返回。
- BoundSql:表示動態(tài)生成的SQL語句以及相應(yīng)的參數(shù)信息。
MyBatis自定義插件的實現(xiàn)
通過 MyBatis 提供的強大機制,使用插件是非常簡單的,只需實現(xiàn) Interceptor 接口,并指定想要攔截的方法簽名即可。
Interceptor 接口的定義如下所示:
public interface Interceptor {//攔截器具體實現(xiàn)Object intercept(Invocation invocation) throws Throwable;//攔截器的代理類Object plugin(Object target);//添加屬性void setProperties(Properties properties);
}
相關(guān)注解:
@Intercepts // 描述:標志該類是一個攔截器
@Signature // 描述:指明該攔截器需要攔截哪一個接口的哪一個方法// @Signature注解中屬性:
type; // 四種類型接口中的某一個接口,如Executor.class;
method; // 對應(yīng)接口中的某一個方法名,比如Executor的query方法;
args; // 對應(yīng)接口中的某一個方法的參數(shù),比如Executor中query方法因為重載原因,有多個,args就是指明參數(shù)類型,從而確定是具體哪一個方法;
下面來看一個自定義的簡單Interceptor示例:
import lombok.extern.slf4j.Slf4j;
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.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Properties;@Component
//攔截StatementHandler類中參數(shù)類型為Statement的prepare方法(prepare=在預(yù)編譯SQL前加入修改的邏輯)
//即攔截 Statement prepare(Connection var1, Integer var2) 方法
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Slf4j
public class MyPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 獲取原始sqlStatementHandler statementHandler = (StatementHandler) invocation.getTarget();BoundSql boundSql = statementHandler.getBoundSql();// 通過MetaObject優(yōu)雅訪問對象的屬性,這里是訪問statementHandler的屬性;:MetaObject是Mybatis提供的一個用于方便、// 優(yōu)雅訪問對象屬性的對象,通過它可以簡化代碼、不需要try/catch各種reflect異常,同時它支持對JavaBean、Collection、Map三種類型對象的操作。MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());// 先攔截到RoutingStatementHandler,里面有個StatementHandler類型的delegate變量,其實現(xiàn)類是BaseStatementHandler,然后就到BaseStatementHandler的成員變量mappedStatementMappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");// 通過反射,攔截方法上帶有自定義@InterceptAnnotation注解的方法,并修改sqlString mSql = sqlAnnotationEnhance(mappedStatement, boundSql);Field field = boundSql.getClass().getDeclaredField("sql");field.setAccessible(true);field.set(boundSql, mSql);return invocation.proceed();}@Overridepublic Object plugin(Object target) {if (target instanceof StatementHandler) {return Plugin.wrap(target, this);} else {return target;}}@Overridepublic void setProperties(Properties properties) {}/*** 通過反射,攔截方法上帶有自定義@InterceptAnnotation注解的方法,并增強sql* @param id 方法全路徑* @param sqlCommandType sql類型* @param sql 所執(zhí)行的sql語句*/private String sqlAnnotationEnhance(MappedStatement mappedStatement, BoundSql boundSql) throws ClassNotFoundException {// 獲取到原始sql語句String sql = boundSql.getSql().toLowerCase();// sql語句類型 select、delete、insert、updateString sqlCommandType = mappedStatement.getSqlCommandType().toString();// 數(shù)據(jù)庫連接信息// Configuration configuration = mappedStatement.getConfiguration();// ComboPooledDataSource dataSource = (ComboPooledDataSource)configuration.getEnvironment().getDataSource();// dataSource.getJdbcUrl();// id為執(zhí)行的mapper方法的全路徑名,如com.cq.UserMapper.insertUser, 便于后續(xù)使用反射String id = mappedStatement.getId();// 獲取當(dāng)前所攔截的方法名稱String mName = id.substring(id.lastIndexOf(".") + 1);// 通過類全路徑獲取Class對象Class<?> classType = Class.forName(id.substring(0, id.lastIndexOf(".")));// 獲得參數(shù)集合String paramString = null;if (boundSql.getParameterObject() != null) {paramString = boundSql.getParameterObject().toString();}// 遍歷類中所有方法名稱,并匹配上當(dāng)前所攔截的方法for (Method method : classType.getDeclaredMethods()) {if (mName.equals(method.getName())) {// 判斷方法上是否帶有自定義@InterceptAnnotation注解InterceptAnnotation interceptorAnnotation = method.getAnnotation(InterceptAnnotation.class);if (interceptorAnnotation != null && interceptorAnnotation.flag()) {log.info("intercept func:{}, type:{}, origin SQL:{}", mName, sqlCommandType, sql);// 場景1:分頁功能: return sql + " limit 1";if ("select".equals(sqlCommandType.toLowerCase())) {if (!sql.toLowerCase().contains("limit")) {sql = sql + " limit 1";}}// 場景2:校驗功能 :update/delete必須要有where條件,并且打印出where中的條件if ("update".equals((sqlCommandType.toLowerCase())) || "delete".equals(sqlCommandType.toLowerCase())) {if (!sql.toLowerCase().contains("where")) {log.warn("update or delete not safe!");}}// 場景3:分庫分表: 根據(jù)userId哈希,替換注解中的表名if (sql.toLowerCase().contains(interceptorAnnotation.value())) {String userId = getValue(paramString, "userId");if (userId != null) {int num = Integer.parseInt(userId);// 模擬分10個庫,5個表String data_source_id = String.valueOf(num % 10);String new_table = interceptorAnnotation.value().concat("_").concat(String.valueOf(num % 5));log.info("set data_source_id:{}, table: {}", data_source_id, new_table);// 設(shè)置data_source_id路由, 替換sql表名sql = StringUtils.replace(sql, interceptorAnnotation.value(), new_table);}}log.info("new SQL:{}", sql);return sql;}}}return sql;}String getValue(String param, String key) {if (param == null) {return null;}String[] keyValuePairs = param.substring(1, param.length() - 1).split(",");for (String pair : keyValuePairs) {String[] entry = pair.split("=");if (entry[0].trim().equals(key)) {return entry[1].trim();}}return null;}
}
自定義注解如下:
import java.lang.annotation.*;@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InterceptAnnotation {String value() default "";/*** true增強、false忽略*/boolean flag() default true;
}
添加插件:
@Component
public class DynamicPluginHelper {@Autowiredprivate List<SqlSessionFactory> sqlSessionFactoryList;@Autowiredprivate MyPlugin myPlugin;@PostConstructpublic void addMysqlInterceptor() {for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();configuration.addInterceptor(myPlugin);}}
}
測試結(jié)果如下:
sql.MyPlugin : intercept func:findUser, type:SELECT, origin SQL:select * from t_user where id = ?
sql.MyPlugin : set data_source_id:4, table: t_user_4
sql.MyPlugin : new SQL:select * from t_user_4 where id = ? limit 1
問題記錄
錯誤描述:
There is no getter for property named 'delegate' in 'class com.sun.proxy.$Proxy32'
錯誤原因:
1、你有多個攔截器,攔截同一對象的同一行為。測試時避免其他攔截器的干擾可以先把注冊的攔截器注釋掉。
2、依賴包版本不對
3、攔截器配置類放置的位置不正確,導(dǎo)致包沒找到
參考:
https://blog.csdn.net/minghao0508/article/details/124420953
https://blog.csdn.net/qq_36881887/article/details/111589294
https://www.cnblogs.com/simplejavahome/p/16617112.html
https://www.cnblogs.com/nefure/p/16948633.html
https://blog.csdn.net/u011602668/article/details/128735771