jimble

考え方#

jimble には守っている決めごとがいくつかあります。 機能を足すかどうかは、たいていここで決まります。

1. 上から順に追えること#

コードを読む人が、main から目的の処理までを指でたどれることを最優先します。

そのために捨てたもの:

  • 注釈でのルート定義(@GET @Path("/x"))。 どのクラスが読まれるのかがコードに出ないため
  • DI コンテナ。 @Inject された実装がどれになるかは、実行するまで分からないため
  • 注釈でのトランザクション(@Transactional)。 どこで始まってどこで終わるかが、メソッドの外にあるため

かわりに new を書きます。install(AdminController::new) と書きます。 db.transaction(tx -> { ... }) と書きます。数行増えます。

2. 起動時にクラスパスを走査しない#

コントローラもバッチも MCP のツールも、自分で登録します。

tool("search_posts", SearchPostsTool::new);
tool("create_post", CreatePostTool::new);

resource("blog://latest", LatestPostsResource::new);

走査しないので、起動が速く、何が登録されたかが起動ログで完結し、 ネイティブイメージや jlink とも喧嘩しません。 登録し忘れは、起動ログのルート一覧で気づけます。

3. 黙って間違えないこと#

いちばん高くつくバグは、落ちないバグです。 jimble は「静かに効かなくなる」状態を潰すことに時間を使っています。

例:

  • env=local なのに cookie.secure = true なら、起動時に WARN を出します。 そのままだとブラウザが Cookie を返さず、セッションも CSRF も Flash も エラーなしで効かなくなるためです
  • トランザクションを開いたまま実行が終わったら、ERROR を出してロールバックします。 黙って接続をプールへ返すと「入れたつもりが入っていない」が残ります
  • ドキュメントのコード片は実コードから抜いています。 印の付いた場所が消えたら、サイトのビルドが落ちます

4. 失敗は例外にする#

**失敗は例外(非検査)です。**戻り値で失敗を返すことはしません。 戻り値は捨てられます。捨てた失敗は、黙って素通りします。

  • 0件は失敗ではありません。select は空の Optional、selectList は空のリスト、update は 0 を返します
  • DB の失敗は SqlExecuteException です。分岐したいのは一意制約くらいなので、それだけ DuplicateKeyException で受けます
  • 書かなければ上まで飛んで 500 になります。トランザクションの中なら巻き戻ります
try (DB db = BlogExample.db()) {

	/*
	 * 0件は空(空リスト・空の Optional・件数 0)、失敗は SqlExecuteException(2.0)。
	 * 書かなければ上まで飛んで 500。トランザクションの中なら巻き戻る。
	 */
	List<Data> rows = db.selectList(SQL.select().from(Post.instance()));

	// 分岐したい失敗は一意制約くらい。それだけを受け止める
	try {
		db.insert(SQL.insert(Post.instance()).value(Post.title, "hello"));
	} catch (DuplicateKeyException ex) {
		Log.info("もうあります");
	}

}

使い方を間違えたときも同じです。壊れ方はコンパイルエラーか例外だけで、黙って違うことはしません (2.0 で作り直したものは 2.0 への移行 にあります)。

5. 一つの実行 = 一つの Context#

Web のリクエスト・バッチの実行・MQ の1件・WebSocket の1メッセージ、 それぞれに Context が1つ作られます。 DB 接続もセッションもここにぶら下がり、終わりで畳まれます。

Context は ScopedValue で渡します。ThreadLocal ではありません。 仮想スレッドの上では ScopedValue のほうが安く、 かつ書き換えられないので、途中ですり替わることがありません。