mybatis源碼分析——自定義插件

簡介:

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主要用於存儲目標類,方法及方法參數列表。

發佈了24 篇原創文章 · 獲贊 11 · 訪問量 5525
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章