最後のクラスでは、同期ブロッキング、非同期非ブロッキング、インプロセス、プロセス間メソッドなど、さまざまなアプリケーション シナリオにおけるいくつかの異なる実装メソッドに焦点を当てて、オブザーバー パターンの原理、実装、およびアプリケーション シナリオを学びました。悟る。
同期ブロッキングは、主にコードの分離を目的とした最も古典的な実装方法です。非同期非ブロッキングは、コードの分離を達成できるだけでなく、コードの実行効率も向上します。オブザーバー モードのプロセス間の分離はより徹底されており、一般にメッセージ キューを使用して実現されます。異なるプロセス間の、観察される側と観察される側の間の相互作用。
今日は、非同期でノンブロッキングのオブザーバー モードに焦点を当て、Google Guava EventBus に似た一般的なフレームワークを実装します。このレッスンを完了すると、フレームワークを実装するのは難しくないことがわかります。
それでは早速、今日の学習を正式に始めましょう!
非同期ノンブロッキングオブザーバーパターンのシンプルな実装
前回のレッスンでは、非同期ノンブロッキング オブザーバー モードでは、汎用性や再利用性をまったく考慮せずに単純なバージョンを実装するのが実際には非常に簡単であると述べました。
2 つの実装があります。1 つは、各 handleRegSuccess() 関数でコード ロジックを実行する新しいスレッドを作成すること、もう 1 つは、UserController の register() 関数でスレッド プールを使用して、各オブザーバーの handleRegSuccess() 関数を実行することです。2 つの実装の具体的なコードは次のとおりです。
// 第一种实现方式,其他类代码不变,就没有再重复罗列
public class RegPromotionObserver implements RegObserver {
private PromotionService promotionService; // 依赖注入
@Override
public void handleRegSuccess(long userId) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
promotionService.issueNewUserExperienceCash(userId);
}
});
thread.start();
}
}
// 第二种实现方式,其他类代码不变,就没有再重复罗列
public class UserController {
private UserService userService; // 依赖注入
private List<RegObserver> regObservers = new ArrayList<>();
private Executor executor;
public UserController(Executor executor) {
this.executor = executor;
}
public void setRegObservers(List<RegObserver> observers) {
regObservers.addAll(observers);
}
public Long register(String telephone, String password) {
//省略输入参数的校验代码
//省略userService.register()异常的try-catch代码
long userId = userService.register(telephone, password);
for (RegObserver observer : regObservers) {
executor.execute(new Runnable() {
@Override
public void run() {
observer.handleRegSuccess(userId);
}
});
}
return userId;
}
}
1つ目の実装方法では、スレッドの作成と破棄を頻繁に行うと時間がかかり、同時スレッド数を制御できず、スレッドを作りすぎるとスタックオーバーフローが発生します。2 番目の実装方法ではスレッド プールを使用して最初の実装方法の問題を解決しますが、スレッド プールと非同期実行ロジックが register() 関数で結合されるため、ビジネス コードのこの部分のメンテナンス コストが増加します。
ニーズがさらに厳しく、同期ブロッキングと非同期非ブロッキングを柔軟に切り替える必要がある場合は、UserController のコードを常に変更する必要があります。さらに、プロジェクト内で複数のビジネス モジュールが非同期ノンブロッキング オブザーバー モードを使用する必要がある場合、そのようなコード実装を再利用することはできません。
フレームワークの役割は、実装の詳細を隠し、開発の困難さを軽減し、コードの再利用を実現し、ビジネス コードと非ビジネス コードを分離し、プログラマーがビジネス開発に集中できるようにすることであることを私たちは知っています。非同期ノンブロッキング オブザーバー モードの場合、これをフレームワークに抽象化してこの効果を実現することもできます。このフレームワークが、このレッスンで説明する EventBus です。
EventBus フレームワークの機能要件の概要
EventBus は「イベント バス」と訳され、オブザーバー パターンを実装するためのスケルトン コードを提供します。このフレームワークに基づいて、最初から開発することなく、独自のビジネス シナリオに Observer パターンを簡単に実装できます。その中でも、Google Guava EventBus は有名な EventBus フレームワークであり、非同期ノンブロッキング モードだけでなく、同期ブロッキング モードもサポートしています
それでは、Guava EventBusにはどのような機能があるのか見ていきましょう。前回のレッスンでのユーザー登録の例を引き続き Guava EventBus で再実装してみましょう。コードは次のとおりです。
public class UserController {
private UserService userService; // 依赖注入
private EventBus eventBus;
private static final int DEFAULT_EVENTBUS_THREAD_POOL_SIZE = 20;
public UserController() {
//eventBus = new EventBus(); // 同步阻塞模式
eventBus = new AsyncEventBus(Executors.newFixedThreadPool(DEFAULT_EVENTBUS_THREAD_POOL_SIZE)); // 异步非阻塞模式
}
public void setRegObservers(List<Object> observers) {
for (Object observer : observers) {
eventBus.register(observer);
}
}
public Long register(String telephone, String password) {
//省略输入参数的校验代码
//省略userService.register()异常的try-catch代码
long userId = userService.register(telephone, password);
eventBus.post(userId);
return userId;
}
}
public class RegPromotionObserver {
private PromotionService promotionService; // 依赖注入
@Subscribe
public void handleRegSuccess(long userId) {
promotionService.issueNewUserExperienceCash(userId);
}
}
public class RegNotificationObserver {
private NotificationService notificationService;
@Subscribe
public void handleRegSuccess(long userId) {
notificationService.sendInboxMessage(userId, "...");
}
}
EventBus フレームワークを利用して実装したオブザーバー モードは、ゼロから作成したオブザーバー モードと比較して、大規模な処理という点ではほぼ同じですが、どちらも Observer の定義と register() 関数による Observer の登録が必要です。 pass 関数 (EventBus の post() 関数など) を呼び出して、メッセージをオブザーバーに送信します (メッセージは EventBus ではイベントと呼ばれます)。
ただし、実装の詳細に関しては多少異なります。EventBus に基づいて、Observer インターフェイスを定義する必要はなく、任意のタイプのオブジェクトを EventBus に登録できます。また、クラス内のどの関数がオブザーバーによって送信されたメッセージを受信できるかを示すために @Subscribe アノテーションが使用されます。
次に、Guava EventBus のいくつかの主要なクラスと関数について詳しく説明します。
- イベントバス、非同期イベントバス
Guava EventBus によって公開されるすべての呼び出し可能なインターフェイスは、EventBus クラスにカプセル化されます。このうち、EventBus は同期ブロッキングのオブザーバー モードを実装し、AsyncEventBus は EventBus を継承して非同期ノンブロッキング オブザーバー モードを提供します。具体的な使い方は以下の通りです。
EventBus eventBus = new EventBus(); // 同步阻塞模式
EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(8));// 异步阻塞模式
- register() 関数
EventBus クラスは、オブザーバーを登録するための register() 関数を提供します。具体的な関数定義は以下の通りです。任意のタイプ (オブジェクト) のオブザーバーを受け入れることができます。古典的な Observer パターンの実装では、 register() 関数は、同じ Observer インターフェイスを実装するクラス オブジェクトを受け入れる必要があります。
public void register(Object object);
- unregister() 関数
register() 関数と比較して、unregister() 関数は EventBus からオブザーバーを削除するために使用されます。多くは説明しませんが、具体的な関数の定義は次のとおりです。
public void unregister(Object object);
- post()関数
EventBus クラスは、オブザーバーにメッセージを送信するための post() 関数を提供します。具体的な関数定義は次のとおりです。
public void post(Object event);
クラシック オブザーバー モードとの違いは、post() 関数を呼び出してメッセージを送信するときに、メッセージがすべてのオブザーバーに送信されるのではなく、一致するオブザーバーに送信されることです。いわゆる matchable とは、受信できるメッセージ タイプが、送信されたメッセージ (ポスト関数定義内のイベント) タイプの親クラスであることを意味します。例を挙げて説明しましょう。
たとえば、AObserver が受信できるメッセージ タイプは XMsg、BObserver が受信できるメッセージ タイプは YMsg、CObserver が受信できるメッセージ タイプは ZMsg です。このうち、XMsg は YMsg の親クラスです。次のようにメッセージを送信すると、メッセージを受信できる対応する一致可能なオブザーバーは次のとおりです。
XMsg xMsg = new XMsg();
YMsg yMsg = new YMsg();
ZMsg zMsg = new ZMsg();
post(xMsg); => AObserver接收到消息
post(yMsg); => AObserver、BObserver接收到消息
post(zMsg); => CObserver接收到消息
各オブザーバーが受信できるメッセージ タイプはどこで定義されているのかと疑問に思われるかもしれません。Guava EventBus の最も特別な機能の 1 つである @Subscribe アノテーションを見てみましょう。
- @Subscribe アノテーション
EventBus は @Subscribe アノテーションを使用して、関数が受信できるメッセージの種類を示します。具体的な使用コードは以下の通りです。DObserver クラスでは、@Subscribe を介して 2 つの関数 f1() と f2() にアノテーションを付けました。
public DObserver {
//...省略其他属性和方法...
@Subscribe
public void f1(PMsg event) { //... }
@Subscribe
public void f2(QMsg event) { //... }
}
DObserver クラス オブジェクトが register() 関数を通じて EventBus に登録されると、EventBus は @Subscribe アノテーションに従って f1() と f2() を見つけ、2 つの関数が受信できるメッセージ タイプ (PMsg->f1、PMsg->f1、 QMsg -> f2)。post() 関数を通じてメッセージ (QMsg メッセージなど) を送信すると、EventBus は前のレコード (QMsg->f2) を通じて対応する関数 (f2) を呼び出します。
EventBus フレームワークを手動で実装する
Guava EventBus の機能をわかりやすく説明しましたが、一般的に言って、それは比較的単純です。次に、ホイールの作成と EventBus の「コテージ」を繰り返します。
EventBus の 2 つのコア関数 register() と post() の実装原理に注目してみましょう。これらを理解すると、基本的に EventBus フレームワーク全体を理解できます。次の 2 つの図は、これら 2 つの機能の実装概略図です。
この図から、最も重要なデータ構造は Observer レジストリであることがわかります。Observer レジストリは、メッセージの種類とメッセージを受信できる関数の間の対応を記録します。register() 関数を呼び出してオブザーバーを登録すると、EventBus は @Subscribe アノテーションを解析してオブザーバー レジストリを生成します。メッセージを送信するために post() 関数が呼び出されるとき、EventBus はレジストリを通じてメッセージを受信できる対応する関数を見つけ、オブジェクトを動的に作成し、Java リフレクション構文を通じて関数を実行します。同期ブロッキング モードの場合、EventBus は対応する関数を 1 つのスレッドで順番に実行します。非同期ノンブロッキング モードの場合、EventBus はスレッド プールを通じて対応する関数を実行します。
原理を理解すれば、実装ははるかに簡単になります。小さなフレームワーク全体のコード実装には、EventBus、AsyncEventBus、Subscribe、ObserverAction、ObserverRegistry の 5 つのクラスが含まれています。次に、これら 5 つのクラスを順番に見てみましょう。
1.購読する
Subscribe は、オブザーバー内のどの関数がメッセージを受信できるかを示すために使用されるアノテーションです。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Beta
public @interface Subscribe {}
2.オブザーバーアクション
ObserverAction クラスは @Subscribe アノテーション メソッドを表すために使用されます。ここで、target はオブザーバー クラスを表し、method はメソッドを表します。これは主に ObserverRegistry オブザーバー レジストリで使用されます。
public class ObserverAction {
private Object target;
private Method method;
public ObserverAction(Object target, Method method) {
this.target = Preconditions.checkNotNull(target);
this.method = method;
this.method.setAccessible(true);
}
public void execute(Object event) { // event是method方法的参数
try {
method.invoke(target, event);
} catch (InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
3.オブザーバーレジストリ
ObserverRegistry クラスは前述の Observer Registry であり、最も複雑なクラスであり、フレームワークのほぼすべてのコア ロジックがこのクラス内にあります。このクラスは Java のリフレクション構文を多用していますが、コード全体としては難解ではなく、その中でも上手いところは CopyOnWriteArraySet の使い方です。
CopyOnWriteArraySet は、名前が示すとおり、データの書き込み時に新しいセットを作成し、元のデータを新しいセットに複製し、新しいセットにデータを書き込んだ後、古いセットを新しいセットで置き換えます。このようにして、データの書き込み時にデータの読み取り動作が影響を受けないことが保証され、同時読み取りと書き込みの問題が解決されます。さらに、CopyOnWriteSet はロックすることで同時書き込みの競合も回避します。具体的な機能については、CopyOnWriteSet クラスのソースコードを確認すれば一目瞭然です。
public class ObserverRegistry {
private ConcurrentMap<Class<?>, CopyOnWriteArraySet<ObserverAction>> registry = new ConcurrentHashMap<>();
public void register(Object observer) {
Map<Class<?>, Collection<ObserverAction>> observerActions = findAllObserverActions(observer);
for (Map.Entry<Class<?>, Collection<ObserverAction>> entry : observerActions.entrySet()) {
Class<?> eventType = entry.getKey();
Collection<ObserverAction> eventActions = entry.getValue();
CopyOnWriteArraySet<ObserverAction> registeredEventActions = registry.get(eventType);
if (registeredEventActions == null) {
registry.putIfAbsent(eventType, new CopyOnWriteArraySet<>());
registeredEventActions = registry.get(eventType);
}
registeredEventActions.addAll(eventActions);
}
}
public List<ObserverAction> getMatchedObserverActions(Object event) {
List<ObserverAction> matchedObservers = new ArrayList<>();
Class<?> postedEventType = event.getClass();
for (Map.Entry<Class<?>, CopyOnWriteArraySet<ObserverAction>> entry : registry.entrySet()) {
Class<?> eventType = entry.getKey();
Collection<ObserverAction> eventActions = entry.getValue();
if (postedEventType.isAssignableFrom(eventType)) {
matchedObservers.addAll(eventActions);
}
}
return matchedObservers;
}
private Map<Class<?>, Collection<ObserverAction>> findAllObserverActions(Object observer) {
Map<Class<?>, Collection<ObserverAction>> observerActions = new HashMap<>();
Class<?> clazz = observer.getClass();
for (Method method : getAnnotatedMethods(clazz)) {
Class<?>[] parameterTypes = method.getParameterTypes();
Class<?> eventType = parameterTypes[0];
if (!observerActions.containsKey(eventType)) {
observerActions.put(eventType, new ArrayList<>());
}
observerActions.get(eventType).add(new ObserverAction(observer, method));
}
return observerActions;
}
private List<Method> getAnnotatedMethods(Class<?> clazz) {
List<Method> annotatedMethods = new ArrayList<>();
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(Subscribe.class)) {
Class<?>[] parameterTypes = method.getParameterTypes();
Preconditions.checkArgument(parameterTypes.length == 1,
"Method %s has @Subscribe annotation but has %s parameters."
+ "Subscriber methods must have exactly 1 parameter.",
method, parameterTypes.length);
annotatedMethods.add(method);
}
}
return annotatedMethods;
}
}
4.イベントバス
EventBus は、ブロッキング同期オブザーバー パターンを実装します。コードを見ると疑問に思うかもしれませんが、これは明らかにスレッド プール Executor を使用しています。実はMoreExecutors.directExecutor()はGoogle Guavaが提供しているツールクラスで、マルチスレッドのように見えますが、実はシングルスレッドです。この実装の理由は主に、コード ロジックを AsyncEventBus と統合し、コードの再利用を実現することです。
public class EventBus {
private Executor executor;
private ObserverRegistry registry = new ObserverRegistry();
public EventBus() {
this(MoreExecutors.directExecutor());
}
protected EventBus(Executor executor) {
this.executor = executor;
}
public void register(Object object) {
registry.register(object);
}
public void post(Object event) {
List<ObserverAction> observerActions = registry.getMatchedObserverActions(event);
for (ObserverAction observerAction : observerActions) {
executor.execute(new Runnable() {
@Override
public void run() {
observerAction.execute(event);
}
});
}
}
}
5.非同期イベントバス
EventBus を使用すると、AsyncEventBus の実装が非常に簡単になります。非同期ノンブロッキング オブザーバー モードを実装するには、MoreExecutors.directExecutor() を引き続き使用することはできませんが、コンストラクターの呼び出し元によってスレッド プールに注入する必要があります。
public class AsyncEventBus extends EventBus {
public AsyncEventBus(Executor executor) {
super(executor);
}
}
これまでのところ、かなり使いやすい EventBus を実装するために 200 行未満のコードを使用しました。機能的に言えば、Google Guava EventBus とほぼ同じです。ただし、Google Guava EventBus のソース コードを見ると、実装の詳細に関して、現在の実装と比較して、メッセージ一致関数を見つけるためのアルゴリズムの最適化など、実際に多くの最適化が行われていることがわかります。レジストリにあります。時間があれば、ソースコードを読むことをお勧めします。
重要なレビュー
さて、今日の内容はここまでです。マスターしておくべき内容をまとめて復習しましょう。
フレームワークの機能には、実装の詳細の隠蔽、開発の困難さの軽減、コードの再利用の実現、ビジネス コードと非ビジネス コードの分離、プログラマーがビジネス開発に集中できるようにすることが含まれます。非同期ノンブロッキング オブザーバー モードの場合、これをフレームワークに抽象化してこの効果を実現することもできます。このフレームワークが、このレッスンで説明した EventBus です。EventBus は「イベント バス」と訳され、オブザーバー パターンを実装するためのスケルトン コードを提供します。このフレームワークに基づいて、最初から開発することなく、独自のビジネス シナリオに Observer パターンを簡単に実装できます。
ビジネス開発には技術的な課題はないと考えている人も多いですが、実際、ビジネス開発には、今日言及した EventBus など、ビジネス以外の機能の開発も数多く含まれます。通常のビジネス開発では、これらの非ビジネス機能や再利用可能な機能を上手に抽象化し、それらを共通のフレームワークに積極的に実装する必要があります。
クラスディスカッション
今日のコンテンツの 2 番目のモジュール「EventBus フレームワークの機能要件の紹介」では、Guava EventBus を使用して UserController を再実装しましたが、実際、コードはまだ十分に分離されていません。UserController は依然として、スレッド プールの作成やオブザーバーの登録など、オブザーバー パターンに関連する多くの非ビジネス コードを結合します。UserController をよりビジネス重視にするためのリファクタリングの提案はありますか?