kagamihogeの日記

kagamihogeの日記です。

Spring BootでRabbitMQの送受信

Spring BootとSpring AMQPを使用してRabbitMQの送受信を行う。hello worldレベルのことをやる。ドキュメント的には https://docs.spring.io/spring-boot/docs/2.0.1.RELEASE/reference/htmlsingle/#boot-features-amqp のあたり。

準備

RabbitMQのインストールとGUIの管理画面を使えるようにしておく。

依存性の追加

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>

設定

ローカルにRabbitMQをインストールしてデフォルト設定の場合、特に何も設定しなくても良い。別マシンに接続するとか、ID・パスワードを入れるとかの場合、externalized configurationで設定する。以下はapplication.yamlでの設定例。

spring:
  rabbitmq:
    host: 192.168.10.23
    port: 5672
    username: kagamihoge
    password: xxxxxx
  main:
    web-environment: false

RabbitMQと直接の関係は無いが、web関連は不要なんでspring.main.web-environment=falseで抑制しておく。

指定可能な値の一覧 -> https://docs.spring.io/spring-boot/docs/2.0.1.RELEASE/reference/htmlsingle/#common-application-properties

受信

package kagamihoge.receive;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ReceiverApplication {
    public static void main(String[] args) throws InterruptedException {
        SpringApplication.run(ReceiverApplication.class, args);
    }

    @RabbitListener(queues = "sample-queue")
    public void receive(String msg) {
        System.out.println(msg);
    }
}

動かす前に http://localhost:15672 の管理コンソールからキューを、sample-queueという名前で、新規作成しておく。

動作確認は管理コンソールからメッセージ送信する。sample-queueを選んでPublish messageする。Propertiesにcontent_type=text/plainと入れればbyte[]じゃなくて文字列を送信できる。

送信

package kagamihoge.send;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SenderApplication implements CommandLineRunner {
    public static void main(String[] args) throws InterruptedException {
        SpringApplication.run(SenderApplication.class, args).close();;
    }

    @Autowired
    AmqpTemplate amqpTemplate;

    @Override
    public void run(String... args) throws Exception {
        Message message = new Message("msg".getBytes(), new MessageProperties());
        amqpTemplate.send("sample-exchange", null, message);
    }
}

動かす前に管理コンソールでexchangeをsample-exchangeという名前で作り、sample-queueにバインドさせておく。

受信側を起動させておいて、上の送信のコードを動かす。sample-exchangeに送信したメッセージがsample-queueに入って、それを受信、という流れが確認できる。

アトラス作品ファンのオフ会 真・眼鏡祭Ⅳ(2018/04/07)に行ってきた

4/7 真・眼鏡祭Ⅳ - Twiplaに行ってきました。

f:id:kagamihoge:20180407142821j:plain

眼鏡祭はペルソナを初めとするアトラス作品ファンコミュニティの大規模オフ会です。参加者数は200人強と、当日がPERSONA5 the Animationの第一話放映日、P3D - ペルソナ3 ダンシング・ムーンナイトおよびP5D - ペルソナ5 ダンシング・スターナイトの発売を控えてか、今回も大変な盛況ぶりでした。天気予報では雨が危ぶまれましたが、天候は晴れ。場所はキリストンカフェ東京を貸し切っての開催です。

当日の様子はTwitterハッシュタグ#眼鏡祭0407から追えます。

アトラス作品のコスプレがたくさんいるオフ会

眼鏡祭の特徴は、色々ありますが、なんといっても目を引くのはコスプレOKな点です。

こちらの全体写真はコスプレ参加者ほぼ全員の集合写真です。キャラクターの分布としては、今回はやはりP5が一大勢力でした。怪盗団やサブキャラクター、早くもP5D衣装の方もいました。ジョーカーは当然のように大人気です。参加者総数のうち7~8割は何らかのコスプレをしているため、集合写真のインパクトはかなりのインパクトがあります。

P5の勢いがあるため他作品は相対的に少ない傾向にありました。しかし、P3やP4のダンス衣装、D×2 真・女神転生 リベレーション魔神転生、女神異聞録、ペルソナ2、など、今となっては懐かしい作品をこよなく愛するコスプレの方、異形の悪魔コスなど、P5と比べれば少数派であっても、相変わらず存在感がありました。

主催曰く「コンテンツは参加者自身」

眼鏡祭はいわゆるオフ会で、その目的は参加者同士のリアルなコミュニケーションです。お酒を飲み、コスプレ肴に、ゲーム・アニメ・マンガなどオタクの話題で気兼ねなく一日中盛り上がる。基本的にはそれだけです。しかし、コスプレを良くするレイヤーさんに聞いてみても、コスプレOKで交流がメイン、しかもアトラス作品に絞った会でここまで大規模なものは他に無いようです。このシンプルだが強力なコンセプトに様々な人が魅力を感じ、集まって来ています。

参加者の年代は、10年近く続いている会ゆえに、30代以上が多数です。ただし、アトラス作品はP5が初めてで眼鏡祭に来た、という20代もかなり居ます。おおむね1~2割は初参加で、初めて来たのが自分だけ、という事にはなりません。新しい方がどんどん来るので、する事は毎回同じでも、いつも新鮮な驚きがあるのが眼鏡祭のすごいところです。

コスプレは最高の会話のきっかけ

眼鏡祭ではコスプレを仮装と呼びます*1。200人超のイベントで何らかの会話のとっかかりを作るのは、普通に考えれば相当な困難です。そこでコスプレの出番ですが、コスプレと呼べるほど凝ったもので無くてもいい、という意味を込めて「仮装」と呼ばれています。

あるキャラのコスプレをしてるということは、そのキャラや作品に何らかの思い入れがある、その意思表示です。コスプレをとっかかりにして会話が広がっていくのは眼鏡祭ではありふれた光景です。そのためにコスプレを眼鏡祭でだけはする、という人も一定数存在します。

もちろんコスプレはそれなりの敷居があります。凝ろうとすれば果てが無いですが、簡単なもので眼鏡祭は十分。コスプレに抵抗があるなら、普段使いの難しい各種のグッズを身に付けたり、モルガナのぬいぐるみを持ってきたり、千枝ちゃんのジャージ羽織るだけでも違います。

眼鏡祭ではコスプレの撮影がメインではなく、コミュニケーションの切欠にするのがメインの目的です。テーマパークでテンションを高めるためにグッズや簡単なフェイスペイントなどをしますが、大半の参加者がコスプレをする眼鏡祭はある意味アトラクションのため、自らのテンションを高めないと会場の熱気に押し負けます。そのため、何らかの手段で己のゲージを高めて、色々な人と交流しやすくすることが推奨されています。

初参加しやすくする仕掛け

眼鏡祭には様々な初参加者向けの仕組みがあります。200人のイベントにいきなり放り込まれても困りものなため、まず来場すると7~8人に分けられたテーブルにつきます。この少人数グループで自己紹介(=好きな作品や推しキャラを語ること)をし、乾杯後は自由時間となります。

なお、不安なので知り合い同士で同じテーブルに座りたい、など初参加者向けの配慮は出来る限り応じてくれるため、申込フォームや主催のツイッターなどにあらかじめ書くと良いです。

それで実際のところ初参加で楽しめるのか? については、ステマっぽいですが、実際に初参加の方のツイートを見ると雰囲気が伝わってきます。

コスイベとは違う点がいくつか

一つ注意点があるとすれば、いわゆるコスプレイベントとは性質がかなり違います。例えば、会場はカフェ・レストランなので、撮影スタジオに比べると設備的にはどうしても劣ります。照明は控え目ですし、更衣室はフロアを区切っただけなので暗めな上にさほど広くはないです。特に女子更衣室は年々コスプレ参加者の増加に伴い中々難しい状況にあるようです。

みんなで作る眼鏡祭

眼鏡祭の運営は有志スタッフが行っています。会場や飲食は別にすると、その他すべては有志に頼っているため、眼鏡祭では運営スタッフを募集しています。

眼鏡祭の運営は多岐に渡ります。当日目に見える仕事だけでも、会場設営・受付・更衣室管理・全体進行係に各テーブル進行係があり、ノベルティのイラストや、名札や進行表印刷など各種物品の準備など。金銭管理や会場予約など事務作業もあります。200人を一日中動かす企画・運営なので、世の中に存在するスキルのほとんどはどこかしらで出番があります。

眼鏡祭は本当にすごいイベントで、これに感謝を示す方法は色々ですが、運営スタッフに参加してみるのも一つの手と言えるでしょう。

眼鏡祭から次につなげる

実を言うと、眼鏡祭の当日だけで知り合いを増やすのは至難の業です。200人も居るので覚えるのも覚えられるのも大変な苦労を伴うためです。そのため、もうちょっと人数の少ない眼鏡祭関連のイベントに顔を出してみるのをオススメします。

まずは眼鏡祭おつかれ的なおかわり会。こちらは単なる飲み会です。

ガンプラをわいわい言いながら作る会。

以下は眼鏡祭関連のイベントではないですが、眼鏡勢で行く人はかなり居ると予想されます。

次回の眼鏡祭は7/28に決定したようです。

他にも、眼鏡祭界隈にはお祭り好きがたくさんいるので、食べ歩きやら聖地巡礼やらP3D・P5Dに向けたダンス練習会やら、多種多様なことをやっている人がいます。お誘いがあれば気軽に足を運んでみてはいかがでしょうか。

さいごに

主催のマソーさんはじめ運営スタッフの皆さん、当日俺の話し相手になって頂いた方、常連顔なじみの皆さんなど、今回も非常にたのしい一日を過ごすことが出来ました。また、この日記を書くにあたり、眼鏡祭の雰囲気を伝えるには極めて効果的なため、沢山のツイートを引用させて頂いた方*2にも感謝致します。

それでは次回もよろしくお願い致します。

URL一覧

*1:この日記では一般的な分かりやすさ優先で「コスプレ」を使います

*2:問題があれば面倒ですが https://twitter.com/kagamihoge にお願いします。

Web on Reactive Stack(version 5.0.3.RELEASE) 2. WebClientをテキトーに訳した

https://docs.spring.io/spring/docs/5.0.3.RELEASE/spring-framework-reference/web-reactive.html#webflux-client

2. WebClient

spring-webfluxモジュールには、Reactive Streamsのバックプレッシャを用いるHTTPリクエスト用のノンブロッキング・リアクティブなクライアントが含まれます。

このクライアントはHTTP codecsおよびサーバのfunctional web frameworkの基礎部分は共通です。

WebClientはHTTPクライアントライブラリを下側に置く高レベルのAPIです。デフォルトではReactor Nettyを使いますが、別のClientHttpConnectorを使用してプラグイン可能です。WebClient APIは出力にFluxMonoを返し、入力にReactive StreamsのPublisherを受けます。(Reactive Libraries参照)

RestTemplateと比較すると、WebClientはより関数型でJava 8のラムダを取り入れたfluent APIです。同期・非同期の両方を備え、ストリーミングを含むので、ノンブロッキングI/Oで有用です。

2.1. Retrieve

retrieve()メソッドで簡単にレスポンスボディを取得してデコードできます。

    WebClient client = WebClient.create("http://example.org");

    Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(Person.class);

また、レスポンスからデコードするオブジェクトのストリームを得られます。

    Flux<Quote> result = client.get()
            .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
            .retrieve()
            .bodyToFlux(Quote.class);

デフォルトでは、4xxか5xxのステータスコードのレスポンスはWebClientResponseExceptionのエラーになりますが、これはカスタマイズ可能です。

    Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .onStatus(HttpStatus::is4xxServerError, response -> ...)
            .onStatus(HttpStatus::is5xxServerError, response -> ...)
            .bodyToMono(Person.class);

2.2. Exchange

exchange()ではより細かい制御ができます。以下の例はretrieve()と同等ですが、ClientResponseにアクセスしています。

    Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.bodyToMono(Person.class));

この段階で、完全なResponseEntityを生成することもできます。

    Mono<ResponseEntity<Person>> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.toEntity(Person.class));

retrieve()と異なり、exchange()では、4xxと5xxレスポンスにおける自動的なエラー通知はありません。自前でステータスコードをチェックして続行するかを決めるコードを書く必要があります。

exchange()を使う場合、ClientResponseのtoEntityかbodyメソッドのいずれかを必ず使用してください。これは、リソースを開放してHTTPコネクションプーリングに潜在的な問題を抱えるのを回避するためです。もしレスポンスの中身が無い場合はbodyToMono(Void.class)を使います。ただし、レスポンスの中身がある場合、コネクションは閉じてプールには戻らない点に注意してください。

2.3. Request body

リクエストボディはObjectからエンコードします。

    Mono<Person> personMono = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .body(personMono, Person.class)
            .retrieve()
            .bodyToMono(Void.class);

エンコードされたオブジェクトのストリームも使えます。

    Flux<Person> personFlux = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_STREAM_JSON)
            .body(personFlux, Person.class)
            .retrieve()
            .bodyToMono(Void.class);

値そのままを使う場合、syncBodyというショートカットのメソッドを使います。

    Person person = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .syncBody(person)
            .retrieve()
            .bodyToMono(Void.class);

2.3.1. Form data

フォームデータの送信にはボディにMultiValueMap<String, String>を使用します。なお、MultiValueMap<String, String>によって自動的に"application/x-www-form-urlencoded"をセットします。

    MultiValueMap<String, String> formData = ... ;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .syncBody(formData)
            .retrieve()
            .bodyToMono(Void.class);

BodyInsertersでインラインにフォームデータを記述できます。

    import static org.springframework.web.reactive.function.BodyInserters.*;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .body(fromFormData("k1", "v1").with("k2", "v2"))
            .retrieve()
            .bodyToMono(Void.class);

2.3.2. Multipart data

マルチパートデータの送信にはMultiValueMap<String, ?>を使用し、その値には、パートのボディを表すObjectか、パートのボディとヘッダーを表すHttpEntity、のどちらかを使います。パートを作るにはMultipartBodyBuilderを使います。

    MultipartBodyBuilder builder = new MultipartBodyBuilder();
    builder.part("fieldPart", "fieldValue");
    builder.part("filePart", new FileSystemResource("...logo.png"));
    builder.part("jsonPart", new Person("Jason"));

    MultiValueMap<String, HttpEntity<?>> parts = builder.build();

    Mono<Void> result = client.post()
            .uri("/path", id)
            .syncBody(parts)
            .retrieve()
            .bodyToMono(Void.class);

なお、各パートのコンテンツタイプは書き込むファイルの拡張子かObjectの型に基づいて自動設定します。また、各パートごとにコンテンツタイプを明示的に指定することも可能です。

BodyInsertersでインラインにマルチパートデータを記述するやり方もあります。

    import static org.springframework.web.reactive.function.BodyInserters.*;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
            .retrieve()
            .bodyToMono(Void.class);

2.4. Builder options

WebClientの生成は簡単で、staticファクトリメソッドcreate()と、すべてのリクエストのベースとなるURLを与えるcreate(String)で行います。WebClient.builder()にはより細かいオプションがあります。

基底のHTTPクライアントをカスタマイズするには以下のようにします。

    SslContext sslContext = ...

    ClientHttpConnector connector = new ReactorClientHttpConnector(
            builder -> builder.sslContext(sslContext));

    WebClient webClient = WebClient.builder()
            .clientConnector(connector)
            .build();

HTTPメッセージのエンコード・デコードで使われるHTTP codecsをカスタマイズするには以下のようにします。

    ExchangeStrategies strategies = ExchangeStrategies.builder()
            .codecs(configurer -> {
                // ...
            })
            .build();

    WebClient webClient = WebClient.builder()
            .exchangeStrategies(strategies)
            .build();

ビルダーはFiltersを追加するのに使用します。

URIの構築やデフォルトヘッダー(とクッキー)などその他のオプションについてはIDEWebClient.Builderを確認してみてください。

WebClientの作成後は、新しくWebClientを生成するための新しいbuilderを常に取得可能です。元のインスタンスをベースにしますが、現在のインスタンスには何も影響を与えません。

    WebClient modifiedClient = client.mutate()
            // user builder methods...
            .build();

2.5. Filters

WebClient supports interception style request filtering:

    WebClient client = WebClient.builder()
            .filter((request, next) -> {
                ClientRequest filtered = ClientRequest.from(request)
                        .header("foo", "bar")
                        .build();
                return next.exchange(filtered);
            })
            .build();

ExchangeFilterFunctionsはベーシック認証用のフィルターです。

    // static import of ExchangeFilterFunctions.basicAuthentication

    WebClient client = WebClient.builder()
            .filter(basicAuthentication("user", "pwd"))
            .build();

また、オリジナルのインスタンスに影響を与えることなくWebClientインスタンスを変更できます*1

*1:You can also mutate an existing WebClient instance without affecting the original:が原文。mutateを「変更」と訳すのはだいぶニュアンスが失われているとは思うが、あんま良い日本語浮かばず。