yoshikipom Tech Blog

プラグインアーキテクチャ(マイクロカーネルアーキテクチャ)の考慮事項とSpring Bootによる実践

はじめに

"ソフトウェアアーキテクチャの基礎" (https://tech-yoshikipom.hatenablog.com/entry/2023/01/24/220503 で紹介した本) に出てきたプラグインアーキテクチャ(またはマイクロカーネルアーキテクチャ)をWebシステムで使う時に考えたことやSpring Bootを用いたプラグインアーキテクチャの実装を紹介する。アーキテクチャのイメージは以下。

プラグインアーキテクチャ

プラグインアーキテクチャを使う時に考えたこと

Webシステムにおけるプラグインアーキテクチャの使い所

主に以下の二つの要求をシンプルに解決できると思う。

  • 機能面: 入力(ユーザ、APIのクライアントシステム、リクエストデータ等)によって振る舞いを追加したり減らしたりしたい
  • 保守性: たくさんの手続型のロジックをコンポーネント化 -> 責務の明確化、付け替えや追加が容易

機能面のほうは主にA/Bテストがしたかったり、複数のクライアントからの要求に柔軟に答える必要がでたときの話である。単純なif文やStrategyパターンだけで実装すると入力ごとの処理に差異が増えてくると差分が分かりづらくなり、保守性が一気に落ちることがある。保守性の方は実際に”プラグイン”が要件として必要なわけではないが、プラグインとして処理を実装することで開発体験を向上できる。

検討事項

プラグインアーキテクチャを使うときは以下の点は設計しておきたい。

  • 利用するプラグインの決定方法
    • ランタイムで決定: なんらかの方法で実行中でも切り替えられる (例: DBに設定を持って、ツールで変更する)
    • コンパイルで決定: ビルドした時点で決まる (例: 設定ファイルにプラグインのリストを書く)
  • 何をプラグインにするか
    • プラグインの同じインターフェースは同じ
    • inputとoutputの設計が必要
  • 複数プラグインの実行方法
    • 順次実行
    • 並列実行

ここで実装するサンプルでは「コンパイルベースの設定ファイル」、「プラグインの内容はダミー」、「並列実行は未対応」とする。

プラグインアーキテクチャのSpring Bootによる実装例

使用例

作るものを明確にするため使用例から。

設定ファイル(application.yml)

myapp:
  plugin:
    pluginExecutorNamesMap:
      groupA:
        - fooPlugin
      groupB:
        - barPlugin
        - fooPlugin

アプリケーションクラス

@SpringBootApplication
@Slf4j
@RequiredArgsConstructor
@ConfigurationPropertiesScan
public class PluginSampleApplication implements CommandLineRunner {

  private final CoreExecutor coreExecutor;

  public static void main(String[] args) {
    SpringApplication.run(PluginSampleApplication.class, args);
    System.exit(0);
  }

  @Override
  public void run(String... args) throws Exception {
    log.info("===== run app for 'groupA' =====");
    coreExecutor.execute(new CoreExecutorInput("groupA"));
    log.info("===== run app for 'groupB' =====");
    coreExecutor.execute(new CoreExecutorInput("groupB"));
    log.info("===== run app for 'groupC (no config)' =====");
    coreExecutor.execute(new CoreExecutorInput("groupC"));
  }
}

アプリケーションログ

===== run app for 'groupA' =====
process before plugins is running
FooPlugin is running!
process after plugins is running
===== run app for 'groupB' =====
process before plugins is running
BarPlugin is running!
FooPlugin is running!
process after plugins is running
===== run app for 'groupC (no config)' =====
process before plugins is running
process after plugins is running

設計

プラグインアーキテクチャクラス設計

  • core package
  • plugin package
    • プラグインの実装をする。今回はサブパッケージは作らず、実装はサンプルでログを出力するのみ。

実装

https://github.com/yoshikipom/java/tree/main/plugin-sample/src/main/java/com/yoshikipom/dev/pluginsample

他のコードはサンプルであまり内容がないため、プラグインの1実装と PluginExecutorのみ解説。

BarPlugin.java

@Component("barPlugin")
@Slf4j
public class BarPlugin implements Plugin {

  @Override
  public PluginOutput execute(PluginInput pluginInput) {
    log.info("BarPlugin is running!");
    return new PluginOutput();
  }
}
  • coreパッケージのPluginインターフェースを実装
  • PluginExecutorにDIするため @Component が必要
  • @Component("barPlugin")"barPlugin"プラグイン名としてapplication.ymlに記述する

PluginExecutor.java

@RequiredArgsConstructor
@Component
@Slf4j
public class PluginExecutor {

  private final PluginConfig pluginConfig;
  private final Map<String, Plugin> pluginMap;

  public void execute(String pluginGroupName) {
    // load config
    List<String> pluginNames = pluginConfig.pluginMap()
        .getOrDefault(pluginGroupName, null);

    // if no plugin config, return
    if (pluginNames == null) {
      return;
    }

    // execute plugins one by one
    for (var pluginName : pluginNames) {
      Plugin plugin = pluginMap.get(pluginName);
      PluginInput pluginInput = new PluginInput(); // you can add any input to this class
      PluginOutput pluginOutput = plugin.execute(pluginInput);
      // consume pluginOutput if necessary
    }
  }
}

おわりに

Spring BootのDIや設定ファイル管理の機能を使うと簡単にプラグインアーキテクチャが実現できた。実装すると検討ポイントや保守性に関するメリットがよくわかった。