簡介:
Mybatis在四大組件(Execurot,StatementHandler,ParameterHandler,ResultSetHandler)處提供了簡單易用的插件擴展機制。Mybatis支持對四大核心對象進行攔截,對mybatis來說插件就是攔截器,用來增強核心對象功能,增強功能的本質上是藉助底層的動態代理實現的。
原理:
-
每個創建出來的對象不是直接返回的,而實interceptorChain.pluginAll(parameterHandler);
-
獲取到所有的Interceptor(攔截器)(插件需要實現的接口);調用interceptor.plugin(target);返回target包裝後的對象
-
插件機制,我們可以使用插件爲目標對象創建一個代理對象;aop(面向切面)我們的插件可以以爲四大對象創建出代理對象,代理對象就可以攔截到四大對象的每一個執行;
mybatis所運行攔截的方法:
- 執行器Executor(update,query,commit,rollback等方法)
- SQL語法構建器StatementHandler(getParemeterObject,setParameters方法);
- 參數處理器ParameterHandler(getParemeterObject,setParameter方法);
- 結果集處理器ResutSetHandler(handleResultSets、handleOutputParameters等方法);
案例:基於插件實現數據權限
編寫自定義插件的時候我們需要通過實現mybatis插件接口Interceptor,添加註解@Intercepts,可以在該註解中定義多個@Signature對多個地方進行攔截,我們需要在@Signature當中指定攔截的接口、方法名、攔截方法的入參(由於存在方法重載情況,需要來確定方法的唯一性);
同時實現其中包含的三個方法:
intercept()
爲插件核心方法,每次執行操作的時候,都會進行這個攔截器的方法內,我們可以在當中編寫插件的具體實現邏輯;plugin()
用來生成target的代理對象,主要用來把攔截器生成一個代理放到攔截器鏈中;setProperties()
會在初始化時調用,將插件配置的屬性從這裏設置進來,以供我們獲取使用。
對sqlMapConfig.xml進行配置添加該自定義的插件即可
@Component
@Intercepts({//這裏可以定義多個@Signature對多個地方攔截,都用這個攔截器
@Signature(
type = StatementHandler.class, //這裏直指攔截哪個接口
method = "prepare",//這個接口內的哪個方法名
args = {Connection.class, Integer.class//這個是攔截的方法的入參,按順序寫到這,不要多也不要少,如果方法重載,可是要通過方法名入參來確定唯一的
})
})
public class MySqlInterceptor implements Interceptor {
@Autowired
HttpServletRequest request;
//這裏是每次執行操作的時候都會進行這個攔截器方法內
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 方法一
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
//先攔截到RoutingStatementHandler,裏面有個StatementHandler類型的delegate變量,其實現類是BaseStatementHandler,然後就到BaseStatementHandler的成員變量mappedStatement
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
//id爲執行的mapper方法的全路徑名,如com.uv.dao.UserMapper.insertUser
String id = mappedStatement.getId();
//sql語句類型 select、delete、insert、update
String sqlCommandType = mappedStatement.getSqlCommandType().toString();
BoundSql boundSql = statementHandler.getBoundSql();
//獲取到原始sql語句
String sql = boundSql.getSql();
String mSql = sql;
//TODO 修改位置
System.out.println(sql);
//註解邏輯判斷 添加註解了才攔截
Class<?> classType = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf(".")));
String mName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1, mappedStatement.getId().length());
for (Method method : classType.getDeclaredMethods()) {
if (method.isAnnotationPresent(InterceptAnnotation.class) && mName.equals(method.getName())) {
InterceptAnnotation interceptorAnnotation = method.getAnnotation(InterceptAnnotation.class);
if (interceptorAnnotation.flag()) {
//根據登錄用戶的數據進行拼接sql處理
mSql = sql + " limit 2";
}
}
}
//通過反射修改sql語句
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, mSql);
return invocation.proceed();
}
//注意爲了把這個攔截器生成一個代理放到攔截器鏈中
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
//插件初始化後調用,也只會調用一次,插件配置的屬性從這裏設置進來
@Override
public void setProperties(Properties properties) {
}
}
InterceptAnnotation:
@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface InterceptAnnotation {
boolean flag() default true;
}
mapper上加該註解的進行攔截對sql處理
public interface UserMapper {
List<User> selectByParams(Map<String,Object> params);
Long selectTotal(Map<String,Object> params);
User selectByUsername(@Param("username")String username);
int deleteByPrimaryKey(Long id);
int insertSelective(User record);
User selectByPrimaryKey(Long id);
int updateByPrimaryKeySelective(User record);
@InterceptAnnotation(flag=true)
List<User> select(Map<String,Object> params);
}
在調用方法select()
後便會進過攔截 ,對sql處理完成了對數據權限的處理。
Plugin源碼分析:
Plugin:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 獲得目標方法是否被攔截
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 如果是,則攔截處理該方法
return interceptor.intercept(new Invocation(target, method, args));
}
// 如果不是,則調用原方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
invoke()
方法首先會檢測被攔截方法是否配置在插件的@Signature
註解中,若是,則執行插件邏輯,否則則執行被攔截方法。插件邏輯封裝在intercept中,該方法參數類型爲Invocation,Invocation主要用於存儲目標類,方法及方法參數列表。