メインコンテンツまでスキップ

Plugin

このドキュメントの目的

PluginLoaderパターンを使って、拡張可能な機能を実装できるようになることが目標です。

所要時間

⏱️ 約30分(実装 + テスト)

前提知識


Pluginパターンとは

複数の実装を動的に切り替える仕組み。

Map<Type, Service> services = PluginLoader.loadFromInternalModule(ServiceClass);
Service service = services.get(type);
service.execute(...);

用途:

  • ✅ Grant Type別のトークン生成(authorization_code, client_credentials等)
  • ✅ 認証方式別の処理(password, sms, fido2等)
  • ✅ 通知チャネル別の送信(fcm, apns, email等)

実装例: 通知送信Plugin

複数の通知チャネル(FCM, APNS, Email)を動的に切り替える例。


Step 1: Pluginインターフェース定義

ファイル: libs/idp-server-core/src/main/java/org/idp/server/core/notification/NotificationSender.java

package org.idp.server.core.notification;

public interface NotificationSender {

/**
* サポートする通知チャネル
*
* ⚠️ 重要: PluginLoaderがこのメソッドでキーを判定
*/
NotificationChannel supportedChannel();

/**
* 通知送信
*
* @param notification 通知内容
* @return 送信結果
*/
NotificationResult send(Notification notification);

/**
* 送信可能かチェック
*
* @param notification 通知内容
* @return 送信可能な場合true
*/
boolean canSend(Notification notification);
}

ポイント:

  • supportedChannel()メソッド必須(PluginLoaderが使用)
  • ✅ ビジネスロジックメソッド(send(), canSend()

Step 2: Plugin実装

FCM Plugin

ファイル: libs/idp-server-notification-fcm-adapter/src/main/java/org/idp/server/adapter/fcm/FcmNotificationSender.java

package org.idp.server.adapter.fcm;

import org.idp.server.core.notification.*;

/**
* FCM通知送信Plugin
*
* ⚠️ 重要: @Pluginアノテーション不要(PluginLoaderが自動検出)
*/
public class FcmNotificationSender implements NotificationSender {

@Override
public NotificationChannel supportedChannel() {
return NotificationChannel.FCM;
}

@Override
public NotificationResult send(Notification notification) {
// FCM固有の送信ロジック
String fcmToken = notification.recipient().fcmToken();
String message = notification.message();

// FCM API呼び出し
FcmClient fcmClient = new FcmClient();
FcmResponse response = fcmClient.send(fcmToken, message);

if (response.isSuccess()) {
return NotificationResult.success(notification.id());
} else {
return NotificationResult.failure(notification.id(), response.error());
}
}

@Override
public boolean canSend(Notification notification) {
return notification.recipient().hasFcmToken();
}
}

APNS Plugin

ファイル: libs/idp-server-notification-apns-adapter/src/main/java/org/idp/server/adapter/apns/ApnsNotificationSender.java

package org.idp.server.adapter.apns;

import org.idp.server.core.notification.*;

/**
* APNS通知送信Plugin
*/
public class ApnsNotificationSender implements NotificationSender {

@Override
public NotificationChannel supportedChannel() {
return NotificationChannel.APNS;
}

@Override
public NotificationResult send(Notification notification) {
// APNS固有の送信ロジック
String apnsToken = notification.recipient().apnsToken();
String message = notification.message();

// APNS API呼び出し
ApnsClient apnsClient = new ApnsClient();
ApnsResponse response = apnsClient.send(apnsToken, message);

if (response.isSuccess()) {
return NotificationResult.success(notification.id());
} else {
return NotificationResult.failure(notification.id(), response.error());
}
}

@Override
public boolean canSend(Notification notification) {
return notification.recipient().hasApnsToken();
}
}

Step 3: Pluginの使用(Handler/Service)

ファイル: libs/idp-server-core/src/main/java/org/idp/server/core/notification/NotificationHandler.java

package org.idp.server.core.notification;

import java.util.Map;
import org.idp.server.platform.plugin.PluginLoader;

public class NotificationHandler {

private final Map<NotificationChannel, NotificationSender> senders;

public NotificationHandler() {
// ✅ PluginLoader: 静的メソッド使用
this.senders = PluginLoader.loadFromInternalModule(NotificationSender.class);
}

/**
* 通知送信
*
* @param notification 通知内容
* @return 送信結果
*/
public NotificationResult send(Notification notification) {
// 1. 通知チャネル取得
NotificationChannel channel = notification.channel();

// 2. 対応するPlugin選択
NotificationSender sender = senders.get(channel);

if (sender == null) {
throw new UnsupportedNotificationChannelException(channel);
}

// 3. 送信可能かチェック
if (!sender.canSend(notification)) {
return NotificationResult.failure(
notification.id(),
"Cannot send notification: recipient does not have required token");
}

// 4. Plugin実行
return sender.send(notification);
}

/**
* サポートしている通知チャネル一覧
*/
public Set<NotificationChannel> supportedChannels() {
return senders.keySet();
}
}

ポイント:

  • PluginLoader.loadFromInternalModule()静的メソッド使用
  • Map<NotificationChannel, NotificationSender>で動的選択
  • ✅ Plugin実装の追加・削除が容易

Step 4: テスト

ユニットテスト

ファイル: libs/idp-server-core/src/test/java/org/idp/server/core/notification/NotificationHandlerTest.java

package org.idp.server.core.notification;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class NotificationHandlerTest {

@Test
void shouldLoadPlugins() {
NotificationHandler handler = new NotificationHandler();

// Pluginが読み込まれているか確認
Set<NotificationChannel> channels = handler.supportedChannels();

assertTrue(channels.contains(NotificationChannel.FCM));
assertTrue(channels.contains(NotificationChannel.APNS));
}

@Test
void shouldSendFcmNotification() {
NotificationHandler handler = new NotificationHandler();

Notification notification = Notification.fcm(
new NotificationId("test-id"),
new Recipient("fcm-token-123"),
"Test message"
);

NotificationResult result = handler.send(notification);

assertTrue(result.isSuccess());
}

@Test
void shouldThrowExceptionForUnsupportedChannel() {
NotificationHandler handler = new NotificationHandler();

Notification notification = Notification.custom(
new NotificationId("test-id"),
NotificationChannel.UNKNOWN, // 未サポート
new Recipient("token"),
"Test message"
);

assertThrows(
UnsupportedNotificationChannelException.class,
() -> handler.send(notification));
}
}

チェックリスト

Plugin実装前に以下を確認:

Pluginインターフェース定義

  • supported{Type}()メソッド定義(PluginLoaderがキー判定に使用)
  • ビジネスロジックメソッド定義

Plugin実装

  • インターフェース実装
  • supported{Type}()メソッド実装
  • 各Plugin固有のロジック実装

Pluginの使用

  • PluginLoader.loadFromInternalModule()静的メソッド使用
  • Map<Type, Plugin>で動的選択
  • null チェック(未サポートチャネル対応)

テスト

  • Plugin読み込みテスト
  • 各Plugin実行テスト
  • 未サポートチャネルエラーテスト

よくあるエラー

エラー1: PluginLoaderインスタンス化

// ❌ 間違い: インスタンス化不可
PluginLoader<NotificationSender> loader =
new PluginLoader<>(NotificationSender.class); // コンパイルエラー

// ✅ 正しい: 静的メソッド使用
Map<NotificationChannel, NotificationSender> senders =
PluginLoader.loadFromInternalModule(NotificationSender.class);

エラー2: supported{Type}()メソッドなし

// ❌ 間違い: PluginLoaderがキー判定できない
public interface NotificationSender {
// supported{Type}()メソッドがない!
NotificationResult send(Notification notification);
}

// ✅ 正しい: supported{Type}()必須
public interface NotificationSender {
NotificationChannel supportedChannel(); // ✅ 必須
NotificationResult send(Notification notification);
}

次のステップ

✅ Plugin実装をマスターした!

📖 次に読むべきドキュメント

  1. 外部サービス連携ガイド - HttpRequestExecutor使用

情報源: PluginLoader.java 最終更新: 2025-10-12