Thymeleaf 3.1にアップグレード

公式に記載の通り、#request、#response、#session、#servletContextが使用できなくなった。

1.6 Removal of web-API based expression utility objects
The #request, #response, #session, and #servletContext are no longer available to expressions in Thymeleaf 3.1.

Thymeleaf 3.1: What’s new and how to migrate

下記の通り修正して対応した。

${#servletContext.getAttribute('assetsVersion')}
       ↓
${application.assetsVersion}
${#request.remoteUser}
       ↓
remoteUserをSessionに保持するようにして、
${session.remoteUser}

※ 2022/11/30 追記
Spring Securityを使用している場合、remoteUserを取得するなら、Sessionに保持は不要で、下記で可能だった。
${#authentication.name}

AWSのUbuntuを22.04.1にアップグレードする

AWS上のUbuntuをアップグレードしたときの手順を記録しておく。

注意)アップグレード後、phpが動かなくなってしまった。5の設定変更を実施したら再び動くようになった。

1.ファイアウォールの確認と設定

do-release-upgradeをリモートで実行し、アップグレード作業を行う場合は、1022 portの使用が推奨されている。ファイアウォールを実行している場合、事前にportの開放を行う。

sudo ufw allow 1022/tcp comment 'Temp open port ssh tcp port 1022 for upgrade'

2.セキュリティグループの設定

セキュリティグループのインバウンドルールに1022 portの許可を追加する。アップグレード作業にしか使わないportであるため、アクセス範囲は可能な限り狭く設定する。

3.アップグレードコマンドの実行

sudo do-release-upgrade

スクリプトの実行後、サーバーを再起動する。

4.Tera Termでログイン出来るようにする

Ubuntu 22ではssh-rsaが無効になっているため、Tera Termを使ってログインすることが出来ない。そこで下記のコマンドを実行して、ssh-rsaを有効にする。(Tera Termが使えないだけで、他の手段であればログイン出来る。Powershellや他のLinuxサーバーのsshコマンドでログインする。)

sudo su -
cp -p /etc/ssh/sshd_config /etc/ssh/sshd_config_org
sed -i '1s/^/PubkeyAcceptedAlgorithms=+ssh-rsa\n/' /etc/ssh/sshd_config
grep PubkeyAcceptedAlgorithms /etc/ssh/sshd_config
systemctl restart ssh.service

5.php8.1を有効にする

Ubuntu 22ではphp7.4が削除され、php 8.1が標準になっている。以下のコマンドでphp7.4の設定を削除して、php8.1を有効にする。

sudo a2dismod php7.4
sudo a2enmod php8.1
sudo service apache2 restart

6.セキュリティグループの設定

セキュリティグループのインバウンドルールの1022 portの許可を削除する。

NovelAIを使ってみる

最近話題のNovelAIを使ってみる。

ユーザー登録する

https://novelai.net/

上記ページにアクセスして、ユーザー登録を行う。画像の生成には後述のサブスク登録が必要になるため、ユーザー登録が必要になる。

サブスク登録する

サブスク登録していないと、画像の生成機能を使用することが出来ない。10ドルのプランで1000 Anlas分使用できる。Nomalサイズの画像生成で5 Anlas必要になるため、このプランでは毎月200枚ほど生成出来る計算になる。足りないなら後でプランをアップグレードしたり、Anlasだけ買い足したりすることが出来るので、お試しであれば10ドルのプランで良いと思う。

画像を生成する

Image Generationをクリックする。

Enter your prompt here. に文字を入力する。(英語)

自分が試しに入力した文字は下記の通り。
masterpiece, highest quality, high quality, cyberpunk,
上記とは別にUndesired Contentに以下の文字列を入力した。
lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, artist name

生成された画像は下記の通り。

簡単にきれいな画像を生成することは出来るが、一発でほしいものが出るとは限らない。設定を変えながら何度も生成することになるので、トークンの減りが激しい。

変数のコピー元を変更するとコピー先が変わる場合と変わらない場合

Javaにおいて変数のコピー先が変わらない例

String a = "りんご";  // (1)
String b = a; // 変数bにaを代入 (2)
System.out.println(a);
System.out.println(b);

実行結果
りんご
りんご
// 変数aに値を設定し直す
a = "みかん";  // (3)
System.out.println(a);
System.out.println(b);

実行結果
みかん
りんご

変数aは「みかん」に置き換わるが、変数bは変わらない。これは下記のようなイメージ。

 (1) String a = “りんご”のイメージ
  「りんご」の写真を撮り、その写真をaという箱に入れる。

 (2) String a = bのイメージ
  aの箱に入っている「りんご」の写真のコピーを取り、そのコピーをbの箱に入れる。

 (3) a = “みかん”のイメージ
  aの箱に入っていた「りんご」の写真を捨てて、aの箱に「みかん」の写真を入れる。

コピーを取ってbの箱に入れた「りんご」の写真はそのままなので、System.out.println(b)は「りんご」となる。

コピー先が変わる例

// 配列を作成する
String[] c = new String[] { "りんご", "バナナ" };  // (1)
String[] d = c;  // (2)

System.out.println(d[0]);
System.out.println(d[1]);

c[0] = "みかん";  // (3)

System.out.println(d[0]);
System.out.println(d[1]);

実行結果
りんご
バナナ
みかん
バナナ

配列の要素c[0]を変更すると、d[0]の内容も変わる。これは下記のようなイメージ。

 (1) String[] c = new String[] { “りんご”, “バナナ” }のイメージ
  「りんご」の写真を撮って、cという棚の[0]という箱に入れる。
  「バナナ」の写真を撮って、cという棚の[1]という箱に入れる。

 (2) String[] d = cのイメージ
  cの棚の箱を全て取り出して、中身をコピーするのは大変なので(今回は2個だけだが、通常はもっと多いことを想定している)、
  dという棚の[0]の位置にcという棚の[0]を見るようにとメモを置く。
  dという棚の[1]の位置にcという棚の[1]を見るようにとメモを置く。

 (3) c[0] = “みかん”のイメージ
  「みかん」の写真を撮って、cという棚の[0]という箱に入れる。

変数dには値のコピーはなく、参照先の情報しかないため、System.out.println(d[0])とするとc[0]の情報を参照することになり、コピー元と同様に表示される値が変わることになる。

同様に変数cとdを逆にしても、表示される値が変わる。

String[] c = new String[] { "りんご", "バナナ" };
String[] d = c;

System.out.println(c[0]);
System.out.println(c[1]);

d[0] = "みかん"; // コピー先を書き換える (4)

System.out.println(c[0]);
System.out.println(c[1]);

実行結果
りんご
バナナ
みかん
バナナ

 (4) d[0] = “みかん”のイメージ
  「みかん」の写真を撮って、dという棚の[0]という箱に入れようとしたら、そこに箱はなく、cという棚の[0]を見るようにとメモがあったので、cという棚の[0]の箱の中に「みかん」の写真を入れる。

(4)の操作でc[0]の内容が変わってしまっているので、System.out.println(c[0])の出力も「みかん」になる。

EclipseでGitの作成者、コミッターの初期設定を変更する

EclipseでGitを使用するとき、作成者、コミッターの欄に、OSのユーザー、コンピューター名が設定されてしまう。コミットする度に修正するのは面倒なので、最初からほしい名称が表示されるようにしたい。

Eclipseの設定のGit関連の設定項目の中から「構成」を選択する。エントリーの追加をクリックし、以下の項目を追加する。

user.name

user.email

Eclipseを一度再起動すると、先ほど設定した値が作成者、コミッターの欄に表示されるようになる。

Window 11でのドラッグアンドドロップの問題

※2022/10/28追記 Window Updateにより、タスクバーへのドラッグアンドドロップが出来るようになりました。

Window 11での問題

隠れていたり、最小化されているWindowにドロップしたい場合、Windows 10まではタスクバーにドラッグして、アプリやWindowのアイコンに重ねれば、それがアクティブになって、前面に表示されるので問題はなかった。

Windows 11ではファイルを掴んだ状態でタスクバーにもっていっても、タスクバーのアプリやWindowがアクティブにならず、ファイルをドロップすることができない。

これはバグではなく、Windows 11の仕様らしいので、今後出来るようになる可能性は低い。新しいやり方に慣れていく必要があるのかもしれない。

Window 11でのやり方

ファイルを掴んだ状態で、「Alt」+「Tab」キーを押す。そうすると実行中のアプリ、開いているWindowの一覧が表示されるので、「Alt」キーを押したまま、「Tab」キーを押し、一覧の選択を切り替える。必要なWindowが選択されたら、「Alt」キーを離す。

選択されていたWindowがアクティブになり、前面に表示されるので、ファイルをドロップすることができるようになる。

Log4jからLogbackに変更する

やりたいこと

色々バグのあったLog4jのログ出力部分のみを、Logbackに変更する。ソースコードには変更が入らないように移行する。

変更前

下記の例ではLog4j-core-2.18.0.jarが読み込まれて、ログ出力を行っている。

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.18.0</version>
        </dependency>

Javaコードは下記のように記述されている。

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class SampleImpl {
    private static final Logger log = LogManager.getLogger(SampleImpl.class);

変更後

依存関係にlogback-classic、logback-coreを追加する。log4j-slf4j-implをlog4j-to-slf4jに変更する。

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.1</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-to-slf4j</artifactId>
            <version>2.18.0</version>
        </dependency>

これで、logback-core-1.4.1.jarでログが出力されるようになる。Javaコードがコンパイルエラーになることもない。

Stable DiffusionをWSL上のUbuntu22.04で動かす

環境

Windows 11
WLS2
Ubuntu 22.04
Python 3.10.4

Hugging Faceのアカウント作成

Hugging Faceのアカウント作成し、Tokenを取得する。ユーザーの設定画面のAccess Tokenを開き、New tokenをクリックしてTokenを作成する。後で使用するためコピーしておく。※Email認証がまだの場合はボタンが非活性になっているので注意

作成時の名前は適当で良い。

インストール

pip install diffusers==0.2.4 transformers scipy ftfy

実行

下記の通り実行し、Tokenを入力する。

$ huggingface-cli login

        _|    _|  _|    _|    _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|_|_|_|    _|_|      _|_|_|  _|_|_|_|
        _|    _|  _|    _|  _|        _|          _|    _|_|    _|  _|            _|        _|    _|  _|        _|
        _|_|_|_|  _|    _|  _|  _|_|  _|  _|_|    _|    _|  _|  _|  _|  _|_|      _|_|_|    _|_|_|_|  _|        _|_|_|
        _|    _|  _|    _|  _|    _|  _|    _|    _|    _|    _|_|  _|    _|      _|        _|    _|  _|        _|
        _|    _|    _|_|      _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|        _|    _|    _|_|_|  _|_|_|_|

        To login, `huggingface_hub` now requires a token generated from https://huggingface.co/settings/tokens .
        
Token: 
Login successful
Your token has been saved to /home/ユーザーID/.huggingface/token

Hugging Faceにログインし、以下のページにアクセスして、Access repositoryをクリックする。
https://huggingface.co/CompVis/stable-diffusion-v1-4

cudaの設定が面倒なので、今回はcudaなしで実行する。

from diffusers import StableDiffusionPipeline

pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", use_auth_token=True)

prompt = "a photograph of an astronaut riding a horse"

image = pipe(prompt)["sample"][0]

image.save("horse.png")

image

実行結果

Hibernate Search 6でFacetを作成したい

初めに

Hibernate Searchを使用して、キーワードに登場する単語の回数を集計したい。※例)残酷な描写あり: 873, 異世界: 481, ハーレム: 471

Hibernate 5まではFacet専用の機能が実装されていたのだが、Hibernate 6からはAggregation機能が実装され、より一般的な集計処理となった。

実装例

集計を実施したいエンティティの変数に下記のようにアノテーションを追加する。

@KeywordField(name = "keyword_facet", aggregable = Aggregable.YES)

集計処理

@Transactional
public Map<String, Long> aggregateByKeywords() {
    AggregationKey<Map<String, Long>> countsByKeywordKey = AggregationKey.of("countsByKeyword");

    return Search.session(entityManager)
            .search(Novel.class) // エンティティ
            .where(f -> f.matchAll()) // 全検索
            .aggregation(countsByKeywordKey, f -> f.terms()
                    .field("keyword_facet", String.class)
                    .maxTermCount(10)) // 登場回数の多い順10件まで
            .fetch(10)
            .aggregation(countsByKeywordKey);
}

集計結果

Mapにキーワードとその文字の登場回数が設定されているので、Loop処理等で展開する。以下はVueの実装例。

<span v-for="(value, name, index) in aggregateByKeywords" :key="index">
    <span v-if="index > 0">, </span>
    {{ name }}: {{ value }}
</span>

R15: 1074, 残酷な描写あり: 873, 異世界: 481, ハーレム: 471, ファンタジー: 451, 異世界転移: 389, チート: 378, 魔法: 362, 冒険: 319, 主人公最強: 296

Hibernate ORMとHibernate Searchで検索結果をソートする

備忘録として、いくつかの実装例を記載しておく。

Hibernate ORMのソート(Spring Data JPAを使用する)

Spring Bootを使用していなくても、下記のように依存関係を読み込むことでSpring Data JPAを使用できる。

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>${spring-data.version}</version>
</dependency>

クエリメソッドを使用する

ルールに沿ったメソッド名を使用することにより、自動的にHibernateへSQLが発行される。

public interface CrudRepository<T, ID> extends Repository<T, ID> {
    // 登録、または更新
    <S extends T> S save(S entity);      
    // プライマリーキーで検索
    Optional<T> findById(ID primaryKey); 
    // 全検索
    Iterable<T> findAll();               
    // 全件数
    long count();                        
    // 削除
    void delete(T entity);               
    // 存在チェック
    boolean existsById(ID primaryKey); 

例えば、小説データを全検索して、小説のタイトルでソートをかけたい場合、下記のように書く。複数のソート条件を指定する場合は続けて記載する。

@Repository
public interface NovelRepository extends JpaRepository<Novel, Long>, JpaSpecificationExecutor<Novel> {
    List<Novel> findAllByOrderByTitle();
}

※「findAll」の後に余分な「By」があるのは、ルール的にそこに検索条件が入ることになるため、今回は全検索なので「By」だけが残っている。

条件 APIを使用する

criteria を記述することにより、クエリの where 句を定義している場合、ソートは以下のように定義する。複数のソート条件を指定する場合は引数を追加する。

novelRepository.findAll(spec, JpaSort.by("title"))
@Repository
public interface NovelRepository extends JpaSpecificationExecutor<Novel> {
}

Hibernate Searchのソート

ソートの処理実装前に、エンティティに@KeywordFieldを追記する必要がある。

    /** タイトル */
    @FullTextField(analyzer = "japanese")
    @KeywordField(name = "titleSort", sortable = Sortable.YES)
    private String title;

検索時に上記で定義した名称をソート条件に指定する。

import org.hibernate.search.mapper.orm.Search;

@Service
public class NovelService {
    private final EntityManager entityManager;

    @Transactional
    public List<Novel> findAllIndex() {
        return Search.session(entityManager)
                .search(Novel.class)
                .where(f -> f.matchAll())
                .sort(f -> f.field("titleSort"))
                .fetchAllHits();
    }

ソートの条件を指定していないときは、スコアの高い順にソートされている。大雑把に言えば、スコアが高いほど、より多くの述語にマッチしているか、より良くマッチしていることを意味する。そのため、Hibernate Searchに関しては明示的にソートの条件を指定しない方が、良いのかもしれない。