mybatis plugin原始碼解析

睦月初雨發表於2024-04-10

概述

Plugin,意為外掛,是mybatis為開發者提供的,對方法進行自定義程式設計的手段。其中用到了動態代理、反射方法,透過指定需要增強的物件與方法,進行程式編寫。

核心類

主要涉及幾個核心類:InterceptorPluginIntercepts

該增強功能的大致執行順序為:

  1. 專案啟動時,查詢實現了Interceptor介面並且註冊為Bean(或在xml檔案中指定)的類,放入SqlSessionFactoryBean的Interceptor[]引數中,再由SqlSessionFactoryBean建立SqlSessionFactory的時候,將其放入Configuration引數中,留作後續呼叫

    // **注意interceptorsProvider,此為SpringBoot的configuration,會自動查詢註冊為Bean的Interceptor**
    public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
          ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
          ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
          ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider,
          ObjectProvider<List<SqlSessionFactoryBeanCustomizer>> sqlSessionFactoryBeanCustomizers) {
        this.properties = properties;
        this.interceptors = interceptorsProvider.getIfAvailable();
        this.typeHandlers = typeHandlersProvider.getIfAvailable();
        this.languageDrivers = languageDriversProvider.getIfAvailable();
        this.resourceLoader = resourceLoader;
        this.databaseIdProvider = databaseIdProvider.getIfAvailable();
        this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
        this.sqlSessionFactoryBeanCustomizers = sqlSessionFactoryBeanCustomizers.getIfAvailable();
     }
     
     
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
      SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
      factory.setDataSource(dataSource);
      .....
      // **注意此處,將interceptors放入factory中**
      if (!ObjectUtils.isEmpty(this.interceptors)) {
        factory.setPlugins(this.interceptors);
      }
    	.....
    }
    
    public class SqlSessionFactoryBean
    		protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    		....... 
    	    if (!isEmpty(this.typeAliases)) {
    	      Stream.of(this.typeAliases).forEach(typeAlias -> {
    	        targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
    	        LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
    	      });
    	    }
    	
    	    if (!isEmpty(this.plugins)) {
    	      Stream.of(this.plugins).forEach(plugin -> {
    	        targetConfiguration.addInterceptor(plugin);
    	        LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
    	      });
    	    }
    			.......
    	
    	    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
    	  }
    }
    
  2. Configuration類在初始化ParameterHandlerResultSetHandlerStatementHandlerExecutor四個類時,會對它們進行一次封裝,封裝內容即為用Interceptors註冊外掛功能,達到增強效果

    public class Configuration {
    
    	public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
      }
    
      public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
          ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
      }
    
      public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
      }
      
      public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
          executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
          executor = new ReuseExecutor(this, transaction);
        } else {
          executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
          executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
    
    }
    
  3. Configuration執行的pluginAll方法,內部是透過遍歷Interceptor陣列的plugin方法實現的。該方法入參和出參都是Object型別,所以可以認為它能為所有型別物件都進行增強封裝。Interceptor內部呼叫了Plugin的wrap方法,對Object物件進行了封裝。

    public class InterceptorChain {
    
      private final List<Interceptor> interceptors = new ArrayList<>();
    
      public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
          target = interceptor.plugin(target);
        }
        return target;
      }
    
      public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
      }
    
      public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
      }
    
    }
    
    public interface Interceptor {
    
      Object intercept(Invocation invocation) throws Throwable;
    
      default Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    
      default void setProperties(Properties properties) {
        // NOP
      }
    
    }
    
  4. Plugin方法實現了InvocationHandler動態代理類,並且wrap方法本身便是建立動態代理類。故Plugin類的職責有兩項:

    1. 建立動態代理類,指定需要被代理(增強)的物件。此處為ExecutorHandler等。
    2. 指定被動態代理的物件,需要執行何種程式。重點關注invoke方法。
    public class Plugin implements InvocationHandler {
    
      private final Object target;
      private final Interceptor interceptor;
      private final Map<Class<?>, Set<Method>> signatureMap;
    
      private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = 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);
        if (interfaces.length > 0) {
          return Proxy.newProxyInstance(
              type.getClassLoader(),
              interfaces,
              new Plugin(target, interceptor, signatureMap));
        }
        return target;
      }
    
    	// **動態代理增強**
      @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);
        }
      }
    	
    	// 省略getSignatureMap,getAllInterfaces方法
    	...
    }
    
  5. wrap方法執行時需要先透過interceptor獲取signatureMap。SignatureIntercepts註解中的value值註解,由於此value的返回值是陣列,所以Signature會多個存在,最後解析出的結果便為signatureMap。

    Signature註解的作用為標註被動態代理的物件,具體的型別(class),具體的方法,方法具體的引數。只有特定型別和方法才會執行Interceptor方法。

    public class Plugin implements InvocationHandler {
    
      private final Object target;
      private final Interceptor interceptor;
      private final Map<Class<?>, Set<Method>> signatureMap;
    
      private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
      }
    
      private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
        **// 透過此程式碼可發現,實現Interceptor的類必須新增Intercepts註解**
        if (interceptsAnnotation == null) {
          throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
        }
        Signature[] sigs = interceptsAnnotation.value();
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
        for (Signature sig : sigs) {
          Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
          try {
    	      **// 透過Siganture的method與args,反射出Method物件,將其新增到map中
    	      // 作用是在執行動態代理invoke方法時,判斷當前方法是否需要被interceptor執行**
            Method method = sig.type().getMethod(sig.method(), sig.args());
            methods.add(method);
          } catch (NoSuchMethodException e) {
            throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
          }
        }
        return signatureMap;
      }
    
    }
    
  6. 透過閱讀原始碼可知,開發者需要自己實現Interceptor,標記Intercepts註解,指定需要攔截的類、方法名,方法上的引數型別。並將Interceptor註冊為Spring Bean。即可在interceptor方法中編寫具體攔截程式碼。

例項

背景:在專案上為每一個需要插入至資料庫中的例項物件,初始化id。

程式碼:

@Component
**// 攔截Executor類的update方法,該update方法會執行insert、update、delete操作**
@Intercepts(@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}))
public class MybatisUpdateInterceptor implements Interceptor {

	// 雪花演算法id生成器
	@Autowired
	private IdGenerator idGenerator;

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		Method method = invocation.getMethod();
		MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
		**// 判斷是否為insert方法**
		if (ms.getSqlCommandType() != SqlCommandType.INSERT) {
			return invocation.proceed();
		}
		BaseEntity entity = (BaseEntity) invocation.getArgs()[1];
		if (entity.getId() == null) {
			entity.setId(idGenerator.generate());
		}
		return method.invoke(invocation.getTarget(), invocation.getArgs());
	}

}

總結

專案開發者可靈活運用plugin,為資料庫操作進行增強。日常開發中也可借鑑此流程,透過動態代理方式設計攔截/增強手段。

相關文章