覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

MBassador - EventBus内で発生した例外をハンドリングする

MBassadorを用いてイベントのハンドリングを行っている場合、デフォルトではハンドラ内部で例外が発生しても何事も無かったようになってしまい、エラーが発生したことすら分からないような状態になってしまいます。

さすがにこれはまずいので、基本的にはエラーをハンドリングをするためのエラーハンドラをEventBusに対してセットする必要があります。


エラーハンドラをセットしない場合

まず、エラーハンドラをセットしなかった場合の動きを確認しておきます

■Handlerの定義

ハンドラ内であえてIllegalArgumentExceptionをスローするようにしています

import net.engio.mbassy.listener.Handler;

public class MsgHandler {

    @Handler
    public void handlMessage(String msg){
        System.out.println("handle start. msg=" + msg);

        System.out.println("---------------");

        throw new IllegalArgumentException("null parameter");
    }
}

■EventBusを作成してメッセージ送信

EventBusに上で定義したハンドラをセットしてメッセージを送信しています。

import net.engio.mbassy.bus.MBassador;

public static void main(String[] args) {

    MBassador<Object> bus = new MBassador<>();
    bus.subscribe(new MsgHandler());

    bus.publishAsync("msg-1");

    try {
        Thread.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("\n>>> App end");
}

■実行結果

handle start. msg=msg-1
---------------

>>> App end

エラー(IllegalArgumentException)が発生したことが全く分からない状態になっています


エラーハンドラをセットする場合

次にEventBusに対してエラーハンドラをセットするサンプルです。
(メッセージのHandlerには先の例と同じMsgHandlerクラスを使います)

■エラーハンドラの定義

エラーハンドラはIPublicationErrorHandlerインターフェースを実装したクラスとして定義する必要があります
(EventBus内での例外発生時に、handleErrorメソッドにエラーの詳細情報が渡されて呼び出されます。)

import net.engio.mbassy.bus.error.IPublicationErrorHandler;
import net.engio.mbassy.bus.error.PublicationError;

final class EventBusErrorHandler implements IPublicationErrorHandler {


    @Override
    public void handleError(PublicationError publicationError) {
        System.out.println("publicationError = " + publicationError);

        publicationError.getCause().printStackTrace();
    }
}

■EventBusを作成してメッセージ送信

EventBusに上で定義したハンドラをセットしてメッセージを送信しています。
先の例とは違いIBusConfigurationを生成してIBusConfiguration#addPublicationErrorHandlerでエラーハンドラをセットします。
そして生成したIBusConfigurationをEventBus(MBassadorのコンストラクタ)に渡します。

import net.engio.mbassy.bus.MBassador;
import net.engio.mbassy.bus.config.BusConfiguration;
import net.engio.mbassy.bus.config.Feature;
import net.engio.mbassy.bus.config.IBusConfiguration;

public static void main(String[] args) {

    IBusConfiguration config = new BusConfiguration()
            .addFeature(Feature.SyncPubSub.Default())
            .addFeature(Feature.AsynchronousHandlerInvocation.Default())
            .addFeature(Feature.AsynchronousMessageDispatch.Default())
            .addPublicationErrorHandler(new EventBusErrorHandler());

    MBassador<Object> bus = new MBassador<>(config);
    bus.subscribe(new MsgHandler());

    bus.publishAsync("msg-1");

    try {
        Thread.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("\n>>> App end");
}

■実行結果

handle start. msg=msg-1
---------------
publicationError = PublicationError{
    cause=java.lang.reflect.InvocationTargetException
    message='Error during invocation of message handler. There might be an access rights problem. Do you use non public inner classes?'
    handler=public void trial.app.mbassador.ErrorMsgHandler.handlMessage(java.lang.String)
    listener=trial.app.mbassador.ErrorMsgHandler@68de145
    publishedMessage=msg-1}
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at net.engio.mbassy.dispatch.ReflectiveHandlerInvocation.invoke(ReflectiveHandlerInvocation.java:29)
    at net.engio.mbassy.dispatch.MessageDispatcher.dispatch(MessageDispatcher.java:30)
    at net.engio.mbassy.dispatch.FilteredMessageDispatcher.dispatch(FilteredMessageDispatcher.java:42)
    at net.engio.mbassy.subscription.Subscription.publish(Subscription.java:72)
    at net.engio.mbassy.bus.MessagePublication.execute(MessagePublication.java:49)
    at net.engio.mbassy.bus.AbstractSyncAsyncMessageBus$1.run(AbstractSyncAsyncMessageBus.java:67)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.IllegalArgumentException: null parameter
    at trial.app.mbassador.ErrorMsgHandler.handlMessage(ErrorMsgHandler.java:13)
    ... 11 more

>>> App end

メッセージのハンドラ内で例外がスローされた際に、エラーハンドラが動作していることが分かります。
エラー発生時の原因例外はPublicationError#getCauseから辿ることができます。



関連エントリ

String#joinで文字列連結

Java8で増えたString#joinを使うと配列やListの要素を文字列連結するのが楽です。地味に便利です。

Listをカンマで連結する例で動きの確認をしておきます


複数要素の場合

List<String> twoList = Arrays.asList("A", "B");
List<String> threeList = Arrays.asList("A", "B", "C");

System.out.println(twoList + " -> " + String.join(",", twoList));
System.out.println(threeList + " -> " + String.join(",", threeList));

■実行結果

[A, B] -> A,B
[A, B, C] -> A,B,C

想定通りに指定したデリミタで各要素が連結されます


1要素の場合

List<String> singleList = Collections.singletonList("Hello");

System.out.println(singleList + " -> " + String.join(",", singleList));

■実行結果

[Hello] -> Hello

1要素でも正常に動作します。当然ですが連結結果にデリミタの文字は現れません。


要素数=0の場合

List<String> emptyList = Collections.emptyList();

System.out.println(emptyList + " -> " + String.join(",", emptyList));

■実行結果

[] -> 

空Listを連結すると空文字列が返されます


要素にnullを含む場合

List<String> includeNullList = Arrays.asList("A", null, "C");

System.out.println(includeNullList + " -> " + String.join(",", includeNullList));

■実行結果

[A, null, C] -> A,null,C

nullの要素は、"null"という文字列として扱われて連結されます


List自体がnullの場合

List<String> nullList = null;
System.out.println(nullList + " -> " + String.join(",", nullList));

■実行結果

Exception in thread "main" java.lang.NullPointerException
    at java.util.Objects.requireNonNull(Objects.java:203)
    at java.lang.String.join(String.java:2501)
    at trial.app.string.StringJoinLauncher.main(StringJoinLauncher.java:26)

List自体がnullの場合はNullPointerExceptionがスローされます



関連エントリ

アプリ実行中に条件に合ったクラスを抽出する

前回のエントリでFastClasspathScannerを利用して特定のアノテーションが付与されたクラスを抽出するサンプルを実行しました。


FastClasspathScannerでは、他にも色々な条件でクラス等を抽出できます。いくつかサンプルを試してみます


インターフェースクラスを全部取得する

あるパッケージ以下に存在する全てのインターフェースクラスを抽出する場合、以下のAPIを利用します

  • FastClasspathScanner#matchAllInterfaceClasses


■サンプルコード

sample.app.domainというパッケージ以下に存在する全インターフェースクラスを取得する例です

import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;

new FastClasspathScanner("sample.app.domain")
        .matchAllInterfaceClasses(
                clazz -> System.out.println("Found class: " + clazz.getName()))
        .scan();

■実行結果

Found class: sample.app.domain.Interface1
Found class: sample.app.domain.Interface1$InnerInterface
Found class: sample.app.domain.Interface2
Found class: sample.app.domain.SampleInterface


特定のインターフェースを実装したクラスを取得する

特定のインターフェースを実装したクラスを取得する場合、以下のAPIを利用します

  • FastClasspathScanner#matchClassesImplementing


■サンプルコード

sample.appというパッケージ以下に存在するクラスの中でSampleInterfaceというインターフェースを実装したものを取得する例です

import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;

new FastClasspathScanner("sample.app")
        .matchClassesImplementing(SampleInterface.class,
                clazz -> System.out.println("Found class: " + clazz.getName()))
        .scan();

■実行結果

Found class: sample.app.domain.Impl1
Found class: sample.app.domain.Impl3


特定のインターフェースを継承したサブインターフェースを取得する

特定のインターフェースを継承したインターフェースクラスを取得する場合、以下のAPIを利用します

  • FastClasspathScanner#matchSubinterfacesOf


■サンプルコード

sample.appというパッケージ以下に存在するインターフェースの中でSampleInterfaceというインターフェースを継承したものを取得する例です

import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;

new FastClasspathScanner("sample.app")
        .matchSubinterfacesOf(SampleInterface.class,
                clazz -> System.out.println("Found class: " + clazz.getName()))
        .scan();

■実行結果

Found class: sample.app.domain.Interface1


特定のクラスを継承したサブクラスを取得する

特定のクラスを継承したサブクラスを取得する場合、以下のAPIを利用します

  • FastClasspathScanner#matchSubclassesOf


■サンプルコード

sample.appというパッケージ以下に存在するクラスの中でAbstractSampleというクラスを継承したものを取得する例です

import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;

new FastClasspathScanner("sample.app")
        .matchSubclassesOf(AbstractSample.class,
                clazz -> System.out.println("Found class: " + clazz.getName()))
        .scan();

■実行結果

Found class: sample.app.domain.SubClass
Found class: sample.app.domain.SubClass$InnerSubClass


メソッドに特定のアノテーションが付与されたものを取得する

メソッドに特定のアノテーションが付与されているクラスとそのメソッドを取得する場合以下のAPIを利用します

  • FastClasspathScanner#matchClassesWithMethodAnnotation


■サンプルコード

sample.app.domainというパッケージ以下に存在するクラスの中で@MethodAnnotationというアノテーションが付与されているメソッドを持つクラスとそのメソッドを取得する例です

import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;

new FastClasspathScanner("sample.app.domain")
        .matchClassesWithMethodAnnotation(MethodAnnotation.class,
                (clazz, method) -> System.out.println("Found : " + clazz.getName() + "#" + method.getName()))
        .scan();

■実行結果

Found : sample.app.domain.SimpleClass#printHello
Found : sample.app.domain.SubClass#execute1
Found : sample.app.domain.SubClass#execute2



関連エントリ

特定のアノテーションが付与されたクラスを実行時に抽出したい

Javaアプリケーションの処理内で、特定のパッケージ配下にあるクラスの中で、指定の条件を満たすものを抽出したい場合があります。

例えば、アプリ実行中に特定のアノテーションが付与されたクラスを探したい 等がこれに該当します。

このような場合、FastClasspathScannerを利用すると簡単に実現できます。

FastClasspathScannerを用いると、classpathをスキャンして指定の条件に合うクラスを抽出することができます。


(追記: FastClasspathScannerClassGraph に名称が変更された? ようです)


準備

pom.xmlに以下を追記します。

<dependency>
    <groupId>io.github.lukehutch</groupId>
    <artifactId>fast-classpath-scanner</artifactId>
    <version>{lib-version}</version>
</dependency>

今回のサンプルではバージョンとして 2.0.13 を指定しました


サンプル

@SampleAnnotation というアノテーションを用意して、sample.appパッケージ以下にあるクラスの中でそのアノテーションが付与されているクラスを取得します。


■サンプルコード

クラスに付与するアノテーション(@SampleAnnotation)は以下です。(何の変哲もないアノテーションです)

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface SampleAnnotation {
}


FastClasspathScannerを用いて@SampleAnnotationが付与されたクラスを探す処理は以下のように記述できます。

import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;


public class SampleMain {

    public static void main(String[] args) {
        new FastClasspathScanner("sample.app")
                .matchClassesWithAnnotation(SampleAnnotation.class,
                        clazz -> System.out.println("Found class : " + clazz.getName()))
                .scan();
    }
}

FastClasspathScanner#matchClassesWithAnnotation で 対象のアノテーションを指定しています


■実行結果

Found class : sample.app.domain.Logic1
Found class : sample.app.util.Util1

(条件を明確に記載していないので分かりにくいのですが)@SampleAnnotation が 付与されたクラスだけが抽出されました


補足

今回は、特定のアノテーションが付与されたクラスを探すサンプルを実行しましたが、FastClasspathScannerでは他にも色々な条件でクラス等を探すことができます。



関連エントリ

Guice - Listに名前を付けて@NamedでInjectionしたい

Guiceを使っている場合に@NamedでListに名前を付けて依存関係を解決するサンプルです。

今回のサンプルではList<String>の値に sample.list という名前を付けてInjection出来るようにします。


依存関係を記述

■ListがInjectionされるクラス

コンストラクタで@Namedsample.list という値を指定して List<String> の値を受け取るようにしてます

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.List;

@Singleton
public class SampleList {

    private final List<String> list = new ArrayList<>();

    @Inject
    public SampleList(@Named("sample.list") List<String> srcList) {
        this.list.addAll(srcList);
    }

    public List<String> list() {
        return list;
    }
}


■GuiceのModuleのクラスを定義

GuiceのModuleのクラスを定義して、sample.list という名前で、文字列Listをbindします
TypeLiteralを利用するのがポイントです

import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;

import java.util.Arrays;
import java.util.List;

public class SampleModule extends AbstractModule {

    @Override
    protected void configure() {
        // 紐づけしたいデータ
        List<String> targetList = Arrays.asList("data1", "data2", "data3");

        bind(new TypeLiteral<List<String>>() {})
                .annotatedWith(Names.named("sample.list")).toInstance(targetList);

        // これではダメ
        // bind(List.class)
        //         .annotatedWith(Names.named("sample.list")).toInstance(targetList);
    }
}


実行

■実行用のサンプルクラス

上で定義したModuleを使ってGuiceのInjectorを生成します。これで準備完了です。

import com.google.inject.Guice;
import com.google.inject.Injector;

public class Launcher {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new SampleModule());

        SampleList sampleList = injector.getInstance(SampleList.class);

        System.out.println("SampleList#list -> " + sampleList.list());
    }
}


■実行結果

SampleList#list -> [data1, data2, data3]

sample.list という名前を付けた文字列ListがうまくInjectionされていることが分かります