MyBatisプラグインの実装メカニズムとSpring統合の詳細解説

カスタムプラグインの実装手順

MyBatisプラグインを実装する際の基本的な手順は以下の通りです。

インターセプタの作成

まず、Interceptorインターフェースを実装するクラスを作成します。

public class PaginationInterceptor implements Interceptor {
    // 実装内容
}

主要メソッドの実装

インターセプタクラスでは以下の3つのメソッドを実装します。

  • intercept(): 実際の処理が行われるコアメソッド
  • plugin(): プロキシオブジェクトの生成
  • setProperties(): 設定値の受け取り
@Override
public Object intercept(Invocation context) throws Throwable {
    // 拡張処理の実装
    return context.proceed();
}

@Override
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties config) {
    // 設定値の処理
}

インターセプト対象の指定

アノテーションを使用して、どのオブジェクトのどのメソッドをインターセプトするかを定義します。

@Intercepts({
    @Signature(type = Executor.class, method = "query", 
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})

プラグインの登録方法

MyBatisの設定ファイルにプラグインを登録します。

<plugins>
    <plugin interceptor="com.example.PaginationInterceptor">
        <property name="pageParam" value="pageNo"/>
        <property name="sizeParam" value="pageSize"/>
        <property name="enableCount" value="true"/>
    </plugin>
</plugins>

プラグインの実行フロー

MyBatis起動時にXMLConfigBuilderがプラグイン設定を読み込み、InterceptorChainに登録します。

private void processPluginConfigurations(XNode pluginsNode) {
    if (pluginsNode != null) {
        for (XNode pluginNode : pluginsNode.getChildren()) {
            String interceptorClass = pluginNode.getStringAttribute("interceptor");
            Properties settings = pluginNode.getChildrenAsProperties();
            Interceptor interceptor = (Interceptor)Class.forName(interceptorClass).newInstance();
            interceptor.setProperties(settings);
            configuration.registerPlugin(interceptor);
        }
    }
}

プロキシ生成の仕組み

MyBatisはプロキシパターンを活用して、プラグインによる処理を実現しています。

public Object createProxyChain(Object target) {
    for (Interceptor interceptor : pluginRegistry) {
        target = interceptor.wrap(target);
    }
    return target;
}

プロキシの生成処理はPluginクラスで行われます。

public static Object wrapTarget(Object original, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> methodSignatures = extractSignatures(interceptor);
    Class<?> targetType = original.getClass();
    Class<?>[] proxyInterfaces = determineProxyInterfaces(targetType, methodSignatures);
    
    if (proxyInterfaces.length == 0) {
        return original;
    }
    
    return Proxy.newProxyInstance(
        targetType.getClassLoader(),
        proxyInterfaces,
        new PluginHandler(original, interceptor, methodSignatures)
    );
}

PluginHandlerInvocationHandlerを実装し、メソッド呼び出しを処理します。

public Object invoke(Object proxy, Method method, Object[] args) {
    if (isTargetMethod(method)) {
        return interceptor.intercept(new MethodInvocation(original, method, args));
    }
    return method.invoke(original, args);
}

ページネーションプラグインの動作原理

ページネーションプラグインはSQLを動的に書き換えることで機能します。

public String rewritePaginationSql(String originalSql, PageParams params) {
    StringBuilder modifiedSql = new StringBuilder(originalSql);
    modifiedSql.append(" LIMIT ").append(params.getOffset());
    modifiedSql.append(", ").append(params.getLimit());
    return modifiedSql.toString();
}

ページ情報はスレッドローカル変数に保持されます。

private static final ThreadLocal<PageParams> PAGE_CONTEXT = new ThreadLocal<>();

public static void setPageInfo(int pageNumber, int pageSize) {
    PAGE_CONTEXT.set(new PageParams(pageNumber, pageSize));
}

public static PageParams getCurrentPage() {
    return PAGE_CONTEXT.get();
}

Springとの統合メカニズム

MyBatisとSpringを統合する際の主要コンポーネントは以下の通りです。

SqlSessionFactoryBeanの役割

Springコンテナ内でSqlSessionFactoryを管理するためのBeanです。

@Configuration
public class MyBatisConfig {
    
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setMapperLocations(new PathMatchingResourcePatternResolver()
            .getResources("classpath*:mappers/**/*.xml"));
        return factory;
    }
}

SqlSessionFactoryBeanは以下のインターフェースを実装しています。

  • FactoryBean: カスタムインスタンス生成
  • InitializingBean: 初期化処理
  • ApplicationListener: アプリケーションイベントの監視

SqlSessionTemplateの重要性

SqlSessionTemplateはスレッドセーフなSqlSessionの実装です。

public class SqlSessionTemplate implements SqlSession {
    
    private final SqlSession sqlSessionProxy;
    
    public SqlSessionTemplate(SqlSessionFactory sessionFactory) {
        this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
            SqlSession.class.getClassLoader(),
            new Class[]{SqlSession.class},
            new SqlSessionInterceptor(sessionFactory)
        );
    }
    
    // データ操作メソッドはすべてsqlSessionProxy経由で呼び出し
}

インターセプタの実装:

private static class SqlSessionInterceptor implements InvocationHandler {
    
    private final SqlSessionFactory sessionFactory;
    
    public SqlSessionInterceptor(SqlSessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        try (SqlSession session = sessionFactory.openSession()) {
            return method.invoke(session, args);
        }
    }
}

Mapperインターフェースの登録

SpringはMapperScannerConfigurerを使用して、MapperインターフェースをBeanとして登録します。

@Configuration
@MapperScan("com.example.repository")
public class MapperConfig {
    // 設定なし
}

内部的にはMapperFactoryBeanが使用され、実際のBeanは動的プロキシとして生成されます。

public class MapperFactoryBean<T> extends SqlSessionSupport implements FactoryBean<T> {
    
    private final Class<T> mapperInterface;
    
    @Override
    public T getObject() {
        return getSqlSession().getMapper(mapperInterface);
    }
    
    // その他の実装
}

タグ: MyBatis Spring Framework Java Plugin Architecture Dynamic Proxy

6月8日 00:20 投稿