はじめに
"ソフトウェアアーキテクチャの基礎" (https://tech-yoshikipom.hatenablog.com/entry/2023/01/24/220503 で紹介した本) に出てきたプラグインアーキテクチャ(またはマイクロカーネルアーキテクチャ)をWebシステムで使う時に考えたことやSpring Bootを用いたプラグインアーキテクチャの実装を紹介する。アーキテクチャのイメージは以下。
プラグインアーキテクチャを使う時に考えたこと
Webシステムにおけるプラグインアーキテクチャの使い所
主に以下の二つの要求をシンプルに解決できると思う。
- 機能面: 入力(ユーザ、APIのクライアントシステム、リクエストデータ等)によって振る舞いを追加したり減らしたりしたい
- 保守性: たくさんの手続型のロジックをコンポーネント化 -> 責務の明確化、付け替えや追加が容易
機能面のほうは主にA/Bテストがしたかったり、複数のクライアントからの要求に柔軟に答える必要がでたときの話である。単純なif文やStrategyパターンだけで実装すると入力ごとの処理に差異が増えてくると差分が分かりづらくなり、保守性が一気に落ちることがある。保守性の方は実際に”プラグイン”が要件として必要なわけではないが、プラグインとして処理を実装することで開発体験を向上できる。
検討事項
プラグインアーキテクチャを使うときは以下の点は設計しておきたい。
ここで実装するサンプルでは「コンパイルベースの設定ファイル」、「プラグインの内容はダミー」、「並列実行は未対応」とする。
プラグインアーキテクチャの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
- プラグインの実装をする。今回はサブパッケージは作らず、実装はサンプルでログを出力するのみ。
実装
他のコードはサンプルであまり内容がないため、プラグインの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 } } }
pluginConfig
-> 設定ファイルの情報をマッピングしたPOJO。pluginMap
-> すべての読み込めるプラグインクラスを持つマップ。キーはpluginGroupName
。Map<String, Plugin>
とすることでPluginインターフェスの実装をSpring BootがDIしてくれるpluginInput
/pluginOutput
の中身は特に実装していない。アプリケーションに合わせて調整が必要。
おわりに
Spring BootのDIや設定ファイル管理の機能を使うと簡単にプラグインアーキテクチャが実現できた。実装すると検討ポイントや保守性に関するメリットがよくわかった。