日本語CLIPを使って画像検索を作ったら素晴らしすぎた

LINEヤフーから日本語CLIPが出ていたので、どうやって使うんだろうと試してたら、なんかめちゃくちゃ便利な画像検索ができてしまいました。

clip-japanese-basic

LINEヤフーの日本語CLIP、clip-japanese-baseはこちらで紹介されています。
高性能な日本語マルチモーダル基盤モデル「clip-japanese-base」を公開しました

HuggingFaceのモデルはこちら。
https://huggingface.co/line-corporation/clip-japanese-base

CLIPとは?

ところでCLIPとは、となりますけど、OpenAIが公開してる、言語と画像を扱える機械学習モデルです。Contrastive Language-Image Pre-Trainingの略らしい。
https://openai.com/index/clip/

言葉と画像に対してそれぞれベクトルを返してくれて、内容が近ければ同じような向きになっているという仕組みです。

なので、一つの画像のベクトルをとってきて、複数の言葉のベクトルと比べてみれば、どの言葉が一番近いかというのがわかります。

HuggingFaceのサンプルでは「犬」「猫」「象」と比べてどれに近いかという分類をしてますね。

text = tokenizer(["犬", "猫", "象"])

そして犬の画像を与えてるので、最初の値が1になってる。

[[1., 0., 0.]]

逆に、一つの言葉のベクトルをとってきて、複数の画像のうち近い向きのものを探してくれば画像の検索になるというわけです。

日本語CLIPは、rinna、Stable AI、Recruitからも出ています。
rinna社、日本語に特化した言語画像モデルCLIPを公開|rinna株式会社
最高性能の、日本語画像言語特徴抽出モデル「Japanese Stable CLIP」をリリースしました — Stability AI Japan
Recruit Data Blog | 日本語CLIP 学習済みモデルと評価用データセットの公開

CLIPサーバーをつくる

ということでCLIPを使ってなにかを作りたいのですけど、HuggingFaceのモデルなので基本的にはPythonで動かします。
けれど処理はJavaで書きたいので、Web APIをつくります。
FastAPIというのを使ってWeb APIをつくりました。

画像からベクトルを得るimage_embedというエンドポイントを用意。あとでこれを使って、画像のインデックスを作ります。

@app.post("/image_embed")
def embed_text(request: TextRequest):
  image = Image.open(request.text)
  image_t = processor(image, return_tensors="pt").to(device)
  with torch.no_grad():
    image_features = model.get_image_features(**image_t)
  embedding = image_features.cpu().numpy().tolist()
  return {"embedding": embedding[0]}

それと、言語からベクトルを得るtext_embedというエンドポイントを用意。検索時にこれで検索語句からベクトルをとってきて、画像インデックスの中から近いベクトルをもってるものを探していきます。

@app.post("/text_embed")
def embed_text(request: TextRequest):
  text_t = tokenizer(request.text).to(device)
  with torch.no_grad():
    text_features = model.get_text_features(**text_t)
  embedding = text_features.cpu().numpy().tolist()
  return {"embedding": embedding[0]}

ソースはこれ。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-clip_server-py

Javaからの呼び出し

PythonのWeb APIJavaから呼び出すコードを書きます。HttpClientを使うのだけど、FastAPIがHTTP2 Upgradeに対応してないようで、HttpClientがUpgrade: h2cというヘッダーをつけないように、HTTP/1.1を指定しておきます。

private static final HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_1_1) 
        .build();

というのを前回のブログにまとめてます。
PythonのFastAPIにJavaのHttpClientから接続しようとするとupgradeできないというエラーになるのでHTTP 1.1を指定する - きしだのHatena

あとは、呼び出すのみ。

String json = mapper.writeValueAsString(new TextRequest(text));
var req = HttpRequest.newBuilder()
        .uri(URI.create(BASE_URL + endPoint))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
var body = mapper.readValue(res.body(), EmbedResponse.class);
return body.embedding();

コードはこれ。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-clipclient-java

画像からインデックスを作る

さて、JavaからWeb APIを経由してCLIPを呼び出せるようになったので、まずは画像からインデックスを作ります。
MongoDBとかに保存しようかと思ったけど、作ったインデックスを変更することはないので、ざっくりJSONで保存してます。

var images = Files.list(Path.of(PATH))
        .filter(p -> !Files.isDirectory(p))
        .map(p -> new ImageData(p, ClipClient.imageEmbedding(p.toString())))
        .toArray(ImageData[]::new);
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(Files.newOutputStream(Path.of("index.json")), images);

ちゃんとやるときはFiles.walkなどを使いましょう。
あと、CLIPの返すベクトルが単位ベクトルではないようなので、ちゃんと長さ1になるよう正規化したほうがいいです。

ソースこれ。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-createindex-java

検索ワードと比較する

インデックスができたら、検索です。
検索語句のベクトルと角度が近いベクトルの画像を探すことになります。

大量の画像に対してまじめにやるなら近似最近傍探索(ANN)を使うほうがいいのだけど、個人の画像で数万件程度だと素朴な処理で十分です。

まずは角度をとるので内積。これVector APISIMD化すると速そうだけど、要素数が少ないので、あまり効きませんでした。

    static float prod(float[] a, float[] b) {
        float score = 0;
        for(int i = 0; i < a.length; ++i) {
            score += a[i] * b[i];
        }
    }

内積ではコサインがとれるので、大きい方が角度が浅く2つのベクトルが近いということになります。あとは大きいものを5つくらいとってくるようにすればいいだけ。

    Result[] search(String text) {
        var emb = ClipClient.textEmbedding(text);

        var top5 = new Result[topCount + 1];
        for (var img : images) {
            var score = prod(emb, img.embedding());
            for (int i = top5.length - 2; i >= 0; --i) {
                if (top5[i] == null) {
                    if (i == 0 || top5[i - 1] != null) {
                        top5[i] = new Result(img, score);
                    }
                    continue;
                }
                if (top5[i].score() < score) {
                    top5[i + 1] = top5[i];
                    top5[i] = new Result(img, score);
                }
            }
        }
        return Arrays.stream(top5).limit(top5.length - 1).toArray(Result[]::new);
    }

件数が少ないので、マルチスレッドで並列化しても効果なかった。

ソースこれ。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-sercher-java

UIをつくる

じゃあ処理が全部できんたのでUIつくりましょう。ふつうのSwingのアプリです。 結果の表示をJTextPaneにHTMLで表示とか試してたのだけど、結局自分で描画しました。
ただ、そのときImageIO.readJPEGのOrientationを見てくれないので、metadata-extractorでOrientaionをみて回転するコードが必要です。

        int ori = 1;
        try {
            var metadata = ImageMetadataReader.readMetadata(Files.newInputStream(path));
            var dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
            ori = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
        } catch(Exception ex) {}
        var trans = new AffineTransform();
        switch (ori) {
            case 6 -> {//右
                trans.translate(scaledH, 0);
                trans.rotate(Math.toRadians(90));
            }
            case 3 -> {//逆
                trans.translate(scaledW, scaledH);
                trans.rotate(Math.toRadians(180));
            }
            case 8 -> {//左
                trans.translate(0, scaledW);
                trans.rotate(Math.toRadians(270));
            }
        }
        var rotated = new BufferedImage(scaledW, scaledH, img.getType());
        var op = new AffineTransformOp(trans, AffineTransformOp.TYPE_BICUBIC);
        op.filter(img, rotated);

https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-drawui-java

結果

結構いい感じです。
「戦う猫」でケンカ中の猫の写真がでてることから、ちゃんとシチュエーションの認識もできていますね。
また、「オブジェクト指向の本」でオブジェクト指向の本や「Object Oriented」と書いてあるプレゼンテーションが出ていることから、文字が読めていることもわかります。

あと、「寝てる猫」で出てる写真は結構気に入ってたけど探そうと思ってもなかなかみつからないやつで、こういったなつかしい写真がいろいろ掘り起こされるのが面白いです。

ということで、割と簡単なプログラムでめちゃくちゃ便利な検索ができるので、みんな作りましょう。
ソース、すでに出してますけど、あらためて。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c

PythonのFastAPIにJavaのHttpClientから接続しようとするとupgradeできないというエラーになるのでHTTP 1.1を指定する

PythonでWeb APIを実装するFastAPIというのがあって、次のようにやればなんかサーバーがたつ。

import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/hello")
def hello():
  return "hello"

uvicorn.run(app, host="0.0.0.0", port=8000)

そして、Java 11で導入されたHttpClientで接続しようとするとエラーになる。

var client = new HttpClient().newHttpClient();
var req = HttpRequest.newBuilder(URI.create("http://localhost:8000/hello")
  .get().build();
var res = client.send(req, BodyHandlers.ofString());
println(res.body());

こんな感じのエラーが返ってくる。

{"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null}]}

でサーバー側を見るとこんなエラー

WARNING:  Unsupported upgrade request.
WARNING:  No supported WebSocket library detected. Please use "pip install 'uvicorn[standard]'", or install 'websockets' or 'wsproto' manually.
INFO:     127.0.0.1:62645 - "POST /text_embed HTTP/1.1" 422 Unprocessable Entity
WARNING:  Invalid HTTP request received.

pipでなんかインストールしろといってるけど、解決しない。

結局のところ、JavaのHttpClientがUpgdadeヘッダーつけてて、それでエラーになってる。

Connection: Upgrade, HTTP2-Settings
Content-Length: 17
Host: localhost:8001
HTTP2-Settings: AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA
Upgrade: h2c

いや、サポートしてないなら無視してよって感じがするのだけど、しかたない。
HTTP1.1を指定したら大丈夫だった。

var client = HttpClient.newBuilder()
  .version(HttpClient.Version.HTTP_1_1)
  .build();

flash_attnをWindowsにインストールするためにやったこと

MicrosoftのPhi-3-vision-128k-instructを動かそうと思ったらflash_attnというのが必要なのにインストールできなくてがんばった。

やったこと

  • 「Developer Command Prompt for VS 2022」 で動かす
    これは必須な気がする

  • CUDAを12.1にする
    いままで11.8を使ってたけど、12.1を使うようにした。PyTorchも入れなおした。これは必要かどうかわからない。TensorFlowが11.8じゃないとダメなので、そのうち戻して試してみる。 -> 戻したけどうまくいかないのでvenvで12.1用の環境をつくった

  • --no-build-isolationをつける
    packagingがないといわれたときに、一度これをつけてpip installしておく必要がありそう。

  • packagingとwheelをインストールする
    なにかの機会にインストールされてると思うけど、なければpip install packaging wheelする

  • DISTUTILS_USE_SDK=1 を設定する
    こんな感じの警告が出てたので従った。これが効いた気がする。

UserWarning: It seems that the VC environment is activated but
 DISTUTILS_USE_SDK is not set.This may lead to multiple 
activations of the VC env.Please set `DISTUTILS_USE_SDK=1` 
and try again.
  • 何回か試す
    試すごとにちょっとずつ進んだ可能性ある

ここで3時間くらい待って追わった。

インストールが長いのには同じ経験の人いた
https://github.com/Dao-AILab/flash-attention/issues/968

コンポーネント型Java WebフレームワークVaadinをSpring Bootで試すメモ

ZK、JSFコンポーネントフレームワークをやってきたので、Vaadinもやっておきます。
https://vaadin.com/

典型的な画面パターンが用意されたアプリケーションビルダーも用意されているので、ベースにできます。
このアプリケーションビルダーもVaadinで作られているようです。
https://start.vaadin.com/app/p

プロジェクト作成

Spring Bootでの始め方はここ。
https://vaadin.com/docs/latest/integrations/spring/spring-boot

まずSprig Bootのプロジェクトを作る。
https://start.spring.io/

Dependencyは何もいらないと思うので、そのまま。

設定

pom.xmlに依存などを追加。 プロパティとしてvaadin.versionでバージョンを指定しておきます。

<vaadin.version>24.3.12</vaadin.version>

dependencyManagementを追加

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-bom</artifactId>
            <!-- declare the latest Vaadin version
                 as a property or directly here -->
            <version>${vaadin.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

そして、dependencyにvaadin-spring-boot-starterを追加

    <dependency>
        <groupId>com.vaadin</groupId>
        <artifactId>
            vaadin-spring-boot-starter
        </artifactId>
        <version>${vaadin.version}</version>
    </dependency>

あと、pluginを追加

    <dependency>
        <groupId>com.vaadin</groupId>
        <artifactId>
            vaadin-spring-boot-starter
        </artifactId>
        <version>${vaadin.version}</version>
    </dependency>

コードを書く

VaddinではJavaコードだけで画面を構築します。

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Route;
import org.springframework.stereotype.Component;

@Component
@Route("")
public class MainView extends VerticalLayout{
    public MainView() {
        TextField textField = new TextField("Enter text");
        Span label = new Span("Hello");
        Button button = new Button("Go");
        button.addClickListener(e -> label.setText(textField.getValue()));

        add(textField, button, label);
    }
}

これで、こんな画面ができます。

Swingっぽいですね。

2024年JSF(Jakarta Faces)をTomcatで試すメモ

JSF(Jakarta Faces)をTomcatで試すメモ
Jakarta Faces」って長いので、ここではJSFって書きます。

Tomcat用のプロジェクトがあるとして追加する、って感じで書くので、基本的な構成は省略。
Tomcat 10.1.24で試してます。
https://tomcat.apache.org/download-10.cgi

最初から構成を作るのであれば、Quarkusが無難かなぁという気はする。
その場合はstarterでPrimeFacesを選ぼう。
https://code.quarkus.io/

Jakarta EEがよければ、Eclipse Starter for Jakarta EEで。そうするとPlatform ProfileかWeb ProfileであればJSFは含まれるので設定不要のはず
https://start.jakarta.ee/

Springとは相性がわるいです、たぶん。

設定

ということでTomcatに組み込む設定

mojarra組み込み

今回はJSFの実装にはmojarraを使います。
導入方法はREADME.mdにだいたい書いてあります。
https://github.com/eclipse-ee4j/mojarra/blob/4.0/README.md

Bean Validator使いたかったら最新バージョンを指定しろと書いてあるので、もし使いたければここで調べましょう。8.0.1が最新かな。
https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator

けど、必要なものだけであれば次のふたつをdependencyに追加すればよさそう。CDIに依存するようになったので、weldも組み込みます。

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.faces</artifactId>
    <version>4.0.7</version>
</dependency>
<dependency>
    <groupId>org.jboss.weld.servlet</groupId>
    <artifactId>weld-servlet-shaded</artifactId>
    <version>4.0.0.Final</version>
</dependency>

mojarraではなくMyFacesを使ってもいいと思う。Quarkusだとこちらになるはず。
https://myfaces.apache.org/

web.xmlの設定

web.xmlにFacesServletの設定が必要になります。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="6.0" xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd">
    <context-param>
        <param-name>jakarta.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
        <!-- <param-value>Production</param-value> -->
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
</web-app>

beans.xml

CDIを有効にするためにbeans.xmlが必要です。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
       bean-discovery-mode="all">
</beans>

試しに動かす

じゃあ動かしてみましょう。

faceletを書く

画面をfaceletとして書きます。
hello.xhtmlとして保存。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        <h1>Hello JSF</h1>
        <f:view>
        <h:form>
            <div>
                <h:inputText id="in" value="#{helloBean.input}"/>
                <h:commandButton value="Hello" actionListener="#{helloBean.go()}">
                    <f:ajax execute="in" render="out"/>
                </h:commandButton>
            </div>
            <h:outputLabel id="out" value="#{helloBean.message}"/>
        </h:form>
        </f:view>
    </h:body>
</html>

管理ビーンを作る

処理をするための管理ビーンを書きます。

package example;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;

@Named
@RequestScoped
public class HelloBean {
    private String message = "Hello!!!";
    private String input = "";

    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public String getInput() {
        return input;
    }
    public void setInput(String input) {
        this.input = input;
    }
    
    public void go() {
        message = "hello " + input;
    }
}

実行

うごいた

コンポーネント型Java WebフレームーワークZKをSpring Bootと一緒に試すメモ

ZKという、コンポーネント型のWebフレームワークがあって、ちょっと面白そうなので試してみた。
https://www.zkoss.org/

コンポーネント型なのでJSFが近い。
とりあえず始め方がここにいろいろある。
https://www.zkoss.org/wiki/ZK_Installation_Guide

プロジェクト作成

まあ、結局Spring Bootと一緒に使うことになるだろうから、Spring Bootで始めるやつを見る。
https://www.zkoss.org/wiki/ZK_Installation_Guide/Quick_Start/Create_and_Run_Your_First_ZK_Application_with_Spring_Boot

まずSprig Bootのプロジェクトを作る。
https://start.spring.io/

Dependencyは何もいらないということなので、そのまま。

Spring MVCなどはZKの依存ライブラリとして入ることになる。

で、pom.xmlにrepositoryの追加。

 <repositories>
        <repository>
            <id>ZK CE</id>
            <name>ZK CE Repository</name>
            <url>https://mavensync.zkoss.org/maven2</url>
        </repository>
    </repositories>

あと、dependencyを追加。

     <dependency>
            <groupId>org.zkoss.zkspringboot</groupId>
            <artifactId>zkspringboot-starter</artifactId>
            <type>pom</type>
            <version>3.2.3</version>
        </dependency>

ドキュメントには、${zkspringboot.version}の部分はmavenレポジトリを確認してくれと書いてあったので、ここにある最新っぽい3.2.3を指定。
http://mavensync.zkoss.org/maven2/org/zkoss/zkspringboot/zkspringboot-starter/

最初の画面を作る

ではZKのコードを書いてみる。zulという拡張子でXMLを書くらしい。
ドキュメントをそのままコピペ。してsrc/main/resources/web/hello.zulに保存

<zk>
    <window title="Hello ZK - Spring Boot!" border="normal">
        You are using ZK version <label value="${session.webApp.version}"/>
    </window>
</zk>

あと、src/main/resources/application.properties ファイルに設定を一行追加

zk.homepage=hello

で実行やーと思ったらorg.zkoss.zk.ui.http.HttpSessionListenerがないというClassNotFoundが・・・。

zkspringboot-starter.3.2.3のPOMを見るとzkbindがprovidedになってるのでプロジェクトに含まれない。provideされてないし。

ということで、自分のpom.xmlにzkbindも追加。zkspringboot-starterにあるものからscopeを取り除いただけ。

     <dependency>
            <groupId>org.zkoss.zk</groupId>
            <artifactId>zkbind</artifactId>
            <version>10.0.0-jakarta</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-jdk14</artifactId>
                </exclusion>
            </exclusions>
        </dependency> 

改めて起動したらlocalhost:8080にアクセスしたら動いた!

独自のファイルを追加する

では独自のファイルをsrc/main/resources/web/world.zul に置いてみる。

<zk>
    <window title="My Page" border="normal">
        It works!!
    </window>
</zk>

で、localhost:8080/worldとかにアクセスしてもNot Found。
どうやってアクセスすればいいのか全然わからなかったのだけど、サンプルコードとか見てたらコントローラーかかないといけないっぽい。

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloController {
    @GetMapping("/helloworld")
    public String hello() {
        return "world";
    }
}

なんかダサいな。パスをそのまま返すコントローラを作ることになりそう。
ドキュメントさがしたら、ここにあった。
https://github.com/zkoss/zkspringboot/blob/master/README.md

改めてコントローラのパスにアクセスしたら見れた。

Javaコードと結び付ける

じゃあ、ボタン押したらなにかJavaのコードを呼び出すっていうのをやろう。
それっぽいタグを書いてコンポーネントを置いてみる。idをちゃんと指定しておけばどうにかなる。

<zk>
  <window title="My Page" border="normal"
      apply="com.example.demo.HelloComposer">
    It works!!
    <vlayout>
      <hlayout>
        <textbox id="input"/>
        <button id="exec" label="Go"/>
      </hlayout>
      <label id="output" value="hello"/>
    </vlayout>
  </window>
</zk>

コンポーネントはこのリファレンスに。
https://www.zkoss.org/wiki/ZK_Component_Reference

そしたら対応するJavaコードを書く。クラス名をwindowタグのapplyに指定している。このクラスはSelectorComposer<Component>を継承する必要がある。

package com.example.demo;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zul.Label;
import org.zkoss.zul.Textbox;

public class HelloComposer extends SelectorComposer<Component>{
    @Wire
    private Label output;
    @Wire
    private Textbox input;
    
    @Listen("onClick = #exec")
    public void go() {
        output.setValue(input.getValue());
    }
}

あとは、コンポーネントに対応するフィールドを定義して@Wireアノテーションを貼る。フィールド名はたぶんIDと同じ名前にする必要がある。違う場合はアノテーションに指定すればいいのかもしれないけど試してない。
イベントハンドラには@ListenをつけてonClickにボタンのIDを指定する。

実行したら動いた!

Spring Bootとの組み合わせはJSFよりもやりやすい。
結構よさそうなんだけど、コンポーネントベースのフレームワークを使いたいのは企業の社内システムで、そうするとJEEアプリケーションサーバーをたててそうだし、いろいろ考えるとJSFがいいかなぁ。

Javaで最低限おさえておいてほしいクラス・インタフェース35 - 2024年版

ま、このくらい知っておいてもらわないと&とりあえずこんだけ知ってればだいたいの処理が書けるクラス・インタフェースをまとめてみました。2024年版。
詳しく知りたい人は「プロになるJava」を!

java.lang.Class
java.lang.Exception <- new
java.lang.Integer
java.lang.Object <- new
java.lang.Runnable
java.lang.String
java.lang.System
java.lang.Thread

java.nio.file.Files <- new
java.nio.file.Path <- new
java.io.InputStream
java.io.InputStreamReader
java.io.BufferedReader
java.io.OutputStream
java.io.PrintWriter

java.math.BigDecimal <- new

java.util.List
java.util.ArrayList
java.util.Map
java.util.HashMap
java.util.Optional <- new

java.util.stream.Stream <- new
java.util.stream.Collectors <- new
java.util.function.Function <- new
java.util.function.Consumer <- new
java.util.function.Supplier <- new

java.time.LocalDateTime <- new
java.time.ZonedDateTime <- new
java.time.LocalDate <- new

java.net.URI <- new
java.net.http.HttpClient <- new

java.sql.Connection
java.sql.DriverManager

java.awt.Graphics
javax.swing.JFrame

Java 5時代の2005年版はこちら
Javaで最低限おさえておいてほしいクラス・インタフェース35 - きしだのHatena

Swing知らんわーという人は、これを10分くらいでちょろっとやっておくといいと思います。
https://qiita.com/nowokay/items/e0b9c676567134e4a622#swing

out

外したものはこちら。代替APIが出たか、FormatやResultSetとかはフレームワークで対応するよね、とか。

java.io.File
java.io.Reader

java.text.DecimalFormat
java.text.SimpleDateFormat

java.util.Iterator
java.util.Calendar
java.util.Date
java.util.Properties

java.sql.PreparedStatement
java.sql.ResultSet
java.sql.Statement