tokuhirom's Blog

magic research をクリアした

https://mcolotto.github.io/magic-research-demo/website/

Android 版をシコシコとやってた。いわゆる放置ゲー的なやつだけど、この手のやつでは一番おもしろかった。 トータルで 49日かかりました。

何が面白いかと言うとですね。。ゲームバランスが絶妙で、ボスが絶妙に倒せないのと、戦術を工夫しないと先に進めないようになっててめっちゃ楽しい。 転生するたびに見える景色が変わっていく感じがたまらないですな。

Marvel's Spider-Man: Miles Morales

https://www.playstation.com/ja-jp/games/marvels-spider-man-miles-morales/

PS5でクリア。 正当な拡張コンテンツという感じ。Spider man 2 も発売決定してるのでそれも楽しみですね。

freemarker の js_string の歴史

freemarker の js_string がどのような変遷をたどってきたのかをまとめてみた。

2.3

Date of release: 2004-June-15

https://freemarker.apache.org/docs/versions_2_3.html

New built-ins for Java and JavaScript string escaping: j_string and js_string

2.3.1

Date of release: 2005-01-04

https://freemarker.apache.org/docs/versions_2_3_1.html

The js_string built-in now escapes > as > (to avoid ).

2.3.20

Date of release: 2013-06-27

https://freemarker.apache.org/docs/versions_2_3_20.html

Bug fix [390] (and other improvements): ?js_string and ?json_string didn't escape the u2028-u2029 line terminators (problem for JavaScript) and the u007F-u009F control characters (maybe a problem in JSON, depending on implementation). Furthermore, the escaping of , <, and > become safer in that now they are escaped whenever it can't be guaranteed that they won't be part of <!, ]]> or </. Earlier they were only escaped when it was known that they are part of these patterns, thus it was possible to assemble these patterns from two adjacent interpolations. Additionally, from now on <? and --> also count as dangerous patterns, and will trigger < and > escaping.

まとめ

2.3.20 以後、2023年4月現在の最新版である 2.3.32 まで変更はなし。

Java 9 以後での InputStream を String にするやり方

commons-io で IOUtils.toString(inputStream, StandardCharsets.UTF_8) としているケースは、Java 9 以後なら new String(inputStream.readAllBytes(), StandardCharsets.UTF_8) と書けて便利。

https://www.baeldung.com/convert-input-stream-to-string

Rust で整数を漢数字に変換したいよー

こんな感じで実装できる。真面目にやればもうちょいちゃんと書けるけどこんな感じで。。

use std::cmp::min;

const NUMS: [&str; 10] = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
const SUBS: [&str; 4] = ["", "十", "百", "千"];
const PARTS: [&str; 18] = [
    "",
    "万",
    "億",
    "兆",
    "京",
    "垓",
    "𥝱",
    "穣",
    "溝",
    "澗",
    "正",
    "載",
    "極",
    "恒河沙",
    "阿僧祇",
    "那由他",
    "不可思議",
    "無量大数",
];

fn int2kanji(i: i64) -> String {
    let s = i.to_string();
    let chars = s.bytes();
    let p = chars.into_iter().rev().enumerate().collect::<Vec<_>>();
    let mut buf: Vec<&'static str> = Vec::new();
    for (i, b) in p.clone() {
        let c = (b - 48) as usize; // 48 is '0'
        if i % 4 == 0
            && i > 0
            && (i..min(i + 4, s.len()))
                .map(|i| {
                    let (_, c) = p.get(i).unwrap();
                    *c
                })
                .any(|n| n != 48)
        {
            buf.push(PARTS[i / 4]);
        }
        if c != 0 {
            // その桁が 0 のときは区切りを追加しない
            buf.push(SUBS[i % 4]);
        }
        if !(i % 4 != 0 && c == 1) {
            // 十百千を表示したときで、一のときは追加しない。
            buf.push(NUMS[c]); // 48 is '0'
        }
    }
    buf.reverse();
    buf.join("")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_int2kanji() {
        assert_eq!(int2kanji(1), "一");
        assert_eq!(int2kanji(9), "九");
        assert_eq!(int2kanji(10), "十");
        assert_eq!(int2kanji(11), "十一");
        assert_eq!(int2kanji(21), "二十一");
        assert_eq!(int2kanji(99), "九十九");
        assert_eq!(int2kanji(100), "百");
        assert_eq!(int2kanji(999), "九百九十九");
        assert_eq!(int2kanji(1000), "千");
        assert_eq!(int2kanji(9999), "九千九百九十九");
        assert_eq!(int2kanji(10000), "一万");
        assert_eq!(int2kanji(10020), "一万二十");
        assert_eq!(int2kanji(1_000_020), "百万二十");
        assert_eq!(int2kanji(100_000_020), "一億二十");
        assert_eq!(int2kanji(1_0000_4423), "一億四千四百二十三");
        assert_eq!(int2kanji(1_8000_4423), "一億八千万四千四百二十三");
    }
}

最近またLinux用の日本語IMEを作っている

本件は mozc の ut がどうこうとかは関係なくて、ふと linux desktop を使おうと昨年末に思いまして、昨年末からちまちまやってます

https://github.com/tokuhirom/akaza

かな漢字変換って作るの難しいのかなぁ、と思ったので作ってみている。これはまさに Just for Fun でやっている。 わりと普通に自分で常用してる分には困らないかな、というところまできている。

以下は、思ってることの垂れ流しという感じで、まとまってないですが。

「日本語入力を支える技術」という本が 2018年に出ていて、この本の内容を読めば、だいたいエンジン部分は実装できる。Amazon のレビューではこの本よんでも実装できないって書いてあるけど、変換エンジン自体は実装できます。 UI が辛い。けど。

エンジンは、ビタビアルゴリズムで最小コスト法を実装する、とかであればプログラミングできる人なら割とまじで誰でも実装できる。

ただ、どうしてもかな漢字変換エンジンは大量の細かいオブジェクトを取り扱う必要が出てくるので、python とかで実装すると結構遅い。 2年前に一回 pure python で実装したが、おそすぎたのでコア部分を C++ で書き直したという経緯がある。 Python で書いてると、色々なアルゴリズムためす上でも、Python が遅いからだめなのか、アルゴリズムがそもそも遅くて無理なのかがよくわからなくなりがちなんだよな。

UI を Python で書いて、ロジックを C++ で実装したのだがどうもインストールまわりが煩雑になるし、Rust 触ってみたかったので rust で全面的に書き直した。 Pure Rust にしたことによって、SEGV したりして謎に死ぬことがなくなったので快適になったし、高速に動いている。所有権とかがまぁめんどくさいとかはありつつ、わりと慣れれば大丈夫になる。

基本的な方針としては、mozc に近い感じの構成になっていて、最小コスト法でビタビアルゴリズムで統計的かな漢字変換である。 個人で開発しているから Google と違って大量のウェブコーパスを利用できるわけではないから、Wikipedia と青空文庫を形態素解析してそれを学習元として利用している。 mozc は Google の大量の資源を利用できるから変換精度がいいんでしょ、と思う人が多いと思うんだけど、それはまったくもってそのとおり。ただ、書き言葉という点においては、wikipedia ぐらいの規模の言語資源があれば、それを使えば割とそこそこの変換精度は出る。逆に話し言葉はめちゃくちゃ弱いので、話し言葉とかくだけた書き方のコーパスがもっとほしい。 利用できる日本語のコーパスとしては BCCWJ がほぼ唯一に近いもので、BCCWJ を使えばもっと変換精度は向上するんだけど、、個人で利用しようとすると 25万円だかかかるので二の足を踏んでいる。

識別モデルを使う というのも試してみた。今どきだと機械学習的なアプローチがナウいかなぁと思い。。だが、どうしても個人のマシンで動かしていると時間がかかってしょうがない。金をかけられない個人の趣味開発においては、統計的かな漢字変換のほうが向いていると思う。 開発を継続的にやるには無料の環境で使えるのが大事かなと思っている。モデルデータを github actions とかで生成できるのが理想的で、github actions で生成するならチープなスペックのインスタンスで 回せるぐらいのものが良い。github actions は 6 時間しか回せない。

基本的に今は単語 Bi-gram でコスト計算している。tri-gram に拡張するとプログラムが複雑になりすぎるし、有効なケースも限定的なのでまぁ。あまり bi-gram で困ってないし、という。mozc のようにクラスNグラムにしたほうがいいのかなぁ、と思いつつ、単語Nグラムで困ってないのでまぁいいかなぁといったところ。単語Nグラムだと品詞のこととか考えなくていいのがとにかく良い。 日本語の品詞難しいよ〜。個人的には、かな漢字変換の辞書登録で品詞を入れさせられるの、だるいなと前々から思ってたし。

https://komachi.hatenablog.com/entry/20081127/p2 http://www.fenix.ne.jp/~G-HAL/soft/nosettle/anthy.html#patch13

↑このへんとかを見ても思うこと。個人的には SKK の学習の具合を気に入っている。システムで提供するモデルはもちろん大事なのだが、個人の語彙などはたかが知れているので、システムで提供するモデルは「ある程度学習が進むまで耐えられる」ぐらいの精度があればよく、細かいところは個人の学習データで補ってくれや。っていうふうにするしかないのかな、と思っている。 だって個人だと Google みたいな規模のコーパス使えないんだもの。

ユーザーの学習は、unigram と bigram をテキストファイルで ~/.local/share/ とかに落とすようになっていて、自分が変換した結果の確率が学習データにあればそっちが優先されるようになっている。ので、誤変換で確定しちゃっても、正しい変換で何回か確定すればそれが優先されるようになる。このへんの学習ロジックはめちゃくちゃシンプル。基本的にユーザーの語彙は wikipedia 全体の語彙よりも少ないから、分母が小さくなるので、ユーザーの学習結果の確率計算のほうがキツく効くっていう仕組みにしている。

ちなみに、この手のかな漢字変換エンジンって、どういうふうに考えて実装したか、みたいな日記を書いてる人が多くてそういうの読むのって面白いなぁと思う。僕はそのへんの経緯を github issues とかに書くのがいいのかなぁと思ったので github issues とか discussions とか wiki に書いている。バザール的に開発できるような感じの構成にしているつもり、ではある。 お気軽になにかアイデアとかあれば書いていただければ嬉しいし、PR 送っていただければなお嬉しい。

https://github.com/tokuhirom/akaza/discussions

今月中ぐらいに一旦スナップショットリリースを出来たらいいなぁと思っている。

cargo test で fork させたい

https://crates.io/crates/rusty-fork

cargo test はプロセスを共有する。これではテストをしづらいケースがある。例えば C のライブラリのラッパーなどの場合、初期化関数を何回も呼んではいけないケースがある。

rusty-fork crate を利用すると、以下のように書くだけで別プロセスでテストを実行させることが可能である。

use rusty_fork::rusty_fork_test;

rusty_fork_test! {
    #[test]
    fn my_test() {
        assert_eq!(2, 1 + 1);
    }

    // more tests...
}

jumpapp を i3 に適用して一発でアプリにフォーカスをあてる on Linux

jumpapp というコマンドがある。これを使うと該当のウィンドウが起動していなければ起動し、起動していればフォアグラウンドにもってくることができる。

僕の場合は i3wm を使っているので、.config/i3/config に以下のように設定する。

bindsym $mod+Ctrl+Shift+o exec "jumpapp obsidian"
bindsym $mod+Ctrl+Shift+c exec "jumpapp -c google-chrome google-chrome-stable"
bindsym $mod+Ctrl+Shift+t exec "jumpapp moderndeck"
bindsym $mod+Ctrl+Shift+w exec "jumpapp wezterm"
bindsym $mod+Ctrl+Shift+f exec "jumpapp -c franz franz"

Ultimate hacking keyboard のバンパーを Ctrl+Shift+Super に割り当てて、2つのキーを押すだけでアプリを起動できるようになった。便利。

arch linux で使うのに最適な twitter クライアントを探す

最近色々ある twitter ですが、なんだかんだ twitter クライアントを起動しておきたいなと思っている今日この頃なので、、

最初は CawBird を使っていたが、vim keybinding が使えなかったりしてちょっと不満。

なので、気合で自分にあうものを探してみようかなと。yay -Ss twitter として twitter 関連のものを探し、その中から GUI クライアントを絞り込むものとする。このキーワード検索では、どうしてもライブラリや CLI ツールなどが混ざってくるので、目で確認していく。

aur/birdie-git 1.1.r12.g20b3c83-1 (+0 0.00) (Orphaned)
    Twitter client for Linux
aur/v2cmod-z 2.19.1-8 (+0 0.00)
    A 2ch/5ch and Twitter browser
aur/moderndeck-bin 9.4.5-1 (+1 0.24)
    A beautiful, powerful Twitter client for desktop.
aur/mikutter-git 3.4.2.r0.gdf0a70c-1 (+2 0.00)
    a moest twitter client (Upstream version)
aur/alice-git latest-1 (+2 0.00)
    A minimal Twitter client based on GTK+3.
aur/nocturn 1.8.4-2 (+6 0.00)
    Desktop twitter client
aur/cawbird-git 1.5.r0.g0a60a0bc38-1 (+7 0.01)
    A fork of the Corebird GTK Twitter client that continues to work with Twitter
aur/choqok-git v1.6.0.r106.gd8a53169-1 (+35 0.00)
    Microblogging client for KDE with support for Pump.io, GNU social and Twitter.com - git version
aur/mikutter 5.0.4-1 (+39 0.00)
    a moest twitter client
community/cawbird 1.5-2 (644.4 KiB 2.0 MiB) (Installed)
    Native GTK Twitter client

cawbird

cawbird が微妙なのは、hjkl 移動できないという点とフォントがなんか汚い。 フォントは設定すればなんとかなりそうだが。。

cawbird で issue たっているが、あんまメンテナが乗り気じゃなさそうだった>ショートカット

birdie

インストール不能

nocturn

すでに試したのだが起動しなかった。

[1022523:1222/153244.185725:FATAL:gpu_data_manager_impl_private.cc(439)] GPU process isn't usable. Goodbye.
zsh: trace trap (core dumped)  nocturn

と言われてしまう。

これは nocturn --no-sandbox すれば起動はできる。

が、結局なんかしらんけどログインができない。

mikutter

twitter client ではなく mastdon クライアントになっていた

v2cmod-z

2ch クライアントがメインっぽい。。

alice-git

なんか github レポジトリが消えてる

choqok

動く。動くのがすごい、みたいになってるのがちょっとあれだけど。。 choqok は一番まともかな。ただ、twitter 特化じゃないのでなんか変。 fav するためのショートカットがなかったり、vim like keybindings がなかったりする。

これだったら cawbird でいいかな。

moderndeck

これが、基本的には twitterdeck なので体験が良い。

ということで、moderndeck を使うことにした。 Use os native title bar を appearance 設定で利用することで、ちゃんと i3 で枠がつくようになる。

というわけで、しばらくは moderndeck を使おうかなと思う。

arch linux を入れる 2022年版

久々に arch linux を使おうかな、と思いまして。なんかもうプライベートで使うマシン、mac じゃなくていいなーって気分が高まりすぎてる。 archlinux の利点は、pacman で管理できて、だいたいのものが AUR 経由でインストールできるので便利という感じなのと、ubuntu と違って、必要なものしか入らない感じにできるってことかな。 (その結果として、必要なものが入ってなくて変な状況になることもあるけど)

昔は頑張って windows のディスクをパーティションきって設定したりしてたけど、めんどくさいので SSD を USB で増設してやる。最近は USB 接続でも十分な速度が出るので良い時代だ。

https://wiki.archlinux.jp/index.php/USB_%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2 rufus で usb memory に書いた。

あとは、公式のガイド通りに設定していけばいい。fdisk はちょっとだるいので、TUI でできる cfdisk でやった。 https://wiki.archlinux.org/title/installation_guide

細かいところでよくわかんなかったところは https://zenn.dev/ytjvdcm/articles/0efb9112468de3 を参考にしたのだが、何を参考にしたのか忘れた。たぶん EFI 周り。

そして、i3wm を startx するような構成にした。 具体的に、入れてるのは以下のようなものです。

#!/bin/bash

set -ex

# install: pacman -S pkgname
# search:  pacman -Ss keyword

# update list
sudo pacman -Syu

# core
sudo pacman -S --needed --noconfirm openssh vim git tmux sudo git-lfs gcc base-devel go the_silver_searcher zsh w3m curl

# nvidia driver(if needed)
sudo pacman -S --needed --noconfirm nvidia

# manage mirror site
sudo pacman -S reflector
sudo reflector --country 'Japan' --age 24 --protocol https --sort rate --save /etc/pacman.d/mirrorlist

# network
# network-manager-applet=nm-applet
sudo pacman -S --needed --noconfirm networkmanager network-manager-applet
sudo systemctl enable NetworkManager

# yay
# yay is the wrapper for AUR and pacman.
if [[ -e /usr/bin/yay ]]; then
    echo "yay exists"
else
    git clone https://aur.archlinux.org/yay.git /tmp/yay && cd /tmp/yay && makepkg -si
fi


# X
sudo pacman -S --needed --noconfirm wezterm firefox i3lock xdg-utils
yay -S --needed --noconfirm google-chrome
yay -S --needed --noconfirm obsidian
# startx
yay -S --needed --noconfirm xorg-xinit

# i3
sudo pacman -S --needed --noconfirm rofi i3-wm polybar xss-lock dunst

# Japanese
sudo pacman -S --needed --noconfirm adobe-source-han-sans-jp-fonts adobe-source-han-serif-jp-fonts otf-ipafont noto-fonts noto-fonts-cjk noto-fonts-emoji
yay -S --needed --noconfirm ttf-mona ttf-monapo ttf-ipa-mona ttf-vlgothic ttf-mplus ttf-koruri ttf-mplus ttf-sazanami ttf-hanazono
sudo pacman -S --needed --noconfirm fcitx5-mozc fcitx5-configtool fitx5-gtk fcitx5-qt fcitx5-im

# JetBrains
yay -S --noconfirm intellij-idea-ultimate-edition intellij-idea-ultimate-edition-jre
yay -S --noconfirm jetbrains-fleet

# Ultimate hacking keyboard
yay -S --noconfirm uhk-agent-appimage

./setup-vimplug.sh

# I need to enable multilib for installing steam app
yay -S --noconfirm steam

# Chatting
yay -S --noconfirm slack-electron

Ultimate hacking keyboard を買った。これは Ultimate かも?

分離型のキーボードとして MD550 を使い始めた。分離型は方が自然な位置になるのでなかなか気に入りまして、Kinesis の freestyle2 などいくつか試しました(Kinesis は微妙に手になじまずすぐに諦めてしまった)。 そんな中、Ultimate hacking keyboard を知って購入したのが数ヶ月前。長らく立って、やっと届いたのが先週ぐらい、という感じ。

特徴としては

もともと値段が高いデバイスであり、さらに円安もあるのとハンガリーという遠い場所で作られてるということもあり、まぁ高い。 ポインティングデバイスがトラックボール、トラックポイント、タッチパッドから選べるのでそれを全種類うっかり頼んだのでそれもあってまぁ高くなった。

買ったもの

サイレントの茶軸。サイレントとはいえまぁそこそこ音はします。打鍵感は最高に良いです。

キーマッピング

キーマッピングはさすがに Ultimate というだけあって、複数をキーボード側に記憶させることができる。 なので、PC用とMac用で完全にワケたキーマッピングを設定している。

Windows 用のキーマッピング

Windows を操作するときに問題になるのが、Mac では Cmd と Ctrl という2つのモディファイアを使うことによって、編集系の操作とメニュー系の操作を行うことができるわけだが、これを行う機能が Windows にはないということだ。これは非常に面倒で、擬似的に Mac 風な操作を AutoHotKey で実現していた。 しかし、AutoHotKey の設定ファイルは記号まみれでいじるのがしんどい感じのファイルなので、あまりメンテナンスする気が起きないものなのである。

Ultimate hacking keyboard なら、そういった悩みはなく、GUIでポチポチ設定すればOK。簡単だった。 マクロも設定できるんで、案外複雑なマッピングもできる。

ベースマップ

ベースのキーマップはそこまでいじらず。

Mod マップ

デフォルトだとカーソルの移動が独特なので、HJKLスタイルに変更。音量の切り替えもここに入れた。音量の切り替え、どうしてもゲームやってるときとかによく使うので、割と触りやすい位置においておきたかった。

Mouse マップ

マウスキー機能がある。が、実際にはアタッチメントをつけてるのであんまいらないかな。と思いつつ一応設定。 HJKL 風にしている。

Fn マップ

基本的にはキーマップの切り替え機能だけを設定している。

Fn2 マップ(Cmd相当)

Cmd キーを押したときをエミュレートしている。

Fn3 マップ(Ctrl 相当)

Ctrl+A を押したときに Home キーを出す、みたいなのを設定している。 Ctrl+K で kill する部分は、マクロで設定している。

Mac 用のキーマップ

基本的には Windows 用と一緒。 ただし、いくつかのカスタマイズを入れている。

進歩するハードウェア

Ultimate hacking keyboard の良いところは、V2 になったことでかなり進化していて、かつファームウェア的にも進歩しているようだ。 初期の頃に Ultimate hacking keyboard を買った人のブログ記事を一通り読んだのだが、初期の頃に買った人が書いている改善点がかなり改善されてきていて、かなり良くなっているように思う(ありがとう先人の人たち)

特に大きいのがスマートマクロ機能で、起動時に設定を変えるとかそういうのができるようになっている。 個人的にはOSごとに違うキーマップを設定しているので、OSの切り替え時に自動的にキーマップを変えられる機能がほしい。今は UHK Agent を自前でビルドすればできるぽいけどまだ試していない。

まとめ

結論、とても満足している。ポインティングデバイスが一体化していることのメリットがとにかくでかいし、キーマッピングをめっちゃこまかく設定できるんで快適。 今までだったらできなかったような設定ができてる(多分似たようなことができるものは存在しているのかもしれないが、僕は他に知らない。) ファームウェアのアップデートなどでできることが増えているようだし、今後使い続けていくのも楽しみでならない。

kotlin 1.7.21 にアップグレードしようとしたら謎エラーになるケースの対応方法

kotlin 1.7.10 から 1.7.21 にアップグレードしようとしたら以下の謎エラーが出る怪奇現象が発生していた。

e: java.lang.NoSuchMethodError: 'void kotlin.script.experimental.api.KotlinType.<init>(kotlin.reflect.KClass, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker)'
        at org.jetbrains.kotlin.scripting.definitions.ScriptCompilationConfigurationFromDefinition$1.invoke(ScriptCompilationConfigurationFromDefinition.kt:32)
        at org.jetbrains.kotlin.scripting.definitions.ScriptCompilationConfigurationFromDefinition$1.invoke(ScriptCompilationConfigurationFromDefinition.kt:28)
        at kotlin.script.experimental.api.ScriptCompilationConfiguration.<init>(scriptCompilation.kt:23)
        at kotlin.script.experimental.api.ScriptCompilationConfiguration.<init>(scriptCompilation.kt:25)
        at org.jetbrains.kotlin.scripting.definitions.ScriptCompilationConfigurationFromDefinition.<init>(ScriptCompilationConfigurationFromDefinition.kt:27)
        at org.jetbrains.kotlin.scripting.definitions.ScriptDefinition$Companion.getDefault(ScriptDefinition.kt:221)
        at org.jetbrains.kotlin.scripting.compiler.plugin.ScriptingCompilerConfigurationExtension.updateConfiguration(ScriptingCompilerConfigurationExtension.kt:67)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment$Companion.configureProjectEnvironment(KotlinCoreEnvironment.kt:578)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.<init>(KotlinCoreEnvironment.kt:199)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.<init>(KotlinCoreEnvironment.kt:108)
        at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment$Companion.createForProduction(KotlinCoreEnvironment.kt:445)
        at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.createCoreEnvironment(K2JVMCompiler.kt:192)
        at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:143)
        at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:53)
        at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:99)
        at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:47)
        at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
        at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:475)
        at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:125)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileIncrementally(IncrementalCompilerRunner.kt:373)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileIncrementally$default(IncrementalCompilerRunner.kt:318)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.rebuild(IncrementalCompilerRunner.kt:114)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:207)
        at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:79)
        at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:625)
        at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:101)
        at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1746)
        at jdk.internal.reflect.GeneratedMethodAccessor26.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
        at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at java.base/java.lang.Thread.run(Thread.java:833)

色々試した結果、spring boot bom の中で kotlin のバージョンを固定しているのが問題なようだった。以下のように dependency management plugin でバージョン指定しているため、spring boot が 2.7.5 だったので、org.jetbrains.kotlin:* のバージョンが 1.6.21 になってしまって、結果として API が齟齬ったようだ。

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
	}

というわけで、以下のようにして解決。

    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
            mavenBom("org.jetbrains.kotlin:kotlin-bom:1.7.21")
        }
	}

20221129 追記

    ext["kotlin.version"] = Versions.KOTLIN

とかするのがスマートかも

Integrate kweb and spring-boot

kweb provides the document to integrate spring-boot. https://docs.kweb.io/book/integrations.html#spring-boot

But in my opinion, it's bit complex and tricky way.

In my opinion, spring-boot users want to use the spring-boot's configuration loading feature and the bean management feature. But the sample code demonstrates the kweb integration with the spring-webmvc's servlet container.

Then, I wrote a sample code to use kweb on spring boot framework. https://github.com/tokuhirom/ktor-spring-boot-demo/blob/kweb-spring-boot-demo/src/main/kotlin/com/example/springbootdemo/SpringbootdemoApplication.kt

This code demonstrates, the code starts kweb server and it runs with spring-boot's beans and configurations. While the shutdown process, spring will call the "close" method on the Kweb instance!

blog に syntax highlight 機能をつけた

https://prismjs.com/

prism.js がナウいのかどうかはよくわからないけれど、とりあえず入れた。markdown のレンダリングに使っている flexmark が prism.js で使うように language-kotlin などの class をふってくれるので、そのまま読み込むだけ。

https://github.com/tokuhirom/blog3/pull/20/files

println("Hello")
s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s s s chr length q q each die local chown rmdir tie chr q semop and s s s chr length q qr our xor getc dump order goto log join eof write time crypt dump exit vec index open ord tied q semop and s s s chr length q q each die local chown rmdir tie chr dump fcntl warn ord eval q semop and s s s chr length q qr our break chop given given pack q semop and s s s chr length q qr our break chop given given pack q semop and s s s chr length q qr our break chop given given pack exit ord goto read undef q semop and s s s chr length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind tell each given q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind tell our log q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob ioctl untie die chr untie pack exp cmp given ref q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob ioctl untie die chr link tie q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind fcntl q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind fcntl q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob ioctl untie die chr untie pack exp cmp q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack q semop and eval eval

CPAN module を release するための docker image を作った

https://github.com/tokuhirom/docker-minil-release

CPAN module をリリースするには、色々と前準備が必要というか、、色々な CPAN module をインストールしなきゃいけなくてちょっと手間だった。最近は年に数回しかしないのだが、、リリースのたびに準備の時間がかかるので、ちょっと時間がかかって面倒なのだった。 面倒なので、ある程度設定した状態の VPS を維持していたのだが、それもなぁ、という。 このブログなども k8s に移行したので、VPS を維持する必要がなくなったのでその VPS を廃止したい。

本来なら github actions などからリリースできるようにしたいのだが、その手間もまぁまぁデカイということで、コスパないしタイパが良い範囲でやろうかなと。。 ということで、ある程度設定された状態の docker image を作っておこうかなと言う感じ。

実装の解説

FROM perl:latest

とすることにより、perl の docker image を利用する。正直、常に最新版を使ってリリースすりゃいいので、latest image を利用。

RUN apt update
RUN apt upgrade -y
RUN apt install -y vim

vim がはいってないと minil の機能である Changes の編集機能がつかえないので、vim を入れる。

ENV PERL_CPANM_OPT="--notest --no-man-pages --with-recommends --with-suggests --with-all-features --with-configure --with-develop"

ENV を設定して、とにかく依存っぽいやつをガンガン入れるように設定する。こうしないとテストコケることが多くなる。

続いて肝心の minilla を入れる。

RUN cpanm Minilla

あとは、インストールに時間かかる感じの依存をガンガン入れていく。

RUN cpanm Amon2
RUN cpanm DBD::SQLite LWP::Protocol::https DBD::mysql
RUN cpanm Catalyst::Runtime
RUN cpanm DBIx::Class
RUN cpanm Dist::Milla
RUN cpanm Tiffany
RUN cpanm Task::BeLike::TOKUHIROM
RUN cpanm Test::Perl::Critic Perl::Critic
RUN cpanm HTTP::Server::Simple::CGI Spiffy WWW::MobileCarrierJP

minilla はリリースプロセスの中で git commit するので、user.email, user.name を設定しておく。

RUN git config --global user.email "[email protected]"
RUN git config --global user.name "Tokuhiro Matsuno"

起動時に、/target/ にマウントされたソースコードを対象に、cpanm を使って依存を入れて、minil release が実行されるようにする。

WORKDIR /target/

CMD [ "/bin/bash", "-c", "cpanm --installdeps . && minil release" ]

利用方法

docker pull してイメージを取得する。

docker pull ghcr.io/tokuhirom/docker-minil-release:latest

あとは適当に volume を mount しながら実行するだけ。簡単。

docker run -it --rm -v $PWD:/target/ -v $HOME/.pause:/root/.pause -v $HOME/.ssh:/root/.ssh --name minil-release-app ghcr.io/tokuhirom/docker-minil-release:latest

Dell U4021QW というウルトラワイドモニターを買った

モニター3枚ほど並べていたのだが配線がぐちゃぐちゃすぎてうんざりしたので、ウルトラワイドモニター一枚にしてみた。 かなりポートがたくさんついているので、これに接続するだけでほとんどの接続が集約されていて、快適です。USB ハブ機能があるのが便利。デスクが広く使える。

Image from Gyazo

色々つないでたんですが、いまは↑のあたりをつないでいます

山善の昇降式デスクを買った

昇降式デスクというと、Flexispot が有名なのだが、Amazon で買えて一人でも古いデスクの回収もやってくれて、新しいデスクの組み立ても代行してくれるものという条件で探したらこれになった。 そこまで天板にこだわりないので、板を自分で選ぶとかめんどくさいなっていう感じ。

yabai と skhd を最近は利用している

yabai という mac で動く tiling window manager を活用している。これと、同じ作者の skhd というショートカットマネージャーを組み合わせることで、ウィンドウの操作をほぼ全てキーボードで実施できるので鬼便利。 僕は ratpoison とかを長年使ってた経験あるので、この手の window manager は結構好き。

.yabairc を以下のように設定している。

# global settings
yabai -m config mouse_follows_focus          off
yabai -m config focus_follows_mouse          off
yabai -m config window_origin_display        default
yabai -m config window_placement             second_child
yabai -m config window_topmost               off
yabai -m config window_shadow                on
yabai -m config window_opacity               off
yabai -m config window_opacity_duration      0.0
yabai -m config active_window_opacity        1.0
yabai -m config normal_window_opacity        0.60
yabai -m config window_border                on
yabai -m config window_border_width          6
yabai -m config active_window_border_color   0xff775759
yabai -m config normal_window_border_color   0xff555555
yabai -m config insert_feedback_color        0xffd75f5f
yabai -m config split_ratio                  0.50
yabai -m config auto_balance                 off
yabai -m config mouse_modifier               fn
yabai -m config mouse_action1                move
yabai -m config mouse_action2                resize
yabai -m config mouse_drop_action            swap

# general space settings
yabai -m config layout                       bsp
yabai -m config top_padding                  0
yabai -m config bottom_padding               0
yabai -m config left_padding                 0
yabai -m config right_padding                0
yabai -m config window_gap                   0

yabai -m rule --add app="^System Preferences$" manage=off
yabai -m rule --add app="^Skitch$" manage=off
yabai -m rule --add app="^Zoom.*$" manage=off

echo "yabai configuration loaded.."

.skhdrc を以下のようにしている。

# focus window
alt - j : ~/dotfiles/bin/yabai-focus.pl
alt - k : ~/dotfiles/bin/yabai-focus.pl -r

# 画面を大きく3つのfloating window で占めて表示させる。
# 一番左に LINE アプリを表示。LINE アプリで本文が表示されるギリギリの横幅がこれ。
alt - 1: yabai -m window --toggle float && yabai -m window --grid 1:10:0:0:3:1
alt - 2: yabai -m window --toggle float && yabai -m window --grid 1:10:3:0:4:1
alt - 3: yabai -m window --toggle float && yabai -m window --grid 1:10:7:0:3:1


# swap window(よく使う)
shift + alt - h : yabai -m window --swap west
shift + alt - j : yabai -m window --swap south
shift + alt - k : yabai -m window --swap north
shift + alt - l : yabai -m window --swap east
# move window
shift + cmd - h : yabai -m window --warp west
shift + cmd - j : yabai -m window --warp south
shift + cmd - k : yabai -m window --warp north
shift + cmd - l : yabai -m window --warp east
# move window
shift + ctrl - a : yabai -m window --move rel:-20:0
shift + ctrl - s : yabai -m window --move rel:0:20
shift + ctrl - w : yabai -m window --move rel:0:-20
shift + ctrl - d : yabai -m window --move rel:20:0
# increase window size(よく使う)
shift + alt - a : yabai -m window --resize left:-20:0
shift + alt - s : yabai -m window --resize bottom:0:20
shift + alt - w : yabai -m window --resize top:0:-20
shift + alt - d : yabai -m window --resize right:20:0
# decrease window size
shift + cmd - a : yabai -m window --resize left:20:0
shift + cmd - s : yabai -m window --resize bottom:0:-20
shift + cmd - w : yabai -m window --resize top:0:20
shift + cmd - d : yabai -m window --resize right:-20:0
# rotate tree
alt - r : yabai -m space --rotate 90
# mirror tree y-axis
alt - y : yabai -m space --mirror y-axis
# mirror tree x-axis
alt - x : yabai -m space --mirror x-axis
# toggle desktop offset
alt - a : yabai -m space --toggle padding && yabai -m space --toggle gap
# toggle window fullscreen zoom
alt - f : yabai -m window --toggle zoom-fullscreen
# toggle window native fullscreen
shift + alt - f : yabai -m window --toggle float
# toggle window split type
alt - e : yabai -m window --toggle split

alt - o : yabai -m space --focus 2
alt - i : yabai -m space --move 2



# https://iter01.com/667321.html

で、ウィンドウの切り替えに以下のスクリプトを利用。

#!/usr/bin/perl
use strict;
use warnings;
use JSON::PP qw/decode_json/;

my $reversed = 0;
if (@ARGV == 1 && $ARGV[0] eq '-r') {
    $reversed = 1;
}

my $json = `yabai -m query --windows --space`;
my @dat = @{decode_json($json)};
my @sorted = sort {
    $a->{'is-floating'} <=> $b->{'is-floating'}
    || $a->{'frame'}{x} <=> $b->{'frame'}{x}
    || $a->{'frame'}{y} <=> $b->{'frame'}{y}
    || $a->{'frame'}{w} <=> $b->{'frame'}{w}
    || $a->{'frame'}{h} <=> $b->{'frame'}{h}
    || $a->{'id'} <=> $b->{'id'}
} @dat;
my $prev_is_focused = 0;
my @targets = 0..@sorted-1;
if ($reversed) {
    @targets = reverse @targets;
}
for my $i (@targets) {
    if ($prev_is_focused != 0) {
        focus($sorted[$i]->{id});
        exit 0;
    }
    if ($sorted[$i]->{'has-focus'}) {
        $prev_is_focused++;
    }
}
focus($sorted[$reversed ? -1 : 0]->{id});
exit 0;

sub focus {
    my $id = shift;
    system("yabai -m window --focus $id");
}

僕は、一般的なタイリングウィンドウマネージャー的な使い方ではなく、ちょっと変わった使い方をしている。 つかっているのがウルトラワイドモニターなので、フツウに bsp にすると画面を有効に使えてない気がするので、ちょいとした工夫をしている。 alt+1,alt+2,alt+3 で、ウィンドウをほぼ3分割して表示するようにしている。中央はやや広め。で、この3つはfloating windowとする。 alt+j, alt+k でのウィンドウの切り替えをするのだが、floating window を左から順番に切り替えて、その後 bsp を左から順番に切り替えるという設定にすることにより、ウィンドウの切り替えが速くなった。

java.util.Random が mockito でモックできないとき

ExponentialBackoffTest > test() FAILED
    org.mockito.exceptions.base.MockitoException: 
    Mockito cannot mock this class: class java.util.Random.

    Mockito can only mock non-private & non-final classes.
    If you're not sure why you're getting this error, please report to the mailing list.


    Java               : 17
    JVM vendor name    : Eclipse Adoptium
    JVM vendor version : 17.0.2+8
    JVM name           : OpenJDK 64-Bit Server VM
    JVM version        : 17.0.2+8
    JVM info           : mixed mode
    OS name            : Mac OS X
    OS version         : 12.3

こういうエラーがでていた。

ネットを探すと --add-opens をつけろという記事が多いのだが、実はmock(Random.class, withSettings().withoutAnnotations()) とすれば良いようだ。

https://stackoverflow.com/questions/70993863/mockito-can-not-mock-random-in-java-17/70994194#70994194

問題が起きていた pyroscope-java については、PR を出して修正済み。 https://github.com/pyroscope-io/pyroscope-java/pull/26

spring-session-data-redis に含まれる RedisSessionRepository を利用して redis の容量を節約する

spring-session-data-redis のデフォルト実装である RedisIndexedSessionRepository を利用すると以下の問題があります。

これに対して、大規模サービス用のよりシンプルな実装として RedisSessionRepository が提供されている。これを用いれば、既存実装の問題点から開放されることができる。

検証

RedisIndexedSessionRepository

↓RedisIndexedSessionRepository で、ログインするまでに実行される redis のコードは以下のようになっている。一人がログインするだけなのに、めちゃくちゃ実行コマンドが多い。

1646916911.858574 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xc2\xe7" "maxInactiveInterval" "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b" "creationTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xc2\xe7" "sessionAttr:SPRING_SECURITY_SAVED_REQUEST" "\xac\xed\x00\x05sr\x00Aorg.springframework.security.web.savedrequest.DefaultSavedRequest\x1e@HD\xf96d\x94\x02\x00\x0eI\x00\nserverPortL\x00\x0bcontextPatht\x00\x12Ljava/lang/String;L\x00\acookiest\x00\x15Ljava/util/ArrayList;L\x00\aheaderst\x00\x0fLjava/util/Map;L\x00\alocalesq\x00~\x00\x02L\x00\x06methodq\x00~\x00\x01L\x00\nparametersq\x00~\x00\x03L\x00\bpathInfoq\x00~\x00\x01L\x00\x0bqueryStringq\x00~\x00\x01L\x00\nrequestURIq\x00~\x00\x01L\x00\nrequestURLq\x00~\x00\x01L\x00\x06schemeq\x00~\x00\x01L\x00\nserverNameq\x00~\x00\x01L\x00\x0bservletPathq\x00~\x00\x01xp\x00\x00\x1f\x90t\x00\x00sr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xsr\x00\x11java.util.TreeMap\x0c\xc1\xf6>-%j\xe6\x03\x00\x01L\x00\ncomparatort\x00\x16Ljava/util/Comparator;xpsr\x00*java.lang.String$CaseInsensitiveComparatorw\x03\\}\\P\xe5\xce\x02\x00\x00xpw\x04\x00\x00\x00\x10t\x00\x06acceptsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x87text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9xt\x00\x0faccept-encodingsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x11gzip, deflate, brxt\x00\x0faccept-languagesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00#ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7xt\x00\rcache-controlsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\tmax-age=0xt\x00\nconnectionsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\nkeep-alivext\x00\x04hostsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x0elocalhost:8080xt\x00\areferersq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x1bhttp://localhost:8080/loginxt\x00\tsec-ch-uasq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00@\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"98\", \"Google Chrome\";v=\"98\"xt\x00\x10sec-ch-ua-mobilesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x02?0xt\x00\x12sec-ch-ua-platformsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\a\"macOS\"xt\x00\x0esec-fetch-destsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\bdocumentxt\x00\x0esec-fetch-modesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\bnavigatext\x00\x0esec-fetch-sitesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x0bsame-originxt\x00\x0esec-fetch-usersq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x02?1xt\x00\x19upgrade-insecure-requestssq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x011xt\x00\nuser-agentsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00yMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36xxsq\x00~\x00\x06\x00\x00\x00\x04w\x04\x00\x00\x00\x04sr\x00\x10java.util.Locale~\xf8\x11`\x9c0\xf9\xec\x03\x00\x06I\x00\bhashcodeL\x00\acountryq\x00~\x00\x01L\x00\nextensionsq\x00~\x00\x01L\x00\blanguageq\x00~\x00\x01L\x00\x06scriptq\x00~\x00\x01L\x00\avariantq\x00~\x00\x01xp\xff\xff\xff\xfft\x00\x02JPq\x00~\x00\x05t\x00\x02jaq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xffq\x00~\x00\x05q\x00~\x00\x05q\x00~\x00Aq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xfft\x00\x02USq\x00~\x00\x05t\x00\x02enq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xffq\x00~\x00\x05q\x00~\x00\x05q\x00~\x00Eq\x00~\x00\x05q\x00~\x00\x05xxt\x00\x03GETsq\x00~\x00\bpw\x04\x00\x00\x00\x00xppt\x00\x01/t\x00\x16http://localhost:8080/t\x00\x04httpt\x00\tlocalhostt\x00\x01/"
1646916911.860858 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916911.862692 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916911.864704 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" ""
1646916911.865652 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "1800000"
1646916911.866575 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "2100000"
1646916911.868053 [0 127.0.0.1:59916] "PUBLISH" "spring:session:event:0:created:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "\xac\xed\x00\x05sr\x00\x11java.util.HashMap\x05\a\xda\xc1\xc3\x16`\xd1\x03\x00\x02F\x00\nloadFactorI\x00\tthresholdxp?@\x00\x00\x00\x00\x00\x04w\b\x00\x00\x00\x04\x00\x00\x00\x00x"
1646916911.882439 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916911.887132 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xc3\x0c" "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" "\xac\xed\x00\x05sr\x006org.springframework.security.web.csrf.DefaultCsrfTokenZ\xef\xb7\xc8/\xa2\xfb\xd5\x02\x00\x03L\x00\nheaderNamet\x00\x12Ljava/lang/String;L\x00\rparameterNameq\x00~\x00\x01L\x00\x05tokenq\x00~\x00\x01xpt\x00\x0cX-CSRF-TOKENt\x00\x05_csrft\x00$31641e92-2d3d-486b-82e4-efcad96ee8fe"
1646916911.887987 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916911.888732 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916911.889398 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" ""
1646916911.890051 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "1800000"
1646916911.890638 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "2100000"
1646916911.891258 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916911.892703 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916914.624004 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916914.825021 [0 127.0.0.1:59916] "RENAME" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.825777 [0 127.0.0.1:59916] "RENAME" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.829586 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xcd\xc4" "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION" "" "sessionAttr:SPRING_SECURITY_CONTEXT" "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$c332c966-2d8b-4cb9-a940-fcced7ba25d2psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user" "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" ""
1646916914.831427 [0 127.0.0.1:59916] "SADD" "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user" "\xac\xed\x00\x05t\x00$44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.832225 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.832900 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916914.833575 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" ""
1646916914.834209 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "1800000"
1646916914.834850 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "2100000"
1646916914.835507 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916914.836309 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "sessionAttr:SPRING_SECURITY_CONTEXT" "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$c332c966-2d8b-4cb9-a940-fcced7ba25d2psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user"
1646916914.837401 [0 127.0.0.1:59916] "SREM" "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user" "\xac\xed\x00\x05t\x00$44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.838195 [0 127.0.0.1:59916] "SADD" "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user" "\xac\xed\x00\x05t\x00$44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.838894 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.839500 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916914.840189 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" ""
1646916914.840861 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "1800000"
1646916914.841486 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "2100000"
1646916914.842081 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916914.847443 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.856865 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xce\xa0" "sessionAttr:SPRING_SECURITY_SAVED_REQUEST" ""
1646916914.860546 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.861292 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916914.861985 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" ""
1646916914.862693 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "1800000"
1646916914.864418 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "2100000"
1646916914.864982 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.866710 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.887045 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"

↓ご存知のとおり、 TTL が設定されていないキーが一つある し、無駄が多そう。 このキーは、spring-security を有効化していると、設定されるようになる ようだ。

127.0.0.1:6379> eval "local t = {} local i = 1 for _,v in ipairs(redis.call('KEYS', '*')) do if redis.call('TTL',v) == -1 then t[i] = v i = i + 1 end end return t" 0
1) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user"
127.0.0.1:6379> keys *
1) "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
2) "spring:session:expirations:1646918760000"
3) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user"
4) "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
127.0.0.1:6379> hgetall spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48
 1) "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN"
 2) ""
 3) "sessionAttr:SPRING_SECURITY_CONTEXT"
 4) "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$c332c966-2d8b-4cb9-a940-fcced7ba25d2psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user"
 5) "sessionAttr:SPRING_SECURITY_SAVED_REQUEST"
 6) ""
 7) "creationTime"
 8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xc2\xe7"
 9) "maxInactiveInterval"
10) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
11) "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION"
12) ""
13) "lastAccessedTime"
14) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xce\xa0"
127.0.0.1:6379> smembers spring:session:expirations:1646918760000
1) "\xac\xed\x00\x05t\x00,expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
2) "\xac\xed\x00\x05t\x00,expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
127.0.0.1:6379> smembers spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user
1) "\xac\xed\x00\x05t\x00$44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
127.0.0.1:6379> get spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48
""

ここから、1分間ごとに spring:session:expirations:1646918760000 をなめて消す処理が走る。しかし、ここには以下のような問題がある。

特定ユーザーのログインセッションを一括で消す機能があって便利だが、これもまぁ別に RedisSessionRepository をベースに自前で用意したほうが良いかもしれない。

RedisSessionRepository

RedisSessionRepository はよりシンプルな新しいクラスである。https://github.com/spring-projects/spring-session/issues/1278 の PR で導入されていて、複雑すぎる RedisIndexedSessionRepository に対する代替案として提示されているものである。今後はこっちをデフォルトにする計画もあるっぽいです。 https://github.com/spring-projects/spring-session/issues/1711

(SimpleRedisOperationsSessionRepositoryという名前から変更された)

↓RedisSessionRepository での実行結果。そこそこ多いが、だいぶマシである。

1646916589.099128 [0 127.0.0.1:59338] "HMSET" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xd6!" "maxInactiveInterval" "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b" "sessionAttr:SPRING_SECURITY_SAVED_REQUEST" "\xac\xed\x00\x05sr\x00Aorg.springframework.security.web.savedrequest.DefaultSavedRequest\x1e@HD\xf96d\x94\x02\x00\x0eI\x00\nserverPortL\x00\x0bcontextPatht\x00\x12Ljava/lang/String;L\x00\acookiest\x00\x15Ljava/util/ArrayList;L\x00\aheaderst\x00\x0fLjava/util/Map;L\x00\alocalesq\x00~\x00\x02L\x00\x06methodq\x00~\x00\x01L\x00\nparametersq\x00~\x00\x03L\x00\bpathInfoq\x00~\x00\x01L\x00\x0bqueryStringq\x00~\x00\x01L\x00\nrequestURIq\x00~\x00\x01L\x00\nrequestURLq\x00~\x00\x01L\x00\x06schemeq\x00~\x00\x01L\x00\nserverNameq\x00~\x00\x01L\x00\x0bservletPathq\x00~\x00\x01xp\x00\x00\x1f\x90t\x00\x00sr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xsr\x00\x11java.util.TreeMap\x0c\xc1\xf6>-%j\xe6\x03\x00\x01L\x00\ncomparatort\x00\x16Ljava/util/Comparator;xpsr\x00*java.lang.String$CaseInsensitiveComparatorw\x03\\}\\P\xe5\xce\x02\x00\x00xpw\x04\x00\x00\x00\x10t\x00\x06acceptsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x87text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9xt\x00\x0faccept-encodingsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x11gzip, deflate, brxt\x00\x0faccept-languagesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00#ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7xt\x00\rcache-controlsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\tmax-age=0xt\x00\nconnectionsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\nkeep-alivext\x00\x04hostsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x0elocalhost:8080xt\x00\areferersq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x1bhttp://localhost:8080/loginxt\x00\tsec-ch-uasq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00@\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"98\", \"Google Chrome\";v=\"98\"xt\x00\x10sec-ch-ua-mobilesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x02?0xt\x00\x12sec-ch-ua-platformsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\a\"macOS\"xt\x00\x0esec-fetch-destsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\bdocumentxt\x00\x0esec-fetch-modesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\bnavigatext\x00\x0esec-fetch-sitesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x0bsame-originxt\x00\x0esec-fetch-usersq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x02?1xt\x00\x19upgrade-insecure-requestssq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x011xt\x00\nuser-agentsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00yMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36xxsq\x00~\x00\x06\x00\x00\x00\x04w\x04\x00\x00\x00\x04sr\x00\x10java.util.Locale~\xf8\x11`\x9c0\xf9\xec\x03\x00\x06I\x00\bhashcodeL\x00\acountryq\x00~\x00\x01L\x00\nextensionsq\x00~\x00\x01L\x00\blanguageq\x00~\x00\x01L\x00\x06scriptq\x00~\x00\x01L\x00\avariantq\x00~\x00\x01xp\xff\xff\xff\xfft\x00\x02JPq\x00~\x00\x05t\x00\x02jaq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xffq\x00~\x00\x05q\x00~\x00\x05q\x00~\x00Aq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xfft\x00\x02USq\x00~\x00\x05t\x00\x02enq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xffq\x00~\x00\x05q\x00~\x00\x05q\x00~\x00Eq\x00~\x00\x05q\x00~\x00\x05xxt\x00\x03GETsq\x00~\x00\bpw\x04\x00\x00\x00\x00xppt\x00\x01/t\x00\x16http://localhost:8080/t\x00\x04httpt\x00\tlocalhostt\x00\x01/" "creationTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xd6!"
1646916589.102109 [0 127.0.0.1:59338] "PEXPIREAT" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "1646918389089"
1646916589.105447 [0 127.0.0.1:59338] "EXISTS" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.116953 [0 127.0.0.1:59338] "HGETALL" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.122376 [0 127.0.0.1:59338] "EXISTS" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.125151 [0 127.0.0.1:59338] "HMSET" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xd6@" "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" "\xac\xed\x00\x05sr\x006org.springframework.security.web.csrf.DefaultCsrfTokenZ\xef\xb7\xc8/\xa2\xfb\xd5\x02\x00\x03L\x00\nheaderNamet\x00\x12Ljava/lang/String;L\x00\rparameterNameq\x00~\x00\x01L\x00\x05tokenq\x00~\x00\x01xpt\x00\x0cX-CSRF-TOKENt\x00\x05_csrft\x00$49938ae3-2c5a-4343-a983-3b941ec5fe34"
1646916589.127423 [0 127.0.0.1:59338] "PEXPIREAT" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "1646918389120"
1646916589.129688 [0 127.0.0.1:59338] "HGETALL" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.133727 [0 127.0.0.1:59338] "EXISTS" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.135493 [0 127.0.0.1:59338] "HGETALL" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916592.188724 [0 127.0.0.1:59338] "HGETALL" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916592.312255 [0 127.0.0.1:59338] "EXISTS" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916592.313174 [0 127.0.0.1:59338] "RENAME" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0"
1646916592.314158 [0 127.0.0.1:59338] "HMSET" "spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xe2>" "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION" "" "sessionAttr:SPRING_SECURITY_CONTEXT" "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$82b6e568-278f-41bf-8ac5-ca7861460d90psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user" "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" ""

↓ ** TTL が設定されていないキーは存在しないし、1セッションにつき1つのハッシュのみがセットされていて、大きくデータ量を削減することができる。**

(デフォルトで30分の TTL っぽい)

127.0.0.1:6379> eval "local t = {} local i = 1 for _,v in ipairs(redis.call('KEYS', '*')) do if redis.call('TTL',v) == -1 then t[i] = v i = i + 1 end end return t" 0
(empty array)
127.0.0.1:6379> keys *
1) "spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0"
127.0.0.1:6379> type spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0
hash
127.0.0.1:6379> hgetall spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0
 1) "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN"
 2) ""
 3) "sessionAttr:SPRING_SECURITY_CONTEXT"
 4) "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$82b6e568-278f-41bf-8ac5-ca7861460d90psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user"
 5) "sessionAttr:SPRING_SECURITY_SAVED_REQUEST"
 6) ""
 7) "creationTime"
 8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xd6!"
 9) "maxInactiveInterval"
10) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
11) "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION"
12) ""
13) "lastAccessedTime"
14) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xe2\xc6"
 
127.0.0.1:6379> ttl spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0
(integer) 1764

現在は、RedisSessionRepository は、デフォルトの実装ではないこともあってドキュメントが少ない。

https://spring.io/blog/2019/06/18/spring-session-corn-m2-and-spring-session-bean-sr6-released

https://github.com/spring-projects/spring-session/tree/main/spring-session-samples/spring-session-sample-boot-redis-simple

このあたりのドキュメントしかないのでこのへんを参照のこと。

↓のように設定すれば、RedisSessionRepository を利用できる。

package com.example.demo;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.RedisSessionRepository;
 
@EnableSpringHttpSession
@Configuration(proxyBeanMethods = false)
public class MySessionConfiguration {
    @Bean
    public RedisOperations<String, Object> sessionRedisOperations(RedisConnectionFactory redisConnectionFactory) {
        org.springframework.data.redis.core.RedisTemplate<String, Object> redisTemplate = new org.springframework.data.redis.core.RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
 
    @Bean
    public RedisSessionRepository redisSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
        return new RedisSessionRepository(sessionRedisOperations);
    }
}

See also

https://kagamihoge.hatenablog.com/entry/2018/05/19/163524