落とし穴#
落ちるバグは自分で見つかります。ここに集めたのは、落ちないバグです。
ローカルで Cookie が効かない#
症状: セッションもログインも CSRF も Flash も、エラーなしで効かない。
原因: cookie.secure の既定は true です。ローカルは http なので、
ブラウザが Cookie を送り返しません。
対処: ローカルの application.conf に入れます。本番では消します。
cookie {
secure = false
}
起動時に WARN が出るので、ログを見てください。
ルートが登録されていない#
症状: 404 になる。コードには書いてある。
原因: install(SomeController::new) を書き忘れています。
jimble はクラスパスを走査しないので、書かないと登録されません。
対処: 起動ログのルート一覧を見てください。無ければ登録されていません。
before が効いていない(同じパスなのに)#
before は書いたブロックの中だけに効きます。パスには付きません。
path("/admin", () -> {
before(requireAuth);
get("/users", ...);
});
install(() -> new SpaController("/admin", ...)); // ← 認証されない
同じ /admin でも、別の場所で登録したルートは別のブロックです。
まとめたいなら、同じブロックに入れてください。
逆の症状(効いてほしくないのに効く)は起きません。これが起きていたのが
移行前の形で、/admin の SPA ログイン画面まで認証に捕まっていました。
なお、ルートが確定した後に before を足すと落ちます。
「足したのに効かない」を黙って通さないためです。
ルート定義はコントローラの初期化ブロックの中で完結させてください。
request() から直接読むと、黙って空になる#
症状: フォームも JSON も送っているのに、値が取れない。例外は出ない。
原因: Request も Data なので、こう書けてしまいます。
context.request().getString("title") // コンパイルは通る。null が返る
Request そのものには本文もクエリも入っていません。
対処: bodyAll()(またはどこから来た値か決め打ちで bodyJson() など)を通します。
Data input = context.request().bodyAll();
String title = input.getString("title");
無いキーの getString は null です。そのまま NOT NULL の列に入れると
Column 'title' cannot be null になります。
セッションが保存されていない#
症状: put() したのに次のリクエストで消えている。
原因: save() を呼んでいません。自動保存はしません。
context.session().put("user_id", 42);
// 明示的に保存する(要件 F-S-02)。自動保存はしない
context.session().save();
select の結果が取れない#
症状: row.getString("title") が空。
原因: 結果はテーブル名でネストしています。
Data row = db.select(
SQL.select()
.from(Post.instance())
.where(Post.id.eq(1L))
);
// SELECT の結果はテーブル名でネストする(要件 F-D-02)
String title = row.getData("post").getString("title");
// Column で引けば、途中の文字列が出てこない
String same = row.getString(Post.title);
Column で引けば間違えません。
DB のエラーに気づかない#
症状: 0件のはずがないのに0件。あるいは NPE。
原因: DB のエラーは例外ではなく戻り値です。select は null、更新系は -1。
try (DB db = BlogExample.db()) {
List<Data> rows = db.selectList(SQL.select().from(Post.instance()));
/*
* DB のエラーは例外ではなく戻り値で返る(要件 F-D-11)。
* select 系は null、更新系は -1。
*/
if (rows == null) {
Log.error("引けませんでした: " + db.getError());
return;
}
}
トランザクションが続いていない#
症状: commit() のあとの更新が、ロールバックしても戻らない。
原因: これは移送元の話です。移送元の commit() はトランザクションを
終わらせていたので、そこから先が自動コミットになっていました。
jimble では commit() は終わらせません。終わらせるのは commitEndTransaction() です。
移送してきたコードを見るときは、commit() の後ろを確認してください。
「コミットもロールバックもされていない」が出る#
ERROR コミットもロールバックもされていないトランザクションが残っていました。ロールバックします
原因: try-with-resources で囲まずに beginTransaction() して、途中で return しています。
対処: 囲んでください。jimble は実行の終わりに拾ってロールバックしますが、 それは事故の後始末であって、正しい書き方ではありません。
入口によって用意されるテーブルが違う#
{"error":"トランザクションのコミットに失敗しました。"}
原因: MqQueue#install() や BatchTables.install() を、一部の入口でしか呼んでいない。
Web・バッチ・スケジューラで入口が分かれていると、それぞれが自分の分だけ用意しがちです。 たとえば MQ のテーブルをバッチの入口でしか作っていないのに、 キューに積むのは Web ということが起こります。
やっかいなのは、いちどでもバッチを動かしたマシンでは動いてしまうことです。 手元では気づけず、まっさらな DB(CI や新しい環境)で初めて出ます。
対処: 起動時に用意するものは1か所にまとめ、どの入口もそれを呼んでください。
public final class Bootstrap {
public static void load () {
Migration.install(); // DBUtil.load より前
// 繋がらなければ false。見ずに進むと、あとで DB の話をしない例外で落ちる
if (!DBUtil.load(Conf.conf().config(), Bootstrap.class)) {
throw new IllegalStateException("DB を読み込めませんでした");
}
BatchTables.install(DBUtil.getMainDB());
new MqQueue(NoticeExecutor.QUEUE_NAME).install();
}
}
**テストからも同じものを呼んでください。**テストだけ別に用意すると、 入口が変わったときにテストだけ通る状態になります。
こつ
まっさらな DB で1回流してみるのがいちばん早い確認です。
examples/blog はこれで壊れているのが分かりました。
製品を変えたら生成物の列順が変わる#
症状: 何も直していないのに codegen の差分が出る。列の並びだけが違う。
原因: 生成される列の並びは物理順です。
ALTER TABLE ... ADD COLUMN の位置が MySQL(AFTER body が書ける)と
PostgreSQL(必ず末尾)で違うので、同じスキーマでも並びが変わります。
対処: 生成は主にする製品で行ってください。codegenCheck も同じ製品で回します。
型も NULL 可否もコメントも一致するので、並び以外は変わりません。
SSE が届かない・全部まとめて届く#
原因: 間にいる nginx が溜め込んでいます。
対処: jimble は X-Accel-Buffering: no を付けています。
それでも駄目なら nginx 側で proxy_buffering off; を設定してください。
SSE のループが終わらない#
原因: sse.isOpen() を見ていません。
isOpen() は寿命が残っているかを返します。切断は検出できません
(helidon が教えてくれず、書き込みが永久にブロックすることがあります)。
ループの中で必ず見てください。詳しくは SSE を読んでください。
WebSocket で Cookie が読めない#
原因: 昇格したあとに読もうとしています。
HTTP のヘッダが見えるのは onUpgrade の時点だけです。
対処: 認証は onUpgrade でやります。false を返せば 403 になります。
MQ が2回実行された#
原因: そういうものです。「ちょうど1回」は作れません。
対処: 何度やっても同じになるように書いてください。詳しくは MQ。
HOCON のコメントで落ちる#
Key '/' may not be followed by token: '*'
原因: /* */ を使っています。HOCON のコメントは # か // です。
jimbleRun で「メイン・マニフェスト属性がありません」#
gradle-wrapper.jar に Main-Class がありません。gradlew と gradle-wrapper.jar が
食い違っています。
いまの gradlew はこう起動します。
exec java -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" ...
これには Main-Class を持つ JAR が要ります。古い(-classpath 前提の)JAR が
残っていると、gradlew も JAR も見た目は揃っているのに動きません。
unzip -p gradle/wrapper/gradle-wrapper.jar META-INF/MANIFEST.MF
# Main-Class: org.gradle.wrapper.GradleWrapperMain ← これが無ければアウト
直すには、そのプロジェクトで一度流してください。
gradle wrapper --gradle-version 9.7.1
jimbleRun は作り直しのときに ./gradlew を子プロセスで叩くので、
保存のたびにこれが出ます。 壊れていると分かったときは
理由を出して PATH の gradle に逃がすようにしてあるので、
ホットリロード自体は続きます。
Gradle が起動しない#
* What went wrong: 25.0.4
原因: Gradle 8 は Java 25 の上では動きません。 (ツールチェーンとして Java 25 を使うのは 8 でもできますが、Gradle 自身が動きません)
対処: Gradle 9 以上を使ってください。
Kotlin のコメントが入れ子になる#
build.gradle.kts のブロックコメントに /* が現れると、
そこから入れ子のコメントが始まります(Kotlin のブロックコメントは入れ子にできます)。
外側が閉じないまま、その下のコードが全部消えます。エラーは出ません。
/*
* docs/site/<言語>/*.md を読む ← この /* が入れ子を開く
*/
tasks.register("site") { ... } ← 登録されない。エラーも出ない
パスを書くときは * を避けるか、// を使ってください。
.gitignore の docs/ が全階層に効く#
症状: 公開したリポジトリからテンプレートが消えている。
原因: .gitignore に docs/ と書くと、git はどの階層の docs/ にも効かせます。
jimble-docs/src/main/jte/docs/ まで巻き込みます。
対処: 先頭に / を付けて /docs/ と書きます。
Gradle 9 で落ちるビルドスクリプト#
Gradle 8 では通り、9 で落ちるものがあります。
| 書き方 | 何が起きるか |
|---|---|
val x by tasks.registering { } |
9.6 で非推奨。Kotlin DSL ではコンパイルエラー。tasks.register("x") { } にします |
"...".formatted(...) |
Java 15 のメソッド。9.7 の Kotlin スクリプトからは解決できません。文字列テンプレートにします |
ビルドスクリプトは、実際に使うバージョンで一度は流してください。