Posts for: #Java

何もしてないのに1日が過ぎた

仲の良い焼き鳥屋さんで軽く飲んできた。2時に寝て7時に起きた。午前中はだらだらしてた。午後から会社の事務手続きをやりながら、実家の法事の段取りをやりながら、ブログの記事などを読んでいた。

jj

茉莉花 (ジャスミン焼酎) をジャスミン茶で割った飲みものを jj と呼ぶらしい。焼き鳥屋さんのマスター曰く、お客さんが 串カツ田中で販売 しているのをみて、焼き鳥屋さんでも扱ってよと言うから置いてみたと話していた。あまりこれまで飲んだことのない不思議な飲みものになっていた。基本はジャスミン茶なんだけど、アルコール入っているなという雰囲気がするカルイ飲みものに感じた。お茶ベースだからどんな食べものにもあいそう。すごくおいしいものではない分、軽く飲みたいときにちょうどいいかもしれない。

法事の出席者管理

来週は35日の だんご転がし がある。2月の上旬には49日もある。過去の葬儀もあわせて親戚や関係者の、出席を管理するためのスプレッドシートを作った。出席確認を母に任せていたらなかなか進まなくてお正月から2週間あってもまだ確認を取り切れていない。人間を調整するのがもっとも面倒で時間がかかる。その後に初盆、1回忌、3回忌と続く。連絡先の電話番号も私の方で管理して、私が電話していった方がよいのかなぁ。

Java は死んだ

この記事の著者はいまも java が通用すると思っているとしたらそれは誤解があるという。その誤解を5つあげるといった記事。

誤解1: Javaには、大規模で活発な開発者コミュニティーがある。

これは誤解ではなく事実だと書いてある (´・ω・`) 著者の意見としては、他言語の進化が速く java は冗長で古い型システムで時代遅れみたいなことを主張している。私の意見だと java の進化もいまは速くていうほど時代遅れというほどではない。過去の資産がいまの java に追いつけていないといったのもある。十分に他言語に機能的に追いついているし開発サイクルも速い。

誤解2: Javaは幅広い用途に使われる。Javaは単なるWeb開発言語ではなく、モバイルアプリやゲーム、エンタープライズレベルのソフトウェアの開発にも利用されている。

これも著者の視点が適切ではない。モバイルでは kotlin が席巻していて、web の開発言語としても java をあげるのは大企業やエンタープライズ開発のみではないかと説いている。たしかに kotlin は java ではないが、jvm 言語ではあるので jvm は未だに必要とされているという事実がある。もう1つ、java はエンタープライズレベルのでミドルウェアで確固とした地位を気付いている。例えば、cassandra, hadoop, kafka など、これらを web 開発で使っている限り、それは web 開発でも使われていると言えるだろう。

誤解3: Javaは基礎となる言語である。多くの新しいプログラミング言語は、Javaの原理と概念に基づいて構築されており、何らかの形でJavaと互換性を持つように設計されている。

こんなこと誰も言っていないと思うけど、著者がそもそも誤解しているのではないか。一方でクリーンアーキテクチャに代表されるような、java のエコシステムで開発されたアーキテクチャなどは java と相性がよい。java と他言語との最大の違いはクラスがないとプログラミングできないという点であり、これはメリット・デメリットをもたらすが、oop においては di 技術を進化させてクリーンアーキテクチャのような概念の下支えをしている。

誤解4: Javaは大手企業の強力なサポートがある。Javaを保守・サポートしているオラクル社は、Javaという言語に強いこだわりを持っており、その開発・改良に投資を続けている。また、GoogleやAmazonなど、多くの大手企業が自社の製品やサービスにJavaを採用している。

oracle という企業とそのプロダクトが市場でのシェアを失っているという視点で懸念を表明している。たしかに oracle はそうかもしれないが、google や amazon だって java を活用しているので oracle がダメになっても web 系の大企業がサポートしていくのではないかと私は推測する。

誤解5: Javaは学校や大学で広く教えられている。

この点だけは著者の意見に違和感はない。一昔前は java が大学でよく教えられていたと思うが、今後は python や go といった、他の言語が最初に学ぶプログラミング言語として人気を博していくのではないかと私も思う。

ざっと読んでみて java 開発をやったことのない経験が浅い開発者が書いた記事だなと思えた。

今年はしばらく svelte に注目

1時に寝て7時に起きた。なんか朝うまく起きれなくなってきた。なんでだろう?

svelte アプリの開発に着手

12月の1週間分ぐらいの工数をかけて行っていた フロントエンドの技術選定 の意志決定をした、というよりはしてもらった。私は調査結果をまとめ、react を選択しても svelte を選択しても開発視点ではどちらも同じという判断を下した。あとはお手伝い先の会社にとってどちらの技術に取り組みたいかという視点しかないなと考えて CTO に最終決断を委ねた。その結果 svelte を採用することに決まった。この調査や意志決定についていずれテックブログに書きたい。私がどのぐらい開発に参加するかはまだ未定だけど、初期のリポジトリの整理ぐらいはしておこうと svelte アプリ開発に着手した。初めて関わる技術はなんにせよおもしろい。お仕事で学びがあれば個人でもなにかしら svelte アプリを作ってみたい。

java の ldap クライアント

昨日のコードリーディングの続き。いま使っているライブラリは Apache Directory LDAP API というものだけど、このライブラリの設計があまりイケてない。古い java の考え方で設計されているライブラリのような印象を受けた。他にも java の ldap クライアントはないのかな?と調べたら so でちょうど議論されていた。

この so によると、UnboundID LDAP SDK for Java がベストアンサーになっている。また機会があれば触ってみようかなと思った。

イベントはしご

1時に寝て4時に起きて6時に起きた。

jackson の tips

jackson で調べているとググったときに stackoverflow に書いてある内容が、機能的には正しいけれど、deprecated になっているアノテーションが多々ある。そのときに新しいやり方はどう設定していいか分からないということがある。今日たまたま実装した処理に2つアノテーションが出てきたので備忘録として書いておく。

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
  • デシリアライズするときに my_field のようなスネークケースのフィールド名を myField のメンバーにマッピングする
@JsonIgnoreProperties(ignoreUnknown = true)
  • デシリアライズするときに json に存在してクラスに存在しないフィールドを無視する
  • データ構造の移行時にエラーを出さないとか、不要なフィールドがあるときに設定するとよい

上司道 リーダーはレジリエンスを高める自己肯定感を学ぼう

以前 SECI モデルのワークショップ に参加して、同じチームにおられた方が 上司道 という勉強会コミュニティを運営しているのでお誘いを受けた。せっかくの縁だと思ったのでコミュニティに参加した。その勉強会で 日本セルフエスティーム普及協会 の工藤氏による自己肯定感の紹介イベントがあったので試しに参加してみた。

軽く私の自己肯定感をチェックしてみると普通。低くも高くもない。私はどこかのタイミングで他人と比較するのをやめた。それは自己肯定感を高めるためにはよい慣習らしい。そのおかげで私は自己肯定感が低くはないようにみえる。自己肯定感には社会的と絶対的の2つの構成概念がある。

  • 社会的自己肯定感
  • 絶対的自己肯定感

重要なのは絶対的の方になるらしいが、多くの人は社会的の方が割合として大きいらしい。社会的に頼っていると、キャリア戦略に失敗したり、会社でリストラされたりすると自尊心が傷つきやすくなる。私もこのタイプだと思う。近い将来リストラされることを想定して自分の会社を作ったという背景もある。自分の会社があると、自分で自分をリストラしない限りは集団に帰属しているという社会的欲求を満たせる。それが社会的自己肯定感と関係があるように思える。いまの私の環境は自尊心が傷つきやすい状況ではないため、あまりセミナーの内容には関心をもてなかった。それよりも参加者が共有していた自尊心を脅かされる状況の経験談にひどいものが多くて、それらと比べると私の環境は恵まれていて世の中に感謝する気持ちになった。

後藤達也さんのオンラインミーティング

たまたま21時半からコアメンバー向けの zoom ミーティングを行うという投稿をみかけたので参加してみた。2時間ぐらいやってたのかな?軽く聞いて帰ろうと思っていたものの、なんだかんだで最後まで参加していた。

軽く計算すると note のメンバーシップだけで約774万円/月の売上になる。すごい。

(500 * 9577) + (3016 * 980) = 7,744,180

こんな儲かっているサブスクリプションの新規募集を停止する背景として、これ以上コアメンバーが増えてもコミュニティとしての運営が難しくなるからやりたくないのだという。例えば、すでに3000人いるが、この人数でオフ会を開催することはできない。月々の料金を高くしてメンバー数を減らすというのが経済学的な解であることは理解しているが、後藤さん1人で提供しているようなサービスに月980円を超える料金をとることに抵抗感があるから値段をあげたくないと話されていた。とても謙虚な人だと思う。コミュニティ運営を難しくせず料金もあげないという戦略からコアメンバーをこれ以上増やさないという判断をしたそうだ。

サイトコントローラーの障害は大変そう

0時に寝て3時に起きて6時半に起きた。だいぶよく眠れるようになってきた。

サイトコントローラーの障害

お手伝いしている宿泊業のシステムでトラブルが発生している。厳密には外部サービスになるのだが、複数のオンライン予約サイトの違いを吸収して単一のインターフェースを提供する宿泊業のハブシステムのような存在をサイトコントローラーと呼ぶらしい。週明けから全国旅行支援という新しいGoToトラベルが開始されて、その予約が想定以上のトラフィックになっていてサイトコントローラーがダウンしてしまった。web 開発者向けに例えれば aws の s3 が落ちて大半のサービスに影響が出てなんもできないみたいな状況かな。

全国レベルのニュースになるぐらいの障害規模が大きいらしい。それだけユーザーが多いシステム/事業者なんだろうとは思う。システムと向き合う上で障害が発生するのは仕方ないが、フォールトトレランスは常に意識して設計・運用しないといけないことを、今回の障害を傍からみていて感じた。

java のちょっとした小技

java で1つのリストを複数のリストに分割したいときに List.subList というメソッドがある。複数の値を並行処理するときなど入力を分割するのに便利そう。使い方は次の通り。toIndex を超えると IndexOutOfBoundsException が投げられるのでそこだけ注意かな。

var total = myList.size();
var step = 20;
for (int i = 0; i < total; i += step) {
    var toIndex = i + step;
    if (toIndex > total) {
        toIndex = total;
    }
    var subList = myList.subList(i, toIndex);
    // do something
}

シャンプー

散髪屋さんのマスターからシャンプーを変えた方がいいんじゃないかとアドバイスされた。髪は油分と水分のバランスが大事らしく、シャンプーによって変わることもあるらしい。いつからか記憶にないけど、少なくとも学生時代から私はずっと メリット リンスのいらないシャンプー を使っている。20年ぐらい?こだわりがあるわけでもないし、このシャンプーを使っていて懸念があったことは一度もない。マスターから h&s scalp がよいとお勧めされた。せっかくの機会だから試してみようかなと思う。

url エンコーディングと uri の仕様

1時に寝ようとして、寝てたか起きてたかわからない時間を過ごして7時に起きた。

WebClient と query string のエンコーディング

以前にも WebClient の基本 について少し書いた。data={"x": 1, "y": 2} のような json 文字列を query string でリクエストしようとしたときに少しはまったので書いておく。java 標準ライブラリの URLEncoder を使ってエンコードするとスペースが + になる。これは html の仕様として正しいが、uri の仕様としては不正になる。そのため +%20 に置き換える必要がある。

private String encode(String data) throws UnsupportedEncodingException {
    // NOTE: the URI doesn't allow '+' character
    return URLEncoder.encode(data, StandardCharsets.UTF_8).replace("+", "%20");
}

このロジックで {"x": 1, "y": 2} を url エンコードすると次の文字列になる。

%7B%22x%22%3A%201%2C%20%22y%22%3A%202%7D

あらかじめ url エンコーディングした文字列を渡すと、今度は WebClient が %%25 にさらにエンコーディングしてしまう。Spring WebClient Requests with Parameters 6.Encoding Mode によると、次の4つのエンコーディングモードをカスタマイズできる。デフォルトは TEMPLATE_AND_VALUES らしい。

  • TEMPLATE_AND_VALUES: Pre-encode the URI template and strictly encode URI variables when expanded
  • VALUES_ONLY: Do not encode the URI template, but strictly encode URI variables after expanding them into the template
  • URI_COMPONENTS: Encode URI component value after expending URI variables
  • NONE: No encoding will be applied

もとの url エンコード済みの文字列が次のようなものになってしまう。

%257B%2522x%2522%253A%25...

既存の実装をあまり変えたくもなくてやや力技で実装した。局所的な変更だからまぁいっか。

webClient.get().uri(uriBuilder -> {
  var uriObj = uriBuilder.path(getControllerBasePath() + path).queryParams(query).build(pathParams);
  if (encodedData != null) {
      var uri = uriObj.toString();
      var connector = uri.contains("?") ? "&" : "?";
      uriObj = URI.create(String.format("%s%sdata=%s", uri, connector, encodedData));
  }
  return uriObj;
}).retrieve()

よいエラーメッセージ、わるいエラーメッセージ

タイトルに惹かれてちょっと期待外れ。art というと日本人は芸術と高度なものを期待しがちだけど、the art of だと技術の体系といった意味合いもあるのでちょとしたノウハウを解説する技術ブログのようなものでも誤っていない。

ユーザー体験をよくするためのエラーメッセージのコツとして次の3つを提案している。

  • 何が起きたのか、なぜ起きたのかを説明する
  • 次のステップを提案する
  • 適切なトーンで書く

この3つの具体例としてどのようなものかを説明している。私にとってはそう目新しいものではないが、エラーメッセージにトーンという概念はなかったので最近の流行りなのかなと思った。

postgresql の json データ型

1時に寝て6時半に起きた。連休中に夜更ししてたから生活が乱れた。

ロガー向けのログ保存 API の開発

先週の休暇前にやっていた作業の開発に着手。一通り web api のエンドポイントの実装は終えてテストをあらかた書いたところ。いまのプロジェクトとしても、過去の私の経験としてもやったことのない新しい挑戦の1つとして postgresql の JSONデータ型 を使う。具体的には json 型と jsonb 型の2つがある。前者はテキストで保持する型で、後者は内部的にバイナリに変換されてインデックスも使える。バイナリに変換してインデックスを作る分、insert 時にテキストで保存するよりは少し余分なオーバーヘッドを要する。json のデータを参照用途で使うのか、検索するのかでこれらの型を使い分ければいいのかな。

実際の sql で json データの条件指定は次のようになる。@> というみたこともない気持ち悪い演算子を使う。

> select * from mytable where data  @> '{"x": 1, "y": 2}';

java の jdbc で扱うには PGobject という型に変換して扱う必要がある。

private PGobject convertData(String value) throws SQLException {
    var data = new PGobject();
    data.setType("jsonb");
    data.setValue(value);
    return data;
}

余談だけど、curl で json 文字列を query string としてリクエストするには url encode しないといけない。

$ curl -s --get --data-urlencode 'data={"x": 1, "y": 2}' http://localhost/path | jq .

quarkus のアプリ開発が楽しくなってきた

4時に寝て8時に起きた。昨日は久しぶりに夜更しして quarkus の調べものをしてた。新しいものを学ぶのはおもしろい。

ストレッチ

今週末は本当は実家に帰る予定だったのが、台風による雨で田んぼのコンディションがよくないので断念した。日曜日の夜、田んぼ仕事を終えて筋肉痛のところにストレッチしてもらう予定は変わってしまった。今日の開脚幅は開始前155cmで、ストレッチ後160cmだった。いつもは朝測っているのが夜になるので数値はよくなかった。とはいえ、あまり規則正しく寝てないわりには体調がよい。気候が涼しいせいかな。トレーナーさんに来週はもう10月ですよと言われて9月は過ぎさるのが早いと改めて思った。

quarkus アプリケーションと認可フロー

昨日の続き。お昼前ぐらいからずっと quarkus のアプリケーション開発をしていた。なんやらかんやらで3日間ずっと bolt や quarkus のソースやドキュメントを読んでいた。徐々に理解度が増えてきて、できることも増えてきて楽しくなってきた。web 系だと di に google/guice を使うものも多いけど、エンタープライズ系だと cdi なのかなぁとか思ってた。わからんけど。以前にも cdi のドキュメントを読んで関心があった。cdi は本当によく出来ていると思う。一方で難し過ぎて、そこまでコンテキストを厳密に管理する必要があるアプリケーションもそうないのかもなぁとは思ってた。今日 quarkus でアプリケーション開発していてドキュメントを読みながらやってみたところが次になる。

だいたい雰囲気は理解できてきたので backlog の Authentication & Authorization に書いてある oauth2 の Authorization Code Grant のフローを実装していた。access token の取得と refresh はできたのでこれを db に保存するのを明日以降にやってみる。

quarkus のビルド環境に手間取った

1時に寝て7時に起きた。休みだとやっぱりだらだらしてしまうな。

bolt for java on quarkus

昨日の続き。スクラッチから quarkus のアプリケーションの設定を gradle で行う。quarkus の上で slack apps としてのコマンドとイベントの振る舞いだけ確認した。

私は新規に開発する java アプリケーションは gradle を使うようにしている。これは java のよくないところだろうけれど、言語コミュニティが提供するパッケージマネージャやビルドツールがないから複数のツールが乱立している。maven から gradle に緩やかに移行していくのかな?と私は考えていたけれど、昔からあるライブラリのビルドツールを変更するのは労力に見合うメリットがないのか、maven も依然としてずっと使われ続けていくのかもしれない。maven と gradle の両対応という保守コストは、この先しばらく java コミュニティが抱えていく保守コストと言えるのかもしれない。quarkus はさらに独自の Quarkus CLI というビルドツールを提供している。そのため、ビルドのための設定だけで quarkus cli, maven, gradle の3つの方法があり、ドキュメントにもそれぞれの設定方法が書いてある。これを保守する方も使う方もややこしくて大変だなぁという印象を受けた。

BUILDING QUARKUS APPS WITH GRADLE をみながら次の maven cli で作った gradle プロジェクトのテンプレートをみながら build.gradle の設定をした。

$ mvn io.quarkus.platform:quarkus-maven-plugin:2.12.3.Final:create \
    -DprojectGroupId=my-groupId \
    -DprojectArtifactId=my-artifactId \
    -Dextensions="resteasy-reactive,resteasy-reactive-jackson" \
    -DbuildTool=gradle

あと私は設定ファイルを yaml で管理したいので次の拡張も追加した。gradle タスクでも定義されていて次のように実行する。

./gradlew addExtension --extensions="quarkus-config-yaml"

この cli がやっていることは基本的に dependencies に次の1行を追加するだけ。

dependencies {
  implementation 'io.quarkus:quarkus-config-yaml'
  ...
}

設定ファイルを yaml から読み込めるようになると初期設定は次のようになる。

$ vi app/src/main/resources/application.yaml 
quarkus:
  http:
    port: 3000
  log:
    level: INFO
    category:
      "com.slack.api":
        level: DEBUG
      "tutorial.bolt.sample":
        level: DEBUG
  package:
    type: uber-jar

開発サーバーは次のようにして起動する。

$ ./gradlew quarkusDev

quarkusDev enables hot deployment with background compilation, which means that when you modify your Java files or your resource files and refresh your browser these changes will automatically take effect. This works too for resource files like the configuration property file. The act of refreshing the browser triggers a scan of the workspace, and if any changes are detected the Java files are compiled, and the application is redeployed, then your request is serviced by the redeployed application. If there are any issues with compilation or deployment an error page will let you know.

https://quarkus.io/guides/gradle-tooling#dev-mode

hot deployment 機能のおかげでソースや設定ファイルを変更すると自動的に反映される。他の言語なら普通の機能かもしれないけど、java でもそういう仕組みが普通になったんだなと思って感心した。変化に付いていけない開発者のような気持ちになった。

slack apps 開発に着手

0時に寝て6時に起きた。あまりうまく眠れなかった。

bolt for java

slack apps を開発するためのフレームワークとして bolt と呼ばれる高レベルのフレームワークが提供されている。このフレームワークは slack sdk を使って作られていて、slack apps の開発が簡単になるようにユーティリティが提供されている。The Bolt family of SDKs によると、javascript, python, java 向けに提供されている。以前 bizpy でも slack アプリ開発のチュートリアルの勉強会をしたことがある。そのときは bolot for python を使っていた。

一度触ったことがあったので bolt がどういうものかはすでに知っている。その java 版を使って slack apps を作ってみようと取り組み始めた。まずはチュートリアルを一通りやってみようと次のリポジトリでやってみた。

チュートリアルの内容を動かすだけならすぐできた。次に java の waf は何を使おうかを調べてた。Supported Web Frameworks によると、次の4つがある。

  • spring boot
  • micronaut
  • quarkus undertow
  • helidon se

さらに slackapi/java-slack-sdk#modules をみると、次の2つも追加されている。どちらも kotlin 向けのフレームワークらしい。

  • http4k
  • ktor

それぞれのフレームワークの説明を読んだり、この機に kotlin をやってみることも検討してみた。長期間の保守を前提にすると、一時的に触るだけの言語を使うのもどうかな?と思うところはあってやはり java でやることにした。spring boot はお仕事でよく使っていてどういうものかを理解しているので選択するなら他の3つのどれか。

Quarkus was created to enable Java developers to create applications for a modern, cloud-native world. Quarkus is a Kubernetes-native Java framework tailored for GraalVM and HotSpot, crafted from best-of-breed Java libraries and standards. The goal is to make Java the leading platform in Kubernetes and serverless environments while offering developers a framework to address a wider range of distributed application architectures.

https://quarkus.io/about/

いま kubernetes に好印象をもっていることもあり、この説明を読んで quarkus を選択することに決めた。そんなことをつぶやいていたら、せらさんからいくつかアドバイスをいただけた。slack について何かをつぶやくと100%返信がくる (ソースは私の経験) 。感謝。

java アプリケーションを実行可能なバイナリにコンパイルする機能を graalvm が提供している。graalvm ではこのバイナリのことを native image と呼んでいる。quarkus は java の web アプリケーションフレームワークであり、graalvm を使って native image を作ることも考慮して設計されている。コンテナでデプロイすることを想定したフレームワークと言える。残念ながら slack sdk が使っているライブラリである gson はリフレクションを多用していて、それが graalvm とは相性が悪いだろうという話しで現時点では native image 化は難しいみたい。たしか native image でリフレクションを使うには使っている箇所を設定にすべて列挙しないといけなかった気がする。リフレクションのような動的に用いるものと静的な設定は相性が悪く、がんばれば特定のバージョンで動くものは設定できるかもしれないけど、ライブラリのようなものでバージョンアップに追随するのはしんどいという話しなのかなと思う。

最近の java の勉強

0時に寝て7時に起きた。

運用対応

ある施設のサービスインのシステム切り替えでほぼ1日を終えた。昨日のロガーの要件を詰めようと思っていたけど、なんの話しもできないまま1日が過ぎた。トラブルの運用対応に開発リーダーが忙しくて、他の外部開発者は遊休中。自分の時間を無駄遣いしているようで辛い。あと1ヶ月の辛抱。

java 勉強会

たまたま Java 19が正式リリース。より軽量な仮想スレッド、RISC-Vへの移植など新機能。1年後のJava 21が次のLTS版に をみかけた。今後は java の lts リリースが3年から2年に変わるらしい。他の言語では軽量プロセスと呼ばれる仕組みを java では Virtual Threads (仮想スレッド:JEP 425) と呼ぶらしい。まだプレビュー版だけど、次の lts には使えるようになっていると思う。サーバー用途で言えば仮想スレッドを使ったサーバーが主流になれば java の運用時のメモリ使用量がいくらか減ることになって嬉しい状況はあるのかもしれない。

その記事と同時にタイムラインで Java仕様勉強会「Java SEの動向 2022夏」 をみかけたので気付いたタイミングで参加した。現在の java の機能拡張をしている様々なプロジェクトの紹介がされていた。半分は知ってたけど、半分ぐらいは知らないものもあって勉強にはなった。プロジェクトが多過ぎてだんだん聞いていて飽きてくるのもあったので勉強会のやり方を見直してもいいかもしれないとも思った。

その後にきしださんが java 19 の紹介をされていたのでそのまま視聴した。switch 式やパターンマッチングとの親和性あたりは私も期待していた内容の通りに拡張されているようにみえてよさそうの思う。次の lts はまだ先だけど、そのときに java を書くのが楽しみになるぐらいの機能拡張だとは思う。仮想スレッドの説明もデモしていた。他の言語で軽量プロセスを扱っている人にとっては意図した内容なので目新しくはないが、java でフレームワークを作っている開発者にとってはパフォーマンスを向上できる可能性があるので関心の高い機能だとも思う。

spring boot におけるプロキシ実装

0時に寝て7時に起きた。

spring boot における簡易的なミドルウェアの実装

bff (backend for frontend) の web api サーバーから別の web api サーバーのエンドポイントを呼び出す処理を書いていてただプロキシする処理のためだけに controller を書くのも面倒だなと気付いた。controller よりも低いレイヤで raw request を扱うにはどうしたらいいかを調べてみたら spring の Filter を使うのが最も簡単そうにみえた。その spring の Filter の1つである OncePerRequestFilter を使って簡易的なプロキシの処理を実装してみた。OncePerRequestFilter はリクエストに対して1度だけ呼ばれることが保証された Filter になる。これらのドキュメントがどこにあるかわからなかったので baeldung のチュートリアル What Is OncePerRequestFilter? をみた方が早いかもしれない。

Filter が複数あるときは実行順序を指定したいときは Order というアノテーションで制御できる。

@Component
@Order(30)
public class MyFilter extends OncePerRequestFilter {
  ...
}

OncePerRequestFilter の doFilterInternal メソッドをオーバーライドすれば任意のミドルウェアっぽい処理を実装できる。

    @Override
    protected void doFilterInternal(
        HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        // ここに任意の前処理を実装する
        filterChain.doFilter(request, response);
        // ここに任意の後処理を実装する
    }

あとは HttpServletRequest と HttpServletResponse を直接操作してリクエストから必要な情報を取り出して、クライアントでリクエストして返ってきたレスポンスをそのまま返してあげればよい。簡単に実装したものが次になる。クライアントには 以前に少し調べたことを書いた WebClient を使っている。WebClient じゃなければ、もう少しシンプルに書ける気もする。

@Component
@Order(30)
public class RequestForwardFilter extends OncePerRequestFilter {
    @Autowired
    private List<RequestForwardMatcher> matchers;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        var matcher = this.getMatcher(request);
        if (matcher.isEmpty()) {
            filterChain.doFilter(request, response);
            return;
        }
        writeResponse(response, this.forwardRequest(request, matcher.get().getWebClient()));
    }

    private Optional<byte[]> forwardRequest(HttpServletRequest request, WebClient webClient) throws IOException {
        var method = HttpMethod.resolve(request.getMethod().toUpperCase());
        var originalUri = request.getRequestURI();
        var uri = this.getUri(originalUri, request.getQueryString());
        var spec = this.getSpec(method, webClient, uri, request.getReader());
        return spec.retrieve().bodyToMono(byte[].class).blockOptional();
    }

    private void writeResponse(HttpServletResponse response, Optional<byte[]> bytes) throws IOException {
        if (bytes.isEmpty()) {
            return;
        }
        var stream = response.getOutputStream();
        stream.write(bytes.get());
        stream.flush();
        stream.close();
    }

    private byte[] getRequestBody(BufferedReader reader) {
        return reader.lines().collect(Collectors.joining()).getBytes();
    }

    private String getUri(String originalUri, String queryString) {
        var path = originalUri.replace("/api", "");
        if (StringUtils.hasLength(queryString)) {
            return String.format("%s?%s", path, queryString);
        }
        return path;
    }

    private WebClient.RequestHeadersSpec<?> getSpec(
            HttpMethod method, WebClient webClient, String uri, BufferedReader reader) {
        switch (method) {
            case GET:
                return webClient.get().uri(uri);
            case POST:
                return webClient.post()
                        .uri(uri)
                        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .bodyValue(this.getRequestBody(reader));
            case PUT:
                return webClient.put()
                        .uri(uri)
                        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .bodyValue(this.getRequestBody(reader));
            case DELETE:
                return webClient.delete().uri(uri);
            default:
                throw new RuntimeException(String.format("Unsupported HTTP method: %s", method));
        }
    }

    private Optional<RequestForwardMatcher> getMatcher(HttpServletRequest request) {
        for (var matcher : this.matchers) {
            if (matcher.match(request)) {
                return Optional.of(matcher);
            }
        }
        return Optional.empty();
    }
}

matcher のサンプル実装。

public class RequestForwardMatcher {
    private static final String ASTERISK = "*";

    private final Pattern pattern;
    private final List<String> methods;
    private final WebClient webClient;

    public RequestForwardMatcher(String pattern, List<String> methods, WebClient webClient) {
        this.pattern = Pattern.compile(pattern);
        this.methods = methods;
        this.webClient = webClient;
    }

    private boolean matchMethod(String method) {
        if (this.methods.contains(ASTERISK)) {
            return true;
        }
        return this.methods.contains(method);
    }

    public boolean match(HttpServletRequest request) {
        if (!matchMethod(request.getMethod())) {
            return false;
        }
        return this.pattern.matcher(request.getRequestURI()).matches();
    }

    public WebClient getWebClient() {
        return this.webClient;
    }
}